From 9901f464720770b733c6cb50743555a7201a9e3c Mon Sep 17 00:00:00 2001 From: psihachina Date: Mon, 26 Sep 2022 18:13:14 +0300 Subject: [PATCH 01/19] added a simple example of a multi-state monorepo --- .../.ize/env/testnut/api/backend.tf | 21 +++++++++++++++++++ .../.ize/env/testnut/api/main.tf | 0 .../.ize/env/testnut/api/terraform.tfvars | 6 ++++++ .../.ize/env/testnut/api/variables.tf | 11 ++++++++++ .../.ize/env/testnut/infra/backend.tf | 21 +++++++++++++++++++ .../.ize/env/testnut/infra/main.tf | 0 .../.ize/env/testnut/infra/terraform.tfvars | 6 ++++++ .../.ize/env/testnut/infra/variables.tf | 11 ++++++++++ .../.ize/env/testnut/ize.toml | 14 +++++++++++++ .../.ize/env/testnut/vpc/backend.tf | 21 +++++++++++++++++++ .../.ize/env/testnut/vpc/main.tf | 0 .../.ize/env/testnut/vpc/terraform.tfvars | 6 ++++++ .../.ize/env/testnut/vpc/variables.tf | 11 ++++++++++ 13 files changed, 128 insertions(+) create mode 100644 examples/multistate-monorepo/.ize/env/testnut/api/backend.tf create mode 100644 examples/multistate-monorepo/.ize/env/testnut/api/main.tf create mode 100644 examples/multistate-monorepo/.ize/env/testnut/api/terraform.tfvars create mode 100644 examples/multistate-monorepo/.ize/env/testnut/api/variables.tf create mode 100644 examples/multistate-monorepo/.ize/env/testnut/infra/backend.tf create mode 100644 examples/multistate-monorepo/.ize/env/testnut/infra/main.tf create mode 100644 examples/multistate-monorepo/.ize/env/testnut/infra/terraform.tfvars create mode 100644 examples/multistate-monorepo/.ize/env/testnut/infra/variables.tf create mode 100644 examples/multistate-monorepo/.ize/env/testnut/ize.toml create mode 100644 examples/multistate-monorepo/.ize/env/testnut/vpc/backend.tf create mode 100644 examples/multistate-monorepo/.ize/env/testnut/vpc/main.tf create mode 100644 examples/multistate-monorepo/.ize/env/testnut/vpc/terraform.tfvars create mode 100644 examples/multistate-monorepo/.ize/env/testnut/vpc/variables.tf diff --git a/examples/multistate-monorepo/.ize/env/testnut/api/backend.tf b/examples/multistate-monorepo/.ize/env/testnut/api/backend.tf new file mode 100644 index 00000000..cb97cefd --- /dev/null +++ b/examples/multistate-monorepo/.ize/env/testnut/api/backend.tf @@ -0,0 +1,21 @@ +provider "aws" { + profile = var.aws_profile + region = var.aws_region + default_tags { + tags = { + env = "testnut" + namespace = "testnut" + terraform = "true" + } + } +} + +terraform { + backend "s3" { + bucket = "testnut-tf-state" + key = "testnut/api.tfstate" + region = "us-east-1" + profile = "default" + dynamodb_table = "tf-state-lock" + } +} diff --git a/examples/multistate-monorepo/.ize/env/testnut/api/main.tf b/examples/multistate-monorepo/.ize/env/testnut/api/main.tf new file mode 100644 index 00000000..e69de29b diff --git a/examples/multistate-monorepo/.ize/env/testnut/api/terraform.tfvars b/examples/multistate-monorepo/.ize/env/testnut/api/terraform.tfvars new file mode 100644 index 00000000..5c47533c --- /dev/null +++ b/examples/multistate-monorepo/.ize/env/testnut/api/terraform.tfvars @@ -0,0 +1,6 @@ +env = "testnut" +aws_profile = "default" +aws_region = "us-east-1" +ec2_key_pair_name = "testnut-testnut" +ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDvQNbvuHiuFdoJZSCYZDt6Npq/QWk+/Bz8MPKiBCQjzBDnHpFzRNVmpAo63GT5BTvmZ2fWzByM1H2O+/hRYYxAEcK45pmvpJlRwMNb5uwTwpniOc0ojO9fR/3EfusnUza37Ag9t3csqjv5H06mrYysPj4kBkAp19a/yCsmssg+3sJOKE4NsO19CRsJ3cCgazJIX7eEoM0L2XI0NX0el8q5r4voe30rNbp/uNBli4LkmdyZ7bUINu85pMsUT3qmRT+9FPC4E7Ega90zAqwZOkwadYia+nUMQPEJjcrycrU/En10D1jDMZxqFjGOSlkatGEKAGoqIj3wIru8mrF6PcyEB59qH65tz9iUjDyR/7k/CWsQXaDL30cO5aLLLl8uQZv/ppkFc0SPyZebS3dqWov/J+aIW7cdKEgkjyiWBabhoIA2MOYz03y07EANTyyNG+lLAOrYWTo6H+BcgSipXzDnnYExJhi7vX6uM8QvHpigylnQcVU3KX/GACv8YHd934lFvKtODo1NS0dXEZgoeH+aSWE9zDhHNEKe2eHvj7m/9ujGQdnYlSPVeYlCEUXN3QfYbvKwJt4pukqfJI+pgo18ReWb4GV57VuuJribERrJtNwZ1pL7Sg4+Gghp35mFebL3rh7c0MyJPK4s5cQlexKtP7XahakObRwgEd4VOla/gw== psihachina@gmail.com" +namespace = "testnut" diff --git a/examples/multistate-monorepo/.ize/env/testnut/api/variables.tf b/examples/multistate-monorepo/.ize/env/testnut/api/variables.tf new file mode 100644 index 00000000..5e7f26d0 --- /dev/null +++ b/examples/multistate-monorepo/.ize/env/testnut/api/variables.tf @@ -0,0 +1,11 @@ +variable "env" {} +variable "namespace" {} +variable "aws_profile" {} +variable "aws_region" {} +variable "ssh_public_key" {} +variable "ec2_key_pair_name" {} + +locals { + env = var.env + namespace = var.namespace +} diff --git a/examples/multistate-monorepo/.ize/env/testnut/infra/backend.tf b/examples/multistate-monorepo/.ize/env/testnut/infra/backend.tf new file mode 100644 index 00000000..3c3f4f7c --- /dev/null +++ b/examples/multistate-monorepo/.ize/env/testnut/infra/backend.tf @@ -0,0 +1,21 @@ +provider "aws" { + profile = var.aws_profile + region = var.aws_region + default_tags { + tags = { + env = "testnut" + namespace = "testnut" + terraform = "true" + } + } +} + +terraform { + backend "s3" { + bucket = "testnut-tf-state" + key = "testnut/infra.tfstate" + region = "us-east-1" + profile = "default" + dynamodb_table = "tf-state-lock" + } +} diff --git a/examples/multistate-monorepo/.ize/env/testnut/infra/main.tf b/examples/multistate-monorepo/.ize/env/testnut/infra/main.tf new file mode 100644 index 00000000..e69de29b diff --git a/examples/multistate-monorepo/.ize/env/testnut/infra/terraform.tfvars b/examples/multistate-monorepo/.ize/env/testnut/infra/terraform.tfvars new file mode 100644 index 00000000..5c47533c --- /dev/null +++ b/examples/multistate-monorepo/.ize/env/testnut/infra/terraform.tfvars @@ -0,0 +1,6 @@ +env = "testnut" +aws_profile = "default" +aws_region = "us-east-1" +ec2_key_pair_name = "testnut-testnut" +ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDvQNbvuHiuFdoJZSCYZDt6Npq/QWk+/Bz8MPKiBCQjzBDnHpFzRNVmpAo63GT5BTvmZ2fWzByM1H2O+/hRYYxAEcK45pmvpJlRwMNb5uwTwpniOc0ojO9fR/3EfusnUza37Ag9t3csqjv5H06mrYysPj4kBkAp19a/yCsmssg+3sJOKE4NsO19CRsJ3cCgazJIX7eEoM0L2XI0NX0el8q5r4voe30rNbp/uNBli4LkmdyZ7bUINu85pMsUT3qmRT+9FPC4E7Ega90zAqwZOkwadYia+nUMQPEJjcrycrU/En10D1jDMZxqFjGOSlkatGEKAGoqIj3wIru8mrF6PcyEB59qH65tz9iUjDyR/7k/CWsQXaDL30cO5aLLLl8uQZv/ppkFc0SPyZebS3dqWov/J+aIW7cdKEgkjyiWBabhoIA2MOYz03y07EANTyyNG+lLAOrYWTo6H+BcgSipXzDnnYExJhi7vX6uM8QvHpigylnQcVU3KX/GACv8YHd934lFvKtODo1NS0dXEZgoeH+aSWE9zDhHNEKe2eHvj7m/9ujGQdnYlSPVeYlCEUXN3QfYbvKwJt4pukqfJI+pgo18ReWb4GV57VuuJribERrJtNwZ1pL7Sg4+Gghp35mFebL3rh7c0MyJPK4s5cQlexKtP7XahakObRwgEd4VOla/gw== psihachina@gmail.com" +namespace = "testnut" diff --git a/examples/multistate-monorepo/.ize/env/testnut/infra/variables.tf b/examples/multistate-monorepo/.ize/env/testnut/infra/variables.tf new file mode 100644 index 00000000..5e7f26d0 --- /dev/null +++ b/examples/multistate-monorepo/.ize/env/testnut/infra/variables.tf @@ -0,0 +1,11 @@ +variable "env" {} +variable "namespace" {} +variable "aws_profile" {} +variable "aws_region" {} +variable "ssh_public_key" {} +variable "ec2_key_pair_name" {} + +locals { + env = var.env + namespace = var.namespace +} diff --git a/examples/multistate-monorepo/.ize/env/testnut/ize.toml b/examples/multistate-monorepo/.ize/env/testnut/ize.toml new file mode 100644 index 00000000..85288089 --- /dev/null +++ b/examples/multistate-monorepo/.ize/env/testnut/ize.toml @@ -0,0 +1,14 @@ +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 + +[terraform.api] +state_name = "" +depends_on = ["vpc"] + +[terraform.vpc] +state_name = "" +depends_on = ["infra"] + +[terraform.main] +state_name = "" \ No newline at end of file diff --git a/examples/multistate-monorepo/.ize/env/testnut/vpc/backend.tf b/examples/multistate-monorepo/.ize/env/testnut/vpc/backend.tf new file mode 100644 index 00000000..1bb93acc --- /dev/null +++ b/examples/multistate-monorepo/.ize/env/testnut/vpc/backend.tf @@ -0,0 +1,21 @@ +provider "aws" { + profile = var.aws_profile + region = var.aws_region + default_tags { + tags = { + env = "testnut" + namespace = "testnut" + terraform = "true" + } + } +} + +terraform { + backend "s3" { + bucket = "testnut-tf-state" + key = "testnut/vpc.tfstate" + region = "us-east-1" + profile = "default" + dynamodb_table = "tf-state-lock" + } +} diff --git a/examples/multistate-monorepo/.ize/env/testnut/vpc/main.tf b/examples/multistate-monorepo/.ize/env/testnut/vpc/main.tf new file mode 100644 index 00000000..e69de29b diff --git a/examples/multistate-monorepo/.ize/env/testnut/vpc/terraform.tfvars b/examples/multistate-monorepo/.ize/env/testnut/vpc/terraform.tfvars new file mode 100644 index 00000000..5c47533c --- /dev/null +++ b/examples/multistate-monorepo/.ize/env/testnut/vpc/terraform.tfvars @@ -0,0 +1,6 @@ +env = "testnut" +aws_profile = "default" +aws_region = "us-east-1" +ec2_key_pair_name = "testnut-testnut" +ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDvQNbvuHiuFdoJZSCYZDt6Npq/QWk+/Bz8MPKiBCQjzBDnHpFzRNVmpAo63GT5BTvmZ2fWzByM1H2O+/hRYYxAEcK45pmvpJlRwMNb5uwTwpniOc0ojO9fR/3EfusnUza37Ag9t3csqjv5H06mrYysPj4kBkAp19a/yCsmssg+3sJOKE4NsO19CRsJ3cCgazJIX7eEoM0L2XI0NX0el8q5r4voe30rNbp/uNBli4LkmdyZ7bUINu85pMsUT3qmRT+9FPC4E7Ega90zAqwZOkwadYia+nUMQPEJjcrycrU/En10D1jDMZxqFjGOSlkatGEKAGoqIj3wIru8mrF6PcyEB59qH65tz9iUjDyR/7k/CWsQXaDL30cO5aLLLl8uQZv/ppkFc0SPyZebS3dqWov/J+aIW7cdKEgkjyiWBabhoIA2MOYz03y07EANTyyNG+lLAOrYWTo6H+BcgSipXzDnnYExJhi7vX6uM8QvHpigylnQcVU3KX/GACv8YHd934lFvKtODo1NS0dXEZgoeH+aSWE9zDhHNEKe2eHvj7m/9ujGQdnYlSPVeYlCEUXN3QfYbvKwJt4pukqfJI+pgo18ReWb4GV57VuuJribERrJtNwZ1pL7Sg4+Gghp35mFebL3rh7c0MyJPK4s5cQlexKtP7XahakObRwgEd4VOla/gw== psihachina@gmail.com" +namespace = "testnut" diff --git a/examples/multistate-monorepo/.ize/env/testnut/vpc/variables.tf b/examples/multistate-monorepo/.ize/env/testnut/vpc/variables.tf new file mode 100644 index 00000000..5e7f26d0 --- /dev/null +++ b/examples/multistate-monorepo/.ize/env/testnut/vpc/variables.tf @@ -0,0 +1,11 @@ +variable "env" {} +variable "namespace" {} +variable "aws_profile" {} +variable "aws_region" {} +variable "ssh_public_key" {} +variable "ec2_key_pair_name" {} + +locals { + env = var.env + namespace = var.namespace +} From 296746b0071ad7e857173c085e9909976d908c9c Mon Sep 17 00:00:00 2001 From: psihachina Date: Mon, 26 Sep 2022 18:14:51 +0300 Subject: [PATCH 02/19] added `depends_on` option for terraform struct --- internal/config/infra.go | 15 ++++++++------- internal/schema/ize-spec.json | 4 ++++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/internal/config/infra.go b/internal/config/infra.go index ad075cd6..236861ab 100644 --- a/internal/config/infra.go +++ b/internal/config/infra.go @@ -6,13 +6,14 @@ type Infra struct { } type Terraform struct { - Version string `mapstructure:",omitempty"` - StateBucketRegion string `mapstructure:"state_bucket_region,omitempty"` - StateBucketName string `mapstructure:"state_bucket_name,omitempty"` - StateName string `mapstructure:"state_name,omitempty"` - RootDomainName string `mapstructure:"root_domain_name,omitempty"` - AwsRegion string `mapstructure:"aws_region,omitempty"` - AwsProfile string `mapstructure:"aws_profile,omitempty"` + Version string `mapstructure:",omitempty"` + StateBucketRegion string `mapstructure:"state_bucket_region,omitempty"` + StateBucketName string `mapstructure:"state_bucket_name,omitempty"` + StateName string `mapstructure:"state_name,omitempty"` + RootDomainName string `mapstructure:"root_domain_name,omitempty"` + AwsRegion string `mapstructure:"aws_region,omitempty"` + AwsProfile string `mapstructure:"aws_profile,omitempty"` + DependsOn []string `mapstructure:"depends_on,omitempty"` } type Tunnel struct { diff --git a/internal/schema/ize-spec.json b/internal/schema/ize-spec.json index b17ca572..5c3911d0 100644 --- a/internal/schema/ize-spec.json +++ b/internal/schema/ize-spec.json @@ -470,6 +470,10 @@ "aws_profile" : { "type": "string", "description": "(optional) Terraform-specific AWS profile (optional) can be specified here (but normally it should be inherited from a global AWS_PROFILE)." + }, + "depends_on": { + "type": "array", + "description": "(optional) expresses startup and shutdown dependencies between states" } }, "description": "Terraform configuration", From 66c47bf0a0c3e29f8734e7ec7a6bbccc7c71d796 Mon Sep 17 00:00:00 2001 From: psihachina Date: Tue, 27 Sep 2022 08:11:39 +0300 Subject: [PATCH 03/19] added `findDuplicates` function --- internal/config/config.go | 82 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/internal/config/config.go b/internal/config/config.go index 9da093c0..b2ac2632 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -86,6 +86,11 @@ func (p *Project) GetConfig() error { return err } + err = findDuplicates(p) + if err != nil { + return err + } + sess, err := utils.GetSession(&utils.SessionConfig{ Region: p.AwsRegion, Profile: p.AwsProfile, @@ -292,6 +297,83 @@ func InitConfig() { } } +func findDuplicates(cfg *Project) error { + existingKeys := map[string]string{} + duplicateKeys := map[string]map[string]string{} + + for k := range cfg.Terraform { + if val, ok := existingKeys[k]; ok { + if duplicateKeys[k] == nil { + duplicateKeys[k] = map[string]string{} + } + duplicateKeys[k]["terraform"] = k + if _, ok := duplicateKeys[k][val]; !ok { + duplicateKeys[k][val] = k + + } + } + existingKeys[k] = "terraform" + } + + for k := range cfg.Ecs { + if val, ok := existingKeys[k]; ok { + if duplicateKeys[k] == nil { + duplicateKeys[k] = map[string]string{} + } + duplicateKeys[k]["ecs"] = k + if _, ok := duplicateKeys[k][val]; !ok { + duplicateKeys[k][val] = k + + } + } + existingKeys[k] = "ecs" + } + + for k := range cfg.Serverless { + if val, ok := existingKeys[k]; ok { + if duplicateKeys[k] == nil { + duplicateKeys[k] = map[string]string{} + } + duplicateKeys[k]["serverless"] = k + if _, ok := duplicateKeys[k][val]; !ok { + duplicateKeys[k][val] = k + + } + } + existingKeys[k] = "serverless" + } + + for k := range cfg.Alias { + if val, ok := existingKeys[k]; ok { + if duplicateKeys[k] == nil { + duplicateKeys[k] = map[string]string{} + } + duplicateKeys[k]["alias"] = k + if _, ok := duplicateKeys[k][val]; !ok { + duplicateKeys[k][val] = k + + } + } + existingKeys[k] = "alias" + } + + errMsg := "" + if len(duplicateKeys) != 0 { + for name, v := range duplicateKeys { + errMsg += fmt.Sprintf("\nOnly one section with the name \"%s\" is allowed. Please rename one of the following:\n", name) + for k, v := range v { + errMsg += fmt.Sprintf("- [%s.%s]\n", k, v) + } + } + } + + if len(errMsg) != 0 { + return fmt.Errorf(errMsg) + } + + return nil +} + func setDefaultInfraDir(cwd string) { viper.SetDefault("IZE_DIR", fmt.Sprintf("%v/.ize", cwd)) viper.SetDefault("ENV_DIR", fmt.Sprintf("%v/.ize/env/%v", cwd, viper.GetString("ENV"))) From cee13483e51fe6e57ac826e9098631db45b4717c Mon Sep 17 00:00:00 2001 From: psihachina Date: Tue, 27 Sep 2022 08:17:09 +0300 Subject: [PATCH 04/19] added `name` parameter for `GenerateTerraformFiles` function This is necessary for generate tf files in state folder. --- internal/commands/tfenv.go | 39 ++++++++++++++------------------------ 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/internal/commands/tfenv.go b/internal/commands/tfenv.go index 23737dae..c4226c2d 100644 --- a/internal/commands/tfenv.go +++ b/internal/commands/tfenv.go @@ -14,6 +14,7 @@ import ( "github.com/spf13/cobra" "io/ioutil" "os" + "path/filepath" ) type TfenvOptions struct { @@ -51,6 +52,7 @@ func NewCmdTfenv(project *config.Project) *cobra.Command { Short: "Generate terraform files", Long: tfenvLongDesc, Example: tfenvExample, + Hidden: true, RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true @@ -69,19 +71,15 @@ func NewCmdTfenv(project *config.Project) *cobra.Command { } func (o *TfenvOptions) Run() error { - return GenerateTerraformFiles( - o.Config, - o.TerraformStateBucketName, - ) + return GenerateTerraformFiles("infra", o.TerraformStateBucketName, o.Config) } -func GenerateTerraformFiles(project *config.Project, terraformStateBucketName string) error { - pterm.DefaultSection.Printfln("Starting generate terraform files") +func GenerateTerraformFiles(name string, terraformStateBucketName string, project *config.Project) error { var tf config.Terraform if project.Terraform != nil { - tf = *project.Terraform["infra"] + tf = *project.Terraform[name] } stateName := tf.StateName @@ -131,27 +129,19 @@ func GenerateTerraformFiles(project *config.Project, terraformStateBucketName st err := template.GenerateBackendTf( backendOpts, - envDir, + filepath.Join(envDir, name), ) if err != nil { - pterm.DefaultSection.Println("Generate terraform file not completed") - return err + pterm.Error.Printfln("Generate terraform file for \"%s\" not completed", name) + return fmt.Errorf("can't generate backent.tf: %s", err) } - pterm.Success.Println("backend.tf generated") - - pterm.Success.Printfln("Read SSH public key") - home, _ := os.UserHomeDir() key, err := ioutil.ReadFile(fmt.Sprintf("%s/.ssh/id_rsa.pub", home)) if err != nil { - pterm.DefaultSection.Println("Generate terraform file not completed") - return err - } + pterm.Error.Printfln("Generate terraform file for \"%s\" not completed", name) + return fmt.Errorf("can't read public ssh key: %s", err) - if err != nil { - pterm.DefaultSection.Println("Generate terraform file not completed") - return err } varsOpts := template.VarsOpts{ @@ -174,15 +164,14 @@ func GenerateTerraformFiles(project *config.Project, terraformStateBucketName st err = template.GenerateVarsTf( varsOpts, - envDir, + filepath.Join(envDir, name), ) if err != nil { - pterm.DefaultSection.Println("Generate terraform file not completed") - return err + pterm.Error.Printfln("Generate terraform file for \"%s\" not completed", name) + return fmt.Errorf("can't generate tfvars: %s", err) } - pterm.Success.Println("terraform.tfvars generated") - pterm.DefaultSection.Printfln("Generate terraform files completed") + pterm.Success.Printfln("Generate terraform file for \"%s\" completed", name) return nil } From 9f9c7eaa7d120e321410bf65905b60245483043d Mon Sep 17 00:00:00 2001 From: psihachina Date: Tue, 27 Sep 2022 08:46:50 +0300 Subject: [PATCH 05/19] added `GetStates` function This function returns all terraform states. --- internal/config/project.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/internal/config/project.go b/internal/config/project.go index da7f76d0..03120f9c 100644 --- a/internal/config/project.go +++ b/internal/config/project.go @@ -163,3 +163,17 @@ func (p *Project) GetApps() map[string]*interface{} { return apps } + +func (p *Project) GetStates() map[string]*interface{} { + states := map[string]*interface{}{} + + for name, body := range p.Terraform { + var v interface{} + v = map[string]interface{}{ + "depends_on": body.DependsOn, + } + states[name] = &v + } + + return states +} From c56e7d46eb0aa453a623c8a3467712778dc45ee2 Mon Sep 17 00:00:00 2001 From: psihachina Date: Tue, 27 Sep 2022 10:36:51 +0300 Subject: [PATCH 06/19] added `ize up apps` command This command build, push and deploy all apps --- internal/commands/up.go | 1 + internal/commands/up_apps.go | 180 +++++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 internal/commands/up_apps.go diff --git a/internal/commands/up.go b/internal/commands/up.go index 0e52fbed..8605ebfa 100644 --- a/internal/commands/up.go +++ b/internal/commands/up.go @@ -102,6 +102,7 @@ func NewCmdUp(project *config.Project) *cobra.Command { cmd.AddCommand( NewCmdUpInfra(project), + NewCmdUpApps(project), ) return cmd diff --git a/internal/commands/up_apps.go b/internal/commands/up_apps.go new file mode 100644 index 00000000..5cdec737 --- /dev/null +++ b/internal/commands/up_apps.go @@ -0,0 +1,180 @@ +package commands + +import ( + "context" + "fmt" + "github.com/aws/aws-sdk-go/aws" + "github.com/hazelops/ize/internal/config" + "github.com/hazelops/ize/internal/manager" + "github.com/hazelops/ize/internal/manager/alias" + "github.com/hazelops/ize/internal/manager/ecs" + "github.com/hazelops/ize/internal/manager/serverless" + "github.com/hazelops/ize/internal/requirements" + "github.com/hazelops/ize/pkg/templates" + "github.com/hazelops/ize/pkg/terminal" + "github.com/spf13/cobra" +) + +type UpAppsOptions struct { + Config *config.Project + UI terminal.UI +} + +var upAppsLongDesc = templates.LongDesc(` + Build, push and deploy all apps. +`) + +var upAppsExample = templates.Examples(` + # Up all apps + ize up apps + + # Up apps with explicitly specified config file + ize --config-file /path/to/config up apps + + # Deploy apps with explicitly specified config file passed via environment variable + export IZE_CONFIG_FILE=/path/to/config + ize up apps +`) + +func NewUpAppsFlags(project *config.Project) *UpAppsOptions { + return &UpAppsOptions{ + Config: project, + } +} + +func NewCmdUpApps(project *config.Project) *cobra.Command { + o := NewUpAppsFlags(project) + + cmd := &cobra.Command{ + Use: "apps", + Short: "Manage apps deployments", + Long: upAppsLongDesc, + Example: upAppsExample, + RunE: func(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true + err := o.Complete() + if err != nil { + return err + } + + err = o.Validate() + if err != nil { + return err + } + + err = o.Run() + if err != nil { + return err + } + + return nil + }, + } + + return cmd +} + +func (o *UpAppsOptions) Complete() error { + if err := requirements.CheckRequirements(requirements.WithIzeStructure(), requirements.WithConfigFile()); err != nil { + return err + } + + if len(o.Config.Serverless) != 0 { + if err := requirements.CheckRequirements(requirements.WithNVM()); err != nil { + return err + } + } + + o.UI = terminal.ConsoleUI(context.Background(), o.Config.PlainText) + + return nil +} + +func (o *UpAppsOptions) Validate() error { + return nil +} + +func (o *UpAppsOptions) Run() error { + ui := o.UI + ui.Output("Deploying apps...", terminal.WithHeaderStyle()) + + err := manager.InDependencyOrder(aws.BackgroundContext(), o.Config.GetApps(), func(c context.Context, name string) error { + o.Config.AwsProfile = o.Config.Terraform["infra"].AwsProfile + + err := deployApp(name, ui, o.Config) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return err + } + + ui.Output("Deploy all completed!\n", terminal.WithSuccessStyle()) + + return nil +} + +func deployApp(name string, ui terminal.UI, cfg *config.Project) error { + var m manager.Manager + var icon string + + m = &ecs.Manager{ + Project: cfg, + App: &config.Ecs{Name: name}, + } + + if app, ok := cfg.Serverless[name]; ok { + app.Name = name + m = &serverless.Manager{ + Project: cfg, + App: app, + } + } + if app, ok := cfg.Alias[name]; ok { + app.Name = name + m = &alias.Manager{ + Project: cfg, + App: app, + } + } + if app, ok := cfg.Ecs[name]; ok { + app.Name = name + m = &ecs.Manager{ + Project: cfg, + App: app, + } + } + + if len(icon) != 0 { + icon += " " + } + + ui.Output("Deploying %s%s app...", icon, name, terminal.WithHeaderStyle()) + sg := ui.StepGroup() + defer sg.Wait() + + // build app container + err := m.Build(ui) + if err != nil { + return fmt.Errorf("can't build app: %w", err) + } + + // push app image + err = m.Push(ui) + if err != nil { + return fmt.Errorf("can't push app: %w", err) + } + + // deploy app image + err = m.Deploy(ui) + if err != nil { + return fmt.Errorf("can't deploy app: %w", err) + } + + ui.Output("Deploy app %s%s completed\n", icon, name, terminal.WithSuccessStyle()) + + return nil +} From ee7414cd61607863eb58f39fde2828dbb15dfdd1 Mon Sep 17 00:00:00 2001 From: psihachina Date: Tue, 27 Sep 2022 15:46:14 +0300 Subject: [PATCH 07/19] added an exception for the `infra` state --- internal/commands/tfenv.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/internal/commands/tfenv.go b/internal/commands/tfenv.go index c4226c2d..a06afa1c 100644 --- a/internal/commands/tfenv.go +++ b/internal/commands/tfenv.go @@ -122,14 +122,17 @@ func GenerateTerraformFiles(name string, terraformStateBucketName string, projec TERRAFORM_AWS_PROVIDER_VERSION: "", NAMESPACE: project.Namespace, } - envDir := project.EnvDir + var statePath string + if name == "infra" { + statePath = filepath.Join(project.EnvDir, name) + } logrus.Debugf("backend opts: %s", backendOpts) - logrus.Debugf("ENV dir path: %s", envDir) + logrus.Debugf("state dir path: %s", statePath) err := template.GenerateBackendTf( backendOpts, - filepath.Join(envDir, name), + statePath, ) if err != nil { pterm.Error.Printfln("Generate terraform file for \"%s\" not completed", name) @@ -160,11 +163,11 @@ func GenerateTerraformFiles(name string, terraformStateBucketName string, projec } logrus.Debugf("backend opts: %s", varsOpts) - logrus.Debugf("ENV dir path: %s", envDir) + logrus.Debugf("state dir path: %s", statePath) err = template.GenerateVarsTf( varsOpts, - filepath.Join(envDir, name), + statePath, ) if err != nil { pterm.Error.Printfln("Generate terraform file for \"%s\" not completed", name) From 66aa4912efc8299c9cdfe229c8d4c5935e0a3213 Mon Sep 17 00:00:00 2001 From: psihachina Date: Wed, 28 Sep 2022 08:51:52 +0300 Subject: [PATCH 08/19] added support multiple terraform states & configurations --- internal/commands/down.go | 91 +++++------- internal/commands/down_infra.go | 21 ++- internal/commands/terraform.go | 4 +- internal/commands/tfenv.go | 5 +- internal/commands/up.go | 244 +++----------------------------- internal/commands/up_apps.go | 2 - internal/commands/up_infra.go | 138 +++++++++++++++++- internal/config/project.go | 3 + internal/terraform/docker.go | 16 ++- internal/terraform/local.go | 26 +++- 10 files changed, 251 insertions(+), 299 deletions(-) diff --git a/internal/commands/down.go b/internal/commands/down.go index 3ac8ef4d..ebd47e97 100644 --- a/internal/commands/down.go +++ b/internal/commands/down.go @@ -16,6 +16,7 @@ import ( "github.com/pterm/pterm" "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "time" ) type DownOptions struct { @@ -172,7 +173,7 @@ func (o *DownOptions) Run() error { return err } } else { - err := destroyApp(ui, o) + err := destroyApp(o.AppName, o.Config, ui) if err != nil { return err } @@ -218,58 +219,34 @@ func destroyAll(ui terminal.UI, o *DownOptions) error { err := manager.InReversDependencyOrder(aws.BackgroundContext(), o.Config.GetApps(), func(c context.Context, name string) error { o.Config.AwsProfile = o.Config.Terraform["infra"].AwsProfile - var m manager.Manager - - if app, ok := o.Config.Serverless[name]; ok { - app.Name = name - m = &serverless.Manager{ - Project: o.Config, - App: app, - } - } - if app, ok := o.Config.Alias[name]; ok { - app.Name = name - m = &alias.Manager{ - Project: o.Config, - App: app, - } - } - if app, ok := o.Config.Ecs[name]; ok { - app.Name = name - m = &ecs.Manager{ - Project: o.Config, - App: app, - } - } - - // destroy - err := m.Destroy(ui) - if err != nil { - return fmt.Errorf("can't destroy app: %w", err) - } - - return nil + return destroyApp(name, o.Config, ui) }) if err != nil { return err } - err = destroyInfra(ui, o.Config, o.SkipGen) - if err != nil { - return err + if _, ok := o.Config.Terraform["infra"]; ok { + err = destroyInfra("infra", o.Config, o.SkipGen, ui) + if err != nil { + return err + } } + err = manager.InReversDependencyOrder(aws.BackgroundContext(), o.Config.GetStates(), func(c context.Context, name string) error { + o.Config.AwsProfile = o.Config.Terraform["infra"].AwsProfile + + return destroyInfra(name, o.Config, o.SkipGen, ui) + }) + ui.Output("Destroy all completed!\n", terminal.WithSuccessStyle()) + time.Sleep(time.Millisecond * 200) return nil } -func destroyInfra(ui terminal.UI, config *config.Project, skipGen bool) error { +func destroyInfra(state string, config *config.Project, skipGen bool, ui terminal.UI) error { if !skipGen { - err := GenerateTerraformFiles( - config, - "", - ) + err := GenerateTerraformFiles(state, "", config) if err != nil { return err } @@ -296,9 +273,9 @@ func destroyInfra(ui terminal.UI, config *config.Project, skipGen bool) error { switch config.PreferRuntime { case "docker": - tf = terraform.NewDockerTerraform(config.Terraform["infra"].Version, []string{"destroy", "-auto-approve"}, env, nil, config) + tf = terraform.NewDockerTerraform(state, []string{"destroy", "-auto-approve"}, env, nil, config) case "native": - tf = terraform.NewLocalTerraform(config.Terraform["infra"].Version, []string{"destroy", "-auto-approve"}, env, nil, config) + tf = terraform.NewLocalTerraform(state, []string{"destroy", "-auto-approve"}, env, nil, config) err = tf.Prepare() if err != nil { return fmt.Errorf("can't destroy infra: %w", err) @@ -319,35 +296,35 @@ func destroyInfra(ui terminal.UI, config *config.Project, skipGen bool) error { return nil } -func destroyApp(ui terminal.UI, o *DownOptions) error { +func destroyApp(name string, cfg *config.Project, ui terminal.UI) error { var m manager.Manager var icon string m = &ecs.Manager{ - Project: o.Config, - App: &config.Ecs{Name: o.AppName}, + Project: cfg, + App: &config.Ecs{Name: name}, } - if app, ok := o.Config.Serverless[o.AppName]; ok { - app.Name = o.AppName + if app, ok := cfg.Serverless[name]; ok { + app.Name = name m = &serverless.Manager{ - Project: o.Config, + Project: cfg, App: app, } icon = app.Icon } - if app, ok := o.Config.Alias[o.AppName]; ok { - app.Name = o.AppName + if app, ok := cfg.Alias[name]; ok { + app.Name = name m = &alias.Manager{ - Project: o.Config, + Project: cfg, App: app, } icon = app.Icon } - if app, ok := o.Config.Ecs[o.AppName]; ok { - app.Name = o.AppName + if app, ok := cfg.Ecs[name]; ok { + app.Name = name m = &ecs.Manager{ - Project: o.Config, + Project: cfg, App: app, } icon = app.Icon @@ -357,16 +334,14 @@ func destroyApp(ui terminal.UI, o *DownOptions) error { icon += " " } - ui.Output("Destroying %s%s app...\n", icon, o.AppName, terminal.WithHeaderStyle()) - sg := ui.StepGroup() - defer sg.Wait() + ui.Output("Destroying %s%s app...\n", icon, name, terminal.WithHeaderStyle()) err := m.Destroy(ui) if err != nil { return fmt.Errorf("can't down: %w", err) } - ui.Output("Destroy app %s%s completed\n", icon, o.AppName, terminal.WithSuccessStyle()) + ui.Output("Destroy app %s%s completed\n", icon, name, terminal.WithSuccessStyle()) return nil } diff --git a/internal/commands/down_infra.go b/internal/commands/down_infra.go index 3bdde052..d545f480 100644 --- a/internal/commands/down_infra.go +++ b/internal/commands/down_infra.go @@ -3,7 +3,9 @@ package commands import ( "context" "fmt" + "github.com/aws/aws-sdk-go/aws" "github.com/hazelops/ize/internal/config" + "github.com/hazelops/ize/internal/manager" "github.com/hazelops/ize/internal/requirements" "github.com/hazelops/ize/pkg/terminal" "github.com/spf13/cobra" @@ -16,6 +18,7 @@ type DownInfraOptions struct { AwsProfile string AwsRegion string SkipGen bool + OnlyInfra bool } func NewDownInfraFlags(project *config.Project) *DownInfraOptions { @@ -55,6 +58,7 @@ func NewCmdDownInfra(project *config.Project) *cobra.Command { cmd.Flags().StringVar(&o.AwsProfile, "infra.terraform.aws-profile", "", "set aws profile") cmd.Flags().StringVar(&o.AwsRegion, "infra.terraform.aws-region", "", "set aws region") cmd.Flags().BoolVar(&o.SkipGen, "skip-gen", false, "skip generating terraform files") + cmd.Flags().BoolVar(&o.OnlyInfra, "only-infra", false, "down only infra state") return cmd } @@ -108,5 +112,20 @@ func (o *DownInfraOptions) Validate() error { func (o *DownInfraOptions) Run() error { ui := o.ui - return destroyInfra(ui, o.Config, o.SkipGen) + + if _, ok := o.Config.Terraform["infra"]; ok { + err := destroyInfra("infra", o.Config, o.SkipGen, ui) + if err != nil { + return err + } + } + + err := manager.InReversDependencyOrder(aws.BackgroundContext(), o.Config.GetApps(), func(c context.Context, name string) error { + return destroyInfra(name, o.Config, o.SkipGen, ui) + }) + if err != nil { + + } + + return nil } diff --git a/internal/commands/terraform.go b/internal/commands/terraform.go index a4e328fd..5bd75204 100644 --- a/internal/commands/terraform.go +++ b/internal/commands/terraform.go @@ -126,9 +126,9 @@ func (o *TerraformOptions) Run(args []string) error { switch o.Config.PreferRuntime { case "docker": - tf = terraform.NewDockerTerraform(o.Version, args, env, nil, o.Config) + tf = terraform.NewDockerTerraform("infra", args, env, nil, o.Config) case "native": - tf = terraform.NewLocalTerraform(o.Version, args, env, nil, o.Config) + tf = terraform.NewLocalTerraform("infra", args, env, nil, o.Config) err = tf.Prepare() if err != nil { return err diff --git a/internal/commands/tfenv.go b/internal/commands/tfenv.go index a06afa1c..453f56f0 100644 --- a/internal/commands/tfenv.go +++ b/internal/commands/tfenv.go @@ -122,9 +122,10 @@ func GenerateTerraformFiles(name string, terraformStateBucketName string, projec TERRAFORM_AWS_PROVIDER_VERSION: "", NAMESPACE: project.Namespace, } - var statePath string + + statePath := filepath.Join(project.EnvDir, name) if name == "infra" { - statePath = filepath.Join(project.EnvDir, name) + statePath = project.EnvDir } logrus.Debugf("backend opts: %s", backendOpts) diff --git a/internal/commands/up.go b/internal/commands/up.go index 8605ebfa..742bacd4 100644 --- a/internal/commands/up.go +++ b/internal/commands/up.go @@ -1,25 +1,16 @@ package commands import ( - "bytes" "context" - "encoding/base64" "fmt" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/ssm" "github.com/hazelops/ize/internal/config" "github.com/hazelops/ize/internal/manager" - "github.com/hazelops/ize/internal/manager/alias" - "github.com/hazelops/ize/internal/manager/ecs" - "github.com/hazelops/ize/internal/manager/serverless" "github.com/hazelops/ize/internal/requirements" - "github.com/hazelops/ize/internal/terraform" "github.com/hazelops/ize/pkg/templates" "github.com/hazelops/ize/pkg/terminal" "github.com/pterm/pterm" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "io/ioutil" ) type UpOptions struct { @@ -179,7 +170,14 @@ func (o *UpOptions) Run() error { return err } } else { - err := deployApp(ui, o) + if _, ok := o.Config.Terraform[o.AppName]; ok { + err := deployInfra(o.AppName, ui, o.Config, o.SkipGen) + if err != nil { + return err + } + } + + err := deployApp(o.AppName, ui, o.Config) if err != nil { return err } @@ -217,239 +215,37 @@ func (o *UpOptions) validateAll() error { } func deployAll(ui terminal.UI, o *UpOptions) error { - err := deployInfra(ui, o.Config, o.SkipGen) - if err != nil { - return err - } - - ui.Output("Deploying apps...", terminal.WithHeaderStyle()) - - err = manager.InDependencyOrder(aws.BackgroundContext(), o.Config.GetApps(), func(c context.Context, name string) error { - o.Config.AwsProfile = o.Config.Terraform["infra"].AwsProfile - - var m manager.Manager - - if app, ok := o.Config.Serverless[name]; ok { - app.Name = name - m = &serverless.Manager{ - Project: o.Config, - App: app, - } - } - if app, ok := o.Config.Alias[name]; ok { - app.Name = name - m = &alias.Manager{ - Project: o.Config, - App: app, - } - } - if app, ok := o.Config.Ecs[name]; ok { - app.Name = name - m = &ecs.Manager{ - Project: o.Config, - App: app, - } - } - - // build app container - err := m.Build(ui) - if err != nil { - return fmt.Errorf("can't build app: %w", err) - } - - // push app image - err = m.Push(ui) - if err != nil { - return fmt.Errorf("can't push app: %w", err) - } - - // deploy app image - err = m.Deploy(ui) + if _, ok := o.Config.Terraform["infra"]; ok { + err := deployInfra("infra", ui, o.Config, o.SkipGen) if err != nil { - return fmt.Errorf("can't deploy app: %w", err) + return err } + } - return nil + err := manager.InDependencyOrder(aws.BackgroundContext(), o.Config.GetStates(), func(c context.Context, name string) error { + return deployInfra(name, ui, o.Config, o.SkipGen) }) if err != nil { return err } - ui.Output("Deploy all completed!\n", terminal.WithSuccessStyle()) - - return nil -} - -func deployApp(ui terminal.UI, o *UpOptions) error { - var m manager.Manager - var icon string - - m = &ecs.Manager{ - Project: o.Config, - App: &config.Ecs{Name: o.AppName}, - } - - if app, ok := o.Config.Serverless[o.AppName]; ok { - app.Name = o.AppName - m = &serverless.Manager{ - Project: o.Config, - App: app, - } - } - if app, ok := o.Config.Alias[o.AppName]; ok { - app.Name = o.AppName - m = &alias.Manager{ - Project: o.Config, - App: app, - } - } - if app, ok := o.Config.Ecs[o.AppName]; ok { - app.Name = o.AppName - m = &ecs.Manager{ - Project: o.Config, - App: app, - } - } - - if len(icon) != 0 { - } - - ui.Output("Deploying %s%s app...", icon, o.AppName, terminal.WithHeaderStyle()) - sg := ui.StepGroup() - defer sg.Wait() - - // build app container - err := m.Build(ui) - if err != nil { - return fmt.Errorf("can't build app: %w", err) - } - - // push app image - err = m.Push(ui) - if err != nil { - return fmt.Errorf("can't push app: %w", err) - } - - // deploy app image - err = m.Deploy(ui) - if err != nil { - return fmt.Errorf("can't deploy app: %w", err) - } - - ui.Output("Deploy app %s%s completed\n", icon, o.AppName, terminal.WithSuccessStyle()) + ui.Output("Deploying apps...", terminal.WithHeaderStyle()) - return nil -} + err = manager.InDependencyOrder(aws.BackgroundContext(), o.Config.GetApps(), func(c context.Context, name string) error { + o.Config.AwsProfile = o.Config.Terraform["infra"].AwsProfile -func deployInfra(ui terminal.UI, config *config.Project, skipGen bool) error { - if !skipGen { - err := GenerateTerraformFiles( - config, - "", - ) + err := deployApp(name, ui, o.Config) if err != nil { return err } - } - - var tf terraform.Terraform - - logrus.Infof("infra: %s", config.Terraform["infra"]) - - v, err := config.Session.Config.Credentials.Get() - if err != nil { - return fmt.Errorf("can't get AWS credentials: %w", err) - } - - env := []string{ - fmt.Sprintf("ENV=%v", config.Env), - fmt.Sprintf("AWS_PROFILE=%v", config.Terraform["infra"].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), - fmt.Sprintf("AWS_SECRET_ACCESS_KEY=%v", v.SecretAccessKey), - fmt.Sprintf("AWS_SESSION_TOKEN=%v", v.SessionToken), - } - - switch config.PreferRuntime { - case "docker": - tf = terraform.NewDockerTerraform(config.Terraform["infra"].Version, []string{"init", "-input=true"}, env, nil, config) - case "native": - tf = terraform.NewLocalTerraform(config.Terraform["infra"].Version, []string{"init", "-input=true"}, env, nil, config) - err = tf.Prepare() - if err != nil { - return fmt.Errorf("can't deploy all: %w", err) - } - default: - return fmt.Errorf("can't supported %s runtime", config.PreferRuntime) - } - - ui.Output(fmt.Sprintf("[%s] Running deploy infra...", config.Env), terminal.WithHeaderStyle()) - ui.Output("Execution terraform init...", terminal.WithHeaderStyle()) - - err = tf.RunUI(ui) - if err != nil { - return fmt.Errorf("can't deploy all: %w", err) - } - - ui.Output("Execution terraform plan...", terminal.WithHeaderStyle()) - - outPath := fmt.Sprintf("%s/.terraform/tfplan", config.EnvDir) - //terraform plan run options - tf.NewCmd([]string{"plan", fmt.Sprintf("-out=%s", outPath)}) - - err = tf.RunUI(ui) - if err != nil { - return err - } - - //terraform apply run options - tf.NewCmd([]string{"apply", "-auto-approve", outPath}) - - ui.Output("Execution terraform apply...", terminal.WithHeaderStyle()) - - err = tf.RunUI(ui) - if err != nil { - return err - } - - //terraform output run options - - tf.NewCmd([]string{"output", "-json"}) - - var output bytes.Buffer - - tf.SetOut(&output) - - ui.Output("Execution terraform output...", terminal.WithHeaderStyle()) - - err = tf.RunUI(ui) - if err != nil { - return err - } - - name := fmt.Sprintf("/%s/terraform-output", config.Env) - - byteValue, _ := ioutil.ReadAll(&output) - sDec := base64.StdEncoding.EncodeToString(byteValue) - if err != nil { - return err - } - - _, err = ssm.New(config.Session).PutParameter(&ssm.PutParameterInput{ - Name: &name, - Value: aws.String(sDec), - Type: aws.String(ssm.ParameterTypeSecureString), - Overwrite: aws.Bool(true), - Tier: aws.String(ssm.ParameterTierIntelligentTiering), - DataType: aws.String("text"), + return nil }) if err != nil { return err } - ui.Output("Deploy infra completed!\n", terminal.WithSuccessStyle()) + ui.Output("Deploy all completed!\n", terminal.WithSuccessStyle()) return nil } diff --git a/internal/commands/up_apps.go b/internal/commands/up_apps.go index 5cdec737..af7c4822 100644 --- a/internal/commands/up_apps.go +++ b/internal/commands/up_apps.go @@ -153,8 +153,6 @@ func deployApp(name string, ui terminal.UI, cfg *config.Project) error { } ui.Output("Deploying %s%s app...", icon, name, terminal.WithHeaderStyle()) - sg := ui.StepGroup() - defer sg.Wait() // build app container err := m.Build(ui) diff --git a/internal/commands/up_infra.go b/internal/commands/up_infra.go index b46e1a2f..a06b8506 100644 --- a/internal/commands/up_infra.go +++ b/internal/commands/up_infra.go @@ -1,13 +1,22 @@ package commands import ( + "bytes" "context" + "encoding/base64" "fmt" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ssm" "github.com/hazelops/ize/internal/config" + "github.com/hazelops/ize/internal/manager" "github.com/hazelops/ize/internal/requirements" + "github.com/hazelops/ize/internal/terraform" "github.com/hazelops/ize/pkg/templates" "github.com/hazelops/ize/pkg/terminal" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "io/ioutil" + "path/filepath" ) type UpInfraOptions struct { @@ -132,5 +141,132 @@ func (o *UpInfraOptions) Validate() error { func (o *UpInfraOptions) Run() error { ui := o.UI - return deployInfra(ui, o.Config, o.SkipGen) + if _, ok := o.Config.Terraform["infra"]; ok { + err := deployInfra("infra", ui, o.Config, o.SkipGen) + if err != nil { + return err + } + } + + err := manager.InDependencyOrder(aws.BackgroundContext(), o.Config.GetStates(), func(c context.Context, name string) error { + return deployInfra(name, ui, o.Config, o.SkipGen) + }) + if err != nil { + return err + } + + return nil +} + +func deployInfra(name string, ui terminal.UI, config *config.Project, skipGen bool) error { + if !skipGen { + err := GenerateTerraformFiles(name, "", config) + if err != nil { + return err + } + } + + var tf terraform.Terraform + + logrus.Infof("infra: %s", config.Terraform["infra"]) + + v, err := config.Session.Config.Credentials.Get() + if err != nil { + return fmt.Errorf("can't get AWS credentials: %w", err) + } + + env := []string{ + fmt.Sprintf("ENV=%v", config.Env), + fmt.Sprintf("AWS_PROFILE=%v", config.Terraform["infra"].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), + fmt.Sprintf("AWS_SECRET_ACCESS_KEY=%v", v.SecretAccessKey), + fmt.Sprintf("AWS_SESSION_TOKEN=%v", v.SessionToken), + } + + switch config.PreferRuntime { + case "docker": + tf = terraform.NewDockerTerraform(name, []string{"init", "-input=true"}, env, nil, config) + case "native": + tf = terraform.NewLocalTerraform(name, []string{"init", "-input=true"}, env, nil, config) + err = tf.Prepare() + if err != nil { + return fmt.Errorf("can't deploy infra: %w", err) + } + default: + return fmt.Errorf("can't supported %s runtime", config.PreferRuntime) + } + + ui.Output(fmt.Sprintf("[%s][%s] Running deploy infra...", config.Env, name), terminal.WithHeaderStyle()) + ui.Output("Execution terraform init...", terminal.WithHeaderStyle()) + + err = tf.RunUI(ui) + if err != nil { + return fmt.Errorf("can't deploy infra: %w", err) + } + + ui.Output("Execution terraform plan...", terminal.WithHeaderStyle()) + + outPath := filepath.Join(config.EnvDir, name, ".terraform", "tfplan") + if name == "infra" { + outPath = filepath.Join(config.EnvDir, ".terraform", "tfplan") + } + + //terraform plan run options + tf.NewCmd([]string{"plan", fmt.Sprintf("-out=%s", outPath)}) + + err = tf.RunUI(ui) + if err != nil { + return fmt.Errorf("can't deploy infra: %w", err) + } + + //terraform apply run options + tf.NewCmd([]string{"apply", "-auto-approve", outPath}) + + ui.Output("Execution terraform apply...", terminal.WithHeaderStyle()) + + err = tf.RunUI(ui) + if err != nil { + return fmt.Errorf("can't deploy infra: %w", err) + } + + //terraform output run options + + tf.NewCmd([]string{"output", "-json"}) + + var output bytes.Buffer + + tf.SetOut(&output) + + ui.Output("Execution terraform output...", terminal.WithHeaderStyle()) + + err = tf.RunUI(ui) + if err != nil { + return fmt.Errorf("can't deploy infra: %w", err) + } + + parameterName := fmt.Sprintf("/%s/terraform-output", config.Env) + + byteValue, _ := ioutil.ReadAll(&output) + sDec := base64.StdEncoding.EncodeToString(byteValue) + if err != nil { + return err + } + + _, err = ssm.New(config.Session).PutParameter(&ssm.PutParameterInput{ + Name: ¶meterName, + Value: aws.String(sDec), + Type: aws.String(ssm.ParameterTypeSecureString), + Overwrite: aws.Bool(true), + Tier: aws.String(ssm.ParameterTierIntelligentTiering), + DataType: aws.String("text"), + }) + if err != nil { + return err + } + + ui.Output("Deploy infra completed!\n", terminal.WithSuccessStyle()) + + return nil } diff --git a/internal/config/project.go b/internal/config/project.go index 03120f9c..2d76430f 100644 --- a/internal/config/project.go +++ b/internal/config/project.go @@ -168,6 +168,9 @@ func (p *Project) GetStates() map[string]*interface{} { states := map[string]*interface{}{} for name, body := range p.Terraform { + if name == "infra" { + continue + } var v interface{} v = map[string]interface{}{ "depends_on": body.DependsOn, diff --git a/internal/terraform/docker.go b/internal/terraform/docker.go index 94681577..ea4155fe 100644 --- a/internal/terraform/docker.go +++ b/internal/terraform/docker.go @@ -6,6 +6,7 @@ import ( "github.com/hazelops/ize/internal/config" "io" "os" + "path/filepath" "strings" "time" @@ -52,11 +53,14 @@ type docker struct { env []string output io.Writer project *config.Project + state string } -func NewDockerTerraform(version string, command []string, env []string, out io.Writer, project *config.Project) *docker { +func NewDockerTerraform(state string, command []string, env []string, out io.Writer, project *config.Project) *docker { + project.Terraform[state].Version = project.TerraformVersion return &docker{ - version: version, + state: state, + version: project.Terraform[state].Version, command: command, env: env, output: out, @@ -146,6 +150,10 @@ func (d *docker) RunUI(ui terminal.UI) error { } logrus.Infof("image name: %s, image tag: %s", imageName, imageTag) + stateDir := filepath.Join(d.project.EnvDir, d.state) + if d.state == "infra" { + stateDir = d.project.EnvDir + } contConfig := &container.Config{ User: fmt.Sprintf("%v:%v", os.Getuid(), os.Getgid()), @@ -156,7 +164,7 @@ func (d *docker) RunUI(ui terminal.UI) error { AttachStdout: true, AttachStderr: true, OpenStdin: true, - WorkingDir: fmt.Sprintf("%v", d.project.EnvDir), + WorkingDir: stateDir, Env: d.env, } @@ -181,7 +189,7 @@ func (d *docker) RunUI(ui terminal.UI) error { }, } - s.Update("[%s] running terraform image %v:%v...", d.project.Env, imageName, imageTag) + s.Update("[%s][%s] running terraform image %v:%v...", d.project.Env, d.state, imageName, imageTag) cont, err := cli.ContainerCreate( context.Background(), diff --git a/internal/terraform/local.go b/internal/terraform/local.go index 68b41d96..95c180ed 100644 --- a/internal/terraform/local.go +++ b/internal/terraform/local.go @@ -35,11 +35,17 @@ type local struct { output io.Writer tfpath string project *config.Project + state string } -func NewLocalTerraform(version string, command []string, env []string, out io.Writer, project *config.Project) *local { +func NewLocalTerraform(state string, command []string, env []string, out io.Writer, project *config.Project) *local { + if len(project.Terraform[state].Version) == 0 { + project.Terraform[state].Version = project.TerraformVersion + } + return &local{ - version: version, + state: state, + version: project.Terraform[state].Version, command: command, env: env, output: out, @@ -52,8 +58,13 @@ func (l *local) Run() error { l.project.EnvDir = "." } + stateDir := filepath.Join(l.project.EnvDir, l.state) + if l.state == "infra" { + stateDir = l.project.EnvDir + } + cmd := exec.Command(l.tfpath, l.command...) - cmd.Dir = l.project.EnvDir + cmd.Dir = stateDir err := term.New(term.WithDir(l.project.EnvDir), term.WithStdin(os.Stdin)).InteractiveRun(cmd) if err != nil { @@ -130,7 +141,7 @@ func (l *local) RunUI(ui terminal.UI) error { sg := ui.StepGroup() defer sg.Wait() - s := sg.Add("[%s] Running terraform v%s...", l.project.Env, l.version) + s := sg.Add("[%s][%s] Running terraform v%s...", l.project.Env, l.state, l.version) defer func() { s.Abort(); time.Sleep(time.Millisecond * 100) }() stdout := s.TermOutput() @@ -142,8 +153,13 @@ func (l *local) RunUI(ui terminal.UI) error { l.project.EnvDir = "." } + stateDir := filepath.Join(l.project.EnvDir, l.state) + if l.state == "infra" { + stateDir = l.project.EnvDir + } + cmd := exec.Command(l.tfpath, l.command...) - cmd.Dir = l.project.EnvDir + cmd.Dir = stateDir _, _, err := runCommand(cmd, stdout) if err != nil { From 44c11eac52c6203986426466dad32ddeacc104b4 Mon Sep 17 00:00:00 2001 From: psihachina Date: Wed, 28 Sep 2022 08:53:37 +0300 Subject: [PATCH 09/19] fixed terraform e2e test --- examples/sls-apps-monorepo/ize.toml | 22 ++++++++++++++++++++++ test-e2e/terraform_test.go | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 examples/sls-apps-monorepo/ize.toml diff --git a/examples/sls-apps-monorepo/ize.toml b/examples/sls-apps-monorepo/ize.toml new file mode 100644 index 00000000..42787f0e --- /dev/null +++ b/examples/sls-apps-monorepo/ize.toml @@ -0,0 +1,22 @@ +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/test-e2e/terraform_test.go b/test-e2e/terraform_test.go index b9af7768..c075d465 100644 --- a/test-e2e/terraform_test.go +++ b/test-e2e/terraform_test.go @@ -157,7 +157,7 @@ func TestIzeTerraformInit(t *testing.T) { t.Errorf("unexpected stderr output ize gen tfenv: %s", err) } - if !strings.Contains(stdout, "Generate terraform files completed") { + if !strings.Contains(stdout, "Generate terraform file for \"infra\" completed") { t.Errorf("No success message detected after gen tfenv:\n%s", stdout) } From dda6448685ea296d486292c2277fc9f193111143 Mon Sep 17 00:00:00 2001 From: psihachina Date: Wed, 28 Sep 2022 13:51:55 +0300 Subject: [PATCH 10/19] updated .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index f051c9be..7323e288 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ ize .terraform examples/**/.ize/env/* !examples/**/.ize/env/testnut +backend.tf +terraform.tfvars From 6487177eac7b96e5be72202333a79426a358186b Mon Sep 17 00:00:00 2001 From: psihachina Date: Wed, 28 Sep 2022 13:55:04 +0300 Subject: [PATCH 11/19] updated ize.toml in examples/multistate-monorepo --- .../.ize/env/testnut/api/backend.tf | 21 ------------------- .../.ize/env/testnut/api/terraform.tfvars | 6 ------ .../.ize/env/testnut/infra/backend.tf | 21 ------------------- .../.ize/env/testnut/infra/terraform.tfvars | 6 ------ .../.ize/env/testnut/ize.toml | 5 +---- .../.ize/env/testnut/vpc/backend.tf | 21 ------------------- .../.ize/env/testnut/vpc/terraform.tfvars | 6 ------ 7 files changed, 1 insertion(+), 85 deletions(-) delete mode 100644 examples/multistate-monorepo/.ize/env/testnut/api/backend.tf delete mode 100644 examples/multistate-monorepo/.ize/env/testnut/api/terraform.tfvars delete mode 100644 examples/multistate-monorepo/.ize/env/testnut/infra/backend.tf delete mode 100644 examples/multistate-monorepo/.ize/env/testnut/infra/terraform.tfvars delete mode 100644 examples/multistate-monorepo/.ize/env/testnut/vpc/backend.tf delete mode 100644 examples/multistate-monorepo/.ize/env/testnut/vpc/terraform.tfvars diff --git a/examples/multistate-monorepo/.ize/env/testnut/api/backend.tf b/examples/multistate-monorepo/.ize/env/testnut/api/backend.tf deleted file mode 100644 index cb97cefd..00000000 --- a/examples/multistate-monorepo/.ize/env/testnut/api/backend.tf +++ /dev/null @@ -1,21 +0,0 @@ -provider "aws" { - profile = var.aws_profile - region = var.aws_region - default_tags { - tags = { - env = "testnut" - namespace = "testnut" - terraform = "true" - } - } -} - -terraform { - backend "s3" { - bucket = "testnut-tf-state" - key = "testnut/api.tfstate" - region = "us-east-1" - profile = "default" - dynamodb_table = "tf-state-lock" - } -} diff --git a/examples/multistate-monorepo/.ize/env/testnut/api/terraform.tfvars b/examples/multistate-monorepo/.ize/env/testnut/api/terraform.tfvars deleted file mode 100644 index 5c47533c..00000000 --- a/examples/multistate-monorepo/.ize/env/testnut/api/terraform.tfvars +++ /dev/null @@ -1,6 +0,0 @@ -env = "testnut" -aws_profile = "default" -aws_region = "us-east-1" -ec2_key_pair_name = "testnut-testnut" -ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDvQNbvuHiuFdoJZSCYZDt6Npq/QWk+/Bz8MPKiBCQjzBDnHpFzRNVmpAo63GT5BTvmZ2fWzByM1H2O+/hRYYxAEcK45pmvpJlRwMNb5uwTwpniOc0ojO9fR/3EfusnUza37Ag9t3csqjv5H06mrYysPj4kBkAp19a/yCsmssg+3sJOKE4NsO19CRsJ3cCgazJIX7eEoM0L2XI0NX0el8q5r4voe30rNbp/uNBli4LkmdyZ7bUINu85pMsUT3qmRT+9FPC4E7Ega90zAqwZOkwadYia+nUMQPEJjcrycrU/En10D1jDMZxqFjGOSlkatGEKAGoqIj3wIru8mrF6PcyEB59qH65tz9iUjDyR/7k/CWsQXaDL30cO5aLLLl8uQZv/ppkFc0SPyZebS3dqWov/J+aIW7cdKEgkjyiWBabhoIA2MOYz03y07EANTyyNG+lLAOrYWTo6H+BcgSipXzDnnYExJhi7vX6uM8QvHpigylnQcVU3KX/GACv8YHd934lFvKtODo1NS0dXEZgoeH+aSWE9zDhHNEKe2eHvj7m/9ujGQdnYlSPVeYlCEUXN3QfYbvKwJt4pukqfJI+pgo18ReWb4GV57VuuJribERrJtNwZ1pL7Sg4+Gghp35mFebL3rh7c0MyJPK4s5cQlexKtP7XahakObRwgEd4VOla/gw== psihachina@gmail.com" -namespace = "testnut" diff --git a/examples/multistate-monorepo/.ize/env/testnut/infra/backend.tf b/examples/multistate-monorepo/.ize/env/testnut/infra/backend.tf deleted file mode 100644 index 3c3f4f7c..00000000 --- a/examples/multistate-monorepo/.ize/env/testnut/infra/backend.tf +++ /dev/null @@ -1,21 +0,0 @@ -provider "aws" { - profile = var.aws_profile - region = var.aws_region - default_tags { - tags = { - env = "testnut" - namespace = "testnut" - terraform = "true" - } - } -} - -terraform { - backend "s3" { - bucket = "testnut-tf-state" - key = "testnut/infra.tfstate" - region = "us-east-1" - profile = "default" - dynamodb_table = "tf-state-lock" - } -} diff --git a/examples/multistate-monorepo/.ize/env/testnut/infra/terraform.tfvars b/examples/multistate-monorepo/.ize/env/testnut/infra/terraform.tfvars deleted file mode 100644 index 5c47533c..00000000 --- a/examples/multistate-monorepo/.ize/env/testnut/infra/terraform.tfvars +++ /dev/null @@ -1,6 +0,0 @@ -env = "testnut" -aws_profile = "default" -aws_region = "us-east-1" -ec2_key_pair_name = "testnut-testnut" -ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDvQNbvuHiuFdoJZSCYZDt6Npq/QWk+/Bz8MPKiBCQjzBDnHpFzRNVmpAo63GT5BTvmZ2fWzByM1H2O+/hRYYxAEcK45pmvpJlRwMNb5uwTwpniOc0ojO9fR/3EfusnUza37Ag9t3csqjv5H06mrYysPj4kBkAp19a/yCsmssg+3sJOKE4NsO19CRsJ3cCgazJIX7eEoM0L2XI0NX0el8q5r4voe30rNbp/uNBli4LkmdyZ7bUINu85pMsUT3qmRT+9FPC4E7Ega90zAqwZOkwadYia+nUMQPEJjcrycrU/En10D1jDMZxqFjGOSlkatGEKAGoqIj3wIru8mrF6PcyEB59qH65tz9iUjDyR/7k/CWsQXaDL30cO5aLLLl8uQZv/ppkFc0SPyZebS3dqWov/J+aIW7cdKEgkjyiWBabhoIA2MOYz03y07EANTyyNG+lLAOrYWTo6H+BcgSipXzDnnYExJhi7vX6uM8QvHpigylnQcVU3KX/GACv8YHd934lFvKtODo1NS0dXEZgoeH+aSWE9zDhHNEKe2eHvj7m/9ujGQdnYlSPVeYlCEUXN3QfYbvKwJt4pukqfJI+pgo18ReWb4GV57VuuJribERrJtNwZ1pL7Sg4+Gghp35mFebL3rh7c0MyJPK4s5cQlexKtP7XahakObRwgEd4VOla/gw== psihachina@gmail.com" -namespace = "testnut" diff --git a/examples/multistate-monorepo/.ize/env/testnut/ize.toml b/examples/multistate-monorepo/.ize/env/testnut/ize.toml index 85288089..7491ed97 100644 --- a/examples/multistate-monorepo/.ize/env/testnut/ize.toml +++ b/examples/multistate-monorepo/.ize/env/testnut/ize.toml @@ -3,12 +3,9 @@ namespace = "testnut" # (required) Namespace of the project ca terraform_version = "1.2.6" # (optional) Terraform version can be set here. 1.1.3 by default [terraform.api] -state_name = "" depends_on = ["vpc"] [terraform.vpc] -state_name = "" depends_on = ["infra"] -[terraform.main] -state_name = "" \ No newline at end of file +[terraform.infra] diff --git a/examples/multistate-monorepo/.ize/env/testnut/vpc/backend.tf b/examples/multistate-monorepo/.ize/env/testnut/vpc/backend.tf deleted file mode 100644 index 1bb93acc..00000000 --- a/examples/multistate-monorepo/.ize/env/testnut/vpc/backend.tf +++ /dev/null @@ -1,21 +0,0 @@ -provider "aws" { - profile = var.aws_profile - region = var.aws_region - default_tags { - tags = { - env = "testnut" - namespace = "testnut" - terraform = "true" - } - } -} - -terraform { - backend "s3" { - bucket = "testnut-tf-state" - key = "testnut/vpc.tfstate" - region = "us-east-1" - profile = "default" - dynamodb_table = "tf-state-lock" - } -} diff --git a/examples/multistate-monorepo/.ize/env/testnut/vpc/terraform.tfvars b/examples/multistate-monorepo/.ize/env/testnut/vpc/terraform.tfvars deleted file mode 100644 index 5c47533c..00000000 --- a/examples/multistate-monorepo/.ize/env/testnut/vpc/terraform.tfvars +++ /dev/null @@ -1,6 +0,0 @@ -env = "testnut" -aws_profile = "default" -aws_region = "us-east-1" -ec2_key_pair_name = "testnut-testnut" -ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDvQNbvuHiuFdoJZSCYZDt6Npq/QWk+/Bz8MPKiBCQjzBDnHpFzRNVmpAo63GT5BTvmZ2fWzByM1H2O+/hRYYxAEcK45pmvpJlRwMNb5uwTwpniOc0ojO9fR/3EfusnUza37Ag9t3csqjv5H06mrYysPj4kBkAp19a/yCsmssg+3sJOKE4NsO19CRsJ3cCgazJIX7eEoM0L2XI0NX0el8q5r4voe30rNbp/uNBli4LkmdyZ7bUINu85pMsUT3qmRT+9FPC4E7Ega90zAqwZOkwadYia+nUMQPEJjcrycrU/En10D1jDMZxqFjGOSlkatGEKAGoqIj3wIru8mrF6PcyEB59qH65tz9iUjDyR/7k/CWsQXaDL30cO5aLLLl8uQZv/ppkFc0SPyZebS3dqWov/J+aIW7cdKEgkjyiWBabhoIA2MOYz03y07EANTyyNG+lLAOrYWTo6H+BcgSipXzDnnYExJhi7vX6uM8QvHpigylnQcVU3KX/GACv8YHd934lFvKtODo1NS0dXEZgoeH+aSWE9zDhHNEKe2eHvj7m/9ujGQdnYlSPVeYlCEUXN3QfYbvKwJt4pukqfJI+pgo18ReWb4GV57VuuJribERrJtNwZ1pL7Sg4+Gghp35mFebL3rh7c0MyJPK4s5cQlexKtP7XahakObRwgEd4VOla/gw== psihachina@gmail.com" -namespace = "testnut" From 32be2ee083325757008382ff7ac590beb3b7feab Mon Sep 17 00:00:00 2001 From: psihachina Date: Wed, 28 Sep 2022 13:55:42 +0300 Subject: [PATCH 12/19] renamed `statePath` to `stackPath` --- internal/commands/tfenv.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/commands/tfenv.go b/internal/commands/tfenv.go index 453f56f0..c41a4f04 100644 --- a/internal/commands/tfenv.go +++ b/internal/commands/tfenv.go @@ -123,17 +123,17 @@ func GenerateTerraformFiles(name string, terraformStateBucketName string, projec NAMESPACE: project.Namespace, } - statePath := filepath.Join(project.EnvDir, name) + stackPath := filepath.Join(project.EnvDir, name) if name == "infra" { - statePath = project.EnvDir + stackPath = project.EnvDir } logrus.Debugf("backend opts: %s", backendOpts) - logrus.Debugf("state dir path: %s", statePath) + logrus.Debugf("state dir path: %s", stackPath) err := template.GenerateBackendTf( backendOpts, - statePath, + stackPath, ) if err != nil { pterm.Error.Printfln("Generate terraform file for \"%s\" not completed", name) @@ -164,11 +164,11 @@ func GenerateTerraformFiles(name string, terraformStateBucketName string, projec } logrus.Debugf("backend opts: %s", varsOpts) - logrus.Debugf("state dir path: %s", statePath) + logrus.Debugf("state dir path: %s", stackPath) err = template.GenerateVarsTf( varsOpts, - statePath, + stackPath, ) if err != nil { pterm.Error.Printfln("Generate terraform file for \"%s\" not completed", name) From a99608226e6435864e75095e9b7229e2b0926b26 Mon Sep 17 00:00:00 2001 From: psihachina Date: Thu, 29 Sep 2022 09:08:46 +0300 Subject: [PATCH 13/19] added `explain` flag When `explain` flag is set, the command shows an alternative to bash that will do exactly what the ize command does --- internal/commands/up_infra.go | 67 +++++++++++++++++++++++++++++++++++ internal/config/config.go | 16 +++++++++ 2 files changed, 83 insertions(+) diff --git a/internal/commands/up_infra.go b/internal/commands/up_infra.go index a06b8506..4c0e2b63 100644 --- a/internal/commands/up_infra.go +++ b/internal/commands/up_infra.go @@ -26,6 +26,7 @@ type UpInfraOptions struct { AwsRegion string Version string UI terminal.UI + Explain bool } var upInfraLongDesc = templates.LongDesc(` @@ -80,6 +81,7 @@ func NewCmdUpInfra(project *config.Project) *cobra.Command { } cmd.Flags().BoolVar(&o.SkipGen, "skip-gen", false, "skip generating terraform files") + cmd.Flags().BoolVar(&o.Explain, "explain", false, "bash alternative shown") cmd.Flags().StringVar(&o.Version, "infra.terraform.version", "", "set terraform version") cmd.Flags().StringVar(&o.AwsRegion, "infra.terraform.aws-region", "", "set aws region") cmd.Flags().StringVar(&o.AwsProfile, "infra.terraform.aws-profile", "", "set aws profile") @@ -113,6 +115,10 @@ func (o *UpInfraOptions) Complete() error { 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 } @@ -139,6 +145,67 @@ func (o *UpInfraOptions) Validate() error { } func (o *UpInfraOptions) Run() error { + if o.Explain { + tmpl := `# Change to the dir +cd {{.EnvDir}} + +# Generate Backend.tf +cat << EOF > backend.tf +provider "aws" { + profile = var.aws_profile + region = var.aws_region + default_tags { + tags = { + env = "{{.Env}}" + namespace = "{{.Namespace}}" + terraform = "true" + } + } +} + +terraform { + backend "s3" { + bucket = "{{.Namespace}}-tf-state" + key = "{{.Env}}/terraform.tfstate" + region = "{{.Terraform.infra.StateBucketRegion}}" + profile = "{{.AwsProfile}}" + dynamodb_table = "tf-state-lock" + } +} + +EOF + +# Generate variables.tfvars +cat << EOF > variables.tfvars +env = "{{.Env}}" +aws_profile = "{{.AwsProfile}}" +aws_region = "{{.AwsRegion}}" +ec2_key_pair_name = "{{.Env}}-{{.Namespace}}" +docker_image_tag = "{{.Tag}}" +ssh_public_key = "" +docker_registry = "{{.DockerRegistry}}" +namespace = "{{.Namespace}}" +root_domain_name = "{{.Terraform.infra.RootDomainName}}" + +EOF + +# Ensure Terraform is v {{.TerraformVersion}} +terraform --version + +# Terraform Plan +terraform plan + +# Terraform Apply +terraform apply +` + err := o.Config.Generate(tmpl) + if err != nil { + return err + } + + return nil + } + ui := o.UI if _, ok := o.Config.Terraform["infra"]; ok { diff --git a/internal/config/config.go b/internal/config/config.go index b2ac2632..0fc7c154 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -10,6 +10,7 @@ import ( "path/filepath" "reflect" "strings" + "text/template" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/sts" @@ -563,3 +564,18 @@ func GetApps(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirecti return apps, cobra.ShellCompDirectiveNoFileComp } + +func (p *Project) Generate(tmpl string) error { + t := template.New("template") + t, err := t.Parse(tmpl) + if err != nil { + return err + } + + err = t.Execute(os.Stdout, p) + if err != nil { + return err + } + + return nil +} From 5e71166787174a29d9188707e0ee5c2de94681a9 Mon Sep 17 00:00:00 2001 From: psihachina Date: Thu, 29 Sep 2022 11:00:57 +0300 Subject: [PATCH 14/19] added call `terraform init` before `terraform destroy` in `ize down`/`ize down infra` commands --- internal/commands/down.go | 16 +++++++++++++--- internal/commands/down_infra.go | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/internal/commands/down.go b/internal/commands/down.go index ebd47e97..7a51ad7d 100644 --- a/internal/commands/down.go +++ b/internal/commands/down.go @@ -273,9 +273,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 +284,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..5eff011e 100644 --- a/internal/commands/down_infra.go +++ b/internal/commands/down_infra.go @@ -120,7 +120,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 { From 01c8478af3e3d548e6780d85ba98e3c1b05683cc Mon Sep 17 00:00:00 2001 From: psihachina Date: Fri, 30 Sep 2022 14:39:52 +0300 Subject: [PATCH 15/19] changed logic of generating terraform state key --- internal/commands/tfenv.go | 36 ++++++++++++++++++++++++++++++--- internal/commands/tfenv_test.go | 15 ++++++++++---- 2 files changed, 44 insertions(+), 7 deletions(-) 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{ From 33263d813833b78177804c30631c9f78e17135d4 Mon Sep 17 00:00:00 2001 From: psihachina Date: Fri, 30 Sep 2022 14:40:23 +0300 Subject: [PATCH 16/19] updated examples --- .../.ize/env/testnut/{infra => }/main.tf | 0 .../.ize/env/testnut/{infra => }/variables.tf | 0 examples/sls-apps-monorepo/ize.toml | 22 ------------------- 3 files changed, 22 deletions(-) rename examples/multistate-monorepo/.ize/env/testnut/{infra => }/main.tf (100%) rename examples/multistate-monorepo/.ize/env/testnut/{infra => }/variables.tf (100%) delete mode 100644 examples/sls-apps-monorepo/ize.toml 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.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` From 3afa764d938ff6fd8956638e934f0c2479b6e4a3 Mon Sep 17 00:00:00 2001 From: psihachina Date: Fri, 30 Sep 2022 14:44:55 +0300 Subject: [PATCH 17/19] removed `infra` as default stack --- internal/commands/down.go | 21 ++++++++-------- internal/commands/down_infra.go | 39 +++++++++++++++--------------- internal/commands/up.go | 21 ++++++++-------- internal/commands/up_infra.go | 43 +++++++++++++++++---------------- internal/config/config.go | 1 - 5 files changed, 64 insertions(+), 61 deletions(-) diff --git a/internal/commands/down.go b/internal/commands/down.go index ebd47e97..6eac97da 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 { diff --git a/internal/commands/down_infra.go b/internal/commands/down_infra.go index d545f480..74a97ced 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) 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 a06b8506..2dabf556 100644 --- a/internal/commands/up_infra.go +++ b/internal/commands/up_infra.go @@ -93,32 +93,33 @@ 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.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) @@ -168,7 +169,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 { @@ -177,7 +178,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 b2ac2632..9a28d315 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -240,7 +240,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 { From 4b28a586d1cf8c0036fbdf5ad3047bacfd903bb6 Mon Sep 17 00:00:00 2001 From: psihachina Date: Fri, 30 Sep 2022 15:17:08 +0300 Subject: [PATCH 18/19] updated sls monorepo example --- examples/sls-apps-monorepo/.ize/env/testnut/ize.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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. From fa1d3a92155b72635df051d967a3c86cdbeffb0d Mon Sep 17 00:00:00 2001 From: Nikita Podshivalov <47272597+psihachina@users.noreply.github.com> Date: Fri, 30 Sep 2022 18:55:13 +0300 Subject: [PATCH 19/19] updated ize.toml --- .../.ize/env/testnut/ize.toml | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/examples/multistate-monorepo/.ize/env/testnut/ize.toml b/examples/multistate-monorepo/.ize/env/testnut/ize.toml index 7491ed97..e4df0421 100644 --- a/examples/multistate-monorepo/.ize/env/testnut/ize.toml +++ b/examples/multistate-monorepo/.ize/env/testnut/ize.toml @@ -1,11 +1,60 @@ 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] +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. +# state_bucket_name = "" # (optional) Terraform state bucket name can be specified here. Normally it's generated and defaults to -tf-state +# 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` [terraform.api] +# 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. +# state_bucket_name = "" # (optional) Terraform state bucket name can be specified here. Normally it's generated and defaults to -tf-state +# 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` depends_on = ["vpc"] [terraform.vpc] +# 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. +# state_bucket_name = "" # (optional) Terraform state bucket name can be specified here. Normally it's generated and defaults to -tf-state +# 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` depends_on = ["infra"] -[terraform.infra] +# [ecs.] +# timeout = "" # (optional) ECS deployment timeout can be specified here. +# docker_registry = "" # (optional) Docker registry can be set here. By default it uses ECR repo with the name of the service. +# skip_deploy = false # skip deploy app +# path = "" # (optional) Path to ecs app folder can be specified here. By default it's derived from apps path and app name. +# unsafe = false # (optional) Enables unsafe mode that increases deploy time on a cost of shorter healtchecks. +# image = "" # (optional) Docker image can be specified here. By default it's derived from the app name. +# cluster = "" # (optional) ECS cluster can be specified here. By default it's derived from env & namespace +# task_definition_revision = "" # (optional) Task definition revision can be specified here. By default latest revision is used to perform a deployment. Normally this parameter can be used via cli during specific deployment needs. + +# [serverless.] +# node_version = "16" # (optional) Node version that will be used by nvm can be specified here that. Default is v14. +# path = "" # (optional) Path to the serverless app directory can be specified here. Normally it's derived from app directory and app name. +# sls_node_modules_cache_mount = "" # (optional) SLS node_modules cache mount path can be specified here. It's used to store cache during CI/CD process. +# file = "" # (optional) Path to serverless file can be specified here. Normally it's serverless.yml in the app directory. +# create_domain = false # (optional) Create domain for the serverless domain manager during the deployment.