mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-07 22:53:08 -06:00
de87267697
Add tf_vars to the data structures sent in terraform push. This takes any value of type []interface{} or map[string]interface{} and marshals it as a string representation of the equivalent HCL. This prevents ambiguity in atlas between a string that looks like a json structure, and an actual json structure. For the time being we will need a way to serialize data as HCL, so the command package has an internal encodeHCL function to do so. We can remove this if we get complete package for marshaling HCL.
437 lines
11 KiB
Go
437 lines
11 KiB
Go
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/terraform"
|
|
)
|
|
|
|
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 = c.Meta.process(args, true)
|
|
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{}{}
|
|
}
|
|
|
|
// The pwd is used for the configuration path if one is not given
|
|
pwd, err := os.Getwd()
|
|
if err != nil {
|
|
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
|
|
return 1
|
|
}
|
|
|
|
// Get the path to the configuration depending on the args.
|
|
var configPath string
|
|
args = cmdFlags.Args()
|
|
if len(args) > 1 {
|
|
c.Ui.Error("The apply command expects at most one argument.")
|
|
cmdFlags.Usage()
|
|
return 1
|
|
} else if len(args) == 1 {
|
|
configPath = args[0]
|
|
} else {
|
|
configPath = pwd
|
|
}
|
|
|
|
// Verify the state is remote, we can't push without a remote state
|
|
s, err := c.State()
|
|
if err != nil {
|
|
c.Ui.Error(fmt.Sprintf("Failed to read state: %s", err))
|
|
return 1
|
|
}
|
|
if !s.State().IsRemote() {
|
|
c.Ui.Error(
|
|
"Remote state 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\n" +
|
|
"configure remote state, use the `terraform remote config`\n" +
|
|
"command.")
|
|
return 1
|
|
}
|
|
|
|
// Build the context based on the arguments given
|
|
ctx, planned, err := c.Context(contextOpts{
|
|
Path: configPath,
|
|
StatePath: c.Meta.statePath,
|
|
})
|
|
|
|
if err != nil {
|
|
c.Ui.Error(err.Error())
|
|
return 1
|
|
}
|
|
if planned {
|
|
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
|
|
}
|
|
|
|
// 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(terraform.VersionHeader, terraform.Version)
|
|
|
|
if atlasToken != "" {
|
|
client.Token = atlasToken
|
|
}
|
|
|
|
c.client = &atlasPushClient{Client: client}
|
|
}
|
|
|
|
// Get the variables we might already have
|
|
atlasVars, err := c.client.Get(name)
|
|
if err != nil {
|
|
c.Ui.Error(fmt.Sprintf(
|
|
"Error looking up previously pushed configuration: %s", err))
|
|
return 1
|
|
}
|
|
for k, v := range atlasVars {
|
|
if _, ok := overwriteMap[k]; ok {
|
|
continue
|
|
}
|
|
|
|
ctx.SetVariable(k, v)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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: c.DataDir(),
|
|
},
|
|
}
|
|
if !moduleUpload {
|
|
// If we're not uploading modules, then exclude the modules dir.
|
|
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
|
|
}
|
|
|
|
// Output to the user the variables that will be uploaded
|
|
var setVars []string
|
|
for k, _ := range ctx.Variables() {
|
|
if _, ok := overwriteMap[k]; !ok {
|
|
if _, ok := atlasVars[k]; ok {
|
|
// Atlas variable not within override, so it came from Atlas
|
|
continue
|
|
}
|
|
}
|
|
|
|
// This variable was set from the local value
|
|
setVars = append(setVars, k)
|
|
}
|
|
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("")
|
|
}
|
|
|
|
variables := ctx.Variables()
|
|
serializedVars, err := tfVars(variables)
|
|
if err != nil {
|
|
c.Ui.Error(fmt.Sprintf(
|
|
"An error has occurred while serializing the variables for uploading:\n"+
|
|
"%s", err))
|
|
return 1
|
|
}
|
|
|
|
// Upsert!
|
|
opts := &pushUpsertOptions{
|
|
Name: name,
|
|
Archive: archiveR,
|
|
Variables: ctx.Variables(),
|
|
TFVars: serializedVars,
|
|
}
|
|
|
|
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)))
|
|
return 0
|
|
}
|
|
|
|
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" is present, it will be
|
|
automatically loaded if this flag is not specified.
|
|
|
|
-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.
|
|
|
|
`
|
|
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
|
|
|
|
case []interface{}:
|
|
hcl, err = encodeHCL(v)
|
|
if err != nil {
|
|
break RANGE
|
|
}
|
|
|
|
tfv.Value = string(hcl)
|
|
tfv.IsHCL = true
|
|
|
|
case map[string]interface{}:
|
|
hcl, err = encodeHCL(v)
|
|
if err != nil {
|
|
break RANGE
|
|
}
|
|
|
|
tfv.Value = string(hcl)
|
|
tfv.IsHCL = true
|
|
default:
|
|
err = fmt.Errorf("unknown type %T for variable %s", v, k)
|
|
}
|
|
|
|
tfVars = append(tfVars, tfv)
|
|
}
|
|
|
|
return tfVars, err
|
|
}
|
|
|
|
func (c *PushCommand) Synopsis() string {
|
|
return "Upload this Terraform module to Atlas to run"
|
|
}
|
|
|
|
// pushClient is implementd internally to control where pushes go. This is
|
|
// either to Atlas or a mock for testing.
|
|
type pushClient interface {
|
|
Get(string) (map[string]interface{}, 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]interface{}, 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
|
|
}
|
|
|
|
var variables map[string]interface{}
|
|
if version != nil {
|
|
// TODO: merge variables and TFVars
|
|
//variables = version.Variables
|
|
}
|
|
|
|
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]interface{}
|
|
GetError error
|
|
|
|
UpsertCalled bool
|
|
UpsertOptions *pushUpsertOptions
|
|
UpsertVersion int
|
|
UpsertError error
|
|
}
|
|
|
|
func (c *mockPushClient) Get(name string) (map[string]interface{}, 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
|
|
}
|