diff --git a/command/hcl_printer.go b/command/hcl_printer.go deleted file mode 100644 index 677e5c0aa..000000000 --- a/command/hcl_printer.go +++ /dev/null @@ -1,196 +0,0 @@ -package command - -// Marshal an object as an hcl value. -import ( - "bytes" - "fmt" - "regexp" - - "github.com/hashicorp/hcl/hcl/printer" -) - -// This will only work operate on []interface{}, map[string]interface{}, and -// primitive types. -func encodeHCL(i interface{}) ([]byte, error) { - state := &encodeState{} - err := state.encode(i) - if err != nil { - return nil, err - } - - hcl := state.Bytes() - if len(hcl) == 0 { - return hcl, nil - } - - // the HCL parser requires an assignment. Strip it off again later - fakeAssignment := append([]byte("X = "), hcl...) - - // use the real hcl parser to verify our output, and format it canonically - hcl, err = printer.Format(fakeAssignment) - if err != nil { - return nil, err - } - - // now strip that first assignment off - eq := regexp.MustCompile(`=\s+`).FindIndex(hcl) - - // strip of an extra \n if it's there - end := len(hcl) - if hcl[end-1] == '\n' { - end -= 1 - } - - return hcl[eq[1]:end], nil -} - -type encodeState struct { - bytes.Buffer -} - -func (e *encodeState) encode(i interface{}) error { - switch v := i.(type) { - case []interface{}: - return e.encodeList(v) - - case map[string]interface{}: - return e.encodeMap(v) - - case int, int8, int32, int64, uint8, uint32, uint64: - return e.encodeInt(i) - - case float32, float64: - return e.encodeFloat(i) - - case string: - return e.encodeString(v) - - case nil: - return nil - - default: - return fmt.Errorf("invalid type %T", i) - } - -} - -func (e *encodeState) encodeList(l []interface{}) error { - e.WriteString("[") - for i, v := range l { - err := e.encode(v) - if err != nil { - return err - } - if i < len(l)-1 { - e.WriteString(", ") - } - } - e.WriteString("]") - return nil -} - -func (e *encodeState) encodeMap(m map[string]interface{}) error { - e.WriteString("{\n") - for i, k := range sortedKeys(m) { - v := m[k] - - e.WriteString(fmt.Sprintf("%q = ", k)) - err := e.encode(v) - if err != nil { - return err - } - if i < len(m)-1 { - e.WriteString("\n") - } - } - e.WriteString("}") - return nil -} - -func (e *encodeState) encodeInt(i interface{}) error { - _, err := fmt.Fprintf(e, "%d", i) - return err -} - -func (e *encodeState) encodeFloat(f interface{}) error { - _, err := fmt.Fprintf(e, "%g", f) - return err -} - -func (e *encodeState) encodeString(s string) error { - e.Write(quoteHCLString(s)) - return nil -} - -// Quote an HCL string, which may contain interpolations. -// Since the string was already parsed from HCL, we have to assume the -// required characters are sanely escaped. All we need to do is escape double -// quotes in the string, unless they are in an interpolation block. -func quoteHCLString(s string) []byte { - out := make([]byte, 0, len(s)) - out = append(out, '"') - - // our parse states - var ( - outer = 1 // the starting state for the string - dollar = 2 // look for '{' in the next character - interp = 3 // inside an interpolation block - escape = 4 // take the next character and pop back to prev state - ) - - // we could have nested interpolations - state := stack{} - state.push(outer) - - for i := 0; i < len(s); i++ { - switch state.peek() { - case outer: - switch s[i] { - case '"': - out = append(out, '\\') - case '$': - state.push(dollar) - case '\\': - state.push(escape) - } - case dollar: - state.pop() - switch s[i] { - case '{': - state.push(interp) - case '\\': - state.push(escape) - } - case interp: - switch s[i] { - case '}': - state.pop() - } - case escape: - state.pop() - } - - out = append(out, s[i]) - } - - out = append(out, '"') - - return out -} - -type stack []int - -func (s *stack) push(i int) { - *s = append(*s, i) -} - -func (s *stack) pop() int { - last := len(*s) - 1 - i := (*s)[last] - *s = (*s)[:last] - return i -} - -func (s *stack) peek() int { - return (*s)[len(*s)-1] -} diff --git a/command/hcl_printer_test.go b/command/hcl_printer_test.go deleted file mode 100644 index af0abec6c..000000000 --- a/command/hcl_printer_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package command - -import "testing" - -// The command package has it's own HCL encoder to encode variables to push. -// Make sure the variable we encode parse correctly -func TestHCLEncoder_parse(t *testing.T) { - cases := []struct { - Name string - Val interface{} - Error bool - }{ - { - Name: "int", - Val: 12345, - }, - { - Name: "float", - Val: 1.2345, - }, - { - Name: "string", - Val: "terraform", - }, - { - Name: "list", - Val: []interface{}{"a", "b", "c"}, - }, - { - Name: "map", - Val: map[string]interface{}{ - "a": 1, - }, - }, - // a numeric looking identifier requires quotes - { - Name: "map_with_quoted_key", - Val: map[string]interface{}{ - "0.0.0.0/24": "mask", - }, - }, - } - - for _, c := range cases { - t.Run(c.Name, func(t *testing.T) { - _, err := encodeHCL(c.Val) - if err != nil { - t.Fatal(err) - } - }) - } -} diff --git a/command/push.go b/command/push.go index bef89c726..45be43d62 100644 --- a/command/push.go +++ b/command/push.go @@ -1,560 +1,36 @@ package command import ( - "fmt" - "io" - "os" - "path/filepath" - "sort" "strings" - "github.com/hashicorp/atlas-go/archive" - "github.com/hashicorp/atlas-go/v1" - "github.com/hashicorp/terraform/backend" - "github.com/hashicorp/terraform/config" - "github.com/hashicorp/terraform/version" + "github.com/hashicorp/terraform/tfdiags" ) type PushCommand struct { Meta - - // client is the client to use for the actual push operations. - // If this isn't set, then the Atlas client is used. This should - // really only be set for testing reasons (and is hence not exported). - client pushClient } func (c *PushCommand) Run(args []string) int { - var atlasAddress, atlasToken string - var archiveVCS, moduleUpload bool - var name string - var overwrite []string - args, err := c.Meta.process(args, true) - if err != nil { - return 1 - } - cmdFlags := c.Meta.flagSet("push") - cmdFlags.StringVar(&atlasAddress, "atlas-address", "", "") - cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path") - cmdFlags.StringVar(&atlasToken, "token", "", "") - cmdFlags.BoolVar(&moduleUpload, "upload-modules", true, "") - cmdFlags.StringVar(&name, "name", "", "") - cmdFlags.BoolVar(&archiveVCS, "vcs", true, "") - cmdFlags.Var((*FlagStringSlice)(&overwrite), "overwrite", "") - cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } - if err := cmdFlags.Parse(args); err != nil { - return 1 - } - - // Make a map of the set values - overwriteMap := make(map[string]struct{}, len(overwrite)) - for _, v := range overwrite { - overwriteMap[v] = struct{}{} - } - - // This is a map of variables specifically from the CLI that we want to overwrite. - // We need this because there is a chance that the user is trying to modify - // a variable we don't see in our context, but which exists in this Terraform - // Enterprise workspace. - cliVars := make(map[string]string) - for k, v := range c.variables { - if _, ok := overwriteMap[k]; ok { - if val, ok := v.(string); ok { - cliVars[k] = val - } else { - c.Ui.Error(fmt.Sprintf("Error reading value for variable: %s", k)) - return 1 - } - } - } - - // Get the path to the configuration depending on the args. - configPath, err := ModulePath(cmdFlags.Args()) - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - // Check if the path is a plan - plan, err := c.Plan(configPath) - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - if plan != nil { - c.Ui.Error( - "A plan file cannot be given as the path to the configuration.\n" + - "A path to a module (directory with configuration) must be given.") - return 1 - } - - // Load the module - mod, diags := c.Module(configPath) - if diags.HasErrors() { - c.showDiagnostics(diags) - return 1 - } - if mod == nil { - c.Ui.Error(fmt.Sprintf( - "No configuration files found in the directory: %s\n\n"+ - "This command requires configuration to run.", - configPath)) - return 1 - } - - var conf *config.Config - if mod != nil { - conf = mod.Config() - } - - // Load the backend - b, err := c.Backend(&BackendOpts{ - Config: conf, - }) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) - return 1 - } - - // We require a non-local backend - if c.IsLocalBackend(b) { - c.Ui.Error( - "A remote backend is not enabled. For Atlas to run Terraform\n" + - "for you, remote state must be used and configured. Remote \n" + - "state via any backend is accepted, not just Atlas. To configure\n" + - "a backend, please see the documentation at the URL below:\n\n" + - "https://www.terraform.io/docs/state/remote.html") - return 1 - } - - // We require a local backend - local, ok := b.(backend.Local) - if !ok { - c.Ui.Error(ErrUnsupportedLocalOp) - return 1 - } - - // Build the operation - opReq := c.Operation() - opReq.Module = mod - opReq.Plan = plan - - // Get the context - ctx, _, err := local.Context(opReq) - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - defer func() { - err := opReq.StateLocker.Unlock(nil) - if err != nil { - c.Ui.Error(err.Error()) - } - }() - - // Get the configuration - config := ctx.Module().Config() - if name == "" { - if config.Atlas == nil || config.Atlas.Name == "" { - c.Ui.Error( - "The name of this Terraform configuration in Atlas must be\n" + - "specified within your configuration or the command-line. To\n" + - "set it on the command-line, use the `-name` parameter.") - return 1 - } - name = config.Atlas.Name - } - - // Initialize the client if it isn't given. - if c.client == nil { - // Make sure to nil out our client so our token isn't sitting around - defer func() { c.client = nil }() - - // Initialize it to the default client, we set custom settings later - client := atlas.DefaultClient() - if atlasAddress != "" { - client, err = atlas.NewClient(atlasAddress) - if err != nil { - c.Ui.Error(fmt.Sprintf("Error initializing Atlas client: %s", err)) - return 1 - } - } - - client.DefaultHeader.Set(version.Header, version.Version) - - if atlasToken != "" { - client.Token = atlasToken - } - - c.client = &atlasPushClient{Client: client} - } - - // Get the variables we already have in atlas - atlasVars, err := c.client.Get(name) - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error looking up previously pushed configuration: %s", err)) - return 1 - } - - // Set remote variables in the context if we don't have a value here. These - // don't have to be correct, it just prevents the Input walk from prompting - // the user for input. - ctxVars := ctx.Variables() - atlasVarSentry := "ATLAS_78AC153CA649EAA44815DAD6CBD4816D" - for k, _ := range atlasVars { - if _, ok := ctxVars[k]; !ok { - ctx.SetVariable(k, atlasVarSentry) - } - } - - // Ask for input - if err := ctx.Input(c.InputMode()); err != nil { - c.Ui.Error(fmt.Sprintf( - "Error while asking for variable input:\n\n%s", err)) - return 1 - } - - // Now that we've gone through the input walk, we can be sure we have all - // the variables we're going to get. - // We are going to keep these separate from the atlas variables until - // upload, so we can notify the user which local variables we're sending. - serializedVars, err := tfVars(ctx.Variables()) - if err != nil { - c.Ui.Error(fmt.Sprintf( - "An error has occurred while serializing the variables for uploading:\n"+ - "%s", err)) - return 1 - } - - // Get the absolute path for our data directory, since the Extra field - // value below needs to be absolute. - dataDirAbs, err := filepath.Abs(c.DataDir()) - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error while expanding the data directory %q: %s", c.DataDir(), err)) - return 1 - } - - // Build the archiving options, which includes everything it can - // by default according to VCS rules but forcing the data directory. - archiveOpts := &archive.ArchiveOpts{ - VCS: archiveVCS, - Extra: map[string]string{ - DefaultDataDir: archive.ExtraEntryDir, - }, - } - - // Always store the state file in here so we can find state - statePathKey := fmt.Sprintf("%s/%s", DefaultDataDir, DefaultStateFilename) - archiveOpts.Extra[statePathKey] = filepath.Join(dataDirAbs, DefaultStateFilename) - if moduleUpload { - // If we're uploading modules, explicitly add that directory if exists. - moduleKey := fmt.Sprintf("%s/%s", DefaultDataDir, "modules") - moduleDir := filepath.Join(dataDirAbs, "modules") - _, err := os.Stat(moduleDir) - if err == nil { - archiveOpts.Extra[moduleKey] = filepath.Join(dataDirAbs, "modules") - } - if err != nil && !os.IsNotExist(err) { - c.Ui.Error(fmt.Sprintf( - "Error checking for module dir %q: %s", moduleDir, err)) - return 1 - } - } else { - // If we're not uploading modules, explicitly exclude add that - archiveOpts.Exclude = append( - archiveOpts.Exclude, - filepath.Join(c.DataDir(), "modules")) - } - - archiveR, err := archive.CreateArchive(configPath, archiveOpts) - if err != nil { - c.Ui.Error(fmt.Sprintf( - "An error has occurred while archiving the module for uploading:\n"+ - "%s", err)) - return 1 - } - - // List of the vars we're uploading to display to the user. - // We always upload all vars from atlas, but only report them if they are overwritten. - var setVars []string - - // variables to upload - var uploadVars []atlas.TFVar - - // first add all the variables we want to send which have been serialized - // from the local context. - for _, sv := range serializedVars { - _, inOverwrite := overwriteMap[sv.Key] - _, inAtlas := atlasVars[sv.Key] - - // We have a variable that's not in atlas, so always send it. - if !inAtlas { - uploadVars = append(uploadVars, sv) - setVars = append(setVars, sv.Key) - } - - // We're overwriting an atlas variable. - // We also want to check that we - // don't send the dummy sentry value back to atlas. This could happen - // if it's specified as an overwrite on the cli, but we didn't set a - // new value. - if inAtlas && inOverwrite && sv.Value != atlasVarSentry { - uploadVars = append(uploadVars, sv) - setVars = append(setVars, sv.Key) - - // remove this value from the atlas vars, because we're going to - // send back the remainder regardless. - delete(atlasVars, sv.Key) - } - } - - // now send back all the existing atlas vars, inserting any overwrites from the cli. - for k, av := range atlasVars { - if v, ok := cliVars[k]; ok { - av.Value = v - setVars = append(setVars, k) - } - uploadVars = append(uploadVars, av) - } - - sort.Strings(setVars) - if len(setVars) > 0 { - c.Ui.Output( - "The following variables will be set or overwritten within Atlas from\n" + - "their local values. All other variables are already set within Atlas.\n" + - "If you want to modify the value of a variable, use the Atlas web\n" + - "interface or set it locally and use the -overwrite flag.\n\n") - for _, v := range setVars { - c.Ui.Output(fmt.Sprintf(" * %s", v)) - } - - // Newline - c.Ui.Output("") - } - - // Upsert! - opts := &pushUpsertOptions{ - Name: name, - Archive: archiveR, - Variables: ctx.Variables(), - TFVars: uploadVars, - } - - c.Ui.Output("Uploading Terraform configuration...") - vsn, err := c.client.Upsert(opts) - if err != nil { - c.Ui.Error(fmt.Sprintf( - "An error occurred while uploading the module:\n\n%s", err)) - return 1 - } - - c.Ui.Output(c.Colorize().Color(fmt.Sprintf( - "[reset][bold][green]Configuration %q uploaded! (v%d)", - name, vsn))) - - c.showDiagnostics(diags) - if diags.HasErrors() { - return 1 - } - - return 0 + // This command is no longer supported, but we'll retain it just to + // give the user some next-steps after upgrading. + c.showDiagnostics(tfdiags.Sourceless( + tfdiags.Error, + "Command \"terraform push\" is no longer supported", + "This command was used to push configuration to Terraform Enterprise legacy (v1), which has now reached end-of-life. To push configuration to Terraform Enterprise v2, use its REST API. Contact Terraform Enterprise support for more information.", + )) + return 1 } func (c *PushCommand) Help() string { helpText := ` Usage: terraform push [options] [DIR] - Upload this Terraform module to an Atlas server for remote - infrastructure management. - -Options: - - -atlas-address= An alternate address to an Atlas instance. Defaults - to https://atlas.hashicorp.com - - -upload-modules=true If true (default), then the modules are locked at - their current checkout and uploaded completely. This - prevents Atlas from running "terraform get". - - -name= Name of the configuration in Atlas. This can also - be set in the configuration itself. Format is - typically: "username/name". - - -token= Access token to use to upload. If blank or unspecified, - the ATLAS_TOKEN environmental variable will be used. - - -overwrite=foo Variable keys that should overwrite values in Atlas. - Otherwise, variables already set in Atlas will overwrite - local values. This flag can be repeated. - - -var 'foo=bar' Set a variable in the Terraform configuration. This - flag can be set multiple times. - - -var-file=foo Set variables in the Terraform configuration from - a file. If "terraform.tfvars" or any ".auto.tfvars" - files are present, they will be automatically loaded. - - -vcs=true If true (default), push will upload only files - committed to your VCS, if detected. - - -no-color If specified, output won't contain any color. - + This command was for the legacy version of Terraform Enterprise (v1), which + has now reached end-of-life. Therefore this command is no longer supported. ` return strings.TrimSpace(helpText) } -func sortedKeys(m map[string]interface{}) []string { - var keys []string - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - return keys -} - -// build the set of TFVars for push -func tfVars(vars map[string]interface{}) ([]atlas.TFVar, error) { - var tfVars []atlas.TFVar - var err error - -RANGE: - for _, k := range sortedKeys(vars) { - v := vars[k] - - var hcl []byte - tfv := atlas.TFVar{Key: k} - - switch v := v.(type) { - case string: - tfv.Value = v - - default: - // everything that's not a string is now HCL encoded - hcl, err = encodeHCL(v) - if err != nil { - break RANGE - } - - tfv.Value = string(hcl) - tfv.IsHCL = true - } - - tfVars = append(tfVars, tfv) - } - - return tfVars, err -} - func (c *PushCommand) Synopsis() string { - return "Upload this Terraform module to Atlas to run" -} - -// pushClient is implemented internally to control where pushes go. This is -// either to Atlas or a mock for testing. We still return a map to make it -// easier to check for variable existence when filtering the overrides. -type pushClient interface { - Get(string) (map[string]atlas.TFVar, error) - Upsert(*pushUpsertOptions) (int, error) -} - -type pushUpsertOptions struct { - Name string - Archive *archive.Archive - Variables map[string]interface{} - TFVars []atlas.TFVar -} - -type atlasPushClient struct { - Client *atlas.Client -} - -func (c *atlasPushClient) Get(name string) (map[string]atlas.TFVar, error) { - user, name, err := atlas.ParseSlug(name) - if err != nil { - return nil, err - } - - version, err := c.Client.TerraformConfigLatest(user, name) - if err != nil { - return nil, err - } - - variables := make(map[string]atlas.TFVar) - - if version == nil { - return variables, nil - } - - // Variables is superseded by TFVars - if version.TFVars == nil { - for k, v := range version.Variables { - variables[k] = atlas.TFVar{Key: k, Value: v} - } - } else { - for _, v := range version.TFVars { - variables[v.Key] = v - } - } - - return variables, nil -} - -func (c *atlasPushClient) Upsert(opts *pushUpsertOptions) (int, error) { - user, name, err := atlas.ParseSlug(opts.Name) - if err != nil { - return 0, err - } - - data := &atlas.TerraformConfigVersion{ - TFVars: opts.TFVars, - } - - version, err := c.Client.CreateTerraformConfigVersion( - user, name, data, opts.Archive, opts.Archive.Size) - if err != nil { - return 0, err - } - - return version, nil -} - -type mockPushClient struct { - File string - - GetCalled bool - GetName string - GetResult map[string]atlas.TFVar - GetError error - - UpsertCalled bool - UpsertOptions *pushUpsertOptions - UpsertVersion int - UpsertError error -} - -func (c *mockPushClient) Get(name string) (map[string]atlas.TFVar, error) { - c.GetCalled = true - c.GetName = name - return c.GetResult, c.GetError -} - -func (c *mockPushClient) Upsert(opts *pushUpsertOptions) (int, error) { - f, err := os.Create(c.File) - if err != nil { - return 0, err - } - defer f.Close() - - data := opts.Archive - size := opts.Archive.Size - if _, err := io.CopyN(f, data, size); err != nil { - return 0, err - } - - c.UpsertCalled = true - c.UpsertOptions = opts - return c.UpsertVersion, c.UpsertError + return "Obsolete command for Terraform Enterprise legacy (v1)" } diff --git a/command/push_test.go b/command/push_test.go deleted file mode 100644 index e57aa73bd..000000000 --- a/command/push_test.go +++ /dev/null @@ -1,877 +0,0 @@ -package command - -import ( - "archive/tar" - "bytes" - "compress/gzip" - "io" - "os" - "path/filepath" - "reflect" - "sort" - "strings" - "testing" - - atlas "github.com/hashicorp/atlas-go/v1" - "github.com/hashicorp/terraform/helper/copy" - "github.com/hashicorp/terraform/terraform" - "github.com/mitchellh/cli" -) - -func TestPush_good(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - // Create remote state file, this should be pulled - conf, srv := testRemoteState(t, testState(), 200) - defer srv.Close() - - // Persist local remote state - s := terraform.NewState() - s.Serial = 5 - s.Remote = conf - testStateFileRemote(t, s) - - // Path where the archive will be "uploaded" to - archivePath := testTempFile(t) - defer os.Remove(archivePath) - - client := &mockPushClient{File: archivePath} - ui := new(cli.MockUi) - c := &PushCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(testProvider()), - Ui: ui, - }, - - client: client, - } - - args := []string{ - "-vcs=false", - testFixturePath("push"), - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - actual := testArchiveStr(t, archivePath) - expected := []string{ - ".terraform/", - ".terraform/terraform.tfstate", - "main.tf", - } - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) - } - - variables := make(map[string]interface{}) - if !reflect.DeepEqual(client.UpsertOptions.Variables, variables) { - t.Fatalf("bad: %#v", client.UpsertOptions) - } - - if client.UpsertOptions.Name != "foo" { - t.Fatalf("bad: %#v", client.UpsertOptions) - } -} - -func TestPush_goodBackendInit(t *testing.T) { - // Create a temporary working directory that is empty - td := tempDir(t) - copy.CopyDir(testFixturePath("push-backend-new"), td) - defer os.RemoveAll(td) - defer testChdir(t, td)() - - // init backend - ui := new(cli.MockUi) - ci := &InitCommand{ - Meta: Meta{ - Ui: ui, - }, - } - if code := ci.Run(nil); code != 0 { - t.Fatalf("bad: %d\n%s", code, ui.ErrorWriter) - } - - // Path where the archive will be "uploaded" to - archivePath := testTempFile(t) - defer os.Remove(archivePath) - - client := &mockPushClient{File: archivePath} - ui = new(cli.MockUi) - c := &PushCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(testProvider()), - Ui: ui, - }, - - client: client, - } - - args := []string{ - "-vcs=false", - td, - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - actual := testArchiveStr(t, archivePath) - expected := []string{ - // Expected weird behavior, doesn't affect unpackaging - ".terraform/", - ".terraform/", - - // this config contains no plugins - // TODO: we should add one or more plugins to this test config, just to - // verfy the pushed data. The expected additional files are listed below: - // - //".terraform/plugins/", - //fmt.Sprintf(".terraform/plugins/%s_%s/", runtime.GOOS, runtime.GOARCH), - //fmt.Sprintf(".terraform/plugins/%s_%s/lock.json", runtime.GOOS, runtime.GOARCH), - - ".terraform/terraform.tfstate", - ".terraform/terraform.tfstate", - "main.tf", - } - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("expected: %#v\ngot: %#v", expected, actual) - } - - variables := make(map[string]interface{}) - if !reflect.DeepEqual(client.UpsertOptions.Variables, variables) { - t.Fatalf("bad: %#v", client.UpsertOptions) - } - - if client.UpsertOptions.Name != "hello" { - t.Fatalf("bad: %#v", client.UpsertOptions) - } -} - -func TestPush_noUploadModules(t *testing.T) { - // Path where the archive will be "uploaded" to - archivePath := testTempFile(t) - defer os.Remove(archivePath) - - client := &mockPushClient{File: archivePath} - ui := new(cli.MockUi) - c := &PushCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(testProvider()), - Ui: ui, - }, - - client: client, - } - - // Path of the test. We have to do some renaming to avoid our own - // VCS getting in the way. - path := testFixturePath("push-no-upload") - defer os.RemoveAll(filepath.Join(path, ".terraform")) - - // Move into that directory - defer testChdir(t, path)() - - // Do a "terraform get" - { - ui := new(cli.MockUi) - c := &GetCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(testProvider()), - Ui: ui, - }, - } - - if code := c.Run([]string{}); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) - } - } - - // Create remote state file, this should be pulled - conf, srv := testRemoteState(t, testState(), 200) - defer srv.Close() - - // Persist local remote state - s := terraform.NewState() - s.Serial = 5 - s.Remote = conf - defer os.Remove(testStateFileRemote(t, s)) - - args := []string{ - "-vcs=false", - "-name=mitchellh/tf-test", - "-upload-modules=false", - path, - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - // NOTE: The duplicates below are not ideal but are how things work - // currently due to how we manually add the files to the archive. This - // is definitely a "bug" we can fix in the future. - actual := testArchiveStr(t, archivePath) - expected := []string{ - ".terraform/", - ".terraform/", - ".terraform/terraform.tfstate", - ".terraform/terraform.tfstate", - "child/", - "child/main.tf", - "main.tf", - } - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) - } -} - -func TestPush_input(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - // Create remote state file, this should be pulled - conf, srv := testRemoteState(t, testState(), 200) - defer srv.Close() - - // Persist local remote state - s := terraform.NewState() - s.Serial = 5 - s.Remote = conf - testStateFileRemote(t, s) - - // Path where the archive will be "uploaded" to - archivePath := testTempFile(t) - defer os.Remove(archivePath) - - client := &mockPushClient{File: archivePath} - ui := new(cli.MockUi) - c := &PushCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(testProvider()), - Ui: ui, - }, - - client: client, - } - - // Disable test mode so input would be asked and setup the - // input reader/writers. - test = false - defer func() { test = true }() - defaultInputReader = bytes.NewBufferString("foo\n") - defaultInputWriter = new(bytes.Buffer) - - args := []string{ - "-vcs=false", - testFixturePath("push-input"), - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - variables := map[string]interface{}{ - "foo": "foo", - } - - if !reflect.DeepEqual(client.UpsertOptions.Variables, variables) { - t.Fatalf("bad: %#v", client.UpsertOptions.Variables) - } -} - -// We want a variable from atlas to fill a missing variable locally -func TestPush_inputPartial(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - // Create remote state file, this should be pulled - conf, srv := testRemoteState(t, testState(), 200) - defer srv.Close() - - // Persist local remote state - s := terraform.NewState() - s.Serial = 5 - s.Remote = conf - testStateFileRemote(t, s) - - // Path where the archive will be "uploaded" to - archivePath := testTempFile(t) - defer os.Remove(archivePath) - - client := &mockPushClient{ - File: archivePath, - GetResult: map[string]atlas.TFVar{ - "foo": atlas.TFVar{Key: "foo", Value: "bar"}, - }, - } - ui := new(cli.MockUi) - c := &PushCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(testProvider()), - Ui: ui, - }, - - client: client, - } - - // Disable test mode so input would be asked and setup the - // input reader/writers. - test = false - defer func() { test = true }() - defaultInputReader = bytes.NewBufferString("foo\n") - defaultInputWriter = new(bytes.Buffer) - - args := []string{ - "-vcs=false", - testFixturePath("push-input-partial"), - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - expectedTFVars := []atlas.TFVar{ - {Key: "bar", Value: "foo"}, - {Key: "foo", Value: "bar"}, - } - if !reflect.DeepEqual(client.UpsertOptions.TFVars, expectedTFVars) { - t.Logf("expected: %#v", expectedTFVars) - t.Fatalf("got: %#v", client.UpsertOptions.TFVars) - } -} - -// This tests that the push command will override Atlas variables -// if requested. -func TestPush_localOverride(t *testing.T) { - // Disable test mode so input would be asked and setup the - // input reader/writers. - test = false - defer func() { test = true }() - defaultInputReader = bytes.NewBufferString("nope\n") - defaultInputWriter = new(bytes.Buffer) - - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - // Create remote state file, this should be pulled - conf, srv := testRemoteState(t, testState(), 200) - defer srv.Close() - - // Persist local remote state - s := terraform.NewState() - s.Serial = 5 - s.Remote = conf - testStateFileRemote(t, s) - - // Path where the archive will be "uploaded" to - archivePath := testTempFile(t) - defer os.Remove(archivePath) - - client := &mockPushClient{File: archivePath} - // Provided vars should override existing ones - client.GetResult = map[string]atlas.TFVar{ - "foo": atlas.TFVar{ - Key: "foo", - Value: "old", - }, - } - ui := new(cli.MockUi) - c := &PushCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(testProvider()), - Ui: ui, - }, - - client: client, - } - - path := testFixturePath("push-tfvars") - args := []string{ - "-var-file", path + "/terraform.tfvars", - "-vcs=false", - "-overwrite=foo", - path, - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - actual := testArchiveStr(t, archivePath) - expected := []string{ - ".terraform/", - ".terraform/terraform.tfstate", - "main.tf", - "terraform.tfvars", - } - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) - } - - if client.UpsertOptions.Name != "foo" { - t.Fatalf("bad: %#v", client.UpsertOptions) - } - - expectedTFVars := pushTFVars() - - if !reflect.DeepEqual(client.UpsertOptions.TFVars, expectedTFVars) { - t.Logf("expected: %#v", expectedTFVars) - t.Fatalf("got: %#v", client.UpsertOptions.TFVars) - } -} - -// This tests that the push command will override Atlas variables -// even if we don't have it defined locally -func TestPush_remoteOverride(t *testing.T) { - // Disable test mode so input would be asked and setup the - // input reader/writers. - test = false - defer func() { test = true }() - defaultInputReader = bytes.NewBufferString("nope\n") - defaultInputWriter = new(bytes.Buffer) - - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - // Create remote state file, this should be pulled - conf, srv := testRemoteState(t, testState(), 200) - defer srv.Close() - - // Persist local remote state - s := terraform.NewState() - s.Serial = 5 - s.Remote = conf - testStateFileRemote(t, s) - - // Path where the archive will be "uploaded" to - archivePath := testTempFile(t) - defer os.Remove(archivePath) - - client := &mockPushClient{File: archivePath} - // Provided vars should override existing ones - client.GetResult = map[string]atlas.TFVar{ - "remote": atlas.TFVar{ - Key: "remote", - Value: "old", - }, - } - ui := new(cli.MockUi) - c := &PushCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(testProvider()), - Ui: ui, - }, - - client: client, - } - - path := testFixturePath("push-tfvars") - args := []string{ - "-var-file", path + "/terraform.tfvars", - "-vcs=false", - "-overwrite=remote", - "-var", - "remote=new", - path, - } - - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - actual := testArchiveStr(t, archivePath) - expected := []string{ - ".terraform/", - ".terraform/terraform.tfstate", - "main.tf", - "terraform.tfvars", - } - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) - } - - if client.UpsertOptions.Name != "foo" { - t.Fatalf("bad: %#v", client.UpsertOptions) - } - - found := false - // find the "remote" var and make sure we're going to set it - for _, tfVar := range client.UpsertOptions.TFVars { - if tfVar.Key == "remote" { - found = true - if tfVar.Value != "new" { - t.Log("'remote' variable should be set to 'new'") - t.Fatalf("sending instead: %#v", tfVar) - } - } - } - - if !found { - t.Fatal("'remote' variable not being sent to atlas") - } -} - -// This tests that the push command prefers Atlas variables over -// local ones. -func TestPush_preferAtlas(t *testing.T) { - // Disable test mode so input would be asked and setup the - // input reader/writers. - test = false - defer func() { test = true }() - defaultInputReader = bytes.NewBufferString("nope\n") - defaultInputWriter = new(bytes.Buffer) - - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - // Create remote state file, this should be pulled - conf, srv := testRemoteState(t, testState(), 200) - defer srv.Close() - - // Persist local remote state - s := terraform.NewState() - s.Serial = 5 - s.Remote = conf - testStateFileRemote(t, s) - - // Path where the archive will be "uploaded" to - archivePath := testTempFile(t) - defer os.Remove(archivePath) - - client := &mockPushClient{File: archivePath} - // Provided vars should override existing ones - client.GetResult = map[string]atlas.TFVar{ - "foo": atlas.TFVar{ - Key: "foo", - Value: "old", - }, - } - ui := new(cli.MockUi) - c := &PushCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(testProvider()), - Ui: ui, - }, - - client: client, - } - - path := testFixturePath("push-tfvars") - args := []string{ - "-var-file", path + "/terraform.tfvars", - "-vcs=false", - path, - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - actual := testArchiveStr(t, archivePath) - expected := []string{ - ".terraform/", - ".terraform/terraform.tfstate", - "main.tf", - "terraform.tfvars", - } - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) - } - - if client.UpsertOptions.Name != "foo" { - t.Fatalf("bad: %#v", client.UpsertOptions) - } - - // change the expected response to match our change - expectedTFVars := pushTFVars() - for i, v := range expectedTFVars { - if v.Key == "foo" { - expectedTFVars[i] = atlas.TFVar{Key: "foo", Value: "old"} - } - } - - if !reflect.DeepEqual(expectedTFVars, client.UpsertOptions.TFVars) { - t.Logf("expected: %#v", expectedTFVars) - t.Fatalf("got: %#v", client.UpsertOptions.TFVars) - } -} - -// This tests that the push command will send the variables in tfvars -func TestPush_tfvars(t *testing.T) { - // Disable test mode so input would be asked and setup the - // input reader/writers. - test = false - defer func() { test = true }() - defaultInputReader = bytes.NewBufferString("nope\n") - defaultInputWriter = new(bytes.Buffer) - - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - // Create remote state file, this should be pulled - conf, srv := testRemoteState(t, testState(), 200) - defer srv.Close() - - // Persist local remote state - s := terraform.NewState() - s.Serial = 5 - s.Remote = conf - testStateFileRemote(t, s) - - // Path where the archive will be "uploaded" to - archivePath := testTempFile(t) - defer os.Remove(archivePath) - - client := &mockPushClient{File: archivePath} - ui := new(cli.MockUi) - c := &PushCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(testProvider()), - Ui: ui, - }, - - client: client, - } - - path := testFixturePath("push-tfvars") - args := []string{ - "-var-file", path + "/terraform.tfvars", - "-vcs=false", - "-var", - "bar=[1,2]", - path, - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - actual := testArchiveStr(t, archivePath) - expected := []string{ - ".terraform/", - ".terraform/terraform.tfstate", - "main.tf", - "terraform.tfvars", - } - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) - } - - if client.UpsertOptions.Name != "foo" { - t.Fatalf("bad: %#v", client.UpsertOptions) - } - - //now check TFVars - tfvars := pushTFVars() - // update bar to match cli value - for i, v := range tfvars { - if v.Key == "bar" { - tfvars[i].Value = "[1, 2]" - tfvars[i].IsHCL = true - } - } - - for i, expected := range tfvars { - got := client.UpsertOptions.TFVars[i] - if got != expected { - t.Logf("%2d expected: %#v", i, expected) - t.Fatalf(" got: %#v", got) - } - } -} - -func TestPush_name(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - // Create remote state file, this should be pulled - conf, srv := testRemoteState(t, testState(), 200) - defer srv.Close() - - // Persist local remote state - s := terraform.NewState() - s.Serial = 5 - s.Remote = conf - testStateFileRemote(t, s) - - // Path where the archive will be "uploaded" to - archivePath := testTempFile(t) - defer os.Remove(archivePath) - - client := &mockPushClient{File: archivePath} - ui := new(cli.MockUi) - c := &PushCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(testProvider()), - Ui: ui, - }, - - client: client, - } - - args := []string{ - "-name", "bar", - "-vcs=false", - testFixturePath("push"), - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - if client.UpsertOptions.Name != "bar" { - t.Fatalf("bad: %#v", client.UpsertOptions) - } -} - -func TestPush_noState(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - ui := new(cli.MockUi) - c := &PushCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(testProvider()), - Ui: ui, - }, - } - - args := []string{} - if code := c.Run(args); code != 1 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) - } -} - -func TestPush_noRemoteState(t *testing.T) { - // Create a temporary working directory that is empty - td := tempDir(t) - copy.CopyDir(testFixturePath("push-no-remote"), td) - defer os.RemoveAll(td) - defer testChdir(t, td)() - - state := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - }, - }, - }, - }, - }, - } - statePath := testStateFile(t, state) - - // Path where the archive will be "uploaded" to - archivePath := testTempFile(t) - defer os.Remove(archivePath) - - client := &mockPushClient{File: archivePath} - ui := new(cli.MockUi) - c := &PushCommand{ - Meta: Meta{ - Ui: ui, - }, - client: client, - } - - args := []string{ - "-vcs=false", - "-state", statePath, - td, - } - if code := c.Run(args); code != 1 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - errStr := ui.ErrorWriter.String() - if !strings.Contains(errStr, "remote backend") { - t.Fatalf("bad: %s", errStr) - } -} - -func TestPush_plan(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - // Create remote state file, this should be pulled - conf, srv := testRemoteState(t, testState(), 200) - defer srv.Close() - - // Persist local remote state - s := terraform.NewState() - s.Serial = 5 - s.Remote = conf - testStateFileRemote(t, s) - - // Create a plan - planPath := testPlanFile(t, &terraform.Plan{ - Module: testModule(t, "apply"), - }) - - ui := new(cli.MockUi) - c := &PushCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(testProvider()), - Ui: ui, - }, - } - - args := []string{planPath} - if code := c.Run(args); code != 1 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } -} - -func testArchiveStr(t *testing.T, path string) []string { - f, err := os.Open(path) - if err != nil { - t.Fatalf("err: %s", err) - } - defer f.Close() - - // Ungzip - gzipR, err := gzip.NewReader(f) - if err != nil { - t.Fatalf("err: %s", err) - } - - // Accumulator - result := make([]string, 0, 10) - - // Untar - tarR := tar.NewReader(gzipR) - for { - header, err := tarR.Next() - if err == io.EOF { - break - } - if err != nil { - t.Fatalf("err: %s", err) - } - - result = append(result, header.Name) - } - - sort.Strings(result) - return result -} - -// we always quote map keys to be safe -func pushTFVars() []atlas.TFVar { - return []atlas.TFVar{ - {Key: "bar", Value: "foo", IsHCL: false}, - {Key: "baz", Value: `{ - "A" = "a" -}`, IsHCL: true}, - {Key: "fob", Value: `["a", "quotes \"in\" quotes"]`, IsHCL: true}, - {Key: "foo", Value: "bar", IsHCL: false}, - } -} - -// the structure returned from the push-tfvars test fixture -func pushTFVarsMap() map[string]atlas.TFVar { - vars := make(map[string]atlas.TFVar) - for _, v := range pushTFVars() { - vars[v.Key] = v - } - return vars -} diff --git a/commands.go b/commands.go index 113c771eb..584763387 100644 --- a/commands.go +++ b/commands.go @@ -73,6 +73,7 @@ func initCommands(config *Config, services *disco.Disco) { "state": struct{}{}, // includes all subcommands "debug": struct{}{}, // includes all subcommands "force-unlock": struct{}{}, + "push": struct{}{}, } Commands = map[string]cli.CommandFactory{