Merge pull request #9633 from hashicorp/f-experiment

helper/experiment: a helper for setting, making experiments
This commit is contained in:
Mitchell Hashimoto 2016-10-27 11:22:07 -04:00 committed by GitHub
commit 69b32223b8
7 changed files with 331 additions and 43 deletions

View File

@ -10,6 +10,7 @@ import (
"github.com/hashicorp/go-getter" "github.com/hashicorp/go-getter"
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/helper/experiment"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
@ -96,7 +97,7 @@ func (c *ApplyCommand) Run(args []string) int {
} }
// Check for the new apply // Check for the new apply
if terraform.X_newApply { if experiment.Enabled(experiment.X_newApply) && !experiment.Force() {
desc := "Experimental new apply graph has been enabled. This may still\n" + desc := "Experimental new apply graph has been enabled. This may still\n" +
"have bugs, and should be used with care. If you'd like to continue,\n" + "have bugs, and should be used with care. If you'd like to continue,\n" +
"you must enter exactly 'yes' as a response." "you must enter exactly 'yes' as a response."
@ -116,7 +117,7 @@ func (c *ApplyCommand) Run(args []string) int {
} }
// Check for the new destroy // Check for the new destroy
if terraform.X_newDestroy { if experiment.Enabled(experiment.X_newDestroy) && !experiment.Force() {
desc := "Experimental new destroy graph has been enabled. This may still\n" + desc := "Experimental new destroy graph has been enabled. This may still\n" +
"have bugs, and should be used with care. If you'd like to continue,\n" + "have bugs, and should be used with care. If you'd like to continue,\n" +
"you must enter exactly 'yes' as a response." "you must enter exactly 'yes' as a response."

View File

@ -11,6 +11,7 @@ import (
"github.com/hashicorp/go-getter" "github.com/hashicorp/go-getter"
"github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/helper/experiment"
"github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
@ -336,8 +337,7 @@ func (m *Meta) flagSet(n string) *flag.FlagSet {
f.BoolVar(&m.shadow, "shadow", true, "shadow graph") f.BoolVar(&m.shadow, "shadow", true, "shadow graph")
// Experimental features // Experimental features
f.BoolVar(&terraform.X_newApply, "Xnew-apply", false, "experiment: new apply") experiment.Flag(f)
f.BoolVar(&terraform.X_newDestroy, "Xnew-destroy", false, "experiment: new destroy")
// Create an io.Writer that writes to our Ui properly for errors. // Create an io.Writer that writes to our Ui properly for errors.
// This is kind of a hack, but it does the job. Basically: create // This is kind of a hack, but it does the job. Basically: create

View File

@ -0,0 +1,162 @@
// experiment package contains helper functions for tracking experimental
// features throughout Terraform.
//
// This package should be used for creating, enabling, querying, and deleting
// experimental features. By unifying all of that onto a single interface,
// we can have the Go compiler help us by enforcing every place we touch
// an experimental feature.
//
// To create a new experiment:
//
// 1. Add the experiment to the global vars list below, prefixed with X_
//
// 2. Add the experiment variable to the All listin the init() function
//
// 3. Use it!
//
// To remove an experiment:
//
// 1. Delete the experiment global var.
//
// 2. Try to compile and fix all the places where the var was referenced.
//
// To use an experiment:
//
// 1. Use Flag() if you want the experiment to be available from the CLI.
//
// 2. Use Enabled() to check whether it is enabled.
//
// As a general user:
//
// 1. The `-Xexperiment-name` flag
// 2. The `TF_X_<experiment-name>` env var.
// 3. The `TF_X_FORCE` env var can be set to force an experimental feature
// without human verifications.
//
package experiment
import (
"flag"
"fmt"
"os"
"strconv"
"strings"
"sync"
)
// The experiments that are available are listed below. Any package in
// Terraform defining an experiment should define the experiments below.
// By keeping them all within the experiment package we force a single point
// of definition and use. This allows the compiler to enforce references
// so it becomes easy to remove the features.
var (
// New apply graph. This will be removed and be the default in 0.8.0.
X_newApply = newBasicID("new-apply", "NEW_APPLY", false)
// New destroy graph. This will be reomved and be the default in 0.8.0.
X_newDestroy = newBasicID("new-destroy", "NEW_DESTROY", false)
// Shadow graph. This is already on by default. Disabling it will be
// allowed for awhile in order for it to not block operations.
X_shadow = newBasicID("shadow", "SHADOW", true)
)
// Global variables this package uses because we are a package
// with global state.
var (
// all is the list of all experiements. Do not modify this.
All []ID
// enabled keeps track of what flags have been enabled
enabled map[string]bool
enabledLock sync.Mutex
// Hidden "experiment" that forces all others to be on without verification
x_force = newBasicID("force", "FORCE", false)
)
func init() {
// The list of all experiments, update this when an experiment is added.
All = []ID{
X_newApply,
X_newDestroy,
X_shadow,
x_force,
}
// Load
reload()
}
// reload is used by tests to reload the global state. This is called by
// init publicly.
func reload() {
// Initialize
enabledLock.Lock()
enabled = make(map[string]bool)
enabledLock.Unlock()
// Set defaults and check env vars
for _, id := range All {
// Get the default value
def := id.Default()
// If we set it in the env var, default it to true
key := fmt.Sprintf("TF_X_%s", strings.ToUpper(id.Env()))
if v := os.Getenv(key); v != "" {
def = v != "0"
}
// Set the default
SetEnabled(id, def)
}
}
// Enabled returns whether an experiment has been enabled or not.
func Enabled(id ID) bool {
enabledLock.Lock()
defer enabledLock.Unlock()
return enabled[id.Flag()]
}
// SetEnabled sets an experiment to enabled/disabled. Please check with
// the experiment docs for when calling this actually affects the experiment.
func SetEnabled(id ID, v bool) {
enabledLock.Lock()
defer enabledLock.Unlock()
enabled[id.Flag()] = v
}
// Force returns true if the -Xforce of TF_X_FORCE flag is present, which
// advises users of this package to not verify with the user that they want
// experimental behavior and to just continue with it.
func Force() bool {
return Enabled(x_force)
}
// Flag configures the given FlagSet with the flags to configure
// all active experiments.
func Flag(fs *flag.FlagSet) {
for _, id := range All {
desc := id.Flag()
key := fmt.Sprintf("X%s", id.Flag())
fs.Var(&idValue{X: id}, key, desc)
}
}
// idValue implements flag.Value for setting the enabled/disabled state
// of an experiment from the CLI.
type idValue struct {
X ID
}
func (v *idValue) IsBoolFlag() bool { return true }
func (v *idValue) String() string { return strconv.FormatBool(Enabled(v.X)) }
func (v *idValue) Set(raw string) error {
b, err := strconv.ParseBool(raw)
if err == nil {
SetEnabled(v.X, b)
}
return err
}

View File

@ -0,0 +1,117 @@
package experiment
import (
"flag"
"fmt"
"os"
"testing"
)
// Test experiments
var (
X_test1 = newBasicID("test1", "TEST1", false)
X_test2 = newBasicID("test2", "TEST2", true)
)
// Reinitializes the package to a clean slate
func testReinit() {
All = []ID{X_test1, X_test2, x_force}
reload()
}
func init() {
testReinit()
// Clear all env vars so they don't affect tests
for _, id := range All {
os.Unsetenv(fmt.Sprintf("TF_X_%s", id.Env()))
}
}
func TestDefault(t *testing.T) {
testReinit()
if Enabled(X_test1) {
t.Fatal("test1 should not be enabled")
}
if !Enabled(X_test2) {
t.Fatal("test2 should be enabled")
}
}
func TestEnv(t *testing.T) {
os.Setenv("TF_X_TEST2", "0")
defer os.Unsetenv("TF_X_TEST2")
testReinit()
if Enabled(X_test2) {
t.Fatal("test2 should be enabled")
}
}
func TestFlag(t *testing.T) {
testReinit()
// Verify default
if !Enabled(X_test2) {
t.Fatal("test2 should be enabled")
}
// Setup a flag set
fs := flag.NewFlagSet("test", flag.ContinueOnError)
Flag(fs)
fs.Parse([]string{"-Xtest2=false"})
if Enabled(X_test2) {
t.Fatal("test2 should not be enabled")
}
}
func TestFlag_overEnv(t *testing.T) {
os.Setenv("TF_X_TEST2", "1")
defer os.Unsetenv("TF_X_TEST2")
testReinit()
// Verify default
if !Enabled(X_test2) {
t.Fatal("test2 should be enabled")
}
// Setup a flag set
fs := flag.NewFlagSet("test", flag.ContinueOnError)
Flag(fs)
fs.Parse([]string{"-Xtest2=false"})
if Enabled(X_test2) {
t.Fatal("test2 should not be enabled")
}
}
func TestForce(t *testing.T) {
os.Setenv("TF_X_FORCE", "1")
defer os.Unsetenv("TF_X_FORCE")
testReinit()
if !Force() {
t.Fatal("should force")
}
}
func TestForce_flag(t *testing.T) {
os.Unsetenv("TF_X_FORCE")
testReinit()
// Setup a flag set
fs := flag.NewFlagSet("test", flag.ContinueOnError)
Flag(fs)
fs.Parse([]string{"-Xforce"})
if !Force() {
t.Fatal("should force")
}
}

34
helper/experiment/id.go Normal file
View File

@ -0,0 +1,34 @@
package experiment
// ID represents an experimental feature.
//
// The global vars defined on this package should be used as ID values.
// This interface is purposely not implement-able outside of this package
// so that we can rely on the Go compiler to enforce all experiment references.
type ID interface {
Env() string
Flag() string
Default() bool
unexported() // So the ID can't be implemented externally.
}
// basicID implements ID.
type basicID struct {
EnvValue string
FlagValue string
DefaultValue bool
}
func newBasicID(flag, env string, def bool) ID {
return &basicID{
EnvValue: env,
FlagValue: flag,
DefaultValue: def,
}
}
func (id *basicID) Env() string { return id.EnvValue }
func (id *basicID) Flag() string { return id.FlagValue }
func (id *basicID) Default() bool { return id.DefaultValue }
func (id *basicID) unexported() {}

View File

@ -11,20 +11,7 @@ import (
"github.com/hashicorp/hcl" "github.com/hashicorp/hcl"
"github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/config/module"
) "github.com/hashicorp/terraform/helper/experiment"
// Variables prefixed with X_ are experimental features. They can be enabled
// by setting them to true. This should be done before any API is called.
// These should be expected to be removed at some point in the future; each
// option should mention a schedule.
var (
// X_newApply will enable the new apply graph. This will be removed
// and be on by default in 0.8.0.
X_newApply = false
// X_newDestroy will enable the new destroy graph. This will be removed
// and be on by default in 0.8.0.
X_newDestroy = false
) )
// InputMode defines what sort of input will be asked for when Input // InputMode defines what sort of input will be asked for when Input
@ -55,11 +42,6 @@ var (
// Plan operation, effectively testing the Diff DeepCopy whenever // Plan operation, effectively testing the Diff DeepCopy whenever
// a Plan occurs. This is enabled for tests. // a Plan occurs. This is enabled for tests.
contextTestDeepCopyOnPlan = false contextTestDeepCopyOnPlan = false
// contextTestShadow will enable the shadow graph for the new graphs.
// This is enabled for tests. This will be removed very shortly and
// be enabled by default.
contextTestShadow = false
) )
// ContextOpts are the user-configurable options to create a context with // ContextOpts are the user-configurable options to create a context with
@ -375,6 +357,8 @@ func (c *Context) Apply() (*State, error) {
// Copy our own state // Copy our own state
c.state = c.state.DeepCopy() c.state = c.state.DeepCopy()
X_newApply := experiment.Enabled(experiment.X_newApply)
X_newDestroy := experiment.Enabled(experiment.X_newDestroy)
newGraphEnabled := (c.destroy && X_newDestroy) || (!c.destroy && X_newApply) newGraphEnabled := (c.destroy && X_newDestroy) || (!c.destroy && X_newApply)
// Build the original graph. This is before the new graph builders // Build the original graph. This is before the new graph builders
@ -432,12 +416,6 @@ func (c *Context) Apply() (*State, error) {
log.Printf("[WARN] terraform: real graph is original, shadow is experiment") log.Printf("[WARN] terraform: real graph is original, shadow is experiment")
} }
// For now, always shadow with the real graph for verification. We don't
// want to shadow yet with the new graphs.
if !contextTestShadow {
shadow = real
}
// Determine the operation // Determine the operation
operation := walkApply operation := walkApply
if c.destroy { if c.destroy {
@ -507,6 +485,9 @@ func (c *Context) Plan() (*Plan, error) {
c.diff.init() c.diff.init()
c.diffLock.Unlock() c.diffLock.Unlock()
// Used throughout below
X_newDestroy := experiment.Enabled(experiment.X_newDestroy)
// Build the graph. We have a branch here since for the pure-destroy // Build the graph. We have a branch here since for the pure-destroy
// plan (c.destroy) we use a much simpler graph builder that simply // plan (c.destroy) we use a much simpler graph builder that simply
// walks the state and reverses edges. // walks the state and reverses edges.
@ -714,6 +695,11 @@ func (c *Context) walk(
// the real work: talking to real providers, modifying real state, etc. // the real work: talking to real providers, modifying real state, etc.
realCtx := c realCtx := c
// If we don't want shadowing, remove it
if !experiment.Enabled(experiment.X_shadow) {
shadow = nil
}
// If we have a shadow graph, walk that as well // If we have a shadow graph, walk that as well
var shadowCtx *Context var shadowCtx *Context
var shadowCloser Shadow var shadowCloser Shadow

View File

@ -15,6 +15,7 @@ import (
"github.com/hashicorp/go-getter" "github.com/hashicorp/go-getter"
"github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/helper/experiment"
"github.com/hashicorp/terraform/helper/logging" "github.com/hashicorp/terraform/helper/logging"
) )
@ -22,19 +23,9 @@ import (
const fixtureDir = "./test-fixtures" const fixtureDir = "./test-fixtures"
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
// Experimental features experiment.Flag(flag.CommandLine)
xNewApply := flag.Bool("Xnew-apply", false, "Experiment: new apply graph")
xNewDestroy := flag.Bool("Xnew-destroy", false, "Experiment: new destroy graph")
// Normal features
shadow := flag.Bool("shadow", true, "Enable shadow graph")
flag.Parse() flag.Parse()
// Setup experimental features
X_newApply = *xNewApply
X_newDestroy = *xNewDestroy
if testing.Verbose() { if testing.Verbose() {
// if we're verbose, use the logging requested by TF_LOG // if we're verbose, use the logging requested by TF_LOG
logging.SetOutput() logging.SetOutput()
@ -49,9 +40,6 @@ func TestMain(m *testing.M) {
// Always DeepCopy the Diff on every Plan during a test // Always DeepCopy the Diff on every Plan during a test
contextTestDeepCopyOnPlan = true contextTestDeepCopyOnPlan = true
// Shadow the new graphs
contextTestShadow = *shadow
os.Exit(m.Run()) os.Exit(m.Run())
} }