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

424 lines
9.9 KiB
Go

// Copyright 2016-2023, 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"
)
// 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 alternative name of an environmental variable.
func (v Var) Alternative() string {
if v.options.alternative == "" {
return ""
}
return v.options.name(v.options.alternative)
}
// 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
alternative string
}
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
}
// Alternative indicates that the variable has an alternative name. This is generally used for backwards compatibility.
func Alternative(name string) Option {
return func(opts *options) {
opts.alternative = name
}
}
// 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
Validate() 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.
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) withStore(store Store) *value {
v.store = store // This is non-mutating since `v` is taken by value.
return &v
}
func (v value) String() string {
_, present := v.Underlying()
if !present {
return "unset"
}
if m := v.missingPrerequs(); m != "" {
return fmt.Sprintf("need %s (%s)", m, v.Var().Value.formattedValue())
}
return v.Var().Value.formattedValue()
}
func (v *value) setVar(variable Var) {
v.variable = variable
}
func (v value) Var() Var {
return v.variable
}
func (v value) Underlying() (string, bool) {
s := v.store
if s == nil {
s = Global
}
raw, has := s.Raw(v.Var().Name())
if has {
return raw, true
}
alt := v.Var().Alternative()
if alt != "" {
return s.Raw(alt)
}
return "", false
}
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 {
if s.variable.options.secret {
return "[secret]"
}
return fmt.Sprintf("%#v", s.Value())
}
func (StringValue) Validate() ValidateError { return ValidateError{} }
// The string value of the variable.
//
// If the variable is unset, "" is returned.
func (s StringValue) Value() string {
if s.missingPrerequs() != "" {
return ""
}
v, ok := s.Underlying()
if !ok {
return ""
}
return v
}
// 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 (b BoolValue) Validate() ValidateError {
v, ok := b.Underlying()
if !ok || b.Value() || 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 {
if b.missingPrerequs() != "" {
return false
}
v, ok := b.Underlying()
if !ok {
return false
}
return v == "1" || strings.EqualFold(v, "true")
}
// An integer retrieved from the environment.
type IntValue struct{ *value }
func (IntValue) Type() string { return "int" }
func (i IntValue) Validate() ValidateError {
v, ok := i.Underlying()
if !ok {
return 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 {
if i.missingPrerequs() != "" {
return 0
}
v, ok := i.Underlying()
if !ok {
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)
}