pulumi/sdk/go/common/util/env/env.go

455 lines
11 KiB
Go

// Copyright 2016-2022, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// A small library for creating typed, consistent and documented environmental variable
// accesses.
//
// Declaring a variable is as simple as declaring a module level constant.
//
// var Var = env.Bool("VAR", "A boolean variable")
//
// Typed values can be retrieved by calling `Var.Value()`.
package env
import (
"fmt"
"os"
"sort"
"strconv"
"strings"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
)
// Store holds a collection of key, value? pairs.
//
// Store acts like the environment that values are drawn from.
type Store interface {
// Retrieve a raw value from the Store. If the value is not present, "", false should
// be returned.
Raw(key string) (string, bool)
}
// A strongly typed environment.
type Env interface {
GetString(val StringValue) string
GetBool(val BoolValue) bool
GetInt(val IntValue) int
}
// Create a new strongly typed Env from an untyped Store.
func NewEnv(store Store) Env {
return env{store}
}
type env struct{ s Store }
func (e env) GetString(val StringValue) string {
return StringValue{val.withStore(e.s)}.Value()
}
func (e env) GetBool(val BoolValue) bool {
return BoolValue{val.withStore(e.s)}.Value()
}
func (e env) GetInt(val IntValue) int {
return IntValue{val.withStore(e.s)}.Value()
}
type envStore struct{}
func (envStore) Raw(key string) (string, bool) {
return os.LookupEnv(key)
}
type MapStore map[string]string
func (m MapStore) Raw(key string) (string, bool) {
v, ok := m[key]
return v, ok
}
// The global store of values that Value.Value() uses.
//
// Setting this value is not thread safe, and should be restricted to testing.
var Global Store = envStore{}
// An environmental variable.
type Var struct {
name string
Value Value
Description string
options options
}
// The default prefix for a environmental variable.
const Prefix = "PULUMI_"
// The name of an environmental variable.
//
// Name accounts for prefix if appropriate.
func (v Var) Name() string {
return v.options.name(v.name)
}
// The list of variables that a must be truthy for `v` to be set.
func (v Var) Requires() []BoolValue { return v.options.prerequs }
var envVars []Var
// Variables is a list of variables declared.
func Variables() []Var {
vars := envVars
sort.SliceStable(vars, func(i, j int) bool {
return vars[i].Name() < vars[j].Name()
})
return vars
}
// An Option to configure a environmental variable.
type Option func(*options)
type options struct {
prerequs []BoolValue
noPrefix bool
secret bool
// The default value to read in. It will be parsed as if it was provided from a
// Store.
defaultValue string
// If err is non-nil, then this value will always fail validation.
err error
}
func (o options) name(underlying string) string {
if o.noPrefix {
return underlying
}
return Prefix + underlying
}
// Needs indicates that a variable can only be set if `val` is truthy.
func Needs(val BoolValue) Option {
return func(o *options) {
o.prerequs = append(o.prerequs, val)
}
}
// NoPrefix indicates that a variable should not have the default prefix applied.
func NoPrefix(opts *options) {
opts.noPrefix = true
}
// Secret indicates that the value should not be displayed in plaintext.
func Secret(opts *options) {
opts.secret = true
}
// Add a default value to the variable.
//
// If f returns a non-nil error, then the variable will fail validation.
func DefaultF(f func() (string, error)) Option {
return func(opts *options) {
s, err := f()
opts.defaultValue = s
opts.err = err
}
}
// The value of a environmental variable.
//
// In general, `Value`s should only be used in collections. For specific values, used the
// typed version (StringValue, BoolValue).
//
// Every implementer of Value also includes a `Value() T` method that returns a typed
// representation of the value.
type Value interface {
fmt.Stringer
// Retrieve the underlying string value associated with the variable.
//
// If the variable was not set, ("", false) is returned.
Underlying() (string, bool)
// Retrieve the Var associated with this value.
Var() Var
Type() string
Validate() ValidateError
// Get the value, or the default value as appropriate. If "" is returned, a type
// specific default value should be provided.
get() string
// This is the inner validate method that should be implemented by Value*
// types. Only [value] should implement [Validate] directly.
validate(input string) ValidateError
// set the associated variable for the value. This is necessary since Value and Var
// are inherently cyclical.
setVar(Var)
// A type correct formatting for the value. This is used for display purposes and
// should not be quoted.
//
// This function should not be called directly, except by [fmtValue].
formattedValue() string
}
type ValidateError struct {
Warning error
Error error
}
// An implementation helper for Value. New Values should be a typed wrapper around *value.
type value struct {
variable Var
store Store
}
func (v value) Validate() ValidateError {
if err := v.variable.options.err; err != nil {
// If we have a preset error, fail with that.
return ValidateError{Error: err}
}
value := v.get()
_, specified := v.Underlying()
var warn error
if m := v.missingPrerequs(); m != "" && specified {
warn = fmt.Errorf("cannot set because %q required %q", v.variable.name, m)
}
// We validate if the user specified a value, or if we set a default value.
var validation ValidateError
if specified || value != "" {
validation = v.variable.Value.validate(value)
}
if validation.Warning == nil && warn != nil {
validation.Warning = warn
}
return validation
}
func (v value) withStore(store Store) *value {
v.store = store // This is non-mutating since `v` is taken by value.
return &v
}
func (v value) hasDefault() bool { return v.variable.options.defaultValue != "" }
func (v value) String() string {
_, present := v.Underlying()
if !present {
msg := "unset"
if v.hasDefault() {
msg = "default " + fmtValue(v)
}
return msg
}
if m := v.missingPrerequs(); m != "" {
return fmt.Sprintf("needs %s (%s)", m, fmtValue(v))
}
return fmtValue(v)
}
func fmtValue(v value) string {
if v.variable.options.secret {
return "[secret]"
}
return v.variable.Value.formattedValue()
}
func (v *value) setVar(variable Var) {
v.variable = variable
}
func (v value) Var() Var {
return v.variable
}
// Get the value that will be used.
func (v value) get() string {
defValue := v.variable.options.defaultValue
if v.missingPrerequs() != "" {
return defValue
}
val, ok := v.Underlying()
if ok {
return val
}
return defValue
}
func (v value) Underlying() (string, bool) {
s := v.store
if s == nil {
s = Global
}
found, ok := s.Raw(v.Var().Name())
if !ok && v.hasDefault() {
found = v.variable.options.defaultValue
}
return found, ok
}
func (v value) missingPrerequs() string {
for _, p := range v.variable.options.prerequs {
if !p.Value() {
return p.Var().Name()
}
}
return ""
}
// A string retrieved from the environment.
type StringValue struct{ *value }
func (StringValue) Type() string { return "string" }
func (s StringValue) formattedValue() string {
return fmt.Sprintf("%#v", s.Value())
}
func (StringValue) validate(string) ValidateError { return ValidateError{} }
// The string value of the variable.
//
// If the variable is unset, "" is returned.
func (s StringValue) Value() string { return s.get() }
// A boolean retrieved from the environment.
type BoolValue struct{ *value }
func (BoolValue) Type() string { return "bool" }
func (b BoolValue) formattedValue() string {
return fmt.Sprintf("%#v", b.Value())
}
func (BoolValue) validate(v string) ValidateError {
if cmdutil.IsTruthy(v) || v == "0" || strings.EqualFold(v, "false") {
return ValidateError{}
}
return ValidateError{
Warning: fmt.Errorf("%#v is falsy, but doesn't look like a boolean", v),
}
}
// The boolean value of the variable.
//
// If the variable is unset, false is returned.
func (b BoolValue) Value() bool { return cmdutil.IsTruthy(b.get()) }
// An integer retrieved from the environment.
type IntValue struct{ *value }
func (IntValue) Type() string { return "int" }
func (IntValue) validate(v string) ValidateError {
_, err := strconv.ParseInt(v, 10, 64)
return ValidateError{
Error: err,
}
}
// The integer value of the variable.
//
// If the variable is unset or not parsable, 0 is returned.
func (i IntValue) Value() int {
v := i.get()
if v == "" {
return 0
}
parsed, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return 0
}
return int(parsed)
}
func (i IntValue) formattedValue() string {
return fmt.Sprintf("%#v", i.Value())
}
func setVar(val Value, variable Var) Value {
variable.Value = val
val.setVar(variable)
envVars = append(envVars, variable)
return val
}
// Declare a new environmental value.
//
// `name` is the runtime name of the variable. Unless `NoPrefix` is passed, name is
// pre-appended with `Prefix`. For example, a variable named "FOO" would be set by
// declaring "PULUMI_FOO=val" in the enclosing environment.
//
// `description` is the string description of what the variable does.
func String(name, description string, opts ...Option) StringValue {
var options options
for _, opt := range opts {
opt(&options)
}
val := StringValue{&value{}}
variable := Var{
name: name,
Description: description,
options: options,
}
return setVar(val, variable).(StringValue)
}
// Declare a new environmental value of type bool.
//
// `name` is the runtime name of the variable. Unless `NoPrefix` is passed, name is
// pre-appended with `Prefix`. For example, a variable named "FOO" would be set by
// declaring "PULUMI_FOO=1" in the enclosing environment.
//
// `description` is the string description of what the variable does.
func Bool(name, description string, opts ...Option) BoolValue {
var options options
for _, opt := range opts {
opt(&options)
}
val := BoolValue{&value{}}
variable := Var{
name: name,
Description: description,
options: options,
}
return setVar(val, variable).(BoolValue)
}
// Declare a new environmental value of type integer.
//
// `name` is the runtime name of the variable. Unless `NoPrefix` is passed, name is
// pre-appended with `Prefix`. For example, a variable named "FOO" would be set by
// declaring "PULUMI_FOO=1" in the enclosing environment.
//
// `description` is the string description of what the variable does.
func Int(name, description string, opts ...Option) IntValue {
var options options
for _, opt := range opts {
opt(&options)
}
val := IntValue{&value{}}
variable := Var{
name: name,
Description: description,
options: options,
}
return setVar(val, variable).(IntValue)
}