command: remove "terraform push"
The remote API this talks to will be going away very soon, before our next major release, and so we'll remove the command altogether in that release. This also removes the "encodeHCL" function, which was used only for adding a .tfvars-formatted file to the uploaded archive.
This commit is contained in:
parent
c51d22bdeb
commit
618883596a
|
@ -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]
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
548
command/push.go
548
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=<url> 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> Name of the configuration in Atlas. This can also
|
||||
be set in the configuration itself. Format is
|
||||
typically: "username/name".
|
||||
|
||||
-token=<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)"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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{
|
||||
|
|
Loading…
Reference in New Issue