From 5f433da4a0f73ae27d60d4b01c3866fb094a19e0 Mon Sep 17 00:00:00 2001 From: lszucs <44788749+lszucs@users.noreply.github.com> Date: Fri, 8 Feb 2019 16:53:00 +0100 Subject: [PATCH] implement 'unset' subcommand (#162) * implement 'unset' subcommand * code review fix: 'unset' option ignored when reading from envstore file * code review fix: use explicitly declared default value --- _tests/integration/unset_test.go | 149 +++++++++++++++++++++++++++++++ cli/commands.go | 9 ++ cli/run.go | 8 ++ cli/run_test.go | 31 +++++++ cli/unset.go | 36 ++++++++ envman/util.go | 6 ++ models/models.go | 1 + models/models_methods.go | 12 +++ 8 files changed, 252 insertions(+) create mode 100644 _tests/integration/unset_test.go create mode 100644 cli/unset.go diff --git a/_tests/integration/unset_test.go b/_tests/integration/unset_test.go new file mode 100644 index 00000000..bb528988 --- /dev/null +++ b/_tests/integration/unset_test.go @@ -0,0 +1,149 @@ +package integration + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/bitrise-io/go-utils/command" + "github.com/bitrise-io/go-utils/pathutil" + "github.com/stretchr/testify/require" +) + +func unsetCommand(key, envstore string) *command.Model { + return command.New(binPath(), "-p", envstore, "unset", "--key", key) +} + +func runCommand(cmd, envstore string) *command.Model { + return command.New(binPath(), "-p", envstore, "run", cmd) +} + +func TestUnset(t *testing.T) { + t.Log("only unset on an empty envstore") + { + // create a fully empty envstore + tmpDir, err := pathutil.NormalizedOSTempDirPath("__envman__") + require.NoError(t, err) + + envstore := filepath.Join(tmpDir, ".envstore") + f, err := os.Create(envstore) + require.NoError(t, err) + require.NoError(t, f.Close()) + + randomEnvKEY := "DONOTEXPORT" + + // unset DONOTEXPORT env + out, err := unsetCommand(randomEnvKEY, envstore).RunAndReturnTrimmedCombinedOutput() + require.NoError(t, err, out) + + // run env command through envman and see the exported env's list + out, err = runCommand("env", envstore).RunAndReturnTrimmedCombinedOutput() + require.NoError(t, err, out) + + // check if the env is surely not exported + if strings.Contains(out, randomEnvKEY) { + t.Errorf("env is exported however it should be unset, complete list of exported envs:\n%s\n", out) + } + } + + t.Log("add env then unset it in an empty envstore") + { + // create a fully empty envstore + tmpDir, err := pathutil.NormalizedOSTempDirPath("__envman__") + require.NoError(t, err) + + envstore := filepath.Join(tmpDir, ".envstore") + f, err := os.Create(envstore) + require.NoError(t, err) + require.NoError(t, f.Close()) + + randomEnvKEY := "DONOTEXPORT" + + // add DONOTEXPORT env + out, err := addCommand(randomEnvKEY, "sample value", envstore).RunAndReturnTrimmedCombinedOutput() + require.NoError(t, err, out) + + // unset DONOTEXPORT env + out, err = unsetCommand(randomEnvKEY, envstore).RunAndReturnTrimmedCombinedOutput() + require.NoError(t, err, out) + + // run env command through envman and see the exported env's list + out, err = runCommand("env", envstore).RunAndReturnTrimmedCombinedOutput() + require.NoError(t, err, out) + + // check if the env is surely not exported + if strings.Contains(out, randomEnvKEY) { + t.Errorf("env is exported however it should be unset, complete list of exported envs:\n%s\n", out) + } + } + + t.Log("set env externally then only unset on an empty envstore") + { + // create a fully empty envstore + tmpDir, err := pathutil.NormalizedOSTempDirPath("__envman__") + require.NoError(t, err) + + envstore := filepath.Join(tmpDir, ".envstore") + f, err := os.Create(envstore) + require.NoError(t, err) + require.NoError(t, f.Close()) + + randomEnvKEY := "DONOTEXPORT" + + require.NoError(t, os.Setenv(randomEnvKEY, "value")) + + // unset DONOTEXPORT env + out, err := unsetCommand(randomEnvKEY, envstore).RunAndReturnTrimmedCombinedOutput() + require.NoError(t, err, out) + + // run env command through envman and see the exported env's list + out, err = runCommand("env", envstore).RunAndReturnTrimmedCombinedOutput() + require.NoError(t, err, out) + + // check if the env is surely not exported + if strings.Contains(out, randomEnvKEY) { + t.Errorf("env is exported however it should be unset, complete list of exported envs:\n%s\n", out) + } + } + + t.Log("set env externally then add env then unset it in an empty envstore") + { + // create a fully empty envstore + tmpDir, err := pathutil.NormalizedOSTempDirPath("__envman__") + require.NoError(t, err) + + envstore := filepath.Join(tmpDir, ".envstore") + f, err := os.Create(envstore) + require.NoError(t, err) + require.NoError(t, f.Close()) + + controlEnvKey := "EXPORT_THIS" + randomEnvKEY := "DONOTEXPORT" + + require.NoError(t, os.Setenv(controlEnvKey, "value")) + require.NoError(t, os.Setenv(randomEnvKEY, "value")) + + // add DONOTEXPORT env + out, err := addCommand(randomEnvKEY, "sample value", envstore).RunAndReturnTrimmedCombinedOutput() + require.NoError(t, err, out) + + // unset DONOTEXPORT env + out, err = unsetCommand(randomEnvKEY, envstore).RunAndReturnTrimmedCombinedOutput() + require.NoError(t, err, out) + + // run env command through envman and see the exported env's list + out, err = runCommand("env", envstore).RunAndReturnTrimmedCombinedOutput() + require.NoError(t, err, out) + + // check if the env is surely not exported + if !strings.Contains(out, controlEnvKey) { + t.Errorf("env %s is not exported, complete list of exported envs:\n%s\n", controlEnvKey, out) + } + + // check if the env is surely not exported + if strings.Contains(out, randomEnvKEY) { + t.Errorf("env is exported however it should be unset, complete list of exported envs:\n%s\n", out) + } + } +} \ No newline at end of file diff --git a/cli/commands.go b/cli/commands.go index f122daaf..314c2ba1 100644 --- a/cli/commands.go +++ b/cli/commands.go @@ -68,5 +68,14 @@ var ( SkipFlagParsing: true, Action: run, }, + { + Name: "unset", + Aliases: []string{"rm"}, + Usage: "Enlist an environment variable to be unset (for example to clear OS inherited vars for the process).", + Action: unset, + Flags: []cli.Flag{ + flKey, + }, + }, } ) diff --git a/cli/run.go b/cli/run.go index fb137240..6e47936b 100644 --- a/cli/run.go +++ b/cli/run.go @@ -1,6 +1,7 @@ package cli import ( + "fmt" "os" log "github.com/Sirupsen/logrus" @@ -33,6 +34,13 @@ func commandEnvs(envs []models.EnvironmentItemModel) ([]string, error) { return []string{}, err } + if opts.Unset != nil && *opts.Unset { + if err := os.Unsetenv(key); err != nil { + return []string{}, fmt.Errorf("unset env (%s): %s", key, err) + } + continue + } + if *opts.SkipIfEmpty && value == "" { continue } diff --git a/cli/run_test.go b/cli/run_test.go index 48646dc4..6c2dbbdf 100644 --- a/cli/run_test.go +++ b/cli/run_test.go @@ -1,6 +1,7 @@ package cli import ( + "fmt" "os" "strings" "testing" @@ -234,4 +235,34 @@ func TestCommandEnvs(t *testing.T) { } require.Equal(t, true, env3Found) } + + t.Log("unset OS envs test") + { + // given + key := "TEST_ENV" + val := "test" + if err := os.Setenv(key, val); err != nil { + require.Equal(t, nil, err, "test setup: error seting env (%s=%s)", key, val) + } + env := models.EnvironmentItemModel{ + key: val, + models.OptionsKey: models.EnvironmentItemOptionsModel{ + Unset: pointers.NewBoolPtr(true), + }, + } + require.Equal(t, nil, env.FillMissingDefaults()) + testEnvs := []models.EnvironmentItemModel{ + env, + } + + // when + envs, err := commandEnvs(testEnvs) + envFmt := "%s=%s" // note: if this format mismatches elements of `envs`, test can be a false positive! + unset := fmt.Sprintf(envFmt, key, val) + + // then + require.Equal(t, nil, err) + require.NotContains(t, envs, unset, "failed to unset env (%s)", key) + + } } diff --git a/cli/unset.go b/cli/unset.go new file mode 100644 index 00000000..ae9ea704 --- /dev/null +++ b/cli/unset.go @@ -0,0 +1,36 @@ +package cli + +import ( + "github.com/bitrise-io/envman/envman" + "github.com/bitrise-io/envman/models" + "github.com/bitrise-io/go-utils/pointers" + "github.com/urfave/cli" +) + +func unset(c *cli.Context) error { + key := c.String(KeyKey) + // Load envs, or create if not exist + environments, err := envman.ReadEnvsOrCreateEmptyList() + if err != nil { + return err + } + + // Add or update envlist + newEnv := models.EnvironmentItemModel{ + key: "", + models.OptionsKey: models.EnvironmentItemOptionsModel{ + Unset: pointers.NewBoolPtr(true), + }, + } + + if err := newEnv.NormalizeValidateFillDefaults(); err != nil { + return err + } + + newEnvSlice, err := envman.UpdateOrAddToEnvlist(environments, newEnv, true) + if err != nil { + return err + } + + return envman.WriteEnvMapToFile(envman.CurrentEnvStoreFilePath, newEnvSlice) +} diff --git a/envman/util.go b/envman/util.go index fd24fa1a..f3e356a3 100644 --- a/envman/util.go +++ b/envman/util.go @@ -119,6 +119,9 @@ func removeDefaults(env *models.EnvironmentItemModel) error { if opts.SkipIfEmpty != nil && *opts.SkipIfEmpty == models.DefaultSkipIfEmpty { opts.SkipIfEmpty = nil } + if opts.Unset != nil && *opts.Unset == models.DefaultUnset { + opts.Unset = nil + } (*env)[models.OptionsKey] = opts return nil @@ -168,6 +171,9 @@ func generateFormattedYMLForEnvModels(envs []models.EnvironmentItemModel) (model if opts.SkipIfEmpty != nil { hasOptions = true } + if opts.Unset != nil { + hasOptions = true + } if !hasOptions { delete(env, models.OptionsKey) diff --git a/models/models.go b/models/models.go index aaec00a7..e3d2d505 100644 --- a/models/models.go +++ b/models/models.go @@ -15,6 +15,7 @@ type EnvironmentItemOptionsModel struct { IsDontChangeValue *bool `json:"is_dont_change_value,omitempty" yaml:"is_dont_change_value,omitempty"` IsTemplate *bool `json:"is_template,omitempty" yaml:"is_template,omitempty"` IsSensitive *bool `json:"is_sensitive,omitempty" yaml:"is_sensitive,omitempty"` + Unset *bool `json:"unset,omitempty" yaml:"unset,omitempty"` // Meta map[string]interface{} `json:"meta,omitempty" yaml:"meta,omitempty"` } diff --git a/models/models_methods.go b/models/models_methods.go index 9f93de66..000d3210 100644 --- a/models/models_methods.go +++ b/models/models_methods.go @@ -29,6 +29,8 @@ const ( DefaultIsDontChangeValue = false // DefaultIsTemplate ... DefaultIsTemplate = false + // DefaultUnset ... + DefaultUnset = false ) // NewEnvJSONList ... @@ -169,6 +171,12 @@ func (envSerModel *EnvironmentItemOptionsModel) ParseFromInterfaceMap(input map[ return fmt.Errorf("failed to parse bool value (%#v) for key (%s)", value, keyStr) } envSerModel.SkipIfEmpty = castedBoolPtr + case "unset": + castedBoolPtr, ok := parseutil.CastToBoolPtr(value) + if !ok { + return fmt.Errorf("failed to parse bool value (%#v) for key (%s)", value, keyStr) + } + envSerModel.Unset = castedBoolPtr case "meta": castedMapStringInterface, ok := parseutil.CastToMapStringInterface(value) if !ok { @@ -294,6 +302,10 @@ func (env *EnvironmentItemModel) FillMissingDefaults() error { if options.Meta == nil { options.Meta = map[string]interface{}{} } + if options.Unset == nil { + options.Unset = pointers.NewBoolPtr(DefaultUnset) + } + (*env)[OptionsKey] = options return nil }