Skip to content

Commit

Permalink
Merge #11362
Browse files Browse the repository at this point in the history
11362: Library for environmental variable access r=iwahbe a=iwahbe

Add a library to encapsulate environmental variable declaration, lookup and parsing. 

### Usage
Variables strongly typed and declared in the module scope like so:
```go
// Automatically prefixed with PULUMI_ by default
var Experimental = env.Bool("EXPERIMENTAL", "Enable experimental options and commands")
```

Values are accessed by calling `.Value()` on the module level variable:
```go
if Experimental.Value() { // .Value() returns a bool here
  // do the new exiting thing
} else {
  // do the well tested thing
}
```

Environmental variables can be predicated on other boolean type variables:
```go
var NewThingParam = env.String("NEW_THING", "pass this to the new thing", 
    env.Needs(Expiremental))
```

Predicated variables will only show up as set if there predicate is `true`:

```go
if v := NewThingParam.Value(); v != "" {
  do_thing(v)
} else {
  // do the well tested thing
}
```
The above `if` check is equivalent to 
```go
if v = os.Getenv("PULUMI_NEW_THING"); v != "" && cmduilt.IsTruthy(os.Getenv("PULUMI_EXPERIMENTAL"))
```

This makes marking and unmarking a variable as experimental a 1 line change.

--- 

All declared variables can then be iterated on for documentation with `env.Variables()`. This is how we can use this lib to build documentation for our environmental variables.

### Assumptions

#### Immutability 
The library assumes that environmental variables are inherited from the environment. Values are captured during program startup, changes to the environment are not observed. We are not resilient to environmental variables changing out from under us, so this complies with current usage. This assumption can be changed without breaking the API.

#### Known values
The library assumes that the names of environmental variables are known at compile time. If the variable to be looked up is derived from user input, a different mechanism should be used.

### Adoption

Adopting this system over our pulumi/pkg codebase will take a while. My plan is to merge in this PR without moving over our existing env vars from `os.Getenv`. We can then gradually move over existing env vars piecemeal in subsequent PRs. Any new env vars added will use the new system.

Prerequisite for #10747.

### Other Options - Viper
Viper looks like an excellent config retrieval package, but doesn't support at-site documentation. It also has less type safety then I would prefer as a top level api. If it would be helpful, we could switch the underlying mechanism from `os.Lookup` to `viper.Get*` without changing the API this library exposes. I don't think viper is necessary right now, but adopting this library doesn't preclude viper. 

Co-authored-by: Ian Wahbe <ian@wahbe.com>
  • Loading branch information
bors[bot] and iwahbe committed Dec 15, 2022
2 parents 6bdca3c + 84f5046 commit 40e0ac2
Show file tree
Hide file tree
Showing 10 changed files with 590 additions and 16 deletions.
8 changes: 5 additions & 3 deletions pkg/cmd/pulumi/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
"github.com/pulumi/pulumi/pkg/v3/engine"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
"github.com/pulumi/pulumi/sdk/v3/go/common/env"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
Expand Down Expand Up @@ -66,7 +67,7 @@ func newConvertCmd() *cobra.Command {
return result.FromError(fmt.Errorf("could not resolve current working directory"))
}

return runConvert(cwd, mappings, from, language, outDir, generateOnly)
return runConvert(env.Global(), cwd, mappings, from, language, outDir, generateOnly)
}),
}

Expand Down Expand Up @@ -159,6 +160,7 @@ func pclEject(directory string, loader schema.ReferenceLoader) (*workspace.Proje
}

