| OLD | NEW |
| (Empty) | |
| 1 // Copyright 2016 The LUCI Authors. All rights reserved. |
| 2 // Use of this source code is governed under the Apache License, Version 2.0 |
| 3 // that can be found in the LICENSE file. |
| 4 |
| 5 // Package textproto implements a textproto config service Resolver. |
| 6 // |
| 7 // It uses the "luci-go" text protobuf multi-line extension. For more |
| 8 // information, see: github.com/luci/luci-go/common/proto |
| 9 // |
| 10 // The textproto protobuf Resolver internally formats its content as a binary |
| 11 // protobuf, rather than its raw text content. This has some advantages over raw |
| 12 // text content caching: |
| 13 // - More space-efficient. |
| 14 // - Decodes faster. |
| 15 // - If the config service protobuf schema differs from the application's |
| 16 // compiled schema, and the schema change is responsible (adding, renamin
g, |
| 17 // repurposing) the binary cache value will continue to load where the te
xt |
| 18 // protobuf would fail. |
| 19 package textproto |
| 20 |
| 21 import ( |
| 22 "bytes" |
| 23 "reflect" |
| 24 "strings" |
| 25 |
| 26 "github.com/luci/luci-go/common/data/cmpbin" |
| 27 "github.com/luci/luci-go/common/errors" |
| 28 luciProto "github.com/luci/luci-go/common/proto" |
| 29 "github.com/luci/luci-go/luci_config/server/cfgclient" |
| 30 "github.com/luci/luci-go/luci_config/server/cfgclient/backend" |
| 31 |
| 32 "github.com/golang/protobuf/proto" |
| 33 ) |
| 34 |
| 35 var typeOfProtoMessage = reflect.TypeOf((*proto.Message)(nil)).Elem() |
| 36 |
| 37 // BinaryFormat is the resolver's binary protobuf format string. |
| 38 const BinaryFormat = "github.com/luci/luci-go/server/config/TextProto:binary" |
| 39 |
| 40 // Message is a cfgclient.Resolver that resolves config data into proto.Message |
| 41 // instances by parsing the config data as a text protobuf. |
| 42 func Message(out proto.Message) interface { |
| 43 cfgclient.Resolver |
| 44 cfgclient.FormattingResolver |
| 45 } { |
| 46 if out == nil { |
| 47 panic("cannot pass a nil proto.Message instance") |
| 48 } |
| 49 return &resolver{ |
| 50 messageName: proto.MessageName(out), |
| 51 single: out, |
| 52 singleType: reflect.TypeOf(out), |
| 53 } |
| 54 } |
| 55 |
| 56 // Slice is a cfgclient.MultiResolver which resolves a slice of configurations |
| 57 // into a TextProto. |
| 58 // |
| 59 // out must be a pointer to a slice of some proto.Message implementation. If it |
| 60 // isn't, this function will panic. |
| 61 // |
| 62 // For example: |
| 63 // |
| 64 // var out []*MyProtoMessages |
| 65 // TextProtoSlice(&out) |
| 66 func Slice(out interface{}) interface { |
| 67 cfgclient.MultiResolver |
| 68 cfgclient.FormattingResolver |
| 69 } { |
| 70 r := resolver{} |
| 71 |
| 72 v := reflect.ValueOf(out) |
| 73 if !r.loadMulti(v) { |
| 74 panic(errors.Reason("%(type)s is not a pointer to a slice of pro
tobuf message types").D("type", v.Type()).Err()) |
| 75 } |
| 76 return &r |
| 77 } |
| 78 |
| 79 type resolver struct { |
| 80 messageName string |
| 81 |
| 82 single proto.Message |
| 83 singleType reflect.Type |
| 84 |
| 85 multiDest reflect.Value |
| 86 } |
| 87 |
| 88 func (r *resolver) loadMulti(v reflect.Value) bool { |
| 89 t := v.Type() |
| 90 if t.Kind() != reflect.Ptr { |
| 91 return false |
| 92 } |
| 93 valElem := t.Elem() |
| 94 if valElem.Kind() != reflect.Slice { |
| 95 return false |
| 96 } |
| 97 sliceContent := valElem.Elem() |
| 98 if sliceContent.Kind() != reflect.Ptr { |
| 99 return false |
| 100 } |
| 101 if !sliceContent.Implements(typeOfProtoMessage) { |
| 102 return false |
| 103 } |
| 104 |
| 105 r.messageName = proto.MessageName(archetypeMessage(sliceContent)) |
| 106 r.singleType = sliceContent |
| 107 r.multiDest = v.Elem() |
| 108 return true |
| 109 } |
| 110 |
| 111 // Resolve implements cfgclient.MultiResolver. |
| 112 func (r *resolver) Resolve(it *backend.Item) error { |
| 113 return r.resolveItem(r.single, it.Content, it.FormatSpec.Formatter) |
| 114 } |
| 115 |
| 116 // PrepareMulti implements cfgclient.MultiResolver. |
| 117 func (r *resolver) PrepareMulti(size int) { |
| 118 slice := reflect.MakeSlice(r.multiDest.Type(), size, size) |
| 119 r.multiDest.Set(slice) |
| 120 } |
| 121 |
| 122 // ResolveItemAt implements cfgclient.MultiResolver. |
| 123 func (r *resolver) ResolveItemAt(i int, it *backend.Item) error { |
| 124 msgV := archetypeInstance(r.singleType) |
| 125 if err := r.resolveItem(msgV.Interface().(proto.Message), it.Content, it
.FormatSpec.Formatter); err != nil { |
| 126 return err |
| 127 } |
| 128 r.multiDest.Index(i).Set(msgV) |
| 129 return nil |
| 130 } |
| 131 |
| 132 func (r *resolver) resolveItem(out proto.Message, content string, format string)
error { |
| 133 switch format { |
| 134 case "": |
| 135 // Not formatted (text protobuf). |
| 136 if err := luciProto.UnmarshalTextML(content, out); err != nil { |
| 137 return errors.Annotate(err).Reason("failed to unmarshal
text protobuf").Err() |
| 138 } |
| 139 return nil |
| 140 |
| 141 case BinaryFormat: |
| 142 if err := parseBinaryContent(content, out); err != nil { |
| 143 return errors.Annotate(err).Reason("failed to unmarshal
binary protobuf").Err() |
| 144 } |
| 145 return nil |
| 146 |
| 147 default: |
| 148 return errors.Reason("unsupported content format: %(format)q").D
("format", format).Err() |
| 149 } |
| 150 } |
| 151 |
| 152 func (r *resolver) Format() backend.FormatSpec { |
| 153 return backend.FormatSpec{BinaryFormat, r.messageName} |
| 154 } |
| 155 |
| 156 // RegisterFormatter registers the textproto Formatter with fr. |
| 157 func RegisterFormatter(fr *cfgclient.FormatterRegistry) { |
| 158 fr.Register(BinaryFormat, &Formatter{}) |
| 159 } |
| 160 |
| 161 // Formatter is a cfgclient.Formatter implementation bound to a specific |
| 162 // protobuf message. |
| 163 // |
| 164 // It takes a text protobuf representation of that message as input and returns |
| 165 // a binary protobuf representation as output. |
| 166 type Formatter struct{} |
| 167 |
| 168 // FormatItem implements cfgclient.Formatter. |
| 169 func (f *Formatter) FormatItem(c, fd string) (string, error) { |
| 170 archetype := proto.MessageType(fd) |
| 171 if archetype == nil { |
| 172 return "", errors.Reason("unknown proto.Message type %(type)q in
formatter data"). |
| 173 D("type", fd).Err() |
| 174 } |
| 175 msg := archetypeMessage(archetype) |
| 176 |
| 177 // Convert from config to protobuf. |
| 178 if err := luciProto.UnmarshalTextML(c, msg); err != nil { |
| 179 return "", errors.Annotate(err).Reason("failed to unmarshal text
protobuf content").Err() |
| 180 } |
| 181 |
| 182 // Binary format. |
| 183 bc, err := makeBinaryContent(msg) |
| 184 if err != nil { |
| 185 return "", errors.Annotate(err).Err() |
| 186 } |
| 187 return bc, nil |
| 188 } |
| 189 |
| 190 // t is a pointer to a proto.Message instance. |
| 191 func archetypeInstance(t reflect.Type) reflect.Value { |
| 192 return reflect.New(t.Elem()) |
| 193 } |
| 194 |
| 195 func archetypeMessage(t reflect.Type) proto.Message { |
| 196 return archetypeInstance(t).Interface().(proto.Message) |
| 197 } |
| 198 |
| 199 // makeBinaryContent constructs a binary content string from text protobuf |
| 200 // content. |
| 201 // |
| 202 // The binary content is formatted by concatenating two "cmpbin" binary values |
| 203 // together: |
| 204 // [Message Name] | [Marshaled Message Data] |
| 205 func makeBinaryContent(msg proto.Message) (string, error) { |
| 206 d, err := proto.Marshal(msg) |
| 207 if err != nil { |
| 208 return "", errors.Annotate(err).Reason("failed to marshal messag
e").Err() |
| 209 } |
| 210 |
| 211 var buf bytes.Buffer |
| 212 if _, err := cmpbin.WriteString(&buf, proto.MessageName(msg)); err != ni
l { |
| 213 return "", errors.Annotate(err).Reason("failed to write message
name").Err() |
| 214 } |
| 215 if _, err := cmpbin.WriteBytes(&buf, d); err != nil { |
| 216 return "", errors.Annotate(err).Reason("failed to write binary m
essage content").Err() |
| 217 } |
| 218 return buf.String(), nil |
| 219 } |
| 220 |
| 221 // parseBinaryContent parses a binary content string, pulling out the message |
| 222 // type and marshalled message data. It then unmarshals the specified type into |
| 223 // a new message based on the archetype. |
| 224 // |
| 225 // If the binary content's declared type doesn't match the archetype, or if the |
| 226 // binary content is invalid, an error will be returned. |
| 227 func parseBinaryContent(v string, msg proto.Message) error { |
| 228 r := strings.NewReader(v) |
| 229 encName, _, err := cmpbin.ReadString(r) |
| 230 if err != nil { |
| 231 return errors.Annotate(err).Reason("failed to read message name"
).Err() |
| 232 } |
| 233 |
| 234 // Construct a message for this. |
| 235 if name := proto.MessageName(msg); name != encName { |
| 236 return errors.Reason("message name %(name)q doesn't match encode
d name %(enc)q"). |
| 237 D("name", name).D("enc", encName).Err() |
| 238 } |
| 239 |
| 240 // We have the right message, unmarshal. |
| 241 d, _, err := cmpbin.ReadBytes(r) |
| 242 if err != nil { |
| 243 return errors.Annotate(err).Reason("failed to read binary messag
e content").Err() |
| 244 } |
| 245 if err := proto.Unmarshal(d, msg); err != nil { |
| 246 return errors.Annotate(err).Reason("failed to unmarshal message"
).Err() |
| 247 } |
| 248 return nil |
| 249 } |
| OLD | NEW |