diff --git a/internal/website/data/examples.json b/internal/website/data/examples.json index e7e7411308..94a29a0328 100644 --- a/internal/website/data/examples.json +++ b/internal/website/data/examples.json @@ -361,7 +361,7 @@ }, "gocloud.dev/runtimevar/constantvar.Example_openVariableFromURL": { "imports": "import (\n\t\"context\"\n\t\"fmt\"\n\n\t\"gocloud.dev/runtimevar\"\n\t_ \"gocloud.dev/runtimevar/constantvar\"\n)", - "code": "// runtimevar.OpenVariable creates a *runtimevar.Variable from a URL.\n\nv, err := runtimevar.OpenVariable(ctx, \"constant://?val=hello+world\u0026decoder=string\")\nif err != nil {\n\treturn err\n}\ndefer v.Close()" + "code": "// runtimevar.OpenVariable creates a *runtimevar.Variable from a URL.\n// The constant value is in the URL param \"val\".\nv, err := runtimevar.OpenVariable(ctx, \"constant://?val=hello+world\u0026decoder=string\")\nif err != nil {\n\treturn err\n}\ndefer v.Close()\n\n// The constant value is read from an environment variable specified in \"envvar\".\nv2, err := runtimevar.OpenVariable(ctx, \"constant://?envvar=MY_ENVIRONMENT_VARIABLE\u0026decoder=string\")\nif err != nil {\n\treturn err\n}\ndefer v2.Close()" }, "gocloud.dev/runtimevar/etcdvar.ExampleOpenVariable": { "imports": "import (\n\t\"go.etcd.io/etcd/client/v3\"\n\t\"gocloud.dev/runtimevar\"\n\t\"gocloud.dev/runtimevar/etcdvar\"\n)", diff --git a/runtimevar/constantvar/constantvar.go b/runtimevar/constantvar/constantvar.go index a8018777ad..ef2590e9c8 100644 --- a/runtimevar/constantvar/constantvar.go +++ b/runtimevar/constantvar/constantvar.go @@ -13,7 +13,7 @@ // limitations under the License. // Package constantvar provides a runtimevar implementation with Variables -// that never change. Use New, NewBytes, or NewError to construct a +// that never change. Use New, NewBytes, NewFromEnv, or NewError to construct a // *runtimevar.Variable. // // # URLs @@ -32,6 +32,7 @@ import ( "errors" "fmt" "net/url" + "os" "time" "gocloud.dev/gcerrors" @@ -53,12 +54,14 @@ const Scheme = "constant" // The following URL parameters are supported: // - val: The value to use for the constant Variable. The bytes from val // are passed to NewBytes. +// - envvar: The name of an environment variable to read the value from. // - err: The error to use for the constant Variable. A new error is created // using errors.New and passed to NewError. // - decoder: The decoder to use. Defaults to runtimevar.BytesDecoder. // See runtimevar.DecoderByName for supported values. // -// If both "err" and "val" are provided, "val" is ignored. +// If multiple of "val", "envvar", or "err" are provided, "err" wins, then "envvar", +// then "val". type URLOpener struct { // Decoder specifies the decoder to use if one is not specified in the URL. // Defaults to runtimevar.BytesDecoder. @@ -73,6 +76,9 @@ func (o *URLOpener) OpenVariableURL(ctx context.Context, u *url.URL) (*runtimeva val := q.Get("val") q.Del("val") + envvar := q.Get("envvar") + q.Del("envvar") + errVal := q.Get("err") q.Del("err") @@ -89,6 +95,9 @@ func (o *URLOpener) OpenVariableURL(ctx context.Context, u *url.URL) (*runtimeva if errVal != "" { return NewError(errors.New(errVal)), nil } + if envvar != "" { + return NewFromEnv(envvar, decoder), nil + } return NewBytes([]byte(val), decoder), nil } @@ -110,6 +119,22 @@ func NewBytes(b []byte, decoder *runtimevar.Decoder) *runtimevar.Variable { return New(value) } +// NewFromEnv reads an environment variable and uses decoder to decode it. +// If the decode succeeds, it constructs a *runtimevar.Variable holding the +// decoded value. If the decode fails, it constructs a runtimevar.Variable +// that always fails with the error. +// Note that the value of the constantvar is frozen at initialization time; +// it does not get a new value if the underlying environment variable value +// changes. +func NewFromEnv(envVarName string, decoder *runtimevar.Decoder) *runtimevar.Variable { + val := os.Getenv(envVarName) + value, err := decoder.Decode(context.Background(), []byte(val)) + if err != nil { + return NewError(err) + } + return New(value) +} + // NewError constructs a *runtimevar.Variable that always fails. Runtimevar // wraps errors returned by drivers, so the error returned // by runtimevar will not equal err. diff --git a/runtimevar/constantvar/constantvar_test.go b/runtimevar/constantvar/constantvar_test.go index 74779300b5..3f0d9a4fdf 100644 --- a/runtimevar/constantvar/constantvar_test.go +++ b/runtimevar/constantvar/constantvar_test.go @@ -17,6 +17,7 @@ package constantvar import ( "context" "errors" + "os" "testing" "time" @@ -133,6 +134,35 @@ func TestNewBytes(t *testing.T) { } } +func TestNewFromEnv(t *testing.T) { + ctx := context.Background() + const ( + content = "hello world" + name = "RUNTIMEVAR_CONST_TEST" + ) + os.Setenv(name, content) + + // Decode succeeds. + v := NewFromEnv(name, runtimevar.StringDecoder) + defer v.Close() + val, err := v.Watch(ctx) + if err != nil { + t.Fatal(err) + } + if val.Value != content { + t.Errorf("got %v want %v", val.Value, content) + } + + // Decode fails. + var jsonData []string + v = NewFromEnv(name, runtimevar.NewDecoder(jsonData, runtimevar.JSONDecode)) + defer v.Close() + val, err = v.Watch(ctx) + if err == nil { + t.Errorf("got nil error and %v, want error", val) + } +} + func TestNewError(t *testing.T) { ctx := context.Background() @@ -145,6 +175,7 @@ func TestNewError(t *testing.T) { } func TestOpenVariable(t *testing.T) { + os.Setenv("RUNTIMEVAR_CONST_TEST", "hello world") tests := []struct { URL string WantErr bool @@ -159,6 +190,8 @@ func TestOpenVariable(t *testing.T) { {"constant://?val=hello+world&decoder=string", false, false, "hello world"}, // JSON value; val parameter is {"Foo": "Bar"}, URL-encoded. {"constant://?val=%7B%22Foo%22%3A%22Bar%22%7d&decoder=jsonmap", false, false, &map[string]interface{}{"Foo": "Bar"}}, + // Environment variable value. + {"constant://?envvar=RUNTIMEVAR_CONST_TEST&decoder=string", false, false, "hello world"}, // Error. {"constant://?err=fail", false, true, nil}, // Invalid decoder. diff --git a/runtimevar/constantvar/example_test.go b/runtimevar/constantvar/example_test.go index 4463ce88fc..08276e7d8d 100644 --- a/runtimevar/constantvar/example_test.go +++ b/runtimevar/constantvar/example_test.go @@ -19,6 +19,7 @@ import ( "errors" "fmt" "log" + "os" "gocloud.dev/runtimevar" "gocloud.dev/runtimevar/constantvar" @@ -56,6 +57,23 @@ func ExampleNewBytes() { // byte slice of length 11 } +func ExampleNewFromEnv() { + // Construct a *runtimevar.Variable with an environment variable name. + os.Setenv("MY_ENVIRONMENT_VARIABLE", "hello world") + v := constantvar.NewFromEnv("MY_ENVIRONMENT_VARIABLE", runtimevar.BytesDecoder) + defer v.Close() + + // We can now read the value from v. + snapshot, err := v.Latest(context.Background()) + if err != nil { + log.Fatal(err) + } + fmt.Printf("byte slice of length %d\n", len(snapshot.Value.([]byte))) + + // Output: + // byte slice of length 11 +} + func ExampleNewError() { // Construct a runtimevar.Variable that always returns errFake. var errFake = errors.New("my error") @@ -82,12 +100,19 @@ func Example_openVariableFromURL() { ctx := context.Background() // runtimevar.OpenVariable creates a *runtimevar.Variable from a URL. - + // The constant value is in the URL param "val". v, err := runtimevar.OpenVariable(ctx, "constant://?val=hello+world&decoder=string") if err != nil { log.Fatal(err) } defer v.Close() + + // The constant value is read from an environment variable specified in "envvar". + v2, err := runtimevar.OpenVariable(ctx, "constant://?envvar=MY_ENVIRONMENT_VARIABLE&decoder=string") + if err != nil { + log.Fatal(err) + } + defer v2.Close() // PRAGMA: On gocloud.dev, hide the rest of the function. snapshot, err := v.Latest(ctx) if err != nil {