func runConvert(
e env.Env,
cwd string, mappings []string, from string, language string,
outDir string, generateOnly bool) result.Result {

Expand All @@ -177,7 +179,7 @@ func runConvert(
case "yaml": // nolint: goconst
projectGenerator = yamlgen.GenerateProject
case "pulumi", "pcl":
if cmdutil.IsTruthy(os.Getenv("PULUMI_DEV")) {
if e.GetBool(env.Dev) {
// No plugin for PCL to install dependencies with
generateOnly = true
projectGenerator = pclGenerateProject
Expand Down Expand Up @@ -215,7 +217,7 @@ func runConvert(
return result.FromError(fmt.Errorf("could not load yaml program: %w", err))
}
} else if from == "pcl" {
if cmdutil.IsTruthy(os.Getenv("PULUMI_DEV")) {
if e.GetBool(env.Dev) {
proj, program, err = pclEject(cwd, loader)
if err != nil {
return result.FromError(fmt.Errorf("could not load pcl program: %w", err))
Expand Down
11 changes: 7 additions & 4 deletions pkg/cmd/pulumi/convert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"
"testing"

"github.com/pulumi/pulumi/sdk/v3/go/common/env"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand All @@ -32,18 +33,20 @@ func TestYamlConvert(t *testing.T) {
t.Fatalf("Pulumi.yaml is a directory, not a file")
}

result := runConvert("convert_testdata", []string{}, "yaml", "go", "convert_testdata/go", true)
result := runConvert(env.Global(), "convert_testdata", []string{}, "yaml", "go", "convert_testdata/go", true)
require.Nil(t, result, "convert failed: %v", result)
}

//nolint:paralleltest // sets env var, must be run in isolation
func TestPclConvert(t *testing.T) {
t.Setenv("PULUMI_DEV", "TRUE")
t.Parallel()
env := env.NewEnv(env.MapStore{
env.Dev.Var().Name(): "true",
})

// Check that we can run convert from PCL to PCL
tmp := t.TempDir()

result := runConvert("pcl_convert_testdata", []string{}, "pcl", "pcl", tmp, true)
result := runConvert(env, "pcl_convert_testdata", []string{}, "pcl", "pcl", tmp, true)
assert.Nil(t, result)

// Check that we made one file
Expand Down
75 changes: 75 additions & 0 deletions pkg/cmd/pulumi/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// 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 consistent and documented environmental variable accesses.
//
// Public environmental variables should be declared as a module level variable.

package main

import (
"fmt"

"github.com/spf13/cobra"

"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
"github.com/pulumi/pulumi/sdk/v3/go/common/env"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
declared "github.com/pulumi/pulumi/sdk/v3/go/common/util/env"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/result"
)

func newEnvCmd() *cobra.Command {
return &cobra.Command{
Use: "env",
Short: "An overview of the environmental variables used by pulumi",
Args: cmdutil.NoArgs,
// Since most variables won't be included here, we hide the command. We will
// unhide once most existing variables are using the new env var framework and
// show up here.
Hidden: !env.Experimental.Value(),
Run: cmdutil.RunResultFunc(func(cmd *cobra.Command, args []string) result.Result {
table := cmdutil.Table{
Headers: []string{"Variable", "Description", "Value"},
}
var foundError bool
for _, v := range declared.Variables() {
foundError = foundError || emitEnvVarDiag(v)
table.Rows = append(table.Rows, cmdutil.TableRow{
Columns: []string{v.Name(), v.Description, v.Value.String()},
})
}
cmdutil.PrintTable(table)
if foundError {
return result.Error("Invalid environmental variables found")
}
return nil
}),
}
}

func emitEnvVarDiag(val declared.Var) bool {
err := val.Value.Validate()
if err.Error != nil {
cmdutil.Diag().Errorf(&diag.Diag{
Message: fmt.Sprintf("%s: %v", val.Name(), err.Error),
})
}
if err.Warning != nil {
cmdutil.Diag().Warningf(&diag.Diag{
Message: fmt.Sprintf("%s: %v", val.Name(), err.Warning),
})
}
return err.Error != nil
}
6 changes: 4 additions & 2 deletions pkg/cmd/pulumi/pulumi.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import (
"github.com/pulumi/pulumi/pkg/v3/version"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
"github.com/pulumi/pulumi/sdk/v3/go/common/env"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/httputil"
Expand Down Expand Up @@ -225,7 +226,7 @@ func NewPulumiCmd() *cobra.Command {
}
}

if cmdutil.IsTruthy(os.Getenv("PULUMI_SKIP_UPDATE_CHECK")) {
if env.SkipUpdateCheck.Value() {
logging.V(5).Infof("skipping update check")
} else {
// Run the version check in parallel so that it doesn't block executing the command.
Expand Down Expand Up @@ -355,6 +356,7 @@ func NewPulumiCmd() *cobra.Command {
newConvertCmd(),
newWatchCmd(),
newLogsCmd(),
newEnvCmd(),
},
},
// We have a set of options that are useful for developers of pulumi
Expand Down Expand Up @@ -396,7 +398,7 @@ func checkForUpdate(ctx context.Context) *diag.Diag {
latestVer, oldestAllowedVer, err := getCLIVersionInfo(ctx)
if err != nil {
logging.V(3).Infof("error fetching latest version information "+
"(set `PULUMI_SKIP_UPDATE_CHECK=true` to skip update checks): %s", err)
"(set `%s=true` to skip update checks): %s", env.SkipUpdateCheck.Var().Name(), err)
}

if oldestAllowedVer.GT(curVer) {
Expand Down
8 changes: 5 additions & 3 deletions pkg/codegen/report/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ import (
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax"
hcl2 "github.com/pulumi/pulumi/pkg/v3/codegen/pcl"
"github.com/pulumi/pulumi/pkg/v3/version"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/env"
)

const ExportTargetDir = "PULUMI_CODEGEN_REPORT_DIR"
var ExportTargetDir = env.String("CODEGEN_REPORT_DIR",
"The directory to generate a codegen report in")

type GenerateProgramFn func(*hcl2.Program) (map[string][]byte, hcl.Diagnostics, error)

Expand Down Expand Up @@ -205,7 +207,7 @@ func (r *reporter) Close() error {
func (r *reporter) DefaultExport() error {
r.m.Lock()
defer r.m.Unlock()
dir, ok := os.LookupEnv(ExportTargetDir)
dir, ok := ExportTargetDir.Underlying()
if !ok || r.reported {
return nil
}
Expand All @@ -215,7 +217,7 @@ func (r *reporter) DefaultExport() error {

func (r *reporter) defaultExport(dir string) error {
if dir == "" {
err := fmt.Errorf("%q set to the empty string", ExportTargetDir)
err := fmt.Errorf("%q set to the empty string", ExportTargetDir.Var().Name())
fmt.Fprintln(os.Stderr, err.Error())
return err
}
Expand Down
47 changes: 47 additions & 0 deletions sdk/go/common/env/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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 consistent and documented environmental variable accesses.
//
// Public environmental variables should be declared as a module level variable.

package env

import "github.com/pulumi/pulumi/sdk/v3/go/common/util/env"

// Re-export some types and functions from the env library.

type Env = env.Env

type MapStore = env.MapStore

func NewEnv(s env.Store) env.Env { return env.NewEnv(s) }

// Global is the environment defined by environmental variables.
func Global() env.Env {
return env.NewEnv(env.Global)
}

// That Pulumi is running in experimental mode.
//
// This is our standard gate for an existing feature that's not quite ready to be stable
// and publicly consumed.
var Experimental = env.Bool("EXPERIMENTAL", "Enable experimental options and commands")

var SkipUpdateCheck = env.Bool("SKIP_UPDATE_CHECK", "Disable checking for a new version of pulumi")

var Dev = env.Bool("DEV", "Enable features for hacking on pulumi itself")

var IgnoreAmbientPlugins = env.Bool("IGNORE_AMBIENT_PLUGINS",
"Discover additional plugins by examining the $PATH")
4 changes: 2 additions & 2 deletions sdk/go/common/resource/plugin/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ import (
"github.com/pkg/errors"

"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
"github.com/pulumi/pulumi/sdk/v3/go/common/env"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/logging"
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
Expand Down Expand Up @@ -339,7 +339,7 @@ func (host *defaultHost) Provider(pkg tokens.Package, version *semver.Version) (
}

// Warn if the plugin version was not what we expected
if version != nil && !cmdutil.IsTruthy(os.Getenv("PULUMI_DEV")) {
if version != nil && !env.Dev.Value() {
if info.Version == nil || !info.Version.GTE(*version) {
var v string
if info.Version != nil {
Expand Down

0 comments on commit 40e0ac2

Please sign in to comment.