blob: dd9bb7b866dca2699d15724666fb63f4a5ffae16 [file] [log] [blame]
// Copyright 2016 The LUCI Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
// Package lucictx implements a Go client for the protocol defined here:
// https://github.com/luci/luci-py/blob/master/client/LUCI_CONTEXT.md
//
// It differs from the python client in a couple ways:
// * The initial LUCI_CONTEXT value is captured once at application start, and
// the environment variable is REMOVED.
// * Writes are cached into the golang context.Context, not a global variable.
// * The LUCI_CONTEXT environment variable is not changed automatically when
// using the Set function. To pass the new context on to a child process,
// you must use the Export function to dump the current context state to
// disk.
package lucictx
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"reflect"
"golang.org/x/net/context"
"github.com/luci/luci-go/common/errors"
)
// EnvKey is the environment variable key for the LUCI_CONTEXT file.
const EnvKey = "LUCI_CONTEXT"
// lctx is wrapper around top-level JSON dict of a LUCI_CONTEXT file.
//
// Note that we must use '*json.RawMessage' as dict value type since only
// pointer *json.RawMessage type implements json.Marshaler and json.Unmarshaler
// interfaces. Without '*' the JSON library treats json.RawMessage as []byte and
// marshals it as base64 blob.
type lctx map[string]*json.RawMessage
func (l lctx) clone() lctx {
ret := make(lctx, len(l))
for k, v := range l {
ret[k] = v
}
return ret
}
var lctxKey = "Holds the current lctx"
// This is the LUCI_CONTEXT loaded from the environment once when the process
// starts.
var externalContext = extractFromEnv(os.Stderr)
func extractFromEnv(out io.Writer) lctx {
path := os.Getenv(EnvKey)
if path == "" {
return nil
}
f, err := os.Open(path)
if err != nil {
fmt.Fprintf(out, "Could not open LUCI_CONTEXT file %q: %s", path, err)
return nil
}
defer f.Close()
dec := json.NewDecoder(f)
dec.UseNumber()
tmp := map[string]interface{}{}
if err := dec.Decode(&tmp); err != nil {
fmt.Fprintf(out, "Could not decode LUCI_CONTEXT file %q: %s", path, err)
return nil
}
ret := make(lctx, len(tmp))
for k, v := range tmp {
if reflect.TypeOf(v).Kind() != reflect.Map {
fmt.Fprintf(out, "Could not reencode LUCI_CONTEXT file %q, section %q: Not a map.", path, k)
continue
}
item, _ := json.Marshal(v)
// This section just came from json.Unmarshal, so we know that json.Marshal
// will work on it.
raw := json.RawMessage(item)
ret[k] = &raw
}
return ret
}
func getCurrent(ctx context.Context) lctx {
if l := ctx.Value(&lctxKey); l != nil {
return l.(lctx)
}
return externalContext
}
// Get retrieves the current section from the current LUCI_CONTEXT, and
// deserializes it into out. Out may be any target for json.Unmarshal. If the
// section exists, it deserializes it into the provided out object. If not, then
// out is unmodified.
func Get(ctx context.Context, section string, out interface{}) error {
data := getCurrent(ctx)[section]
if data == nil || len(*data) == 0 {
return nil
}
return json.Unmarshal(*data, out)
}
// Lookup retrieves the current section from the current LUCI_CONTEXT, and
// deserializes it into out. Out may be any target for json.Unmarshal. It
// returns a deserialization error (if any), and a boolean indicating if the
// section was actually found.
func Lookup(ctx context.Context, section string, out interface{}) (bool, error) {
data, ok := getCurrent(ctx)[section]
if data == nil || len(*data) == 0 {
return ok, nil
}
return ok, json.Unmarshal(*data, out)
}
// Set writes the json serialization of `in` as the given section into the
// LUCI_CONTEXT, returning the new ctx object containing it. This ctx can be
// passed to Export to serialize it to disk.
//
// If in is nil, it will clear that section of the LUCI_CONTEXT.
//
// The returned context is always safe to use, even if this returns an error.
// This only returns an error if `in` cannot be marshalled to a JSON Object.
func Set(ctx context.Context, section string, in interface{}) (context.Context, error) {
err := error(nil)
data := json.RawMessage(nil)
if in != nil {
if data, err = json.Marshal(in); err != nil {
return ctx, err
}
if data[0] != '{' {
return ctx, errors.New("LUCI_CONTEXT sections must always be JSON Objects")
}
}
cur := getCurrent(ctx)
if _, alreadyHas := cur[section]; data == nil && !alreadyHas {
// Removing a section which is already missing is a no-op
return ctx, nil
}
newLctx := cur.clone()
if data == nil {
delete(newLctx, section)
} else {
newLctx[section] = &data
}
return context.WithValue(ctx, &lctxKey, newLctx), nil
}
// Export takes the current LUCI_CONTEXT information from ctx, writes it to
// a file and returns a wrapping Exported object. This exported value must then
// be installed into the environment of any subcommands (see the methods on
// Exported).
//
// It is required that the caller of this function invoke Close() on the
// returned Exported object, or they will leak temporary files.
//
// 'dir', if not "", specifies a directory to put the exported file in.
// If empty, os.TempDir() will be used.
func Export(ctx context.Context, dir string) (Exported, error) {
cur := getCurrent(ctx)
if len(cur) == 0 {
return &nullExport{}, nil
}
if dir == "" {
dir = os.TempDir()
}
// Note: this makes a file in 0600 mode. This is what we want, the context
// may have secrets.
f, err := ioutil.TempFile(dir, "luci_context.")
if err != nil {
return nil, errors.Annotate(err).Reason("creating luci_context file").Err()
}
l := &liveExport{path: f.Name()}
data, _ := json.Marshal(cur)
_, err = f.Write(data)
f.Close() // intentionally do this even on error.
if err != nil {
l.Close() // cleans up the tempfile
return nil, errors.Annotate(err).Reason("writing luci_context").Err()
}
return l, nil
}