diff --git a/examples/multistate-monorepo/.ize/env/testnut/infra/main.tf b/examples/multistate-monorepo/.ize/env/testnut/main.tf similarity index 100% rename from examples/multistate-monorepo/.ize/env/testnut/infra/main.tf rename to examples/multistate-monorepo/.ize/env/testnut/main.tf diff --git a/examples/multistate-monorepo/.ize/env/testnut/infra/variables.tf b/examples/multistate-monorepo/.ize/env/testnut/variables.tf similarity index 100% rename from examples/multistate-monorepo/.ize/env/testnut/infra/variables.tf rename to examples/multistate-monorepo/.ize/env/testnut/variables.tf diff --git a/examples/sls-apps-monorepo/.ize/env/testnut/ize.toml b/examples/sls-apps-monorepo/.ize/env/testnut/ize.toml index 25d78300..81813392 100644 --- a/examples/sls-apps-monorepo/.ize/env/testnut/ize.toml +++ b/examples/sls-apps-monorepo/.ize/env/testnut/ize.toml @@ -18,8 +18,8 @@ terraform_version = "1.2.6" # (optional) Terraform version can be se # config_file = "" # (optional) Path to ize.toml config file can be specified, but normally it's read from the environment's directory automatically. # home = "" # (optional) User home directory can be specified here. Normally $HOME is used. -# [terraform.infra] -# aws_region = "" # (optional) Terraform-specific AWS Region of this environment should be specified here. Normally global AWS_REGION is used. + [terraform.infra] + aws_region = "us-east-1" # (optional) Terraform-specific AWS Region of this environment should be specified here. Normally global AWS_REGION is used. # aws_profile = "" # (optional) Terraform-specific AWS profile (optional) can be specified here (but normally it should be inherited from a global AWS_PROFILE). # version = "" # (optional) Terraform version can be set here. 1.1.3 by default. # state_bucket_region = "" # (optional) Terraform state bucket region can be specified here. Normally AWS_REGION is used here. Can be overriden via env vars or flags. diff --git a/examples/sls-apps-monorepo/ize.toml b/examples/sls-apps-monorepo/ize.toml deleted file mode 100644 index 42787f0e..00000000 --- a/examples/sls-apps-monorepo/ize.toml +++ /dev/null @@ -1,22 +0,0 @@ -aws_region = "us-east-1" # (required) AWS Region of this environment should be specified here. Can be overriden by AWS_PROFILE env var or --aws-region flag. -namespace = "testnut" # (required) Namespace of the project can be specified here. It is used as a base for all naming. It can be overridden by NAMESPACE env var or --namespace flag. -terraform_version = "1.2.6" # (optional) Terraform version can be set here. 1.1.3 by default -# prefer_runtime = "" # (optional) Prefer a specific runtime. (native or docker) (default 'native') -# tag = "" # (optional) Tag can be set statically. Normally it is being constructed automatically based on the git revision. -# plain_text = false # (optional) Plain text output can be enabled here. Default is false. Can be overridden by IZE_PLAIN_TEXT env var or --plain-text flag. -# env = "dev" # (optional) Environment name can be specified here. Normally it should be passed via `ENV` variable or --env flag. -# env_dir = "" # (optional) Environment directory can be specified here. Normally it's calculated automatically based on the directory structure convention. -# docker_registry = "" # (optional) Docker registry can be set here. By default it uses ECR repo with the name of the service. -# tf_log_path = "" # (optional) TF_LOG_PATH can be set here. -# custom_prompt = false # (optional) Custom prompt can be enabled here for all console connections. Default: false. -# aws_profile = "" # (optional) AWS Profile can be specified here (but normally it's specified via AWS_PROFILE env var) -# log_level = "" # (optional) Log level can be specified here. Possible levels: info, debug, trace, panic, warn, error, fatal(default). Can be overridden via IZE_LOG_LEVEL env var or via --log-level flag. -# ize_dir = "" # (optional) Ize directory can be specified here. Normally it's assumed to be .infra or .ize in the current repo. -# apps_path = "" # (optional) Path to apps directory can be set. By default apps are searched in 'apps' and 'projects' directories. This is needed in case your repo structure is not purely ize-structured (let's say you have 'src' repo in your dotnet app, as an example) -# root_dir = "" # (optional) Project directory can be set here. By default it's the current directory, but in case you prefer to run ize from the outside of repo it may be useful (uncommon). -# tf_log = "" # (optional) Terraform TF_LOG can be set here. Can be TRACE, DEBUG, INFO, WARN or ERROR. -# config_file = "" # (optional) Path to ize.toml config file can be specified, but normally it's read from the environment's directory automatically. -# home = "" # (optional) User home directory can be specified here. Normally $HOME is used. - -[terraform.infra] -root_domain_name = "" # (optional) Root domain name can be set here. This is the main domain that will be passed to the terraform. Generally if your app lives at 'api.dev.nutcorp.net' the root domain is `nutcorp.net` diff --git a/internal/commands/down.go b/internal/commands/down.go index ebd47e97..632676d9 100644 --- a/internal/commands/down.go +++ b/internal/commands/down.go @@ -115,20 +115,21 @@ func (o *DownOptions) Complete(cmd *cobra.Command, args []string) error { } if o.Config.Terraform == nil { - o.Config.Terraform = map[string]*config.Terraform{} - o.Config.Terraform["infra"] = &config.Terraform{} + return fmt.Errorf("you must specify at least one terraform stack in ize.toml") } - if len(o.Config.Terraform["infra"].AwsProfile) == 0 { - o.Config.Terraform["infra"].AwsProfile = o.Config.AwsProfile - } + if _, ok := o.Config.Terraform["infra"]; ok { + if len(o.Config.Terraform["infra"].AwsProfile) == 0 { + o.Config.Terraform["infra"].AwsProfile = o.Config.AwsProfile + } - if len(o.Config.Terraform["infra"].AwsRegion) == 0 { - o.Config.Terraform["infra"].AwsRegion = o.Config.AwsRegion - } + if len(o.Config.Terraform["infra"].AwsRegion) == 0 { + o.Config.Terraform["infra"].AwsRegion = o.Config.AwsRegion + } - if len(o.Config.Terraform["infra"].Version) == 0 { - o.Config.Terraform["infra"].Version = o.Config.TerraformVersion + if len(o.Config.Terraform["infra"].Version) == 0 { + o.Config.Terraform["infra"].Version = o.Config.TerraformVersion + } } } else { if err := requirements.CheckRequirements(requirements.WithIzeStructure(), requirements.WithConfigFile()); err != nil { @@ -273,9 +274,9 @@ func destroyInfra(state string, config *config.Project, skipGen bool, ui termina switch config.PreferRuntime { case "docker": - tf = terraform.NewDockerTerraform(state, []string{"destroy", "-auto-approve"}, env, nil, config) + tf = terraform.NewDockerTerraform(state, []string{"init", "-input=true"}, env, nil, config) case "native": - tf = terraform.NewLocalTerraform(state, []string{"destroy", "-auto-approve"}, env, nil, config) + tf = terraform.NewLocalTerraform(state, []string{"init", "-input=true"}, env, nil, config) err = tf.Prepare() if err != nil { return fmt.Errorf("can't destroy infra: %w", err) @@ -284,13 +285,23 @@ func destroyInfra(state string, config *config.Project, skipGen bool, ui termina return fmt.Errorf("can't supported %s runtime", config.PreferRuntime) } - ui.Output("Running terraform destroy...", terminal.WithHeaderStyle()) + ui.Output("Execution terraform init...", terminal.WithHeaderStyle()) err = tf.RunUI(ui) if err != nil { return err } + //terraform destroy run options + tf.NewCmd([]string{"destroy", "-auto-approve"}) + + ui.Output("Execution terraform destroy...", terminal.WithHeaderStyle()) + + err = tf.RunUI(ui) + if err != nil { + return fmt.Errorf("can't deploy infra: %w", err) + } + ui.Output("Terraform destroy completed!\n", terminal.WithSuccessStyle()) return nil diff --git a/internal/commands/down_infra.go b/internal/commands/down_infra.go index d545f480..8145116f 100644 --- a/internal/commands/down_infra.go +++ b/internal/commands/down_infra.go @@ -69,32 +69,33 @@ func (o *DownInfraOptions) Complete() error { } if o.Config.Terraform == nil { - o.Config.Terraform = map[string]*config.Terraform{} - o.Config.Terraform["infra"] = &config.Terraform{} + return fmt.Errorf("you must specify at least one terraform stack in ize.toml") } - if len(o.AwsProfile) != 0 { - o.Config.Terraform["infra"].AwsProfile = o.AwsProfile - } + if _, ok := o.Config.Terraform["infra"]; ok { + if len(o.AwsProfile) != 0 { + o.Config.Terraform["infra"].AwsProfile = o.AwsProfile + } - if len(o.Config.Terraform["infra"].AwsProfile) == 0 { - o.Config.Terraform["infra"].AwsProfile = o.Config.AwsProfile - } + if len(o.Config.Terraform["infra"].AwsProfile) == 0 { + o.Config.Terraform["infra"].AwsProfile = o.Config.AwsProfile + } - if len(o.AwsProfile) != 0 { - o.Config.Terraform["infra"].AwsRegion = o.AwsRegion - } + if len(o.AwsProfile) != 0 { + o.Config.Terraform["infra"].AwsRegion = o.AwsRegion + } - if len(o.Config.Terraform["infra"].AwsRegion) == 0 { - o.Config.Terraform["infra"].AwsRegion = o.Config.AwsRegion - } + if len(o.Config.Terraform["infra"].AwsRegion) == 0 { + o.Config.Terraform["infra"].AwsRegion = o.Config.AwsRegion + } - if len(o.Version) != 0 { - o.Config.Terraform["infra"].Version = o.Version - } + if len(o.Version) != 0 { + o.Config.Terraform["infra"].Version = o.Version + } - if len(o.Config.Terraform["infra"].Version) == 0 { - o.Config.Terraform["infra"].Version = o.Config.TerraformVersion + if len(o.Config.Terraform["infra"].Version) == 0 { + o.Config.Terraform["infra"].Version = o.Config.TerraformVersion + } } o.ui = terminal.ConsoleUI(context.Background(), o.Config.PlainText) @@ -120,7 +121,7 @@ func (o *DownInfraOptions) Run() error { } } - err := manager.InReversDependencyOrder(aws.BackgroundContext(), o.Config.GetApps(), func(c context.Context, name string) error { + err := manager.InReversDependencyOrder(aws.BackgroundContext(), o.Config.GetStates(), func(c context.Context, name string) error { return destroyInfra(name, o.Config, o.SkipGen, ui) }) if err != nil { diff --git a/internal/commands/tfenv.go b/internal/commands/tfenv.go index c41a4f04..15e6dc6e 100644 --- a/internal/commands/tfenv.go +++ b/internal/commands/tfenv.go @@ -82,9 +82,6 @@ func GenerateTerraformFiles(name string, terraformStateBucketName string, projec tf = *project.Terraform[name] } - stateName := tf.StateName - stateKey := fmt.Sprintf("%v/%v.tfstate", project.Env, stateName) - if len(terraformStateBucketName) != 0 { tf.StateBucketName = terraformStateBucketName } @@ -107,6 +104,20 @@ func GenerateTerraformFiles(name string, terraformStateBucketName string, projec } } + stateKey := fmt.Sprintf("%v/%v.tfstate", project.Env, name) + if len(tf.StateName) != 0 { + stateKey = fmt.Sprintf("%v/%v.tfstate", project.Env, tf.StateName) + } + + if name == "infra" { + if checkTFStateKey(project, tf.StateBucketName, filepath.Join(project.Env, "terraform.tfstate")) { + stateKey = filepath.Join(project.Env, "terraform.tfstate") + pterm.Warning.Printfln("%s/terraform.tfstate location is deprecated, please move to %s/infra.tfstate", project.Env, project.Env) + } else { + stateKey = filepath.Join(project.Env, "infra.tfstate") + } + } + if len(tf.StateBucketRegion) == 0 { tf.StateBucketRegion = project.AwsRegion } @@ -197,3 +208,22 @@ func checkTFStateBucket(project *config.Project, name string) bool { return true } + +func checkTFStateKey(project *config.Project, bucket, key string) bool { + _, err := project.AWSClient.S3Client.HeadObject(&s3.HeadObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(key), + }) + if err != nil { + if aerr, ok := err.(awserr.Error); ok { + switch aerr.Code() { + case s3.ErrCodeNoSuchBucket: + return false + default: + return false + } + } + } + + return true +} diff --git a/internal/commands/tfenv_test.go b/internal/commands/tfenv_test.go index 0bd98443..e320fc99 100644 --- a/internal/commands/tfenv_test.go +++ b/internal/commands/tfenv_test.go @@ -47,6 +47,7 @@ func TestTfenv(t *testing.T) { withECS: true, mockS3Client: func(m *mocks.MockS3API) { m.EXPECT().HeadBucket(gomock.Any()).Return(nil, nil).AnyTimes() + m.EXPECT().HeadObject(gomock.Any()).Return(nil, nil).AnyTimes() }, mockSTSClient: func(m *mocks.MockSTSAPI) {}, wantBackend: `provider "aws" { @@ -90,6 +91,7 @@ root_domain_name = "examples.ize.sh" wantErr: false, mockS3Client: func(m *mocks.MockS3API) { m.EXPECT().HeadBucket(gomock.Any()).Return(nil, nil).AnyTimes() + m.EXPECT().HeadObject(gomock.Any()).Return(nil, nil).AnyTimes() }, mockSTSClient: func(m *mocks.MockSTSAPI) {}, withECS: true, @@ -134,6 +136,7 @@ root_domain_name = "examples.ize.sh" wantErr: false, mockS3Client: func(m *mocks.MockS3API) { m.EXPECT().HeadBucket(gomock.Any()).Return(nil, nil).AnyTimes() + m.EXPECT().HeadObject(gomock.Any()).Return(nil, nil).AnyTimes() }, withECS: true, mockSTSClient: func(m *mocks.MockSTSAPI) {}, @@ -177,6 +180,7 @@ root_domain_name = "test" wantErr: false, mockS3Client: func(m *mocks.MockS3API) { m.EXPECT().HeadBucket(gomock.Any()).Return(nil, awserr.New(s3.ErrCodeNoSuchKey, "message", nil)).Times(1) + m.EXPECT().HeadObject(gomock.Any()).Return(nil, nil).AnyTimes() }, mockSTSClient: func(m *mocks.MockSTSAPI) { m.EXPECT().GetCallerIdentity(gomock.Any()).Return(&sts.GetCallerIdentityOutput{ @@ -214,10 +218,12 @@ namespace = "testnut" `, }, { - name: "success (only flags)", - args: []string{"-e=test", "-r=us-east-1", "-p=test", "-n=testnut", "gen", "tfenv", "--terraform-state-bucket-name=test"}, - wantErr: false, - mockS3Client: func(m *mocks.MockS3API) {}, + name: "success (only flags)", + args: []string{"-e=test", "-r=us-east-1", "-p=test", "-n=testnut", "gen", "tfenv", "--terraform-state-bucket-name=test"}, + wantErr: false, + mockS3Client: func(m *mocks.MockS3API) { + m.EXPECT().HeadObject(gomock.Any()).Return(nil, nil).AnyTimes() + }, mockSTSClient: func(m *mocks.MockSTSAPI) {}, wantBackend: `provider "aws" { profile = var.aws_profile @@ -256,6 +262,7 @@ namespace = "testnut" wantErr: false, mockS3Client: func(m *mocks.MockS3API) { m.EXPECT().HeadBucket(gomock.Any()).Return(nil, awserr.New(s3.ErrCodeNoSuchBucket, "message", nil)).Times(1) + m.EXPECT().HeadObject(gomock.Any()).Return(nil, nil).AnyTimes() }, mockSTSClient: func(m *mocks.MockSTSAPI) { m.EXPECT().GetCallerIdentity(gomock.Any()).Return(&sts.GetCallerIdentityOutput{ diff --git a/internal/commands/up.go b/internal/commands/up.go index 742bacd4..be27886b 100644 --- a/internal/commands/up.go +++ b/internal/commands/up.go @@ -112,20 +112,21 @@ func (o *UpOptions) Complete(cmd *cobra.Command, args []string) error { } if o.Config.Terraform == nil { - o.Config.Terraform = map[string]*config.Terraform{} - o.Config.Terraform["infra"] = &config.Terraform{} + return fmt.Errorf("you must specify at least one terraform stack in ize.toml") } - if len(o.Config.Terraform["infra"].AwsProfile) == 0 { - o.Config.Terraform["infra"].AwsProfile = o.Config.AwsProfile - } + if _, ok := o.Config.Terraform["infra"]; ok { + if len(o.Config.Terraform["infra"].AwsProfile) == 0 { + o.Config.Terraform["infra"].AwsProfile = o.Config.AwsProfile + } - if len(o.Config.Terraform["infra"].AwsRegion) == 0 { - o.Config.Terraform["infra"].AwsProfile = o.Config.AwsRegion - } + if len(o.Config.Terraform["infra"].AwsRegion) == 0 { + o.Config.Terraform["infra"].AwsRegion = o.Config.AwsRegion + } - if len(o.Config.Terraform["infra"].Version) == 0 { - o.Config.Terraform["infra"].Version = o.Config.TerraformVersion + if len(o.Config.Terraform["infra"].Version) == 0 { + o.Config.Terraform["infra"].Version = o.Config.TerraformVersion + } } } else { if err := requirements.CheckRequirements(requirements.WithIzeStructure(), requirements.WithConfigFile()); err != nil { diff --git a/internal/commands/up_infra.go b/internal/commands/up_infra.go index 4c0e2b63..5f6c7cbb 100644 --- a/internal/commands/up_infra.go +++ b/internal/commands/up_infra.go @@ -95,36 +95,39 @@ func (o *UpInfraOptions) Complete() error { } if o.Config.Terraform == nil { - o.Config.Terraform = map[string]*config.Terraform{} - o.Config.Terraform["infra"] = &config.Terraform{} + return fmt.Errorf("you must specify at least one terraform stack in ize.toml") } - if len(o.AwsProfile) != 0 { - o.Config.Terraform["infra"].AwsProfile = o.AwsProfile - } + if _, ok := o.Config.Terraform["infra"]; ok { + if len(o.AwsProfile) != 0 { + o.Config.Terraform["infra"].AwsProfile = o.AwsProfile + } - if len(o.Config.Terraform["infra"].AwsProfile) == 0 { - o.Config.Terraform["infra"].AwsProfile = o.Config.AwsProfile - } + if len(o.Config.Terraform["infra"].AwsProfile) == 0 { + o.Config.Terraform["infra"].AwsProfile = o.Config.AwsProfile + } - if len(o.AwsProfile) != 0 { - o.Config.Terraform["infra"].AwsRegion = o.AwsRegion - } + if len(o.AwsProfile) != 0 { + o.Config.Terraform["infra"].AwsRegion = o.AwsRegion + } - if len(o.Config.Terraform["infra"].AwsRegion) == 0 { - o.Config.Terraform["infra"].AwsRegion = o.Config.AwsRegion - } + if len(o.Config.Terraform["infra"].AwsRegion) == 0 { + o.Config.Terraform["infra"].AwsRegion = o.Config.AwsRegion + } - if len(o.Config.Terraform["infra"].StateBucketRegion) == 0 { - o.Config.Terraform["infra"].StateBucketRegion = o.Config.Terraform["infra"].AwsRegion - } - if len(o.Version) != 0 { - o.Config.Terraform["infra"].Version = o.Version - } + if len(o.Config.Terraform["infra"].StateBucketRegion) == 0 { + o.Config.Terraform["infra"].StateBucketRegion = o.Config.Terraform["infra"].AwsRegion + } - if len(o.Config.Terraform["infra"].Version) == 0 { - o.Config.Terraform["infra"].Version = o.Config.TerraformVersion + if len(o.Version) != 0 { + o.Config.Terraform["infra"].Version = o.Version + } + + + if len(o.Config.Terraform["infra"].Version) == 0 { + o.Config.Terraform["infra"].Version = o.Config.TerraformVersion + } } o.UI = terminal.ConsoleUI(context.Background(), o.Config.PlainText) @@ -235,7 +238,7 @@ func deployInfra(name string, ui terminal.UI, config *config.Project, skipGen bo var tf terraform.Terraform - logrus.Infof("infra: %s", config.Terraform["infra"]) + logrus.Infof("infra: %s", config.Terraform[name]) v, err := config.Session.Config.Credentials.Get() if err != nil { @@ -244,7 +247,7 @@ func deployInfra(name string, ui terminal.UI, config *config.Project, skipGen bo env := []string{ fmt.Sprintf("ENV=%v", config.Env), - fmt.Sprintf("AWS_PROFILE=%v", config.Terraform["infra"].AwsProfile), + fmt.Sprintf("AWS_PROFILE=%v", config.Terraform[name].AwsProfile), fmt.Sprintf("TF_LOG=%v", config.TFLog), fmt.Sprintf("TF_LOG_PATH=%v", config.TFLogPath), fmt.Sprintf("AWS_ACCESS_KEY_ID=%v", v.AccessKeyID), diff --git a/internal/config/config.go b/internal/config/config.go index 0fc7c154..3c745069 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -241,7 +241,6 @@ func InitConfig() { viper.SetDefault("PREFER_RUNTIME", "native") viper.SetDefault("CUSTOM_PROMPT", false) viper.SetDefault("PLAIN_TEXT", false) - viper.SetDefault("terraform.infra.state_name", "terraform") home, err := os.UserHomeDir() if err != nil {