Skip to content

Commit

Permalink
runtimevar/constantvar: Add support for reading the constantvar from …
Browse files Browse the repository at this point in the history
…an environment variable (#3318)
  • Loading branch information
vangent authored Sep 16, 2023
1 parent 16a4757 commit db7e808
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 4 deletions.
2 changes: 1 addition & 1 deletion internal/website/data/examples.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 27 additions & 2 deletions runtimevar/constantvar/constantvar.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,6 +32,7 @@ import (
"errors"
"fmt"
"net/url"
"os"
"time"

"gocloud.dev/gcerrors"
Expand All @@ -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.
Expand All @@ -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")

Expand All @@ -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
}

Expand All @@ -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.
Expand Down
33 changes: 33 additions & 0 deletions runtimevar/constantvar/constantvar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package constantvar
import (
"context"
"errors"
"os"
"testing"
"time"

Expand Down Expand Up @@ -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()

Expand All @@ -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
Expand All @@ -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.
Expand Down
27 changes: 26 additions & 1 deletion runtimevar/constantvar/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"errors"
"fmt"
"log"
"os"

"gocloud.dev/runtimevar"
"gocloud.dev/runtimevar/constantvar"
Expand Down Expand Up @@ -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")
Expand All @@ -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 {
Expand Down

0 comments on commit db7e808

Please sign in to comment.