go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/lucictx/lucictx.go (about) 1 // Copyright 2016 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package lucictx implements a Go client for the protocol defined here: 16 // 17 // https://github.com/luci/luci-py/blob/master/client/LUCI_CONTEXT.md 18 // 19 // It differs from the python client in a couple ways: 20 // - The initial LUCI_CONTEXT value is captured once at application start. 21 // - Writes are cached into the golang context.Context, not a global variable. 22 // - The LUCI_CONTEXT environment variable is not changed automatically when 23 // using the Set function. To pass the new context on to a child process, 24 // you must use the Export function to dump the current context state to 25 // disk and call exported.SetInCmd(cmd) to configure new command's 26 // environment. 27 package lucictx 28 29 import ( 30 "bytes" 31 "context" 32 "encoding/json" 33 "fmt" 34 "io" 35 "io/ioutil" 36 "os" 37 "reflect" 38 "sync" 39 40 "github.com/golang/protobuf/jsonpb" 41 protoV1 "github.com/golang/protobuf/proto" 42 "google.golang.org/protobuf/encoding/protojson" 43 "google.golang.org/protobuf/proto" 44 45 "go.chromium.org/luci/common/errors" 46 ) 47 48 // EnvKey is the environment variable key for the LUCI_CONTEXT file. 49 const EnvKey = "LUCI_CONTEXT" 50 51 // lctx is wrapper around top-level JSON dict of a LUCI_CONTEXT file. 52 // 53 // Note that we must use '*json.RawMessage' as dict value type since only 54 // pointer *json.RawMessage type implements json.Marshaler and json.Unmarshaler 55 // interfaces. Without '*' the JSON library treats json.RawMessage as []byte and 56 // marshals it as base64 blob. 57 type lctx struct { 58 sections map[string]*json.RawMessage // readonly! lives outside the lock 59 60 lock sync.Mutex 61 path string // non-empty if exists as file on disk 62 refs int // number of open references to the dropped file 63 } 64 65 func alloc(size int) *lctx { 66 return &lctx{sections: make(map[string]*json.RawMessage, size)} 67 } 68 69 func (l *lctx) clone() *lctx { 70 ret := alloc(len(l.sections)) 71 for k, v := range l.sections { 72 ret.sections[k] = v 73 } 74 return ret 75 } 76 77 var lctxKey = "Holds the current lctx" 78 79 // This is the LUCI_CONTEXT loaded from the environment once when the process 80 // starts. 81 var externalContext = extractFromEnv(os.Stderr) 82 83 func extractFromEnv(out io.Writer) *lctx { 84 path := os.Getenv(EnvKey) 85 if path == "" { 86 return &lctx{} 87 } 88 f, err := os.Open(path) 89 if err != nil { 90 fmt.Fprintf(out, "Could not open LUCI_CONTEXT file %q: %s\n", path, err) 91 return &lctx{} 92 } 93 defer f.Close() 94 95 dec := json.NewDecoder(f) 96 dec.UseNumber() 97 tmp := map[string]any{} 98 if err := dec.Decode(&tmp); err != nil { 99 fmt.Fprintf(out, "Could not decode LUCI_CONTEXT file %q: %s\n", path, err) 100 return &lctx{} 101 } 102 103 ret := alloc(len(tmp)) 104 for k, v := range tmp { 105 if reflect.TypeOf(v).Kind() != reflect.Map { 106 fmt.Fprintf(out, "Could not re-encode LUCI_CONTEXT file %q, section %q: Not a map.\n", path, k) 107 continue 108 } 109 item, err := json.Marshal(v) 110 if err != nil { 111 fmt.Fprintf(out, "Could not marshal LUCI_CONTEXT %v: %s\n", v, err) 112 return &lctx{} 113 } 114 115 // This section just came from json.Unmarshal, so we know that json.Marshal 116 // will work on it. 117 raw := json.RawMessage(item) 118 ret.sections[k] = &raw 119 } 120 121 ret.path = path // reuse existing external file in Export() 122 ret.refs = 1 // never decremented, ensuring we don't delete the external file 123 return ret 124 } 125 126 // Note: it never returns nil. 127 func getCurrent(ctx context.Context) *lctx { 128 if l := ctx.Value(&lctxKey); l != nil { 129 return l.(*lctx) 130 } 131 return externalContext 132 } 133 134 // Get retrieves the current section from the current LUCI_CONTEXT, and 135 // deserializes it into out. Out may be any target for json.Unmarshal. If the 136 // section exists, it deserializes it into the provided out object. If not, then 137 // out is unmodified. 138 func Get(ctx context.Context, section string, out proto.Message) error { 139 _, err := Lookup(ctx, section, out) 140 return err 141 } 142 143 // Lookup retrieves the current section from the current LUCI_CONTEXT, and 144 // deserializes it into out. Out may be any target for json.Unmarshal. It 145 // returns a deserialization error (if any), and a boolean indicating if the 146 // section was actually found. 147 func Lookup(ctx context.Context, section string, out proto.Message) (bool, error) { 148 data, _ := getCurrent(ctx).sections[section] 149 if data == nil { 150 return false, nil 151 } 152 unmarshaler := &jsonpb.Unmarshaler{ 153 AllowUnknownFields: true, 154 } 155 if err := unmarshaler.Unmarshal(bytes.NewReader(*data), protoV1.MessageV1(out)); err != nil { 156 return true, errors.Annotate(err, "failed to unmarshal json: %s", string(*data)).Err() 157 } 158 return true, nil 159 } 160 161 // Set writes the json serialization of `in` as the given section into the 162 // LUCI_CONTEXT, returning the new ctx object containing it. This ctx can be 163 // passed to Export to serialize it to disk. 164 // 165 // If in is nil, it will clear that section of the LUCI_CONTEXT. 166 // 167 // The returned context is always safe to use, even if this returns an error. 168 func Set(ctx context.Context, section string, in proto.Message) context.Context { 169 var data json.RawMessage 170 if in != nil && !reflect.ValueOf(in).IsNil() { 171 buf, err := protojson.Marshal(in) 172 if err != nil { 173 panic(err) // Only errors could be from writing to buf. 174 } 175 data = buf 176 } 177 cur := getCurrent(ctx) 178 if _, alreadyHas := cur.sections[section]; data == nil && !alreadyHas { 179 // Removing a section which is already missing is a no-op 180 return ctx 181 } 182 newLctx := cur.clone() 183 if data == nil { 184 delete(newLctx.sections, section) 185 } else { 186 newLctx.sections[section] = &data 187 } 188 return context.WithValue(ctx, &lctxKey, newLctx) 189 } 190 191 // Export takes the current LUCI_CONTEXT information from ctx, writes it to 192 // a file in os.TempDir and returns a wrapping Exported object. This exported 193 // value must then be installed into the environment of any subcommands (see 194 // the methods on Exported). 195 // 196 // It is required that the caller of this function invoke Close() on the 197 // returned Exported object, or they will leak temporary files. 198 // 199 // Internally this function reuses existing files, when possible, so if you 200 // anticipate calling a lot of subcommands with exported LUCI_CONTEXT, you can 201 // export it in advance (thus grabbing a reference to the exported file). Then 202 // subsequent Export() calls with this context will be extremely cheap, since 203 // they will just reuse the existing file. Don't forget to release it with 204 // Close() when done. 205 func Export(ctx context.Context) (Exported, error) { 206 return getCurrent(ctx).export("") 207 } 208 209 // ExportInto is like Export, except it places the temporary file into the given 210 // directory. 211 // 212 // Exports done via this method are not reused: each individual ExportInto call 213 // produces a new temporary file. 214 func ExportInto(ctx context.Context, dir string) (Exported, error) { 215 return getCurrent(ctx).export(dir) 216 } 217 218 func (l *lctx) export(dir string) (Exported, error) { 219 if len(l.sections) == 0 { 220 return &nullExport{}, nil 221 } 222 223 if dir != "" { 224 path, err := dropToDisk(l.sections, dir) 225 if err != nil { 226 return nil, err 227 } 228 return &liveExport{ 229 path: path, 230 closer: func() { removeFromDisk(path) }, 231 }, nil 232 } 233 234 l.lock.Lock() 235 defer l.lock.Unlock() 236 237 if l.refs == 0 { 238 if l.path != "" { 239 panic("lctx.path is supposed to be empty here") 240 } 241 path, err := dropToDisk(l.sections, "") 242 if err != nil { 243 return nil, err 244 } 245 l.path = path 246 } 247 248 l.refs++ 249 return &liveExport{ 250 path: l.path, 251 closer: func() { 252 l.lock.Lock() 253 defer l.lock.Unlock() 254 if l.refs == 0 { 255 panic("lctx.refs can't be zero here") 256 } 257 l.refs-- 258 if l.refs == 0 { 259 removeFromDisk(l.path) 260 l.path = "" 261 } 262 }, 263 }, nil 264 } 265 266 func dropToDisk(sections map[string]*json.RawMessage, dir string) (string, error) { 267 // Note: this makes a file in 0600 mode. This is what we want, the context 268 // may have secrets. 269 f, err := ioutil.TempFile(dir, "luci_context.") 270 if err != nil { 271 return "", errors.Annotate(err, "creating luci_context file").Err() 272 } 273 274 err = json.NewEncoder(f).Encode(sections) 275 if clErr := f.Close(); err == nil { 276 err = clErr 277 } 278 if err != nil { 279 removeFromDisk(f.Name()) 280 return "", errors.Annotate(err, "writing luci_context").Err() 281 } 282 283 return f.Name(), nil 284 } 285 286 func removeFromDisk(path string) { 287 if err := os.Remove(path); err != nil && !os.IsNotExist(err) { 288 fmt.Fprintf(os.Stderr, "Could not remove LUCI_CONTEXT file %q: %s\n", path, err) 289 } 290 }