diff --git a/.github/workflows/release-dev.build-and-publish.yml b/.github/workflows/release-dev.build-and-publish.yml index b22fee37..e44c23b2 100644 --- a/.github/workflows/release-dev.build-and-publish.yml +++ b/.github/workflows/release-dev.build-and-publish.yml @@ -14,9 +14,9 @@ jobs: steps: - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: - go-version: 1.18.x + go-version: 1.22.x - name: Checkout code uses: actions/checkout@v2 @@ -29,7 +29,7 @@ jobs: # TODO: This should run only if all tests are satisfying - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v2 + uses: goreleaser/goreleaser-action@v5 with: # either 'goreleaser' (default) or 'goreleaser-pro' distribution: goreleaser diff --git a/.github/workflows/release-dev.create-release-and-publish.yml b/.github/workflows/release-dev.create-release-and-publish.yml index 759ed3ac..c574cce8 100644 --- a/.github/workflows/release-dev.create-release-and-publish.yml +++ b/.github/workflows/release-dev.create-release-and-publish.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: push: branches: - - main + - v2.x jobs: update_github_dev_release: name: Update Github dev Release diff --git a/.github/workflows/release-prod.build-and-publish.yml b/.github/workflows/release-prod.build-and-publish.yml index 96e177a7..56b2136c 100644 --- a/.github/workflows/release-prod.build-and-publish.yml +++ b/.github/workflows/release-prod.build-and-publish.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: go-version: 1.18.x diff --git a/.github/workflows/run.brew-and-apt-install-tests.yml b/.github/workflows/run.brew-and-apt-install-tests.yml index 52d5b167..e09be3bb 100644 --- a/.github/workflows/run.brew-and-apt-install-tests.yml +++ b/.github/workflows/run.brew-and-apt-install-tests.yml @@ -9,6 +9,7 @@ env: on: workflow_dispatch: + # TODO: Re-enable this after the tests are stable # schedule: # - cron: '0 3 * * */2' diff --git a/.github/workflows/run.e2e-tests.yml b/.github/workflows/run.e2e-tests.yml index 039d02e9..2e50f57f 100644 --- a/.github/workflows/run.e2e-tests.yml +++ b/.github/workflows/run.e2e-tests.yml @@ -9,7 +9,7 @@ env: on: workflow_dispatch: - pull_request: +# pull_request: # Re-enable after nvm bug is fixed jobs: build: @@ -24,9 +24,9 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: - go-version: 1.18.x + go-version: 1.22.x - name: Checkout Code uses: actions/checkout@v2 @@ -62,16 +62,19 @@ jobs: echo "ENV=${{ github.job }}-$(echo $GITHUB_SHA | cut -c 1-6)" >> $GITHUB_ENV - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_PROD }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_PROD }} aws-region: ${{ env.AWS_REGION }} + env: + AWS_PROFILE: # This is required due to a bug https://stackoverflow.com/a/77731682 + - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: - go-version: 1.18.x + go-version: 1.22.x - name: Checkout Code uses: actions/checkout@v2 @@ -126,16 +129,18 @@ jobs: echo "ENV=${{ github.job }}-$(echo $GITHUB_SHA | cut -c 1-6)" >> $GITHUB_ENV - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_PROD }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_PROD }} aws-region: ${{ env.AWS_REGION }} + env: + AWS_PROFILE: # This is required due to a bug https://stackoverflow.com/a/77731682 - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: - go-version: 1.18.x + go-version: 1.22.x - name: Checkout Code uses: actions/checkout@v2 @@ -151,7 +156,7 @@ jobs: - name: Make Executable run: | chmod +rx "${{ github.workspace }}/bin/ize" - ize --version + ize --version - name: Create AWS Profile run: ize gen aws-profile @@ -197,16 +202,18 @@ jobs: echo "ENV=${{ github.job }}-$(echo $GITHUB_SHA | cut -c 1-6)" >> $GITHUB_ENV - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_PROD }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_PROD }} aws-region: ${{ env.AWS_REGION }} + env: + AWS_PROFILE: # This is required due to a bug https://stackoverflow.com/a/77731682 - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: - go-version: 1.18.x + go-version: 1.22.x - name: Checkout Code uses: actions/checkout@v2 diff --git a/.github/workflows/run.unit-tests.yml b/.github/workflows/run.unit-tests.yml index 43023310..d11a1f2b 100644 --- a/.github/workflows/run.unit-tests.yml +++ b/.github/workflows/run.unit-tests.yml @@ -13,6 +13,7 @@ on: push: branches: - main + - v2.x jobs: build: @@ -27,9 +28,9 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: - go-version: 1.18.x + go-version: 1.22.x - name: Checkout Code uses: actions/checkout@v2 @@ -54,9 +55,9 @@ jobs: - uses: actions/checkout@v3 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: - go-version: 1.18 + go-version: 1.22.x - name: Generate run: | @@ -81,6 +82,7 @@ jobs: cat report.txt | go-junit-report -set-exit-code > report.xml else go test -v ./... -coverprofile=coverage.out -covermode=atomic 2>&1 ./... > report.txt + cat report.txt | go-junit-report -set-exit-code > report.xml fi - name: Publish Test Report @@ -89,5 +91,16 @@ jobs: with: report_paths: './report.xml' - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v2 + - name: Test Summary + uses: test-summary/action@v2 + if: always() + with: + paths: ./report.xml + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v4.0.1 + with: + files: coverage.out + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true + name: coverage-${{ github.sha }} diff --git a/cmd/main.go b/cmd/main.go index 1db4429f..efdd471a 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,11 +1,11 @@ /* -Copyright © 2021 NAME HERE +Copyright © 2021 HazelOps OÜ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -23,6 +23,7 @@ import ( func main() { // Running a version check in goroutine and waiting finished + var wg sync.WaitGroup wg.Add(1) go func() { diff --git a/examples/bastion-tunnel-monorepo/.ize/env/testnut/main.tf b/examples/bastion-tunnel-monorepo/.ize/env/testnut/main.tf index e92cfd8e..766013d9 100644 --- a/examples/bastion-tunnel-monorepo/.ize/env/testnut/main.tf +++ b/examples/bastion-tunnel-monorepo/.ize/env/testnut/main.tf @@ -11,7 +11,7 @@ resource "aws_key_pair" "root" { module "vpc" { source = "terraform-aws-modules/vpc/aws" - version = "~> 3.0" + version = "~> 5.0" name = "${var.env}-vpc" cidr = "10.0.0.0/16" @@ -79,7 +79,7 @@ module "ec2_profile" { module "bastion" { source = "hazelops/ec2-openvpn-connector/aws" - version = "~>0.2" + version = "~>0.4.1" vpn_enabled = false env = var.env diff --git a/examples/bastion-tunnel-monorepo/.ize/env/testnut/versions.tf b/examples/bastion-tunnel-monorepo/.ize/env/testnut/versions.tf index 03a50665..970268d9 100644 --- a/examples/bastion-tunnel-monorepo/.ize/env/testnut/versions.tf +++ b/examples/bastion-tunnel-monorepo/.ize/env/testnut/versions.tf @@ -2,7 +2,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 3.0" + version = ">= 3.0" } } required_version = ">= 0.13" diff --git a/examples/ecs-apps-monorepo/.ize/env/testnut/main.tf b/examples/ecs-apps-monorepo/.ize/env/testnut/main.tf index d3ed8295..c167b7ac 100644 --- a/examples/ecs-apps-monorepo/.ize/env/testnut/main.tf +++ b/examples/ecs-apps-monorepo/.ize/env/testnut/main.tf @@ -11,7 +11,7 @@ resource "aws_key_pair" "root" { module "vpc" { source = "registry.terraform.io/terraform-aws-modules/vpc/aws" - version = "~> 3.0" + version = "~> 5.0" name = "${var.env}-vpc" cidr = "10.0.0.0/16" diff --git a/examples/ecs-apps-monorepo/.ize/env/testnut/versions.tf b/examples/ecs-apps-monorepo/.ize/env/testnut/versions.tf index 970268d9..6793a836 100644 --- a/examples/ecs-apps-monorepo/.ize/env/testnut/versions.tf +++ b/examples/ecs-apps-monorepo/.ize/env/testnut/versions.tf @@ -2,7 +2,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 3.0" + version = ">= 4.42.0" } } required_version = ">= 0.13" diff --git a/examples/multistack-monorepo/vpc/main.tf b/examples/multistack-monorepo/vpc/main.tf index 02f3d2af..3e6d3424 100644 --- a/examples/multistack-monorepo/vpc/main.tf +++ b/examples/multistack-monorepo/vpc/main.tf @@ -1,6 +1,6 @@ module "vpc" { source = "registry.terraform.io/terraform-aws-modules/vpc/aws" - version = "~> 3.0" + version = "~> 5.0" name = "${var.env}-vpc" cidr = "10.0.0.0/16" diff --git a/examples/multistate-monorepo/.ize/env/testnut/api/main.tf b/examples/multistate-monorepo/.ize/env/testnut/api/main.tf deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/multistate-monorepo/.ize/env/testnut/api/variables.tf b/examples/multistate-monorepo/.ize/env/testnut/api/variables.tf deleted file mode 100644 index 5e7f26d0..00000000 --- a/examples/multistate-monorepo/.ize/env/testnut/api/variables.tf +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index f6dd1729..00000000 --- a/examples/multistate-monorepo/.ize/env/testnut/ize.toml +++ /dev/null @@ -1,60 +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-output 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"] - -# [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. diff --git a/examples/multistate-monorepo/.ize/env/testnut/main.tf b/examples/multistate-monorepo/.ize/env/testnut/main.tf deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/multistate-monorepo/.ize/env/testnut/variables.tf b/examples/multistate-monorepo/.ize/env/testnut/variables.tf deleted file mode 100644 index 5e7f26d0..00000000 --- a/examples/multistate-monorepo/.ize/env/testnut/variables.tf +++ /dev/null @@ -1,11 +0,0 @@ -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/vpc/main.tf b/examples/multistate-monorepo/.ize/env/testnut/vpc/main.tf deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/multistate-monorepo/.ize/env/testnut/vpc/variables.tf b/examples/multistate-monorepo/.ize/env/testnut/vpc/variables.tf deleted file mode 100644 index 5e7f26d0..00000000 --- a/examples/multistate-monorepo/.ize/env/testnut/vpc/variables.tf +++ /dev/null @@ -1,11 +0,0 @@ -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/go.mod b/go.mod index 90cd49de..cc906bd2 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/hazelops/ize -go 1.18 +go 1.21 + +toolchain go1.22.2 require ( github.com/AlecAivazis/survey/v2 v2.3.4 @@ -28,26 +30,27 @@ require ( github.com/olekukonko/tablewriter v0.0.5 github.com/psihachina/path-parser v1.0.1 github.com/psihachina/terraform-switcher v0.13.1275 - github.com/pterm/pterm v0.12.49 + github.com/pterm/pterm v0.12.79 github.com/santhosh-tekuri/jsonschema v1.2.4 github.com/sirupsen/logrus v1.8.1 github.com/spf13/cobra v1.4.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.12.0 - github.com/stretchr/testify v1.8.0 + github.com/stretchr/testify v1.8.4 github.com/xeipuuv/gojsonschema v1.2.0 github.com/zclconf/go-cty v1.10.0 golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 - golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 - golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 - golang.org/x/text v0.3.7 + golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 + golang.org/x/sync v0.1.0 + golang.org/x/term v0.16.0 + golang.org/x/text v0.14.0 gopkg.in/ini.v1 v1.66.6 ) require ( - atomicgo.dev/cursor v0.1.1 // indirect - atomicgo.dev/keyboard v0.2.8 // indirect + atomicgo.dev/cursor v0.2.0 // indirect + atomicgo.dev/keyboard v0.2.9 // indirect + atomicgo.dev/schedule v0.1.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Microsoft/go-winio v0.5.2 // indirect github.com/Microsoft/hcsshim v0.9.2 // indirect @@ -71,7 +74,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/go-cmp v0.5.8 // indirect - github.com/gookit/color v1.5.2 // indirect + github.com/gookit/color v1.5.4 // indirect github.com/hashicorp/go-hclog v1.2.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/imdario/mergo v0.3.12 // indirect @@ -82,10 +85,10 @@ require ( github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect github.com/kr/pretty v0.3.0 // indirect github.com/kr/text v0.2.0 // indirect - github.com/lithammer/fuzzysearch v1.1.5 // indirect + github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect @@ -102,7 +105,7 @@ require ( github.com/pelletier/go-toml/v2 v2.0.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rivo/uniseg v0.2.0 // indirect + github.com/rivo/uniseg v0.4.4 // indirect github.com/rogpeppe/go-internal v1.6.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sergi/go-diff v1.2.0 // indirect @@ -114,11 +117,11 @@ require ( github.com/xanzy/ssh-agent v0.3.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/y0ssar1an/q v1.0.7 // indirect go.opencensus.io v0.23.0 // indirect - golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect - golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect + golang.org/x/net v0.6.0 // indirect + golang.org/x/sys v0.16.0 // indirect golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 64877d7d..babe6f9c 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,13 @@ atomicgo.dev/cursor v0.1.1 h1:0t9sxQomCTRh5ug+hAMCs59x/UmC9QL6Ci5uosINKD4= atomicgo.dev/cursor v0.1.1/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= +atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw= +atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= atomicgo.dev/keyboard v0.2.8 h1:Di09BitwZgdTV1hPyX/b9Cqxi8HVuJQwWivnZUEqlj4= atomicgo.dev/keyboard v0.2.8/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= +atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= +atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= +atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= +atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= bazil.org/fuse v0.0.0-20200407214033-5883e5a4b512/go.mod h1:FbcW6z/2VytnFDhZfumh8Ss8zxHE6qpMP5sHTRe0EaM= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= @@ -560,6 +566,8 @@ github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQ github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= github.com/gookit/color v1.5.2 h1:uLnfXcaFjlrDnQDT+NCBcfhrXqYTx/rcCa6xn01Y8yI= github.com/gookit/color v1.5.2/go.mod h1:w8h4bGiHeeBpvQVePTutdbERIUf3oJE5lZ8HM0UgXyg= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= @@ -683,6 +691,8 @@ github.com/lab47/vterm v0.0.0-20211107042118-80c3d2849f9c/go.mod h1:IODMeTGM8OBi github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= github.com/lithammer/fuzzysearch v1.1.5 h1:Ag7aKU08wp0R9QCfF4GoGST9HbmAIeLP7xwMrOBEp1c= github.com/lithammer/fuzzysearch v1.1.5/go.mod h1:1R1LRNk7yKid1BaQkmuLQaHruxcC4HmAH30Dh61Ih1Q= +github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= +github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -715,6 +725,8 @@ github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= @@ -905,8 +917,12 @@ github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5b github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= github.com/pterm/pterm v0.12.49 h1:qeNm0wTWawy6WhKoY8ZKq6qTXFr0s2UtUyRW0yVztEg= github.com/pterm/pterm v0.12.49/go.mod h1:D4OBoWNqAfXkm5QLTjIgjNiMXPHemLJHnIreGUsWzWg= +github.com/pterm/pterm v0.12.79 h1:lH3yrYMhdpeqX9y5Ep1u7DejyHy7NSQg9qrBjF9dFT4= +github.com/pterm/pterm v0.12.79/go.mod h1:1v/gzOF1N0FsjbgTHZ1wVycRkKiatFvJSJC4IGaQAAo= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -984,6 +1000,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= @@ -1029,6 +1047,8 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/y0ssar1an/q v1.0.7 h1:s3ckTY+wjk6Y0sFce4rIS1Ezf8S6d0UFJrKwe40MyiQ= github.com/y0ssar1an/q v1.0.7/go.mod h1:Q1Rk1StqWjSOfA/CF4zJEW1fLmkl5Cy8EsILdkB+DgE= @@ -1037,6 +1057,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= @@ -1117,6 +1138,7 @@ golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWP golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -1132,6 +1154,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 h1:tnebWN09GYg9OLPss1KXj8txwZc6X6uMr6VFdcGNbHw= golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1157,6 +1181,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1218,6 +1244,9 @@ golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 h1:NWy5+hlRbC7HK+PmcXVUmW1IMyFce7to56IUvhUFm7Y= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1244,6 +1273,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1352,8 +1383,12 @@ golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1361,6 +1396,9 @@ golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4 golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1371,6 +1409,10 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1446,6 +1488,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/aws/utils/utils.go b/internal/aws/utils/utils.go index d3304ba7..2c56ec14 100644 --- a/internal/aws/utils/utils.go +++ b/internal/aws/utils/utils.go @@ -4,14 +4,15 @@ import ( "fmt" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/service/iam" "github.com/sirupsen/logrus" "os" + "strings" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials/stscreds" "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/service/sts" "gopkg.in/ini.v1" ) @@ -21,14 +22,30 @@ const ( ) type SessionConfig struct { - Region string - Profile string + Region string + Profile string + EndpointUrl string } func GetSession(c *SessionConfig) (*session.Session, error) { upd := false - config := aws.NewConfig().WithRegion(c.Region).WithCredentials(credentials.NewSharedCredentials("", c.Profile)) + config := aws.NewConfig().WithRegion(c.Region).WithCredentials(credentials.NewSharedCredentials("", c.Profile)).WithEndpoint(c.EndpointUrl) + + if len(c.EndpointUrl) > 0 { + logrus.Debug(fmt.Sprintf("Session established. Endpoint: %s", c.EndpointUrl)) + } else { + logrus.Debug(fmt.Sprintf("Session established with a default endpoint")) + } + + // + //if len(c.EndpointUrl) > 0 { + // // If EndpointUrl is set to a non-default value specify it + // + //} else { + // config = aws.NewConfig().WithRegion(c.Region).WithCredentials(credentials.NewSharedCredentials("", c.Profile)) + //} + sess, err := session.NewSessionWithOptions(session.Options{ Config: *config, }) @@ -41,9 +58,15 @@ func GetSession(c *SessionConfig) (*session.Session, error) { switch aerr.Code() { case "SharedCredsLoad": logrus.Error(err) - return nil, fmt.Errorf("AWS_PROFILE is not set. Please set it via AWS_PROFILE env var,--aws-profile flag or aws_profile config entry in ize.toml") + return nil, fmt.Errorf("AWS_PROFILE is not set. Please set it via AWS_PROFILE env var, --aws-profile flag or aws_profile config entry in ize.toml") default: - return nil, err + // Error only if it's not a localhost endpoint + if !(strings.Contains(c.EndpointUrl, "localhost") || strings.Contains(c.EndpointUrl, "127.0.0.1")) { + // If endpoint is not related to LocalStack then it's an error + return nil, err + } + + logrus.Debug("[NO MFA] Using Endpoint: ", c.EndpointUrl) } } diff --git a/internal/commands/boostrap.go b/internal/commands/boostrap.go new file mode 100644 index 00000000..73a781c4 --- /dev/null +++ b/internal/commands/boostrap.go @@ -0,0 +1,21 @@ +package commands + +import ( + "github.com/hazelops/ize/internal/config" + "github.com/spf13/cobra" +) + +func NewCmdBoostrap(project *config.Project) *cobra.Command { + + cmd := &cobra.Command{ + Use: "boostrap", + Short: "Boostrap resources", + TraverseChildren: true, + } + + cmd.AddCommand( + NewBoostrapTerraformState(project), + ) + + return cmd +} diff --git a/internal/commands/boostrap_terraform_state.go b/internal/commands/boostrap_terraform_state.go new file mode 100644 index 00000000..63f8994f --- /dev/null +++ b/internal/commands/boostrap_terraform_state.go @@ -0,0 +1,115 @@ +package commands + +import ( + "fmt" + "github.com/hazelops/ize/internal/config" + "github.com/hazelops/ize/pkg/templates" + "github.com/spf13/cobra" + "text/template" +) + +type BootstrapTerraformStateOptions struct { + Config *config.Project + AppName string + Explain bool +} + +var boostrapTerraformStateExplainTmpl = ` +SERVICE_SECRETS_FILE={{.EnvDir}}/secrets/{{svc}}.json +SERVICE_SECRETS=$(cat $SERVICE_SECRETS_FILE | jq -e -r '. | keys[]') +for item in $(echo $SERVICE_SECRETS); do + aws --profile={{.AwsProfile}} ssm put-parameter --name="/{{.Env}}/{{svc}}/${item}" --value="$(cat $SERVICE_SECRETS_FILE | jq -r .$item )" --type SecureString --overwrite && \ + aws --profile={{.AwsProfile}} ssm add-tags-to-resource --resource-type "Parameter" --resource-id "/{{.Env}}/{{svc}}/${item}" \ + --tags "Key=Application,Value={{svc}}" "Key=EnvVarName,Value=${item}" +done +` + +var boostrapTerraformStateExample = templates.Examples(` + # Boostrap Terraform State: + + TBD +`) + +func NewBoostrapTerraformStateFlags(project *config.Project) *BootstrapTerraformStateOptions { + return &BootstrapTerraformStateOptions{ + Config: project, + } +} + +func NewBoostrapTerraformState(project *config.Project) *cobra.Command { + o := NewBoostrapTerraformStateFlags(project) + + cmd := &cobra.Command{ + Use: "terraform-state", + Example: boostrapTerraformStateExample, + Short: "Boostrap Terraform State", + Long: "This command creates Terraform State bucket and DynamoDB table based on ize name convention", + //Args: cobra.MinimumNArgs(1), + //ValidArgsFunction: config.GetApps, + RunE: func(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true + + err := o.Complete(cmd) + 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 *BootstrapTerraformStateOptions) Complete(cmd *cobra.Command) error { + //o.AppName = cmd.Flags().Args()[0] + + println("Done") + return nil +} + +func (o *BootstrapTerraformStateOptions) Validate() error { + if len(o.Config.Env) == 0 { + return fmt.Errorf("env must be specified\n") + } + println("Valid") + + return nil +} + +func (o *BootstrapTerraformStateOptions) Run() error { + if o.Explain { + err := o.Config.Generate(boostrapTerraformStateExplainTmpl, template.FuncMap{ + "svc": func() string { + return o.AppName + }, + }) + if err != nil { + return err + } + + return nil + } + println("Running") + // TODO: Create bucket via S3 Client with forcePAth ON + //name := "test" + // + //_, err := o.Config.AWSClient.S3Client().CreateBucket(&s3.CreateBucketInput{ + // Bucket: &name, + //}) + //if err != nil { + // return err + //} + + return nil +} diff --git a/internal/commands/build_test.go b/internal/commands/build_test.go index c3dff542..2f7c8dbd 100644 --- a/internal/commands/build_test.go +++ b/internal/commands/build_test.go @@ -2,13 +2,17 @@ package commands import ( _ "embed" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ecs" "github.com/golang/mock/gomock" _ "github.com/golang/mock/mockgen/model" "github.com/hazelops/ize/internal/config" "github.com/hazelops/ize/internal/generate" + "github.com/hazelops/ize/pkg/mocks" "github.com/spf13/pflag" "github.com/spf13/viper" "os" + "os/exec" "path/filepath" "strings" "testing" @@ -18,12 +22,21 @@ import ( var buildToml string func TestBuild(t *testing.T) { + + mockECS := func(m *mocks.MockECSAPI) { + m.EXPECT().ListTasks(gomock.Any()).Return(&ecs.ListTasksOutput{ + NextToken: nil, + TaskArns: []*string{aws.String("test")}, + }, nil).AnyTimes() + } + tests := []struct { name string args []string wantErr bool withConfigFile bool env map[string]string + mockECSClient func(m *mocks.MockECSAPI) }{ { name: "success (only config file)", @@ -31,36 +44,42 @@ func TestBuild(t *testing.T) { env: map[string]string{"ENV": "test", "AWS_PROFILE": "test"}, withConfigFile: true, wantErr: false, + mockECSClient: mockECS, }, { name: "success (flags and config file)", args: []string{"-e=test", "-p=test", "build", "test"}, withConfigFile: true, wantErr: false, + mockECSClient: mockECS, }, { name: "success (flags, env and config file)", args: []string{"-p=test", "build", "goblin"}, - env: map[string]string{"ENV": "test"}, + env: map[string]string{"ENV": "testnut"}, withConfigFile: true, wantErr: false, + mockECSClient: mockECS, }, { - name: "success (flags and env)", - args: []string{"--aws-region", "us-east-1", "--namespace", "test-testnut", "build", "goblin"}, - env: map[string]string{"ENV": "testnut", "AWS_PROFILE": "test"}, - wantErr: false, + name: "success (flags and env)", + args: []string{"--aws-region", "us-east-1", "--namespace", "test-testnut", "build", "goblin"}, + env: map[string]string{"ENV": "testnut", "AWS_PROFILE": "test"}, + wantErr: false, + mockECSClient: mockECS, }, { - name: "success (only flags)", - args: []string{"-e=test", "-r=us-east-1", "-p=test", "-n=test", "build", "squibby"}, - wantErr: false, + name: "success (only flags)", + args: []string{"-e=test", "-r=us-east-1", "-p=test", "-n=test", "build", "squibby"}, + wantErr: false, + mockECSClient: mockECS, }, { - name: "success (only env)", - args: []string{"build", "goblin"}, - env: map[string]string{"ENV": "test", "AWS_PROFILE": "test", "NAMESPACE": "dev-testnut", "AWS_REGION": "us-west-2"}, - wantErr: false, + name: "success (only env)", + args: []string{"build", "goblin"}, + env: map[string]string{"ENV": "test", "AWS_PROFILE": "test", "NAMESPACE": "dev-testnut", "AWS_REGION": "us-west-2"}, + wantErr: false, + mockECSClient: mockECS, }, } for _, tt := range tests { @@ -78,17 +97,26 @@ func TestBuild(t *testing.T) { t.Error(err) return } - err = os.Chdir(temp) + + err = initTestGitRepo(t, temp) if err != nil { t.Error(err) return } - err = os.MkdirAll(filepath.Join(temp, ".ize", "env", "test"), 0777) + + err = os.Chdir(temp) if err != nil { t.Error(err) return } + //err = os.MkdirAll(filepath.Join(temp, ".ize", "env", "test"), 0777) + //if err != nil { + // t.Error(err) + // return + //} + + // Test with config file specified as a parameter if tt.withConfigFile { setConfigFile(filepath.Join(temp, "ize.toml"), buildToml, t) } @@ -104,6 +132,9 @@ func TestBuild(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + mockECSAPI := mocks.NewMockECSAPI(ctrl) + tt.mockECSClient(mockECSAPI) + cfg := new(config.Project) cmd := newRootCmd(cfg) @@ -129,6 +160,12 @@ func TestBuild(t *testing.T) { os.Exit(1) } + cfg.AWSClient = config.NewAWSClient( + config.WithECSClient(mockECSAPI), + ) + + cfg.Session = getSession(false) + err = cmd.Execute() if (err != nil) != tt.wantErr { t.Errorf("ize build error = %v, wantErr %v", err, tt.wantErr) @@ -142,3 +179,25 @@ func TestBuild(t *testing.T) { }) } } + +func initTestGitRepo(t *testing.T, dir string) error { + cmd := exec.Command("git", "init") + if _, err := os.Stat(dir); os.IsNotExist(err) { + if err != nil { + t.Errorf("Can't find a directory: %s", err) + } + } + cmd.Dir = dir + err := cmd.Run() + + if err != nil { + t.Errorf("Failed to initialize git repository: %s", err) + } + + cmd = exec.Command("git", "commit -am 'test initial'") + if err != nil { + t.Errorf("Failed to commit to git repository: %s", err) + } + + return err +} diff --git a/internal/commands/console.go b/internal/commands/console.go index 9d7ded28..86e43f31 100644 --- a/internal/commands/console.go +++ b/internal/commands/console.go @@ -1,6 +1,7 @@ package commands import ( + "errors" "fmt" "text/template" @@ -16,13 +17,14 @@ import ( ) type ConsoleOptions struct { - Config *config.Project - AppName string - EcsCluster string - Task string - CustomPrompt bool - ContainerName string - Explain bool + Config *config.Project + AppName string + EcsCluster string + EcsServiceName string + Task string + CustomPrompt bool + EcsContainerName string + Explain bool } var explainConsoleTmpl = ` @@ -74,7 +76,7 @@ func NewCmdConsole(project *config.Project) *cobra.Command { } cmd.Flags().StringVar(&o.EcsCluster, "ecs-cluster", "", "set ECS cluster name") - cmd.Flags().StringVar(&o.ContainerName, "container-name", "", "set container name") + cmd.Flags().StringVar(&o.EcsContainerName, "container-name", "", "set container name") cmd.Flags().StringVar(&o.Task, "task", "", "set task id") cmd.Flags().BoolVar(&o.Explain, "explain", false, "bash alternative shown") cmd.Flags().BoolVar(&o.CustomPrompt, "custom-prompt", false, "enable custom prompt in the console") @@ -97,10 +99,6 @@ func (o *ConsoleOptions) Complete(cmd *cobra.Command) error { o.AppName = cmd.Flags().Args()[0] - if len(o.ContainerName) == 0 { - o.ContainerName = o.AppName - } - return nil } @@ -113,12 +111,19 @@ func (o *ConsoleOptions) Validate() error { } func (o *ConsoleOptions) Run() error { - appName := fmt.Sprintf("%s-%s", o.Config.Env, o.AppName) + var err error + + if len(o.EcsServiceName) == 0 { + o.EcsServiceName, err = getEcsServiceName(o) + if err != nil { + return err + } + } if o.Explain { err := o.Config.Generate(explainConsoleTmpl, template.FuncMap{ "svc": func() string { - return o.AppName + return o.EcsServiceName }, }) if err != nil { @@ -128,23 +133,27 @@ func (o *ConsoleOptions) Run() error { return nil } - logrus.Infof("app name: %s, cluster name: %s", appName, o.EcsCluster) + logrus.Infof("app name: %s, cluster name: %s", o.EcsServiceName, o.EcsCluster) logrus.Infof("region: %s, profile: %s", o.Config.AwsProfile, o.Config.AwsRegion) s, _ := pterm.DefaultSpinner.WithRemoveWhenDone().Start("Getting access to container...") if o.Task == "" { + // Infer task name from the app name lto, err := o.Config.AWSClient.ECSClient.ListTasks(&ecs.ListTasksInput{ Cluster: &o.EcsCluster, DesiredStatus: aws.String(ecs.DesiredStatusRunning), - ServiceName: &appName, + ServiceName: &o.EcsServiceName, }) + if aerr, ok := err.(awserr.Error); ok { switch aerr.Code() { case "ClusterNotFoundException": return fmt.Errorf("ECS cluster %s not found", o.EcsCluster) default: - return err + { + return err + } } } @@ -157,6 +166,13 @@ func (o *ConsoleOptions) Run() error { o.Task = *lto.TaskArns[0] } + if len(o.EcsContainerName) == 0 { + o.EcsContainerName, err = getEcsContainerName(o) + if err != nil { + return err + } + } + s.UpdateText("Executing command...") consoleCommand := `/bin/sh` @@ -168,7 +184,7 @@ func (o *ConsoleOptions) Run() error { } out, err := o.Config.AWSClient.ECSClient.ExecuteCommand(&ecs.ExecuteCommandInput{ - Container: &o.ContainerName, + Container: &o.EcsContainerName, Interactive: aws.Bool(true), Cluster: &o.EcsCluster, Task: &o.Task, @@ -193,3 +209,125 @@ func (o *ConsoleOptions) Run() error { return nil } + +func getEcsServiceName(o *ConsoleOptions) (string, error) { + // TODO: Move core logic to a shared function (since it's used in deploy too) + ecsServiceCandidates := []string{ + fmt.Sprintf("%s-%s-%s", o.Config.Env, o.Config.Namespace, o.AppName), + fmt.Sprintf("%s-%s", o.Config.Env, o.AppName), + o.AppName, + } + + for _, v := range ecsServiceCandidates { + // Searching for a service that has running tasks + logrus.Debugf("Checking if ECS service %s exists in cluster %s.", v, o.EcsCluster) + _, err := o.Config.AWSClient.ECSClient.ListTasks(&ecs.ListTasksInput{ + Cluster: &o.EcsCluster, + DesiredStatus: aws.String(ecs.DesiredStatusRunning), + ServiceName: &v, + }) + + // If during the search we encounter exceptions, handle them. + var aerr awserr.Error + if errors.As(err, &aerr) { + switch aerr.Code() { + case "ClusterNotFoundException": + return "", fmt.Errorf("ECS cluster %s not found", o.EcsCluster) + case "ServiceNotFoundException": + { + logrus.Infof("ECS Service not found: %s in cluster %s.", v, o.EcsCluster) + continue + } + default: + { + return "", err + } + } + + } + return v, err + } + err := errors.New("ECS Service not found") + return "", err +} + +func getEcsContainerName(o *ConsoleOptions) (string, error) { + ecsContainerNameCandidates := []string{ + o.AppName, + fmt.Sprintf("%s-%s", o.Config.Namespace, o.AppName), + fmt.Sprintf("%s-%s", o.Config.Env, o.AppName), + fmt.Sprintf("%s-%s-%s", o.Config.Env, o.Config.Namespace, o.AppName), + } + + for _, v := range ecsContainerNameCandidates { + logrus.Debugf("Checking if ECS container %s exists in task %s.", v, o.Task) + t, err := o.Config.AWSClient.ECSClient.ListTasks(&ecs.ListTasksInput{ + Cluster: &o.EcsCluster, + DesiredStatus: aws.String(ecs.DesiredStatusRunning), + ServiceName: &o.EcsServiceName, + }) + + if err != nil { + return "", err + } + + if len(t.TaskArns) > 0 { + tasks, err := o.Config.AWSClient.ECSClient.DescribeTasks(&ecs.DescribeTasksInput{ + Cluster: &o.EcsCluster, + Tasks: t.TaskArns, + }) + + if err != nil { + return "", err + } + + if len(tasks.Tasks) > 0 { + for _, task := range tasks.Tasks { + logrus.Debugf("Task arn is %s", *task.TaskArn) + + for _, container := range task.Containers { + for _, ecsContainerNameCandidate := range ecsContainerNameCandidates { + logrus.Debugf("Checking if %s==%s", ecsContainerNameCandidate, *container.Name) + if ecsContainerNameCandidate == *container.Name { + return *container.Name, nil + } else { + continue + } + } + } + + return "", errors.New(fmt.Sprintf("Can't find a container for %s in %s", o.AppName, *task.TaskDefinitionArn)) + } + } else { + fmt.Println("No tasks found.") + } + + } + + //_, err := o.Config.AWSClient.ECSClient.ListContainerInstances(&ecs.ListContainerInstancesInput{ + // Cluster: &o.EcsCluster, + // //Filter: "", + //}) + + var aerr awserr.Error + if errors.As(err, &aerr) { + switch aerr.Code() { + case "ClusterNotFoundException": + return "", fmt.Errorf("ECS cluster %s not found", o.EcsCluster) + case "ServiceNotFoundException": + { + logrus.Infof("ECS Service not found: %s in cluster %s.", v, o.EcsCluster) + continue + } + default: + { + return "", err + } + } + + } + return v, err + } + err := errors.New(fmt.Sprintf("ECS Container for %s not found", o.AppName)) + return "", err +} diff --git a/internal/commands/deploy.go b/internal/commands/deploy.go index 47319df9..2a430ee6 100644 --- a/internal/commands/deploy.go +++ b/internal/commands/deploy.go @@ -2,16 +2,17 @@ package commands import ( "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/helm" "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/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -132,38 +133,50 @@ func (o *DeployOptions) Run() error { defer sg.Wait() var m manager.Manager - - m = &ecs.Manager{ - Project: o.Config, - App: &config.Ecs{ - Name: o.AppName, - TaskDefinitionRevision: o.TaskDefinitionRevision, - Unsafe: o.Unsafe, - }, + var providerUsed string + // Note, Viper doesn't read empty TOML sections (https://github.com/spf13/viper/issues/1131 so if there are no app sections, we'll use apps_provider + logrus.Debugf("FYI, Viper can't read/see empty TOML sections. If they are empty, we'll try to use `apps_provider` config if it's set in ize.toml. See more here https://github.com/spf13/viper/issues/1131") + //if o.Config.AppsProvider == "helm" { + // logrus.Debugf("Found helm app") + // + + if app, ok := o.Config.Ecs[o.AppName]; o.Config.AppsProvider == "ecs" || ok { + providerUsed = "ecs" + app.Name = o.AppName + m = &ecs.Manager{ + Project: o.Config, + App: app, + } } - if app, ok := o.Config.Serverless[o.AppName]; ok { + if app, ok := o.Config.Helm[o.AppName]; o.Config.AppsProvider == "helm" || ok { + providerUsed = "helm" app.Name = o.AppName app.Force = o.Force - m = &serverless.Manager{ + m = &helm.Manager{ Project: o.Config, App: app, } } - if app, ok := o.Config.Alias[o.AppName]; ok { + + if app, ok := o.Config.Serverless[o.AppName]; o.Config.AppsProvider == "serverless" || ok { + providerUsed = "serverless" app.Name = o.AppName - m = &alias.Manager{ + app.Force = o.Force + + m = &serverless.Manager{ Project: o.Config, App: app, } } - if app, ok := o.Config.Ecs[o.AppName]; ok { - app.Name = o.AppName - app.TaskDefinitionRevision = o.TaskDefinitionRevision - app.Unsafe = o.Unsafe - m = &ecs.Manager{ + + if _, ok := o.Config.Alias[o.AppName]; o.Config.AppsProvider == "alias" || ok { + providerUsed = "alias" + m = &alias.Manager{ Project: o.Config, - App: app, + App: &config.Alias{ + Name: o.AppName, + }, } } @@ -178,6 +191,7 @@ func (o *DeployOptions) Run() error { return nil } + logrus.Debugf("Deploying using %s. (default_app_provier=%s)", providerUsed, o.Config.AppsProvider) err := m.Deploy(ui) if err != nil { return err diff --git a/internal/commands/aws_profile.go b/internal/commands/gen_aws_profile.go similarity index 84% rename from internal/commands/aws_profile.go rename to internal/commands/gen_aws_profile.go index 116db944..e9e48f2c 100644 --- a/internal/commands/aws_profile.go +++ b/internal/commands/gen_aws_profile.go @@ -77,7 +77,17 @@ func ConfigureAwsProfile() (string, error) { return awsCredentialsPath, fmt.Errorf("AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_PROFILE must be set") } - _, err = f.WriteString(fmt.Sprintf("[%v]\naws_access_key_id = %v\naws_secret_access_key = %v\nregion = %v\n\n", p, ak, sk, r)) + ls := "" + localStackEnabled := viper.GetBool("LOCALSTACK") + if localStackEnabled == true { + localstackEndpoint := viper.GetString("LOCALSTACK_ENDPOINT") + if localstackEndpoint == "" { + localstackEndpoint = "http://127.0.0.1:4566" + } + ls = fmt.Sprintf("endpoint_url = %s", localstackEndpoint) + } + + _, err = f.WriteString(fmt.Sprintf("[%v]\naws_access_key_id = %v\naws_secret_access_key = %v\nregion = %v\n%s\n\n", p, ak, sk, r, ls)) if err != nil { return awsCredentialsPath, fmt.Errorf("can't write to %s", filepath.Join(awsCredentialsPath)) } diff --git a/internal/commands/aws_profile_test.go b/internal/commands/gen_aws_profile_test.go similarity index 100% rename from internal/commands/aws_profile_test.go rename to internal/commands/gen_aws_profile_test.go diff --git a/internal/commands/ci.go b/internal/commands/gen_ci.go similarity index 100% rename from internal/commands/ci.go rename to internal/commands/gen_ci.go diff --git a/internal/commands/ci_test.go b/internal/commands/gen_ci_test.go similarity index 100% rename from internal/commands/ci_test.go rename to internal/commands/gen_ci_test.go diff --git a/internal/commands/completion.go b/internal/commands/gen_completion.go similarity index 100% rename from internal/commands/completion.go rename to internal/commands/gen_completion.go diff --git a/internal/commands/doc.go b/internal/commands/gen_doc.go similarity index 100% rename from internal/commands/doc.go rename to internal/commands/gen_doc.go diff --git a/internal/commands/mfa.go b/internal/commands/gen_mfa.go similarity index 100% rename from internal/commands/mfa.go rename to internal/commands/gen_mfa.go diff --git a/internal/commands/tfenv.go b/internal/commands/gen_tfenv.go similarity index 92% rename from internal/commands/tfenv.go rename to internal/commands/gen_tfenv.go index 755e8c17..c3f880d8 100644 --- a/internal/commands/tfenv.go +++ b/internal/commands/gen_tfenv.go @@ -2,8 +2,6 @@ package commands import ( "fmt" - "io/ioutil" - "os" "path/filepath" "github.com/aws/aws-sdk-go/aws" @@ -118,7 +116,7 @@ func GenerateTerraformFiles(name string, terraformStateBucketName string, projec backendOpts := template.BackendOpts{ ENV: project.Env, - LOCALSTACK_ENDPOINT: "", + LOCALSTACK_ENDPOINT: project.EndpointUrl, TERRAFORM_STATE_BUCKET_NAME: tf.StateBucketName, TERRAFORM_STATE_KEY: stateKey, TERRAFORM_STATE_REGION: tf.StateBucketRegion, @@ -147,15 +145,7 @@ func GenerateTerraformFiles(name string, terraformStateBucketName string, projec ) if err != nil { pterm.Error.Printfln("Generate terraform file for \"%s\" not completed", name) - return fmt.Errorf("can't generate backent.tf: %s", err) - } - - home, _ := os.UserHomeDir() - key, err := ioutil.ReadFile(fmt.Sprintf("%s/.ssh/id_rsa.pub", home)) - if err != nil { - pterm.Error.Printfln("Generate terraform file for \"%s\" not completed", name) - return fmt.Errorf("can't read public ssh key: %s", err) - + return fmt.Errorf("can't generate backend.tf: %s", err) } varsOpts := template.VarsOpts{ @@ -164,7 +154,7 @@ func GenerateTerraformFiles(name string, terraformStateBucketName string, projec AWS_REGION: project.AwsRegion, EC2_KEY_PAIR_NAME: fmt.Sprintf("%v-%v", project.Env, project.Namespace), ROOT_DOMAIN_NAME: tf.RootDomainName, - SSH_PUBLIC_KEY: string(key)[:len(string(key))-1], + SSH_PUBLIC_KEY: project.SshPublicKey, NAMESPACE: project.Namespace, } diff --git a/internal/commands/tfenv_test.go b/internal/commands/gen_tfenv_test.go similarity index 100% rename from internal/commands/tfenv_test.go rename to internal/commands/gen_tfenv_test.go diff --git a/internal/commands/initialize.go b/internal/commands/init.go similarity index 100% rename from internal/commands/initialize.go rename to internal/commands/init.go diff --git a/internal/commands/initialize_test.go b/internal/commands/init_test.go similarity index 100% rename from internal/commands/initialize_test.go rename to internal/commands/init_test.go diff --git a/internal/commands/ize.go b/internal/commands/ize.go index 41a97074..f146f109 100644 --- a/internal/commands/ize.go +++ b/internal/commands/ize.go @@ -89,6 +89,7 @@ func newRootCmd(project *config.Project) *cobra.Command { NewCmdPush(project), NewCmdUp(project), NewCmdNvm(project), + NewCmdBoostrap(project), NewValidateCmd(), NewVersionCmd()) @@ -119,8 +120,8 @@ func Execute() { } func getConfig(cfg *config.Project) { - if slices.Contains(os.Args, "terraform") || - slices.Contains(os.Args, "nvm") || + if slices.Contains(os.Args, "terraform") || + slices.Contains(os.Args, "nvm") || !(slices.Contains(os.Args, "aws-profile") || slices.Contains(os.Args, "doc") || slices.Contains(os.Args, "completion") || diff --git a/internal/commands/logs.go b/internal/commands/logs.go index 3936ba5d..98688aae 100644 --- a/internal/commands/logs.go +++ b/internal/commands/logs.go @@ -1,7 +1,9 @@ package commands import ( + "errors" "fmt" + "github.com/pterm/pterm" "os" "strings" "time" @@ -17,10 +19,12 @@ import ( ) type LogsOptions struct { - Config *config.Project - AppName string - EcsCluster string - Task string + Config *config.Project + AppName string + EcsCluster string + Task string + LogGroupName string + LogStreamName string } func NewLogsFlags(project *config.Project) *LogsOptions { @@ -82,21 +86,29 @@ func (o *LogsOptions) Validate() error { } func (o *LogsOptions) Run() error { - logGroup := fmt.Sprintf("%s-%s", o.Config.Env, o.AppName) + var err error + s, _ := pterm.DefaultSpinner.WithRemoveWhenDone().Start("Getting access to logs...") + + if len(o.LogGroupName) == 0 { + o.LogGroupName, err = getEcsServiceLogGroupName(o) + if err != nil { + return err + } + } taskID := o.Task if len(taskID) == 0 { lto, err := o.Config.AWSClient.ECSClient.ListTasks(&ecs.ListTasksInput{ Cluster: &o.EcsCluster, DesiredStatus: aws.String("RUNNING"), - ServiceName: &logGroup, + ServiceName: &o.LogGroupName, MaxResults: aws.Int64(1), }) - logrus.Infof("log group: %s, cluster name: %s", logGroup, o.EcsCluster) + logrus.Infof("log group: %s, cluster name: %s", o.LogGroupName, o.EcsCluster) if err != nil { - return fmt.Errorf("can't run logs: %w", err) + return fmt.Errorf("can't get logs: %w", err) } taskID = *lto.TaskArns[0] @@ -104,9 +116,19 @@ func (o *LogsOptions) Run() error { } var token *string - logStreamName := fmt.Sprintf("main/%s/%s", o.AppName, taskID) + if len(o.LogStreamName) == 0 { + var logStreamPrefix string + logStreamPrefix, err = getEcsServiceLogStreamPrefix(o) + if err != nil { + logrus.Errorf("can't get log stream prefix: %s", err) + } + + o.LogStreamName = fmt.Sprintf("%s/%s", logStreamPrefix, taskID) + } + + s.Success() - GetLogs(o.Config.AWSClient.CloudWatchLogsClient, logGroup, logStreamName, token) + GetLogs(o.Config.AWSClient.CloudWatchLogsClient, o.LogGroupName, o.LogStreamName, token) return nil } @@ -147,3 +169,69 @@ func formatMessage(e *cloudwatchlogs.OutputLogEvent) (t time.Time, m string) { m = t.Format("2006-01-02 15:04:05 ") + m return } + +func getEcsServiceLogGroupName(o *LogsOptions) (string, error) { + // TODO: Move core logic to a shared function (since it's used in deploy too) + ecsServiceLogGroupCandidates := []string{ + fmt.Sprintf("%s-%s-%s", o.Config.Env, o.Config.Namespace, o.AppName), + fmt.Sprintf("%s-%s", o.Config.Env, o.AppName), + o.AppName, + } + + for _, v := range ecsServiceLogGroupCandidates { + logrus.Debugf("Checking if Log Group %s exists.", v) + + resp, err := o.Config.AWSClient.CloudWatchLogsClient.DescribeLogGroups(&cloudwatchlogs.DescribeLogGroupsInput{ + LogGroupNamePrefix: aws.String(v), + }) + + if len(resp.LogGroups) == 0 { + logrus.Debugf("No log groups with prefix %s. Trying other options", v) + continue + } + + for _, logGroup := range resp.LogGroups { + if aws.StringValue(logGroup.LogGroupName) == v { + logrus.Debugf("Found Log Group %s", v) + return v, err + } + } + + return v, err + } + err := errors.New("Log group not found") + return "", err +} + +func getEcsServiceLogStreamPrefix(o *LogsOptions) (string, error) { + ecsServiceLogStreamNameCandidates := []string{ + fmt.Sprintf("main/%s-%s-%s", o.Config.Env, o.Config.Namespace, o.AppName), + fmt.Sprintf("main/%s-%s", o.Config.Env, o.AppName), + fmt.Sprintf("main/%s-%s", o.Config.Namespace, o.AppName), + o.AppName, + } + + for _, v := range ecsServiceLogStreamNameCandidates { + logrus.Debugf("Checking if logStream %s/* exists", v) + resp, err := o.Config.AWSClient.CloudWatchLogsClient.DescribeLogStreams(&cloudwatchlogs.DescribeLogStreamsInput{ + LogGroupName: aws.String(o.LogGroupName), + LogStreamNamePrefix: aws.String(v), + }) + + if len(resp.LogStreams) == 0 { + logrus.Debugf("No log streams with prefix %s. Trying other options", v) + continue + } + + for _, logStream := range resp.LogStreams { + if strings.Contains(aws.StringValue(logStream.LogStreamName), v) { + logrus.Debugf("Found Log Stream %s", v) + return v, err + } + } + + return v, err + } + err := errors.New(fmt.Sprintf("ECS Container for %s not found", o.AppName)) + return "", err +} diff --git a/internal/commands/nvm.go b/internal/commands/nvm.go index a17a9468..9c3db098 100644 --- a/internal/commands/nvm.go +++ b/internal/commands/nvm.go @@ -52,7 +52,11 @@ func NewCmdNvm(project *config.Project) *cobra.Command { Long: nvmLongDesc, ValidArgsFunction: config.GetApps, RunE: func(cmd *cobra.Command, args []string) error { - cmd.SilenceUsage = true + if o.Config.LogLevel != "debug" && o.Config.LogLevel != "trace" { + cmd.SilenceUsage = true + } else { + cmd.SilenceUsage = false + } if len(cmd.Flags().Args()) == 0 { return fmt.Errorf("app name must be specified") diff --git a/internal/commands/status.go b/internal/commands/status.go index 4f6c1186..db616d07 100644 --- a/internal/commands/status.go +++ b/internal/commands/status.go @@ -35,12 +35,13 @@ func NewDebugCmd(project *config.Project) *cobra.Command { {"ENV", project.Env}, {"NAMESPACE", project.Namespace}, {"TAG", project.Tag}, - {"INFRA DIR", project.InfraDir}, - {"PWD", cwd}, - {"IZE VERSION", version.FullVersionNumber()}, - {"GIT REVISION", version.GitCommit}, - {"ENV DIR", project.EnvDir}, + {"IZE_VERSION", version.FullVersionNumber()}, + {"GIT_REVISION", version.GitCommit}, {"PREFER_RUNTIME", project.PreferRuntime}, + {"INFRA_DIR", project.InfraDir}, + {"ENV_DIR", project.EnvDir}, + {"APPS_PATH", project.AppsPath}, + {"PWD", cwd}, }).WithLeftAlignment().Render() v := project.TerraformVersion @@ -84,41 +85,28 @@ func NewDebugCmd(project *config.Project) *cobra.Command { } } - tags, err := project.AWSClient.IAMClient.ListUserTags(&iam.ListUserTagsInput{ - UserName: guo.User.UserName, - }) - if aerr, ok := err.(awserr.Error); ok { - switch aerr.Code() { - case "NoSuchEntity": - return fmt.Errorf("error obtaining AWS user with aws_profile=%s: username %s is not found in account %s", project.AwsProfile, *guo.User.UserName, *resp.Account) - default: - return err - } - } - - devEnvName := "" - - for _, k := range tags.Tags { - if *k.Key == "devEnvironmentName" { - devEnvName = *k.Value - } + var userName string + if *guo.User.UserId == "000000000000" { + userName = "root" + } else { + userName = *guo.User.UserId } _ = dt.WithData(pterm.TableData{ - {"AWS PROFILE", project.AwsProfile}, - {"AWS USER", *guo.User.UserName}, - {"AWS ACCOUNT", *resp.Account}, + {"AWS_PROFILE", project.AwsProfile}, + {"AWS_USER", userName}, + {"AWS_ACCOUNT", *resp.Account}, }).WithLeftAlignment().Render() - if len(devEnvName) > 0 { + if len(project.EndpointUrl) > 0 { _ = dt.WithData(pterm.TableData{ - {"AWS_DEV_ENV_NAME", devEnvName}, + {"AWS_ENDPOINT_URL", project.EndpointUrl}, }).WithLeftAlignment().Render() } } else { pterm.Println("No AWS profile credentials detected. Parameters used:") _ = dt.WithData(pterm.TableData{ - {"AWS PROFILE", project.AwsProfile}, + {"AWS_PROFILE", project.AwsProfile}, }).WithLeftAlignment().Render() } diff --git a/internal/commands/up.go b/internal/commands/up.go index 0992a5ef..802e521b 100644 --- a/internal/commands/up.go +++ b/internal/commands/up.go @@ -95,7 +95,7 @@ func NewCmdUp(project *config.Project) *cobra.Command { cmd.Flags().BoolVar(&o.UseYarn, "use-yarn", false, "execute sls commands using yarn") cmd.Flags().BoolVar(&o.SkipGen, "skip-gen", false, "skip generating terraform files") cmd.Flags().BoolVar(&o.Explain, "explain", false, "bash alternative shown") - + cmd.AddCommand( NewCmdUpInfra(project), NewCmdUpApps(project), diff --git a/internal/commands/up_apps.go b/internal/commands/up_apps.go index 1e4abfe0..7743ea06 100644 --- a/internal/commands/up_apps.go +++ b/internal/commands/up_apps.go @@ -2,6 +2,7 @@ package commands import ( "context" + "errors" "fmt" "github.com/aws/aws-sdk-go/aws" @@ -103,9 +104,16 @@ func (o *UpAppsOptions) Run() error { 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 err error + if len(o.Config.AwsProfile) == 0 { + if v, exists := o.Config.Terraform["infra"]; exists { + o.Config.AwsProfile = v.AwsProfile + } else { + err = errors.New("can't detect aws_profile. Please set it via env var (AWS_PROFILE) or in ize.toml") + } + } - err := deployApp(name, ui, o.Config, false) + err = deployApp(name, ui, o.Config, false) if err != nil { return err } @@ -116,7 +124,7 @@ func (o *UpAppsOptions) Run() error { return err } - ui.Output("Deploy all completed!\n", terminal.WithSuccessStyle()) + ui.Output("Deploy complete!\n", terminal.WithSuccessStyle()) return nil } @@ -160,7 +168,7 @@ func deployApp(name string, ui terminal.UI, cfg *config.Project, isExplain bool) icon += " " } - ui.Output("Deploying %s%s app...", icon, name, terminal.WithHeaderStyle()) + ui.Output("%s%s: bringing up...", icon, name, terminal.WithHeaderStyle()) // build app container err := m.Build(ui) @@ -180,8 +188,7 @@ func deployApp(name string, ui terminal.UI, cfg *config.Project, isExplain bool) return fmt.Errorf("can't deploy app: %w", err) } - ui.Output("Deploy app %s%s completed\n", icon, name, terminal.WithSuccessStyle()) + ui.Output("%s%s: done.\n", icon, name, terminal.WithSuccessStyle()) return nil } - diff --git a/internal/commands/up_infra.go b/internal/commands/up_infra.go index e2cd0ab3..28e8b3d3 100644 --- a/internal/commands/up_infra.go +++ b/internal/commands/up_infra.go @@ -148,6 +148,7 @@ func (o *UpInfraOptions) Validate() error { func (o *UpInfraOptions) Run() error { if o.Explain { + // TODO: Get actual backend.tf from the ize gen tfenv template tmpl := `# Change to the dir cd {{.EnvDir}} diff --git a/internal/config/app.go b/internal/config/app.go index f3f0da7c..33c50a20 100644 --- a/internal/config/app.go +++ b/internal/config/app.go @@ -14,22 +14,23 @@ type Ecs struct { AwsProfile string `mapstructure:"aws_profile,omitempty"` AwsRegion string `mapstructure:"aws_region,omitempty"` DependsOn []string `mapstructure:"depends_on,omitempty"` + ServiceName string `mapstructure:"service_name,omitempty"` } -type K8s struct { - Name string `mapstructure:",omitempty"` - Path string `mapstructure:",omitempty"` - Image string `mapstructure:",omitempty"` - Cluster string `mapstructure:",omitempty"` - TaskDefinitionRevision string `mapstructure:"task_definition_revision"` - DockerRegistry string `mapstructure:"docker_registry,omitempty"` - Timeout int `mapstructure:",omitempty"` - Unsafe bool `mapstructure:",omitempty"` - SkipDeploy bool `mapstructure:"skip_deploy,omitempty"` - Icon string `mapstructure:"icon,omitempty"` - AwsProfile string `mapstructure:"aws_profile,omitempty"` - AwsRegion string `mapstructure:"aws_region,omitempty"` - DependsOn []string `mapstructure:"depends_on,omitempty"` +type Helm struct { + Name string `mapstructure:",omitempty"` + Path string `mapstructure:",omitempty"` + Image string `mapstructure:",omitempty"` + Namespace string `mapstructure:",omitempty"` + HelmRelease string `mapstructure:"helm_release,omitempty"` + DockerRegistry string `mapstructure:"docker_registry,omitempty"` + Timeout int `mapstructure:",omitempty"` + SkipDeploy bool `mapstructure:"skip_deploy,omitempty"` + Force bool `mapstructure:"force"` + Icon string `mapstructure:"icon,omitempty"` + AwsProfile string `mapstructure:"aws_profile,omitempty"` + AwsRegion string `mapstructure:"aws_region,omitempty"` + DependsOn []string `mapstructure:"depends_on,omitempty"` } type Serverless struct { diff --git a/internal/config/config.go b/internal/config/config.go index 77bc904b..bbc5a925 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "io/ioutil" "os" "os/exec" "path/filepath" @@ -26,10 +27,12 @@ const ( ) type Config struct { - AwsRegion string `mapstructure:"aws_region"` - AwsProfile string `mapstructure:"aws_profile"` - Namespace string `mapstructure:"namespace"` - Env string `mapstructure:"env"` + AwsRegion string `mapstructure:"aws_region"` + AwsProfile string `mapstructure:"aws_profile"` + Namespace string `mapstructure:"namespace"` + Env string `mapstructure:"env"` + LocalStack bool `mapstructure:"localstack"` + Session *session.Session IsGlobal bool IsDockerRuntime bool @@ -37,43 +40,25 @@ type Config struct { } func (p *Project) GetConfig() error { - switch viper.GetString("log_level") { - case "info": - logrus.SetLevel(logrus.InfoLevel) - case "debug": - logrus.SetLevel(logrus.DebugLevel) - case "trace": - logrus.SetLevel(logrus.TraceLevel) - case "panic": - logrus.SetLevel(logrus.PanicLevel) - case "warn": - logrus.SetLevel(logrus.WarnLevel) - case "error": - logrus.SetLevel(logrus.ErrorLevel) - case "fatal": - logrus.SetLevel(logrus.FatalLevel) - default: - logrus.SetLevel(logrus.FatalLevel) - } - err := ConvertApps() + err := MigrateAppsConfig() if err != nil { return err } - err = ConvertInfra() + err = MigrateInfraConfig() if err != nil { return err } - err = ConvertTunnel() + err = MigrateTunnelConfig() if err != nil { return err } - SetTag() + err = SetTag() if err != nil { - return fmt.Errorf("can't set tag: %w", err) + return fmt.Errorf("can't set tag: %w. \nIs it a git repo?", err) } err = schema.Validate(viper.AllSettings()) @@ -81,8 +66,6 @@ func (p *Project) GetConfig() error { return err } - logrus.Debug("config file used:", viper.ConfigFileUsed()) - err = viper.Unmarshal(p) if err != nil { return err @@ -93,9 +76,29 @@ func (p *Project) GetConfig() error { return err } + if p.LocalStack { + // Set default Endpoint URL for localstack if it's enabled + if len(p.EndpointUrl) == 0 { + p.EndpointUrl = "http://127.0.0.1:4566" + } + } + + if len(p.SshPublicKey) == 0 { + // Read id_rsa if it's not set + home, _ := os.UserHomeDir() + key, err := ioutil.ReadFile(fmt.Sprintf("%s/.ssh/id_rsa.pub", home)) + if err != nil { + return fmt.Errorf("can't read public ssh key: %s", err) + + } + + p.SshPublicKey = string(key)[:len(string(key))-1] + } + sess, err := utils.GetSession(&utils.SessionConfig{ - Region: p.AwsRegion, - Profile: p.AwsProfile, + Region: p.AwsRegion, + Profile: p.AwsProfile, + EndpointUrl: p.EndpointUrl, }) if err != nil { return err @@ -111,7 +114,12 @@ func (p *Project) GetConfig() error { } if len(p.DockerRegistry) == 0 { - p.DockerRegistry = fmt.Sprintf("%v.dkr.ecr.%v.amazonaws.com", *resp.Account, p.AwsRegion) + if p.LocalStack { + p.DockerRegistry = fmt.Sprintf("%v.dkr.ecr.%v.localhost.localstack.cloud:4512", *resp.Account, p.AwsRegion) + } else { + p.DockerRegistry = fmt.Sprintf("%v.dkr.ecr.%v.amazonaws.com", *resp.Account, p.AwsRegion) + } + logrus.Debugf("Setting Docker Registry to %s", p.DockerRegistry) } // Reset env directory to default because env may change if len(p.DockerRegistry) == 0 { @@ -149,24 +157,24 @@ func (p *Project) GetTestConfig() error { logrus.SetLevel(logrus.FatalLevel) } - err := ConvertApps() + err := MigrateAppsConfig() if err != nil { return err } - err = ConvertInfra() + err = MigrateInfraConfig() if err != nil { return err } - err = ConvertTunnel() + err = MigrateTunnelConfig() if err != nil { return err } SetTag() if err != nil { - return fmt.Errorf("can't set tag: %w", err) + return fmt.Errorf("can't set tag: %w. \nIs it a git repo?", err) } err = schema.Validate(viper.AllSettings()) @@ -174,7 +182,7 @@ func (p *Project) GetTestConfig() error { return err } - logrus.Debug("config file used:", viper.ConfigFileUsed()) + logrus.Debug("Config file used: ", viper.ConfigFileUsed()) err = viper.Unmarshal(p) if err != nil { @@ -182,8 +190,9 @@ func (p *Project) GetTestConfig() error { } sess, err := utils.GetTestSession(&utils.SessionConfig{ - Region: p.AwsRegion, - Profile: p.AwsProfile, + Region: p.AwsRegion, + Profile: p.AwsProfile, + EndpointUrl: p.EndpointUrl, }) if err != nil { return err @@ -210,7 +219,7 @@ func (p *Project) GetTestConfig() error { return nil } -func SetTag() { +func SetTag() error { out, err := exec.Command("git", "rev-parse", "--short", "HEAD").Output() if err != nil { if viper.GetString("ENV") == "" { @@ -222,6 +231,7 @@ func SetTag() { } else { viper.SetDefault("TAG", strings.Trim(string(out), "\n")) } + return err } func InitConfig() { @@ -231,40 +241,75 @@ func InitConfig() { viper.SetEnvKeyReplacer(replacer) viper.AutomaticEnv() + // Set Log Level + switch viper.GetString("log_level") { + case "info": + logrus.SetLevel(logrus.InfoLevel) + case "debug": + logrus.SetLevel(logrus.DebugLevel) + case "trace": + logrus.SetLevel(logrus.TraceLevel) + case "panic": + logrus.SetLevel(logrus.PanicLevel) + case "warn": + logrus.SetLevel(logrus.WarnLevel) + case "error": + logrus.SetLevel(logrus.ErrorLevel) + case "fatal": + logrus.SetLevel(logrus.FatalLevel) + default: + logrus.SetLevel(logrus.FatalLevel) + } + + // Variables that would be read even without IZE_ prefix (in addition to IZE_ENV, IZE_TAG, etc) _ = viper.BindEnv("ENV", "ENV") _ = viper.BindEnv("TAG", "TAG") _ = viper.BindEnv("AWS_PROFILE", "AWS_PROFILE") _ = viper.BindEnv("AWS_REGION", "AWS_REGION") _ = viper.BindEnv("NAMESPACE", "NAMESPACE") + _ = viper.BindEnv("SSH_PUBLIC_KEY", "SSH_PUBLIC_KEY") // TODO: those static defaults should probably go to a separate package and/or function. Also would include image names and such. viper.SetDefault("TERRAFORM_VERSION", "1.1.3") + viper.SetDefault("NVM_VERSION", "0.39.7") viper.SetDefault("PREFER_RUNTIME", "native") viper.SetDefault("CUSTOM_PROMPT", false) viper.SetDefault("PLAIN_TEXT_OUTPUT", false) + viper.SetDefault("LOCALSTACK", false) + viper.SetDefault("apps_provider", "ecs") home, err := os.UserHomeDir() if err != nil { - logrus.Fatalln("can't initialize config: %w", err) + logrus.Fatalln("Can't initialize config: %w", err) } + cwd, err := os.Getwd() if err != nil { - logrus.Fatalln("can't initialize config: %w", err) + logrus.Fatalln("Can't initialize config: %w", err) } - // set default apps folder - _, err = os.Stat(filepath.Join(cwd, "projects")) + // Set default apps folder - first option is `projects` + appsPath := filepath.Join(cwd, "projects") + _, err = os.Stat(appsPath) if os.IsNotExist(err) { - viper.SetDefault("APPS_PATH", filepath.Join(cwd, "apps")) - } else { - viper.SetDefault("APPS_PATH", filepath.Join(cwd, "projects")) + // Second option `apps` + appsPath = filepath.Join(cwd, "apps") + + // TODO: Add multi-repo support (cwd is the app directory). + // Maybe use ../../cwd? so the repo directory would be the app name? and ize.toml would be in the root of the repo. + // For now try setting `export APPS_PATH=../ } + logrus.Debugf("Setting APPS_PATH to %v", appsPath) + viper.SetDefault("APPS_PATH", appsPath) + viper.SetDefault("ROOT_DIR", cwd) viper.SetDefault("HOME", fmt.Sprintf("%v", home)) + setDefaultInfraDir(cwd) - cfg, err := readConfigFile(viper.GetString("config_file")) + configFileLocation := viper.GetString("config_file") + cfg, err := readConfigFile(configFileLocation) if err != nil { logrus.Fatal("can't initialize config: %w", err) } @@ -276,7 +321,7 @@ func InitConfig() { } } - logrus.Debug("config file used:", viper.ConfigFileUsed()) + logrus.Debug("Config file used: ", viper.ConfigFileUsed()) if cfg == nil { if err := viper.Unmarshal(&cfg); err != nil { @@ -286,6 +331,7 @@ func InitConfig() { viper.SetDefault("TF_LOG", "") + // Global Config is an experimental feature where you can have one config in your home directory if cfg.IsGlobal { viper.SetDefault("ENV_DIR", fmt.Sprintf("%s/.ize/%s/%s", home, cfg.Namespace, cfg.Env)) _, err := os.Stat(viper.GetString("ENV_DIR")) @@ -376,13 +422,33 @@ func findDuplicates(cfg *Project) error { } 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"))) - _, err := os.Stat(viper.GetString("IZE_DIR")) + izeDir := fmt.Sprintf("%v/.ize", cwd) + envDir := fmt.Sprintf("%v/env/%v", izeDir, viper.GetString("ENV")) + + _, err := os.Stat(izeDir) // Check if directory that we've set exists if err != nil { - viper.SetDefault("IZE_DIR", fmt.Sprintf("%v/.infra", cwd)) - viper.SetDefault("ENV_DIR", fmt.Sprintf("%v/.infra/env/%v", cwd, viper.GetString("ENV"))) + // izeDir doesn't exist, so setting the default to .infra + logrus.Debugf("Tried %v, but not found.", izeDir) + + izeDir = fmt.Sprintf("%v/.infra", cwd) + envDir = fmt.Sprintf("%v/env/%v", izeDir, viper.GetString("ENV")) + + _, err := os.Stat(izeDir) // Check if directory that we've set exists + if err != nil { + // izeDir doesn't exist, so setting the default to .infra + logrus.Debugf("Tried %v, but not found.", izeDir) + + izeDir = fmt.Sprintf("%v", cwd) + envDir = fmt.Sprintf("%v/env/%v", izeDir, viper.GetString("ENV")) + } } + + logrus.Debug("Setting IZE_DIR to ", izeDir) + viper.SetDefault("IZE_DIR", izeDir) + + logrus.Debug("Setting ENV_DIR to ", envDir) + viper.SetDefault("ENV_DIR", envDir) + } func readGlobalConfigFile() (*Config, error) { @@ -425,13 +491,25 @@ func readGlobalConfigFile() (*Config, error) { } func readConfigFile(path string) (*Config, error) { + if len(path) != 0 { + // If path is defined use it to read config + logrus.Debug("Reading config file:", path) viper.SetConfigFile(path) + } else { + // If path is undefined read using viper's ConfigPath + logrus.Debug("Config file is not overriden via `config_file`") + viper.SetConfigName("ize") viper.SetConfigType("toml") viper.AddConfigPath(".") - viper.AddConfigPath(viper.GetString("ENV_DIR")) + + envDir := filepath.Join(viper.GetString("ENV_DIR")) + + logrus.Debug(fmt.Sprintf("Adding config path to viper: %s", envDir)) + viper.AddConfigPath(envDir) + } if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); ok { @@ -449,7 +527,7 @@ func readConfigFile(path string) (*Config, error) { return &cfg, nil } -func ConvertApps() error { +func MigrateAppsConfig() error { ecs := map[string]interface{}{} serverless := map[string]interface{}{} @@ -491,7 +569,7 @@ func ConvertApps() error { return nil } -func ConvertInfra() error { +func MigrateInfraConfig() error { tf := viper.GetStringMap("infra.terraform") version, ok := tf["terraform_version"] @@ -510,7 +588,7 @@ func ConvertInfra() error { return nil } -func ConvertTunnel() error { +func MigrateTunnelConfig() error { tunnel := viper.GetStringMap("infra.tunnel") err := viper.MergeConfigMap(map[string]interface{}{ diff --git a/internal/config/project.go b/internal/config/project.go index 1ef7395f..3331fb30 100644 --- a/internal/config/project.go +++ b/internal/config/project.go @@ -24,22 +24,27 @@ type Project struct { TerraformVersion string `mapstructure:"terraform_version,omitempty"` AwsRegion string `mapstructure:"aws_region,omitempty"` AwsProfile string `mapstructure:"aws_profile,omitempty"` - Namespace string `mapstructure:",omitempty"` - Env string `mapstructure:",omitempty"` + Namespace string `mapstructure:"namespace,omitempty"` + Env string `mapstructure:"env,omitempty"` LogLevel string `mapstructure:"log_level,omitempty"` PlainText bool `mapstructure:"plain_text_output,omitempty"` CustomPrompt bool `mapstructure:"custom_prompt,omitempty"` PreferRuntime string `mapstructure:"prefer_runtime,omitempty"` - Tag string `mapstructure:",omitempty"` + Tag string `mapstructure:"tag,omitempty"` DockerRegistry string `mapstructure:"docker_registry,omitempty"` - - Home string `mapstructure:",omitempty"` - RootDir string `mapstructure:"root_dir,omitempty"` - InfraDir string `mapstructure:"ize_dir,omitempty"` - EnvDir string `mapstructure:"env_dir,omitempty"` - AppsPath string `mapstructure:"apps_path,omitempty"` - TFLog string `mapstructure:"tf_log,omitempty"` - TFLogPath string `mapstructure:"tf_log_path,omitempty"` + EndpointUrl string `mapstructure:"endpoint_url,omitempty"` + LocalStack bool `mapstructure:"localstack,omitempty"` + SshPublicKey string `mapstructure:"ssh_public_key,omitempty"` + NvmVersion string `mapstructure:"nvm_version"` + + Home string `mapstructure:"home,omitempty"` + RootDir string `mapstructure:"root_dir,omitempty"` + InfraDir string `mapstructure:"ize_dir,omitempty"` + EnvDir string `mapstructure:"env_dir,omitempty"` + AppsPath string `mapstructure:"apps_path,omitempty"` + TFLog string `mapstructure:"tf_log,omitempty"` + TFLogPath string `mapstructure:"tf_log_path,omitempty"` + AppsProvider string `mapstructure:"apps_provider,omitempty"` Session *session.Session AWSClient *awsClient @@ -48,6 +53,7 @@ type Project struct { Terraform map[string]*Terraform `mapstructure:",omitempty"` Ecs map[string]*Ecs `mapstructure:",omitempty"` Serverless map[string]*Serverless `mapstructure:",omitempty"` + Helm map[string]*Helm `mapstructure:",omitempty"` Alias map[string]*Alias `mapstructure:",omitempty"` } diff --git a/internal/localstack/localstack.go b/internal/localstack/localstack.go new file mode 100644 index 00000000..b48d6092 --- /dev/null +++ b/internal/localstack/localstack.go @@ -0,0 +1,358 @@ +package localstack + +const ( + defaultName = "localstack-main" + version = "1.0.4" +) + +//func cleanupOldContainers(cli *client.Client) error { +// containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{ +// All: true, +// }) +// if err != nil { +// return err +// } +// +// for _, c := range containers { +// if strings.Contains(c.Names[0], defaultName) { +// err = cli.ContainerRemove(context.Background(), c.ID, types.ContainerRemoveOptions{}) +// if err != nil { +// return err +// } +// } +// } +// +// return nil +//} +// +//type docker struct { +// version string +// command []string +// env []string +// output io.Writer +// project *config.Project +// state string +//} +// +//func NewDockerLocalstack(state string, command []string, env []string, out io.Writer, project *config.Project) *docker { +// project.Terraform[state].Version = project.TerraformVersion +// return &docker{ +// state: state, +// version: version, +// command: command, +// env: env, +// output: out, +// project: project, +// } +//} +// +//func (d *docker) Prepare() error { +// return nil +//} +// +//func (d *docker) NewCmd(cmd []string) { +// d.command = cmd +//} +// +//func (d *docker) SetOut(out io.Writer) { +// d.output = out +//} +// +//func (d *docker) RunUI(ui terminal.UI) error { +// sg := ui.StepGroup() +// defer sg.Wait() +// +// s := sg.Add("Initializing Docker client...") +// defer func() { s.Abort(); time.Sleep(time.Millisecond * 50) }() +// +// cli, err := client.NewClientWithOpts(client.FromEnv) +// if err != nil { +// return err +// } +// +// s.Done() +// s = sg.Add("Cleaning up old containers...") +// +// err = cleanupOldContainers(cli) +// if err != nil { +// return err +// } +// +// imageName := "localstack/localstack" +// imageTag := d.version +// +// imageRef, err := reference.ParseNormalizedNamed(fmt.Sprintf("%s:%s", imageName, imageTag)) +// if err != nil { +// return fmt.Errorf("error parsing Docker image: %s", err) +// } +// +// imageList, err := cli.ImageList(context.Background(), types.ImageListOptions{ +// Filters: filters.NewArgs(filters.KeyValuePair{ +// Key: "reference", +// Value: reference.FamiliarString(imageRef), +// }), +// }) +// if err != nil { +// return err +// } +// +// stdout, _, err := ui.OutputWriters() +// if err != nil { +// return err +// } +// +// var termFd uintptr +// if f, ok := stdout.(*os.File); ok { +// termFd = f.Fd() +// } +// +// if len(imageList) == 0 { +// s.Update("pulling terraform image %v:%v...", imageName, imageTag) +// out, err := cli.ImagePull(context.Background(), reference.FamiliarString(imageRef), types.ImagePullOptions{}) +// if err != nil { +// return err +// } +// defer out.Close() +// +// if err != nil { +// return err +// } +// +// err = jsonmessage.DisplayJSONMessagesStream(out, s.TermOutput(), termFd, true, nil) +// if err != nil { +// return fmt.Errorf("unable to stream pull logs to the terminal: %s", err) +// } +// +// s.Done() +// s = sg.Add("") +// } +// +// 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()), +// Image: fmt.Sprintf("%v:%v", imageName, imageTag), +// Tty: true, +// Cmd: d.command, +// AttachStdin: true, +// AttachStdout: true, +// AttachStderr: true, +// OpenStdin: true, +// WorkingDir: stateDir, +// Env: d.env, +// } +// +// contHostConfig := &container.HostConfig{ +// AutoRemove: true, +// Mounts: []mount.Mount{ +// //{ +// // Type: mount.TypeBind, +// // Source: fmt.Sprintf("%v", d.project.EnvDir), +// // Target: fmt.Sprintf("%v", d.project.EnvDir), +// //}, +// //{ +// // Type: mount.TypeBind, +// // Source: fmt.Sprintf("%v", d.project.InfraDir), +// // Target: fmt.Sprintf("%v", d.project.InfraDir), +// //}, +// //{ +// // Type: mount.TypeBind, +// // Source: fmt.Sprintf("%v/.aws", d.project.Home), +// // Target: "/.aws", +// //}, +// }, +// } +// +// s.Update("[%s][%s] running terraform image %v:%v...", d.project.Env, d.state, imageName, imageTag) +// +// cont, err := cli.ContainerCreate( +// context.Background(), +// contConfig, +// contHostConfig, +// nil, +// nil, +// defaultName, +// ) +// +// if err != nil { +// return err +// } +// +// if err := cli.ContainerStart(context.Background(), cont.ID, types.ContainerStartOptions{}); err != nil { +// return err +// } +// +// utils.SetupSignalHandlers(cli, cont.ID) +// +// reader, err := cli.ContainerLogs(context.Background(), cont.ID, types.ContainerLogsOptions{ +// ShowStdout: true, +// ShowStderr: true, +// Follow: true, +// Timestamps: false, +// }) +// if err != nil { +// panic(err) +// } +// +// defer reader.Close() +// +// if d.output != nil { +// io.Copy(d.output, reader) +// +// } else { +// io.Copy(s.TermOutput(), reader) +// } +// +// wait, errC := cli.ContainerWait(context.Background(), cont.ID, container.WaitConditionRemoved) +// +// select { +// case status := <-wait: +// if status.StatusCode != 0 { +// return fmt.Errorf("container exit status code %d\n", status.StatusCode) +// } +// s.Done() +// return nil +// case err := <-errC: +// return err +// } +//} +// +//func (d *docker) Run() error { +// cli, err := client.NewClientWithOpts(client.FromEnv) +// if err != nil { +// return err +// } +// +// err = cleanupOldContainers(cli) +// if err != nil { +// return err +// } +// +// imageName := "hashicorp/terraform" +// imageTag := d.version +// +// imageRef, err := reference.ParseNormalizedNamed(fmt.Sprintf("%s:%s", imageName, imageTag)) +// if err != nil { +// return fmt.Errorf("error parsing Docker image: %s", err) +// } +// +// imageList, err := cli.ImageList(context.Background(), types.ImageListOptions{ +// Filters: filters.NewArgs(filters.KeyValuePair{ +// Key: "reference", +// Value: reference.FamiliarString(imageRef), +// }), +// }) +// if err != nil { +// return err +// } +// +// if len(imageList) == 0 { +// out, err := cli.ImagePull(context.Background(), reference.FamiliarString(imageRef), types.ImagePullOptions{}) +// if err != nil { +// return err +// } +// defer out.Close() +// +// if err != nil { +// return err +// } +// +// err = jsonmessage.DisplayJSONMessagesStream(out, os.Stderr, os.Stderr.Fd(), true, nil) +// if err != nil { +// return fmt.Errorf("unable to stream pull logs to the terminal: %s", err) +// } +// } +// +// logrus.Infof("image name: %s, image tag: %s", imageName, imageTag) +// +// contConfig := &container.Config{ +// User: fmt.Sprintf("%v:%v", os.Getuid(), os.Getgid()), +// Image: fmt.Sprintf("%v:%v", imageName, imageTag), +// Tty: true, +// Cmd: d.command, +// AttachStdin: true, +// AttachStdout: true, +// AttachStderr: true, +// OpenStdin: true, +// WorkingDir: fmt.Sprintf("%v", d.project.EnvDir), +// Env: d.env, +// } +// +// contHostConfig := &container.HostConfig{ +// AutoRemove: true, +// Mounts: []mount.Mount{ +// { +// Type: mount.TypeBind, +// Source: fmt.Sprintf("%v", d.project.EnvDir), +// Target: fmt.Sprintf("%v", d.project.EnvDir), +// }, +// { +// Type: mount.TypeBind, +// Source: fmt.Sprintf("%v", d.project.InfraDir), +// Target: fmt.Sprintf("%v", d.project.InfraDir), +// }, +// { +// Type: mount.TypeBind, +// Source: fmt.Sprintf("%v/.aws", d.project.Home), +// Target: "/.aws", +// }, +// }, +// } +// +// cont, err := cli.ContainerCreate( +// context.Background(), +// contConfig, +// contHostConfig, +// nil, +// nil, +// defaultName, +// ) +// +// if err != nil { +// return err +// } +// +// if err := cli.ContainerStart(context.Background(), cont.ID, types.ContainerStartOptions{}); err != nil { +// return err +// } +// +// waiter, err := cli.ContainerAttach(context.Background(), cont.ID, types.ContainerAttachOptions{ +// Stream: true, +// Stdin: true, +// Stdout: true, +// Stderr: true, +// }) +// if err != nil { +// return err +// } +// +// go io.Copy(os.Stdout, waiter.Reader) +// go io.Copy(os.Stderr, waiter.Reader) +// go io.Copy(waiter.Conn, os.Stdin) +// +// fd := int(os.Stdin.Fd()) +// var oldState *t.State +// if t.IsTerminal(fd) { +// oldState, err = t.MakeRaw(fd) +// if err != nil { +// return err +// } +// defer t.Restore(fd, oldState) +// } +// +// wait, errC := cli.ContainerWait(context.Background(), cont.ID, container.WaitConditionRemoved) +// +// select { +// case status := <-wait: +// if status.StatusCode != 0 { +// return fmt.Errorf("container exit status code %d\n", status.StatusCode) +// } +// return nil +// case err := <-errC: +// return err +// } +//} diff --git a/internal/manager/ecs/ecs.go b/internal/manager/ecs/ecs.go index a809814d..b893dd0b 100644 --- a/internal/manager/ecs/ecs.go +++ b/internal/manager/ecs/ecs.go @@ -4,7 +4,9 @@ import ( "context" "encoding/base64" "encoding/json" + "errors" "fmt" + "github.com/aws/aws-sdk-go/aws/awserr" "os" "path" "path/filepath" @@ -29,6 +31,7 @@ const ecsDeployImage = "hazelops/ecs-deploy:latest" type Manager struct { Project *config.Project App *config.Ecs + config *config.Config } func (e *Manager) prepare() { @@ -58,6 +61,14 @@ func (e *Manager) prepare() { if e.App.Timeout == 0 { e.App.Timeout = 300 } + + if len(e.App.ServiceName) == 0 { + var err error + e.App.ServiceName, err = getEcsServiceName(e) + if err != nil { + + } + } } // Deploy deploys app container to ECS via ECS deploy @@ -69,8 +80,9 @@ func (e *Manager) Deploy(ui terminal.UI) error { if len(e.App.AwsRegion) != 0 && len(e.App.AwsProfile) != 0 { sess, err := utils.GetSession(&utils.SessionConfig{ - Region: e.App.AwsRegion, - Profile: e.App.AwsProfile, + Region: e.App.AwsRegion, + Profile: e.App.AwsProfile, + EndpointUrl: e.Project.EndpointUrl, }) if err != nil { return fmt.Errorf("can't get session: %w", err) @@ -80,7 +92,7 @@ func (e *Manager) Deploy(ui terminal.UI) error { } if e.App.SkipDeploy { - s := sg.Add("%s: deploy will be skipped", e.App.Name) + s := sg.Add("%s: deploy is skipped", e.App.Name) defer func() { s.Abort(); time.Sleep(50 * time.Millisecond) }() s.Done() return nil @@ -95,10 +107,11 @@ func (e *Manager) Deploy(ui terminal.UI) error { - Unhealthy Threshold Count: 2`)) } - s := sg.Add("%s: deploying app container...", e.App.Name) + s := sg.Add("%s: deploying to ECS %s", e.App.Name, e.App.ServiceName) defer func() { s.Abort(); time.Sleep(50 * time.Millisecond) }() if e.App.Image == "" { + // Set image name to /-: by default e.App.Image = fmt.Sprintf("%s/%s:%s", e.App.DockerRegistry, fmt.Sprintf("%s-%s", e.Project.Namespace, e.App.Name), @@ -133,8 +146,9 @@ func (e *Manager) Redeploy(ui terminal.UI) error { if len(e.App.AwsRegion) != 0 && len(e.App.AwsProfile) != 0 { sess, err := utils.GetSession(&utils.SessionConfig{ - Region: e.App.AwsRegion, - Profile: e.App.AwsProfile, + Region: e.App.AwsRegion, + Profile: e.App.AwsProfile, + EndpointUrl: e.Project.EndpointUrl, }) if err != nil { return fmt.Errorf("can't get session: %w", err) @@ -143,7 +157,7 @@ func (e *Manager) Redeploy(ui terminal.UI) error { e.Project.SettingAWSClient(sess) } - s := sg.Add("%s: redeploying app container...", e.App.Name) + s := sg.Add("%s: redeploying to ECS %s", e.App.Name, e.App.ServiceName) defer func() { s.Abort(); time.Sleep(50 * time.Millisecond) }() if e.Project.PreferRuntime == "native" { @@ -172,11 +186,11 @@ func (e *Manager) Push(ui terminal.UI) error { sg := ui.StepGroup() defer sg.Wait() - s := sg.Add("%s: push app image...", e.App.Name) + s := sg.Add("%s: pushing docker image...", e.App.Name) defer func() { s.Abort(); time.Sleep(50 * time.Millisecond) }() if len(e.App.Image) != 0 { - s.Update("%s: pushing app image... (skipped, using %s) ", e.App.Name, e.App.Image) + s.Update("%s: pushing docker image... (skipped, using %s) ", e.App.Name, e.App.Image) s.Done() return nil @@ -208,6 +222,7 @@ func (e *Manager) Push(ui terminal.UI) error { repository = out.Repository } else { repository = dro.Repositories[0] + logrus.Debugf("Using ECR repository: %s", *repository.RepositoryUri) } gat, err := svc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{}) @@ -259,11 +274,11 @@ func (e *Manager) Build(ui terminal.UI) error { sg := ui.StepGroup() defer sg.Wait() - s := sg.Add("%s: building app container...", e.App.Name) + s := sg.Add("%s: building docker image...", e.App.Name) defer func() { s.Abort(); time.Sleep(50 * time.Millisecond) }() if len(e.App.Image) != 0 { - s.Update("%s: building app container... (skipped, using %s)", e.App.Name, e.App.Image) + s.Update("%s: building docker image... (skipped, using %s)", e.App.Name, e.App.Image) s.Done() return nil @@ -277,10 +292,16 @@ func (e *Manager) Build(ui terminal.UI) error { return fmt.Errorf("unable to get relative path: %w", err) } + cache := []string{fmt.Sprintf("%s:%s", imageUri, fmt.Sprintf("%s-latest", e.Project.Env))} + + logrus.Debugf("Using CACHE_IMAGE: %s", cache) + buildArgs := map[string]*string{ "PROJECT_PATH": &relProjectPath, "APP_PATH": &relProjectPath, "APP_NAME": &e.App.Name, + "CACHE_IMAGE": &cache[0], + "TAG": &e.Project.Tag, } tags := []string{ @@ -291,8 +312,6 @@ func (e *Manager) Build(ui terminal.UI) error { dockerfile := path.Join(e.App.Path, "Dockerfile") - cache := []string{fmt.Sprintf("%s:%s", imageUri, fmt.Sprintf("%s-latest", e.Project.Env))} - platform := "linux/amd64" if e.Project.PreferRuntime == "docker-arm64" { platform = "linux/arm64" @@ -375,3 +394,43 @@ func (e *Manager) Destroy(ui terminal.UI, autoApprove bool) error { return nil } + +func getEcsServiceName(e *Manager) (string, error) { + // TODO: Move core logic to a shared function (since it's used in deploy too) + ecsServiceCandidates := []string{ + fmt.Sprintf("%s-%s-%s", e.Project.Env, e.Project.Namespace, e.App.Name), + fmt.Sprintf("%s-%s", e.Project.Env, e.App.Name), + e.App.Name, + } + + for _, v := range ecsServiceCandidates { + logrus.Debugf("Checking if ECS service %s exists in cluster %s.", v, e.App.Cluster) + + _, err := e.Project.AWSClient.ECSClient.ListTasks(&ecs.ListTasksInput{ + Cluster: &e.App.Cluster, + DesiredStatus: aws.String(ecs.DesiredStatusRunning), + ServiceName: &v, + }) + + var aerr awserr.Error + if errors.As(err, &aerr) { + switch aerr.Code() { + case "ClusterNotFoundException": + return "", fmt.Errorf("ECS cluster %s not found", e.App.Cluster) + case "ServiceNotFoundException": + { + logrus.Infof("ECS Service not found: %s in cluster %s. Checking other options.", v, e.App.Cluster) + continue + } + default: + { + return "", err + } + } + + } + return v, err + } + err := errors.New("ECS Service not found") + return "", err +} diff --git a/internal/manager/ecs/native.go b/internal/manager/ecs/native.go index 047b5479..b30241f7 100644 --- a/internal/manager/ecs/native.go +++ b/internal/manager/ecs/native.go @@ -1,7 +1,9 @@ package ecs import ( + "encoding/json" "fmt" + "github.com/sirupsen/logrus" "io" "strconv" "strings" @@ -17,22 +19,19 @@ import ( ) func (e *Manager) deployLocal(w io.Writer) error { - pterm.SetDefaultOutput(w) svc := e.Project.AWSClient.ECSClient - name := fmt.Sprintf("%s-%s", e.Project.Env, e.App.Name) - dso, err := svc.DescribeServices(&ecs.DescribeServicesInput{ Cluster: &e.App.Cluster, - Services: []*string{&name}, + Services: []*string{&e.App.ServiceName}, }) if err != nil { return err } if len(dso.Services) == 0 { - return fmt.Errorf("app %s not found not found in %s cluster", name, e.App.Cluster) + return fmt.Errorf("app %s not found not found in %s cluster", e.App.ServiceName, e.App.Cluster) } dtdo, err := svc.DescribeTaskDefinition(&ecs.DescribeTaskDefinitionInput{ @@ -43,7 +42,7 @@ func (e *Manager) deployLocal(w io.Writer) error { } definitions, err := svc.ListTaskDefinitions(&ecs.ListTaskDefinitionsInput{ - FamilyPrefix: &name, + FamilyPrefix: &e.App.ServiceName, Sort: aws.String(ecs.SortOrderDesc), }) if err != nil { @@ -66,7 +65,13 @@ func (e *Manager) deployLocal(w io.Writer) error { oldTaskDef = *dtdo.TaskDefinition } - pterm.Printfln("Deploying based on task definition: %s:%d", *oldTaskDef.Family, *oldTaskDef.Revision) + oldTaskDefJson, err := json.Marshal(oldTaskDef) + if err != nil { + return err + } + + logrus.Debugf("oldTaskDef: %s", string(oldTaskDefJson)) + pterm.Fprintln(w, fmt.Sprintf("Deploying based on task definition: %s:%d", *oldTaskDef.Family, *oldTaskDef.Revision)) var image string @@ -82,12 +87,12 @@ func (e *Manager) deployLocal(w io.Writer) error { image = e.App.Image } - pterm.Printfln(`Changed image of container "%s" to : "%s" (was: "%s")`, *container.Name, image, *container.Image) + pterm.Fprintln(w, fmt.Sprintf(`Changed image of container "%s" to : "%s" (was: "%s")`, *container.Name, image, *container.Image)) container.Image = &image } } - pterm.Println("Creating new task definition revision") + pterm.Fprintln(w, "Creating new task definition revision") rtdo, err := svc.RegisterTaskDefinition(&ecs.RegisterTaskDefinitionInput{ ContainerDefinitions: oldTaskDef.ContainerDefinitions, @@ -107,28 +112,37 @@ func (e *Manager) deployLocal(w io.Writer) error { newTaskDef = *rtdo.TaskDefinition - pterm.Printfln("Successfully created revision: %s:%d", *rtdo.TaskDefinition.Family, *rtdo.TaskDefinition.Revision) + newTaskDefJson, err := json.Marshal(newTaskDef) + if err != nil { + return err + } - if err = e.updateTaskDefinition(&newTaskDef, &oldTaskDef, name, "Deploying new task definition"); err != nil { - err := e.getLastContainerLogs(fmt.Sprintf("%s-%s", e.Project.Env, e.App.Name)) + logrus.Debugf("newTaskDef: %s", string(newTaskDefJson)) + pterm.Fprintln(w, fmt.Sprintf("Successfully created revision: %s:%d", *rtdo.TaskDefinition.Family, *rtdo.TaskDefinition.Revision)) + + if err = e.updateTaskDefinition(w, &newTaskDef, &oldTaskDef, e.App.ServiceName, "Deploying new task definition"); err != nil { + err := e.getLastContainerLogs(w, fmt.Sprintf("%s", e.App.ServiceName)) if err != nil { - pterm.Println("Failed to get logs:", err) + pterm.Fprintln(w, "Failed to get logs:", err) } - sr, err := getStoppedReason(e.App.Cluster, name, svc) + sr, err := getStoppedReason(e.App.Cluster, e.App.ServiceName, svc) if err != nil { return err } - pterm.Printfln("Container %s couldn't start: %s", name, sr) + pterm.Fprintln(w, fmt.Sprintf("Container %s couldn't start: %s", e.App.ServiceName, sr)) + + pterm.Fprintln(w, fmt.Sprintf("Rolling back to old task definition: %s:%d", *oldTaskDef.Family, *oldTaskDef.Revision)) - pterm.Printfln("Rolling back to old task definition: %s:%d", *oldTaskDef.Family, *oldTaskDef.Revision) e.App.Timeout = 600 - if err = e.updateTaskDefinition(&oldTaskDef, &newTaskDef, name, "Deploying previous task definition"); err != nil { + logrus.Debugf("Setting timeout to %d seconds", e.App.Timeout) + + if err = e.updateTaskDefinition(w, &oldTaskDef, &newTaskDef, e.App.ServiceName, "Deploying previous task definition"); err != nil { return fmt.Errorf("unable to rollback to old task definition: %w", err) } - pterm.Println("Rollback successful") + pterm.Fprintln(w, "Rollback successful") return fmt.Errorf("deployment failed, but service has been rolled back to previous task definition: %s", *oldTaskDef.Family) } @@ -195,11 +209,11 @@ func (e *Manager) redeployLocal(w io.Writer) error { } } - if err = e.updateTaskDefinition(td, nil, name, "Redeploying new task definition"); err != nil { - pterm.Println(err) - err := e.getLastContainerLogs(fmt.Sprintf("%s-%s", e.Project.Env, e.App.Name)) + if err = e.updateTaskDefinition(w, td, nil, name, "Redeploying new task definition"); err != nil { + pterm.Fprintln(w, err) + err := e.getLastContainerLogs(w, fmt.Sprintf("%s", e.App.ServiceName)) if err != nil { - pterm.Println("Failed to get logs:", err) + pterm.Fprintln(w, "Failed to get logs:", err) } return fmt.Errorf("redeployment failed") } @@ -222,8 +236,8 @@ func getService(name string, cluster string, svc ecsiface.ECSAPI) (*ecs.Describe return dso, nil } -func (e *Manager) updateTaskDefinition(newTD *ecs.TaskDefinition, oldTD *ecs.TaskDefinition, serviceName string, title string) error { - pterm.Println("Updating service") +func (e *Manager) updateTaskDefinition(w io.Writer, newTD *ecs.TaskDefinition, oldTD *ecs.TaskDefinition, serviceName string, title string) error { + pterm.Fprintln(w, fmt.Sprintf("Updating ECS service: %s (timeout: %d)", e.App.ServiceName, e.App.Timeout)) svc := e.Project.AWSClient.ECSClient @@ -259,8 +273,8 @@ func (e *Manager) updateTaskDefinition(newTD *ecs.TaskDefinition, oldTD *ecs.Tas } } - pterm.Printfln("Successfully changed task definition to: %s:%d", *newTD.Family, *newTD.Revision) - pterm.Println(title) + pterm.Fprintln(w, fmt.Sprintf("Successfully changed task definition to: %s:%d", *newTD.Family, *newTD.Revision)) + pterm.Fprintln(w, title) waitingTimeout := time.Now().Add(time.Duration(e.App.Timeout) * time.Second) waiting := true @@ -279,7 +293,7 @@ func (e *Manager) updateTaskDefinition(newTD *ecs.TaskDefinition, oldTD *ecs.Tas } if waiting && time.Now().After(waitingTimeout) { - pterm.Println("Deployment failed due to timeout") + pterm.Fprintln(w, "Deployment failed due to timeout") return fmt.Errorf("deployment failed due to timeout") } @@ -297,7 +311,7 @@ func (e *Manager) updateTaskDefinition(newTD *ecs.TaskDefinition, oldTD *ecs.Tas } if oldTD != nil { - if err = deregisterTaskDefinition(svc, oldTD); err != nil { + if err = deregisterTaskDefinition(w, svc, oldTD); err != nil { return err } } @@ -389,8 +403,8 @@ func getStoppedReason(cluster string, name string, svc ecsiface.ECSAPI) (string, return *dto.Tasks[0].StoppedReason, nil } -func deregisterTaskDefinition(svc ecsiface.ECSAPI, td *ecs.TaskDefinition) error { - pterm.Println("Deregister task definition revision") +func deregisterTaskDefinition(w io.Writer, svc ecsiface.ECSAPI, td *ecs.TaskDefinition) error { + pterm.Fprintln(w, "Deregister task definition revision") _, err := svc.DeregisterTaskDefinition(&ecs.DeregisterTaskDefinitionInput{ TaskDefinition: td.TaskDefinitionArn, @@ -399,12 +413,12 @@ func deregisterTaskDefinition(svc ecsiface.ECSAPI, td *ecs.TaskDefinition) error return err } - pterm.Printfln("Successfully deregistered revision: %s:%d", *td.Family, *td.Revision) + pterm.Fprintln(w, fmt.Sprintf("Successfully deregistered revision: %s:%d", *td.Family, *td.Revision)) return nil } -func (e *Manager) getLastContainerLogs(logGroup string) error { +func (e *Manager) getLastContainerLogs(w io.Writer, logGroup string) error { cwl := e.Project.AWSClient.CloudWatchLogsClient out, err := cwl.DescribeLogStreams(&cloudwatchlogs.DescribeLogStreamsInput{ LogGroupName: &logGroup, @@ -420,7 +434,7 @@ func (e *Manager) getLastContainerLogs(logGroup string) error { return nil } - pterm.Println("Container logs:") + pterm.Fprintln(w, "Container logs:") for _, stream := range out.LogStreams { out, err := cwl.GetLogEvents(&cloudwatchlogs.GetLogEventsInput{ @@ -432,11 +446,11 @@ func (e *Manager) getLastContainerLogs(logGroup string) error { } for _, event := range out.Events { - pterm.Println("| " + *event.Message) + pterm.Fprintln(w, "| "+*event.Message) } } - pterm.Println() + pterm.Fprintln(w) return nil } diff --git a/internal/manager/k8s/k8s.go b/internal/manager/helm/helm.go similarity index 61% rename from internal/manager/k8s/k8s.go rename to internal/manager/helm/helm.go index fd7bfd74..ca8e2732 100644 --- a/internal/manager/k8s/k8s.go +++ b/internal/manager/helm/helm.go @@ -1,33 +1,34 @@ -package k8s +package helm import ( "context" "encoding/base64" "encoding/json" "fmt" + "github.com/hazelops/ize/pkg/term" + "io" "os" + "os/exec" "path" "path/filepath" "time" - "github.com/hazelops/ize/internal/aws/utils" - "github.com/hazelops/ize/internal/config" - "github.com/hazelops/ize/pkg/templates" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ecr" "github.com/docker/docker/api/types" + "github.com/hazelops/ize/internal/aws/utils" + "github.com/hazelops/ize/internal/config" "github.com/hazelops/ize/internal/docker" "github.com/hazelops/ize/pkg/terminal" "github.com/pterm/pterm" "github.com/sirupsen/logrus" ) -const k8sDeployImage = "hazelops/k8s-deploy:latest" +const helmDeployImage = "hazelops/helm-deploy:latest" type Manager struct { Project *config.Project - App *config.K8s + App *config.Helm } func (e *Manager) prepare() { @@ -46,8 +47,8 @@ func (e *Manager) prepare() { } } - if len(e.App.Cluster) == 0 { - e.App.Cluster = fmt.Sprintf("%s-%s", e.Project.Env, e.Project.Namespace) + if len(e.App.Namespace) == 0 { + e.App.Namespace = fmt.Sprintf("%s-%s", e.Project.Env, e.Project.Namespace) } if len(e.App.DockerRegistry) == 0 { @@ -59,7 +60,7 @@ func (e *Manager) prepare() { } } -// Deploy deploys app container to k8s via k8s deploy +// Deploy deploys app container to helm via helm deploy func (e *Manager) Deploy(ui terminal.UI) error { e.prepare() @@ -68,8 +69,9 @@ func (e *Manager) Deploy(ui terminal.UI) error { if len(e.App.AwsRegion) != 0 && len(e.App.AwsProfile) != 0 { sess, err := utils.GetSession(&utils.SessionConfig{ - Region: e.App.AwsRegion, - Profile: e.App.AwsProfile, + Region: e.App.AwsRegion, + Profile: e.App.AwsProfile, + EndpointUrl: e.Project.EndpointUrl, }) if err != nil { return fmt.Errorf("can't get session: %w", err) @@ -85,14 +87,14 @@ func (e *Manager) Deploy(ui terminal.UI) error { return nil } - if e.App.Unsafe && e.Project.PreferRuntime == "native" { - pterm.Warning.Println(templates.Dedent(` - deployment will be accelerated (unsafe): - - Health Check Interval: 5s - - Health Check Timeout: 2s - - Healthy Threshold Count: 2 - - Unhealthy Threshold Count: 2`)) - } + //if e.App.Unsafe && e.Project.PreferRuntime == "native" { + // pterm.Warning.Println(templates.Dedent(` + // deployment will be accelerated (unsafe): + // - Health Check Interval: 5s + // - Health Check Timeout: 2s + // - Healthy Threshold Count: 2 + // - Unhealthy Threshold Count: 2`)) + //} s := sg.Add("%s: deploying app container...", e.App.Name) defer func() { s.Abort(); time.Sleep(50 * time.Millisecond) }() @@ -105,16 +107,17 @@ func (e *Manager) Deploy(ui terminal.UI) error { } if e.Project.PreferRuntime == "native" { - err := e.deployLocal(s.TermOutput()) + + //err := e.deployLocal(s.TermOutput()) pterm.SetDefaultOutput(os.Stdout) + s = sg.Add("%s: deploying app [run helm deploy]...", e.App.Name) + err := e.runDeploy(s.TermOutput()) + if err != nil { return fmt.Errorf("unable to deploy app: %w", err) } } else { - err := e.deployWithDocker(s.TermOutput()) - if err != nil { - return fmt.Errorf("unable to deploy app: %w", err) - } + return fmt.Errorf("runtime not implemented. use native") } s.Done() @@ -124,45 +127,37 @@ func (e *Manager) Deploy(ui terminal.UI) error { return nil } -func (e *Manager) Redeploy(ui terminal.UI) error { - e.prepare() - - sg := ui.StepGroup() - defer sg.Wait() - - if len(e.App.AwsRegion) != 0 && len(e.App.AwsProfile) != 0 { - sess, err := utils.GetSession(&utils.SessionConfig{ - Region: e.App.AwsRegion, - Profile: e.App.AwsProfile, - }) - if err != nil { - return fmt.Errorf("can't get session: %w", err) - } - - e.Project.SettingAWSClient(sess) - } - - s := sg.Add("%s: redeploying app container...", e.App.Name) - defer func() { s.Abort(); time.Sleep(50 * time.Millisecond) }() - - if e.Project.PreferRuntime == "native" { - err := e.redeployLocal(s.TermOutput()) - pterm.SetDefaultOutput(os.Stdout) - if err != nil { - return fmt.Errorf("unable to redeploy app: %w", err) - } - } else { - err := e.redeployWithDocker(s.TermOutput()) - if err != nil { - return fmt.Errorf("unable to redeploy app: %w", err) - } - } - - s.Done() - s = sg.Add("%s: redeployment completed!", e.App.Name) - s.Done() - - return nil +func (helm *Manager) runDeploy(w io.Writer) error { + + // TODO build image name as a part of helm manager + logrus.Debugf(helm.Project.Namespace) + helmImageName := fmt.Sprintf("%s/%s-%s", helm.App.DockerRegistry, helm.Project.Namespace, helm.App.Name) + helmChartDir := fmt.Sprintf("%s", path.Join(helm.App.Path, "helm/api")) + + var command string + namespace := fmt.Sprintf("%s-%s", helm.Project.Env, helm.Project.Namespace) + command = fmt.Sprintf( + `AWS_PROFILE="localstack-user" KUBECONFIG=/Users/dmitry/.kube/metameetings-local \ + helm upgrade --atomic --install --namespace "%s" "%s" %s \ + --set image.repository=%s \ + --set image.tag="%s" + `, namespace, helm.App.Name, helmChartDir, helmImageName, helm.Project.Tag) + + //if sls.App.Force { + // command += ` \ + // --force` + //} + + logrus.SetOutput(w) + logrus.Debugf("command: %s", command) + + cmd := exec.Command("bash", "-c", command) + + return term.New( + term.WithDir(helm.App.Path), + term.WithStdout(w), + term.WithStderr(w), + ).InteractiveRun(cmd) } func (e *Manager) Push(ui terminal.UI) error { @@ -171,11 +166,11 @@ func (e *Manager) Push(ui terminal.UI) error { sg := ui.StepGroup() defer sg.Wait() - s := sg.Add("%s: push app image...", e.App.Name) + s := sg.Add("%s: push docker image...", e.App.Name) defer func() { s.Abort(); time.Sleep(50 * time.Millisecond) }() if len(e.App.Image) != 0 { - s.Update("%s: pushing app image... (skipped, using %s) ", e.App.Name, e.App.Image) + s.Update("%s: pushing docker image... (skipped, using %s) ", e.App.Name, e.App.Image) s.Done() return nil @@ -276,10 +271,14 @@ func (e *Manager) Build(ui terminal.UI) error { return fmt.Errorf("unable to get relative path: %w", err) } + cache := []string{fmt.Sprintf("%s:%s", imageUri, fmt.Sprintf("%s-latest", e.Project.Env))} + logrus.Debugf("Using CACHE_IMAGE: %s", cache) + buildArgs := map[string]*string{ "PROJECT_PATH": &relProjectPath, "APP_PATH": &relProjectPath, "APP_NAME": &e.App.Name, + "CACHE_IMAGE": &cache[0], } tags := []string{ @@ -290,8 +289,6 @@ func (e *Manager) Build(ui terminal.UI) error { dockerfile := path.Join(e.App.Path, "Dockerfile") - cache := []string{fmt.Sprintf("%s:%s", imageUri, fmt.Sprintf("%s-latest", e.Project.Env))} - platform := "linux/amd64" if e.Project.PreferRuntime == "docker-arm64" { platform = "linux/arm64" @@ -315,58 +312,58 @@ func (e *Manager) Build(ui terminal.UI) error { return nil } -func definitionsToBulletItems(definitions *k8s.ListTaskDefinitionsOutput) []pterm.BulletListItem { - var items []pterm.BulletListItem - for _, arn := range definitions.TaskDefinitionArns { - items = append(items, pterm.BulletListItem{Level: 0, Text: *arn}) - } - - return items -} +//func definitionsToBulletItems(definitions *Helm.ListTaskDefinitionsOutput) []pterm.BulletListItem { +// var items []pterm.BulletListItem +// //for _, arn := range definitions.TaskDefinitionArns { +// // items = append(items, pterm.BulletListItem{Level: 0, Text: *arn}) +// //} +// +// return items +//} func (e *Manager) Destroy(ui terminal.UI, autoApprove bool) error { sg := ui.StepGroup() defer sg.Wait() - s := sg.Add("%s: destroying task defintions...", e.App.Name) + s := sg.Add("%s: destroying Helm application...", e.App.Name) defer func() { s.Abort(); time.Sleep(time.Millisecond * 200) }() - name := fmt.Sprintf("%s-%s", e.Project.Env, e.App.Name) - - svc := e.Project.AWSClient.k8sClient - - definitions, err := svc.ListTaskDefinitions(&k8s.ListTaskDefinitionsInput{ - FamilyPrefix: &name, - Sort: aws.String(k8s.SortOrderDesc), - }) - if err != nil { - return fmt.Errorf("can't get list task definitions of '%s': %v", name, err) - } - - if !autoApprove { - pterm.SetDefaultOutput(s.TermOutput()) - - pterm.Printfln("this will destroy the following:") - pterm.DefaultBulletList.WithItems(definitionsToBulletItems(definitions)).Render() - - isContinue, err := pterm.DefaultInteractiveConfirm.WithDefaultText("Continue?").Show() - if err != nil { - return err - } - - if !isContinue { - return fmt.Errorf("destroying was canceled") - } - } - - for _, tda := range definitions.TaskDefinitionArns { - _, err := e.Project.AWSClient.k8sClient.DeregisterTaskDefinition(&k8s.DeregisterTaskDefinitionInput{ - TaskDefinition: tda, - }) - if err != nil { - return fmt.Errorf("can't deregister task definition '%s': %v", *tda, err) - } - } + //name := fmt.Sprintf("%s-%s", e.Project.Env, e.App.Name) + // + //svc := e.Project.AWSClient.helmClient + // + //definitions, err := svc.ListTaskDefinitions(&helm.ListTaskDefinitionsInput{ + // FamilyPrefix: &name, + // Sort: aws.String(helm.SortOrderDesc), + //}) + //if err != nil { + // return fmt.Errorf("can't get list task definitions of '%s': %v", name, err) + //} + // + //if !autoApprove { + // pterm.SetDefaultOutput(s.TermOutput()) + // + // pterm.Printfln("this will destroy the following:") + // pterm.DefaultBulletList.WithItems(definitionsToBulletItems(definitions)).Render() + // + // isContinue, err := pterm.DefaultInteractiveConfirm.WithDefaultText("Continue?").Show() + // if err != nil { + // return err + // } + // + // if !isContinue { + // return fmt.Errorf("destroying was canceled") + // } + //} + // + //for _, tda := range definitions.TaskDefinitionArns { + // _, err := e.Project.AWSClient.helmClient.DeregisterTaskDefinition(&helm.DeregisterTaskDefinitionInput{ + // TaskDefinition: tda, + // }) + // if err != nil { + // return fmt.Errorf("can't deregister task definition '%s': %v", *tda, err) + // } + //} s.Done() s = sg.Add("%s: destroying completed!", e.App.Name) @@ -374,3 +371,9 @@ func (e *Manager) Destroy(ui terminal.UI, autoApprove bool) error { return nil } +func (e *Manager) Redeploy(ui terminal.UI) error { + return nil +} +func (e *Manager) Explain() error { + return nil +} diff --git a/internal/manager/serverless/native.go b/internal/manager/serverless/native.go index e01c4c76..054a9dab 100644 --- a/internal/manager/serverless/native.go +++ b/internal/manager/serverless/native.go @@ -1,6 +1,7 @@ package serverless import ( + "bytes" "fmt" "io" "os" @@ -40,24 +41,48 @@ func (sls *Manager) runNpmInstall(w io.Writer) error { } func (sls *Manager) nvm(w io.Writer, command string) error { - nvmDir := os.Getenv("NVM_DIR") - if len(nvmDir) == 0 { - nvmDir = "$HOME/.nvm" + + nvmDir, err := sls.installNvm() + if err != nil { + return err } - err := sls.readNvmrc() + + err = sls.readNvmrc() if err != nil { return err } - cmd := exec.Command("bash", "-c", + logrus.Debugf("Running: bash -c source %s/nvm.sh && nvm install %s && %s", nvmDir, sls.App.NodeVersion, command) + + var bashFlags = "-c" + // If log level is debug or trace, enable verbose mode for bash wrapper + if sls.Project.LogLevel == "debug" || sls.Project.LogLevel == "trace" { + bashFlags = "-xvc" + } + + cmd := exec.Command("bash", bashFlags, fmt.Sprintf("source %s/nvm.sh && nvm install %s && %s", nvmDir, sls.App.NodeVersion, command), ) - return term.New( + // Capture stderr in a buffer + var stderr bytes.Buffer + var stdout bytes.Buffer + cmd.Stderr = &stderr + cmd.Stdout = &stdout + + t := term.New( term.WithDir(sls.App.Path), term.WithStdout(w), - term.WithStderr(w), - ).InteractiveRun(cmd) + term.WithStderr(&stderr), + ) + + err = t.InteractiveRun(cmd) + if err != nil { + // Return the error along with stderr output + return fmt.Errorf("command failed with error: %w, %s %s", err, stderr.String(), stdout.String()) + } + + return nil } func (sls *Manager) readNvmrc() error { @@ -70,16 +95,17 @@ func (sls *Manager) readNvmrc() error { } sls.App.NodeVersion = strings.TrimSpace(string(file)) } + return nil } func (sls *Manager) runNvm(w io.Writer) error { - nvmDir := os.Getenv("NVM_DIR") - if len(nvmDir) == 0 { - nvmDir = "$HOME/.nvm" + nvmDir, err := sls.installNvm() + if err != nil { + return err } - err := sls.readNvmrc() + err = sls.readNvmrc() if err != nil { return err } @@ -91,18 +117,31 @@ func (sls *Manager) runNvm(w io.Writer) error { cmd := exec.Command("bash", "-c", command) - return term.New( + // Capture stderr in a buffer + var stderr bytes.Buffer + var stdout bytes.Buffer + cmd.Stderr = &stderr + cmd.Stdout = &stdout + + t := term.New( term.WithDir(sls.App.Path), term.WithStdout(w), - term.WithStderr(w), - ).InteractiveRun(cmd) + term.WithStderr(&stderr), + ) + + err = t.InteractiveRun(cmd) + if err != nil { + // Return the error along with stderr output + return fmt.Errorf("command failed with error: %w, %s %s", err, stderr.String(), stdout.String()) + } + + return nil } func (sls *Manager) runDeploy(w io.Writer) error { - - nvmDir := os.Getenv("NVM_DIR") - if len(nvmDir) == 0 { - nvmDir = "$HOME/.nvm" + nvmDir, err := sls.installNvm() + if err != nil { + return err } var command string @@ -151,18 +190,32 @@ func (sls *Manager) runDeploy(w io.Writer) error { cmd := exec.Command("bash", "-c", command) - return term.New( + // Capture stderr in a buffer + var stderr bytes.Buffer + var stdout bytes.Buffer + cmd.Stderr = &stderr + cmd.Stdout = &stdout + + t := term.New( term.WithDir(sls.App.Path), term.WithStdout(w), - term.WithStderr(w), - ).InteractiveRun(cmd) + term.WithStderr(&stderr), + ) + + err = t.InteractiveRun(cmd) + if err != nil { + // Return the error along with stderr output + return fmt.Errorf("command failed with error: %w, %s %s", err, stderr.String(), stdout.String()) + } + + return nil } func (sls *Manager) runRemove(w io.Writer) error { - nvmDir := os.Getenv("NVM_DIR") - if len(nvmDir) == 0 { - nvmDir = "$HOME/.nvm" + nvmDir, err := sls.installNvm() + if err != nil { + return err } var command string @@ -207,17 +260,31 @@ func (sls *Manager) runRemove(w io.Writer) error { cmd := exec.Command("bash", "-c", command) - return term.New( + // Capture stderr in a buffer + var stderr bytes.Buffer + var stdout bytes.Buffer + cmd.Stderr = &stderr + cmd.Stdout = &stdout + + t := term.New( term.WithDir(sls.App.Path), term.WithStdout(w), - term.WithStderr(w), - ).InteractiveRun(cmd) + term.WithStderr(&stderr), + ) + + err = t.InteractiveRun(cmd) + if err != nil { + // Return the error along with stderr output + return fmt.Errorf("command failed with error: %w, %s %s", err, stderr.String(), stdout.String()) + } + + return nil } func (sls *Manager) runCreateDomain(w io.Writer) error { - nvmDir := os.Getenv("NVM_DIR") - if len(nvmDir) == 0 { - nvmDir = "$HOME/.nvm" + nvmDir, err := sls.installNvm() + if err != nil { + return err } command := fmt.Sprintf( @@ -240,17 +307,31 @@ func (sls *Manager) runCreateDomain(w io.Writer) error { cmd := exec.Command("bash", "-c", command) - return term.New( + // Capture stderr in a buffer + var stderr bytes.Buffer + var stdout bytes.Buffer + cmd.Stderr = &stderr + cmd.Stdout = &stdout + + t := term.New( term.WithDir(sls.App.Path), term.WithStdout(w), - term.WithStderr(w), - ).InteractiveRun(cmd) + term.WithStderr(&stderr), + ) + + err = t.InteractiveRun(cmd) + if err != nil { + // Return the error along with stderr output + return fmt.Errorf("command failed with error: %w, %s %s", err, stderr.String(), stdout.String()) + } + + return nil } func (sls *Manager) runRemoveDomain(w io.Writer) error { - nvmDir := os.Getenv("NVM_DIR") - if len(nvmDir) == 0 { - nvmDir = "$HOME/.nvm" + nvmDir, err := sls.installNvm() + if err != nil { + return err } command := fmt.Sprintf( @@ -273,14 +354,77 @@ func (sls *Manager) runRemoveDomain(w io.Writer) error { cmd := exec.Command("bash", "-c", command) - return term.New( + // Capture stderr in a buffer + var stderr bytes.Buffer + var stdout bytes.Buffer + cmd.Stderr = &stderr + cmd.Stdout = &stdout + + t := term.New( term.WithDir(sls.App.Path), term.WithStdout(w), - term.WithStderr(w), - ).InteractiveRun(cmd) + term.WithStderr(&stderr), + ) + + err = t.InteractiveRun(cmd) + if err != nil { + // Return the error along with stderr output + return fmt.Errorf("command failed with error: %w, %s %s", err, stderr.String(), stdout.String()) + } + + return nil } func npmToYarn(cmd string) string { cmd = strings.ReplaceAll(cmd, "npm", "yarn") return strings.ReplaceAll(cmd, "npx", "yarn") } + +func (sls *Manager) installNvm() (string, error) { + var err error + + nvmDir := os.Getenv("NVM_DIR") + if len(nvmDir) == 0 { + nvmDir = "$HOME/.nvm" + } + + // Check if nvm.sh exists in the nvmDir + nvmShPath := filepath.Join(nvmDir, "nvm.sh") + _, err = os.Stat(nvmShPath) + if !os.IsNotExist(err) { + logrus.Debug("nvm.sh found in the directory:", nvmDir) + + //check if version is what we expect + cmd := exec.Command("bash", "-c", fmt.Sprintf("source %s/nvm.sh && nvm --version", nvmDir)) + cmd.Dir = sls.Project.RootDir + + var out bytes.Buffer + cmd.Stdout = &out + err = cmd.Run() + if err != nil { + logrus.Debug("Error checking nvm version:", err) + return "", err + } + + if strings.TrimSpace(out.String()) == sls.Project.NvmVersion { + logrus.Debugf("nvm version is correct. Expected: %s, Found: %s", sls.Project.NvmVersion, strings.TrimSpace(out.String())) + return nvmDir, nil + } + + } + logrus.Debug("No correct nvm version found is incorrect, (re)installing nvm") + + // Install nvm. + cmd := exec.Command("bash", "-c", fmt.Sprintf("curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v%s/install.sh | bash", sls.Project.NvmVersion)) + cmd.Dir = sls.Project.RootDir + err = cmd.Run() + if err != nil { + logrus.Debugf("Error installing nvm:", err) + return "", err + } + + // TODO: If nvm.sh doesn't exist in the nvmDir, we should install it + // curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash + + return nvmDir, nil +} diff --git a/internal/manager/serverless/serverless.go b/internal/manager/serverless/serverless.go index ce786539..d98c2dda 100644 --- a/internal/manager/serverless/serverless.go +++ b/internal/manager/serverless/serverless.go @@ -29,7 +29,7 @@ func (sls *Manager) Nvm(ui terminal.UI, command []string) error { err := sls.nvm(s.TermOutput(), strings.Join(command, " ")) if err != nil { - return fmt.Errorf("can't run nvm: %w", err) + return fmt.Errorf("can't run nvm: %s ", err) } s.Done() diff --git a/internal/requirements/requirements.go b/internal/requirements/requirements.go index 7640c41b..0222f044 100644 --- a/internal/requirements/requirements.go +++ b/internal/requirements/requirements.go @@ -46,12 +46,12 @@ func CheckRequirements(options ...Option) error { switch viper.GetString("prefer_runtime") { case "native": - logrus.Debug("use native runtime") + logrus.Debug("Using native runtime") case "docker": if err := checkDocker(); err != nil { return err } - logrus.Debug("use docker runtime") + logrus.Debug("Using docker runtime (deprecated)") default: return fmt.Errorf("unknown runtime type: %s", viper.GetString("prefer_runtime")) } @@ -124,6 +124,11 @@ func isStructured() bool { isStructured = true } + _, err = os.Stat(filepath.Join(cwd, "ize.toml")) + if !os.IsNotExist(err) { + isStructured = true + } + return isStructured } diff --git a/internal/schema/ize-spec.json b/internal/schema/ize-spec.json index 0a4cda92..00702527 100644 --- a/internal/schema/ize-spec.json +++ b/internal/schema/ize-spec.json @@ -65,6 +65,29 @@ "type": "string", "description": "(optional) Terraform version can be set here. 1.1.3 by default" }, + "nvm_version": { + "type": "string", + "description": "(optional) Nvm version can be set here. 0.39.7 by default" + }, + "endpoint_url": { + "type": "string", + "description": "(optional) AWS Endpoint url (can be used with Localstack)" + }, + "localstack": { + "description": "(optional) Whether enable Localstack", + "anyOf": [ + { + "type": "string" + }, + { + "type": "boolean" + } + ] + }, + "ssh_public_key": { + "type": "string", + "description": "(optional) provide public key for resources" + }, "docker_registry": { "type": "string", "description": "(optional) Docker registry can be set here. By default it uses ECR repo with the name of the service." @@ -92,6 +115,10 @@ ], "description": "(optional) Custom prompt can be enabled here for all console connections. Default: false." }, + "apps_provider": { + "type": "string", + "description": "(optional) When there is no apps in the config which provider to use during `ize deploy app`" + }, "tunnel": { "type": "object", "properties": { @@ -141,6 +168,17 @@ "description": "Ecs apps configuration.", "additionalProperties": false }, + "helm": { + "id": "#/properties/helm", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/helm" + } + }, + "description": "Helm apps configuration.", + "additionalProperties": false + }, "serverless": { "id": "#/properties/serverless", "type": "object", @@ -367,12 +405,68 @@ "type": "string", "description": "(optional) ECS-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 apps" + }, + "service_name" : { + "type": "string", + "description": "(optional) ECS-specific service name (optional) can be specified here (but normally it should be deducted from namespace/app name." + } + }, + "description": "ECS app configuration.", + "additionalProperties": false + }, + "helm": { + "id": "#/definitions/helm", + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "(optional) Path to helm app folder can be specified here. By default it's derived from apps path and app name." + }, + "force": { + "type": "boolean", + "description": "(optional) forces a deployment to take place" + }, + "image" : { + "type": "string", + "description": "(optional) Docker image can be specified here. By default it's derived from the app name." + }, + "helm_release" : { + "type": "string", + "description": "(optional) Helm release can be specified here. By default a new release is created during deployment. Normally this parameter can be used via cli during specific release needs." + }, + "timeout" : { + "type": "integer", + "description": "(optional) helm deployment timeout can be specified here." + }, + "docker_registry" : { + "type": "string", + "description": "(optional) Docker registry can be set here. By default it uses ECR repo with the name of the service." + }, + "skip_deploy": { + "type": "boolean", + "description": "(optional) skip deploy app." + }, + "icon": { + "type": "string", + "description": "(optional) set icon" + }, + "aws_region" : { + "type": "string", + "description": "(optional) helm-specific AWS Region of this environment should be specified here. Normally global AWS_REGION is used." + }, + "aws_profile" : { + "type": "string", + "description": "(optional) helm-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 apps" } }, - "description": "Ecs app configuration.", + "description": "helm app configuration.", "additionalProperties": false }, "serverless": { diff --git a/internal/schema/schema.go b/internal/schema/schema.go index de96481d..6b8ea7bf 100644 --- a/internal/schema/schema.go +++ b/internal/schema/schema.go @@ -21,10 +21,12 @@ func Validate(config map[string]interface{}) error { if err := compiler.AddResource("schema.json", strings.NewReader(Schema)); err != nil { panic(err) } + schema, err := compiler.Compile("schema.json") if err != nil { panic(err) } + err = schema.ValidateInterface(config) if err != nil { i, m := GetErrorMessage(err.(*jsonschema.ValidationError)) @@ -33,6 +35,7 @@ func Validate(config map[string]interface{}) error { } else { i = strings.ReplaceAll(i[2:], "/", ".") } + errMsg := fmt.Sprintf("%s in %s of config file (or environment variables)", m, i) if strings.Contains(errMsg, "additionalProperties") { errMsg += ". The following options are available:\n" diff --git a/internal/template/template.go b/internal/template/template.go index 01635a85..4569317c 100644 --- a/internal/template/template.go +++ b/internal/template/template.go @@ -4,16 +4,14 @@ import ( "crypto/md5" "errors" "fmt" - "os" - "path/filepath" - "reflect" - "strings" - "github.com/AlecAivazis/survey/v2" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclwrite" "github.com/pterm/pterm" "github.com/zclconf/go-cty/cty" + "os" + "path/filepath" + "reflect" ) const ( @@ -133,50 +131,79 @@ func GenerateConfigFile(opts ConfigOpts, path string) error { func GenerateBackendTf(opts BackendOpts, path string) error { f := hclwrite.NewEmptyFile() - if strings.Contains(opts.ENV, "localstack") || strings.Contains(opts.ENV, "local") { + if len(opts.LOCALSTACK_ENDPOINT) > 0 { rootBody := f.Body() // AWS Provider block providerBlock := rootBody.AppendNewBlock("provider", []string{"aws"}) - providerBlock.Body().SetAttributeTraversal("profile", hcl.Traversal{ - hcl.TraverseRoot{Name: "var"}, - hcl.TraverseAttr{Name: "aws_profile"}, - }) - providerBlock.Body().SetAttributeTraversal("region", hcl.Traversal{ - hcl.TraverseRoot{Name: "var"}, - hcl.TraverseAttr{Name: "aws_region"}, - }) - providerBlock.Body().SetAttributeValue("s3_force_path_style", cty.True) - providerBlock.Body().SetAttributeValue("secret_key", cty.StringVal("mock_secret_key")) - providerBlock.Body().SetAttributeValue("skip_credentials_validation", cty.True) - providerBlock.Body().SetAttributeValue("skip_metadata_api_check", cty.True) - providerBlock.Body().SetAttributeValue("skip_requesting_account_id", cty.True) + providerBlock.Body().SetAttributeValue("shared_credentials_files", cty.ListVal([]cty.Value{cty.StringVal("./localstack-user-credentials.config")})) + + //providerBlock.Body().SetAttributeValue("access_key", cty.StringVal("test")) + //providerBlock.Body().SetAttributeValue("secret_key", cty.StringVal("test")) + //providerBlock.Body().SetAttributeTraversal("profile", hcl.Traversal{ + // hcl.TraverseRoot{Name: "var"}, + // hcl.TraverseAttr{Name: "aws_profile"}, + //}) + + //providerBlock.Body().SetAttributeTraversal("region", hcl.Traversal{ + // hcl.TraverseRoot{Name: "var"}, + // hcl.TraverseAttr{Name: "aws_region"}, + //}) + //providerBlock.Body().SetAttributeValue("s3_use_path_style", cty.True) + //providerBlock.Body().SetAttributeValue("secret_key", cty.StringVal("mock_secret_key")) + //providerBlock.Body().SetAttributeValue("skip_credentials_validation", cty.True) + //providerBlock.Body().SetAttributeValue("skip_metadata_api_check", cty.True) + //providerBlock.Body().SetAttributeValue("skip_requesting_account_id", cty.True) rootBody.AppendNewline() - // Endpoints - endpointBlock := rootBody.AppendNewBlock("endpoints", []string{}) - endpointBlock.Body().SetAttributeValue("apigateway", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) - endpointBlock.Body().SetAttributeValue("acm", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) - endpointBlock.Body().SetAttributeValue("cloudformation", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) - endpointBlock.Body().SetAttributeValue("cloudwatch", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) - endpointBlock.Body().SetAttributeValue("ec2", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) - endpointBlock.Body().SetAttributeValue("dynamodb", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) - endpointBlock.Body().SetAttributeValue("es", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) - endpointBlock.Body().SetAttributeValue("firehose", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) - endpointBlock.Body().SetAttributeValue("iam", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) - endpointBlock.Body().SetAttributeValue("kinesis", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) - endpointBlock.Body().SetAttributeValue("lambda", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) - endpointBlock.Body().SetAttributeValue("route53", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) - endpointBlock.Body().SetAttributeValue("redshift", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) - endpointBlock.Body().SetAttributeValue("s3", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) - endpointBlock.Body().SetAttributeValue("secretsmanager", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) - endpointBlock.Body().SetAttributeValue("ses", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) - endpointBlock.Body().SetAttributeValue("sns", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) - endpointBlock.Body().SetAttributeValue("sqs", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) - endpointBlock.Body().SetAttributeValue("ssm", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) - endpointBlock.Body().SetAttributeValue("stepfunctions", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) - endpointBlock.Body().SetAttributeValue("sts", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) - endpointBlock.Body().SetAttributeValue("ecs", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) - endpointBlock.Body().SetAttributeValue("ecr", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + //// Endpoints + //endpointsBlock := providerBlock.Body().AppendNewBlock("endpoints", nil) + //endpointsBlock.Body().SetAttributeValue("apigateway", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + //endpointsBlock.Body().SetAttributeValue("acm", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + //endpointsBlock.Body().SetAttributeValue("cloudformation", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + //endpointsBlock.Body().SetAttributeValue("cloudwatch", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + //endpointsBlock.Body().SetAttributeValue("ec2", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + //endpointsBlock.Body().SetAttributeValue("dynamodb", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + //endpointsBlock.Body().SetAttributeValue("es", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + //endpointsBlock.Body().SetAttributeValue("firehose", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + //endpointsBlock.Body().SetAttributeValue("iam", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + //endpointsBlock.Body().SetAttributeValue("kinesis", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + //endpointsBlock.Body().SetAttributeValue("lambda", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + //endpointsBlock.Body().SetAttributeValue("route53", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + //endpointsBlock.Body().SetAttributeValue("redshift", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + //endpointsBlock.Body().SetAttributeValue("s3", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + //endpointsBlock.Body().SetAttributeValue("secretsmanager", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + //endpointsBlock.Body().SetAttributeValue("ses", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + //endpointsBlock.Body().SetAttributeValue("sns", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + //endpointsBlock.Body().SetAttributeValue("sqs", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + //endpointsBlock.Body().SetAttributeValue("ssm", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + //endpointsBlock.Body().SetAttributeValue("stepfunctions", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + //endpointsBlock.Body().SetAttributeValue("sts", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + //endpointsBlock.Body().SetAttributeValue("ecs", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + //endpointsBlock.Body().SetAttributeValue("ecr", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + + // Terraform block + terraformBlock := f.Body().AppendNewBlock("terraform", []string{}) + // backend s3 block + backendBlock := terraformBlock.Body().AppendNewBlock("backend", []string{"s3"}) + backendBlock.Body().SetAttributeValue("bucket", cty.StringVal(opts.TERRAFORM_STATE_BUCKET_NAME)) + backendBlock.Body().SetAttributeValue("key", cty.StringVal(opts.TERRAFORM_STATE_KEY)) + backendBlock.Body().SetAttributeValue("region", cty.StringVal(opts.TERRAFORM_STATE_REGION)) + backendBlock.Body().SetAttributeValue("profile", cty.StringVal(opts.TERRAFORM_STATE_PROFILE)) + backendBlock.Body().SetAttributeValue("dynamodb_table", cty.StringVal(opts.TERRAFORM_STATE_DYNAMODB_TABLE)) + backendBlock.Body().SetAttributeValue("endpoint", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + backendBlock.Body().SetAttributeValue("sts_endpoint", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + backendBlock.Body().SetAttributeValue("iam_endpoint", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + backendBlock.Body().SetAttributeValue("dynamodb_endpoint", cty.StringVal(opts.LOCALSTACK_ENDPOINT)) + backendBlock.Body().SetAttributeValue("force_path_style", cty.BoolVal(true)) + backendBlock.Body().SetAttributeValue("shared_credentials_file", cty.StringVal("./localstack-user-credentials.config")) + + defaultTagsBlock := providerBlock.Body().AppendNewBlock("default_tags", nil) + defaultTagsBlock.Body().SetAttributeValue("tags", cty.ObjectVal(map[string]cty.Value{ + "terraform": cty.StringVal("true"), + "env": cty.StringVal(opts.ENV), + "namespace": cty.StringVal(opts.NAMESPACE), + })) + } else { rootBody := f.Body() // AWS Provider block