From ea078d2923ab6e19c33f0dc446bbc24c2e581c76 Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Sun, 21 Apr 2024 07:32:48 +0000 Subject: [PATCH 01/30] Progress (will reset-cleanup later) --- cmd/main.go | 3 +- internal/aws/utils/utils.go | 26 +- internal/commands/status.go | 29 +- internal/commands/tfenv.go | 14 +- internal/config/config.go | 31 +- internal/config/project.go | 3 + internal/localstack/localstack.go | 358 ++++++++++++++++++ internal/manager/ecs/ecs.go | 11 +- internal/manager/{k8s/k8s.go => helm/helm.go} | 12 +- internal/manager/serverless/native.go | 1 - internal/schema/ize-spec.json | 12 + internal/template/template.go | 113 +++--- 12 files changed, 515 insertions(+), 98 deletions(-) create mode 100644 internal/localstack/localstack.go rename internal/manager/{k8s/k8s.go => helm/helm.go} (97%) diff --git a/cmd/main.go b/cmd/main.go index 1db4429f..7a314e74 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -5,7 +5,7 @@ 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/internal/aws/utils/utils.go b/internal/aws/utils/utils.go index d3304ba7..f37c5281 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,23 @@ 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 { + // // 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 +51,13 @@ 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")) { + return nil, err + } + logrus.Debug("[NO MFA] Using Endpoint: ", c.EndpointUrl) } } diff --git a/internal/commands/status.go b/internal/commands/status.go index 4f6c1186..3098d718 100644 --- a/internal/commands/status.go +++ b/internal/commands/status.go @@ -84,35 +84,22 @@ 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 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 { diff --git a/internal/commands/tfenv.go b/internal/commands/tfenv.go index 755e8c17..581d6d1f 100644 --- a/internal/commands/tfenv.go +++ b/internal/commands/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, @@ -150,21 +148,13 @@ func GenerateTerraformFiles(name string, terraformStateBucketName string, projec 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) - - } - varsOpts := template.VarsOpts{ ENV: project.Env, AWS_PROFILE: project.AwsProfile, 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/config/config.go b/internal/config/config.go index 77bc904b..036d3426 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" @@ -93,9 +94,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 @@ -182,8 +203,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 @@ -236,6 +258,7 @@ func InitConfig() { _ = 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") diff --git a/internal/config/project.go b/internal/config/project.go index 1ef7395f..6911e5b9 100644 --- a/internal/config/project.go +++ b/internal/config/project.go @@ -32,6 +32,9 @@ type Project struct { PreferRuntime string `mapstructure:"prefer_runtime,omitempty"` Tag string `mapstructure:",omitempty"` DockerRegistry string `mapstructure:"docker_registry,omitempty"` + EndpointUrl string `mapstructure:"endpoint_url,omitempty"` + LocalStack bool `mapstructure:"localstack,omitempty"` + SshPublicKey string `mapstructure:"ssh_public_key,omitempty"` Home string `mapstructure:",omitempty"` RootDir string `mapstructure:"root_dir,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..323df1fd 100644 --- a/internal/manager/ecs/ecs.go +++ b/internal/manager/ecs/ecs.go @@ -29,6 +29,7 @@ const ecsDeployImage = "hazelops/ecs-deploy:latest" type Manager struct { Project *config.Project App *config.Ecs + config *config.Config } func (e *Manager) prepare() { @@ -69,8 +70,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) @@ -133,8 +135,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) diff --git a/internal/manager/k8s/k8s.go b/internal/manager/helm/helm.go similarity index 97% rename from internal/manager/k8s/k8s.go rename to internal/manager/helm/helm.go index fd7bfd74..16caffd2 100644 --- a/internal/manager/k8s/k8s.go +++ b/internal/manager/helm/helm.go @@ -1,4 +1,4 @@ -package k8s +package helm import ( "context" @@ -68,8 +68,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) @@ -132,8 +133,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) diff --git a/internal/manager/serverless/native.go b/internal/manager/serverless/native.go index e01c4c76..79890d6e 100644 --- a/internal/manager/serverless/native.go +++ b/internal/manager/serverless/native.go @@ -99,7 +99,6 @@ func (sls *Manager) runNvm(w io.Writer) error { } func (sls *Manager) runDeploy(w io.Writer) error { - nvmDir := os.Getenv("NVM_DIR") if len(nvmDir) == 0 { nvmDir = "$HOME/.nvm" diff --git a/internal/schema/ize-spec.json b/internal/schema/ize-spec.json index 0a4cda92..9d8a9c85 100644 --- a/internal/schema/ize-spec.json +++ b/internal/schema/ize-spec.json @@ -65,6 +65,18 @@ "type": "string", "description": "(optional) Terraform version can be set here. 1.1.3 by default" }, + "endpoint_url": { + "type": "string", + "description": "(optional) AWS Endpoint url (can be used with Localstack)" + }, + "localstack": { + "type": "boolean", + "description": "(optional) Whether enable Localstack" + }, + "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." diff --git a/internal/template/template.go b/internal/template/template.go index 01635a85..76b5dcc2 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,77 @@ 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("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)) + + 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 From e0d7e903b7f21eeba313ff2f3b3f3d68ceb4132e Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Sun, 21 Apr 2024 19:27:29 +0000 Subject: [PATCH 02/30] Progress (will reset-cleanup later) --- cmd/main.go | 2 +- internal/aws/utils/utils.go | 9 ++ internal/commands/boostrap.go | 21 +++ internal/commands/boostrap_terraform_state.go | 115 ++++++++++++++ .../{aws_profile.go => gen_aws_profile.go} | 0 ...rofile_test.go => gen_aws_profile_test.go} | 0 internal/commands/{ci.go => gen_ci.go} | 0 .../commands/{ci_test.go => gen_ci_test.go} | 0 .../{completion.go => gen_completion.go} | 0 internal/commands/{doc.go => gen_doc.go} | 0 internal/commands/{mfa.go => gen_mfa.go} | 0 internal/commands/{tfenv.go => gen_tfenv.go} | 0 .../{tfenv_test.go => gen_tfenv_test.go} | 0 internal/commands/{initialize.go => init.go} | 0 .../{initialize_test.go => init_test.go} | 0 internal/commands/ize.go | 5 +- internal/commands/up_infra.go | 1 + internal/config/app.go | 2 +- internal/config/config.go | 146 ++++++++++++------ internal/config/project.go | 8 +- internal/manager/helm/helm.go | 145 +++++++++-------- internal/schema/ize-spec.json | 11 +- internal/schema/schema.go | 3 + 23 files changed, 331 insertions(+), 137 deletions(-) create mode 100644 internal/commands/boostrap.go create mode 100644 internal/commands/boostrap_terraform_state.go rename internal/commands/{aws_profile.go => gen_aws_profile.go} (100%) rename internal/commands/{aws_profile_test.go => gen_aws_profile_test.go} (100%) rename internal/commands/{ci.go => gen_ci.go} (100%) rename internal/commands/{ci_test.go => gen_ci_test.go} (100%) rename internal/commands/{completion.go => gen_completion.go} (100%) rename internal/commands/{doc.go => gen_doc.go} (100%) rename internal/commands/{mfa.go => gen_mfa.go} (100%) rename internal/commands/{tfenv.go => gen_tfenv.go} (100%) rename internal/commands/{tfenv_test.go => gen_tfenv_test.go} (100%) rename internal/commands/{initialize.go => init.go} (100%) rename internal/commands/{initialize_test.go => init_test.go} (100%) diff --git a/cmd/main.go b/cmd/main.go index 7a314e74..efdd471a 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,5 +1,5 @@ /* -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. diff --git a/internal/aws/utils/utils.go b/internal/aws/utils/utils.go index f37c5281..2c56ec14 100644 --- a/internal/aws/utils/utils.go +++ b/internal/aws/utils/utils.go @@ -31,6 +31,13 @@ func GetSession(c *SessionConfig) (*session.Session, error) { upd := false 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 @@ -55,8 +62,10 @@ func GetSession(c *SessionConfig) (*session.Session, error) { default: // 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/aws_profile.go b/internal/commands/gen_aws_profile.go similarity index 100% rename from internal/commands/aws_profile.go rename to internal/commands/gen_aws_profile.go 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 100% rename from internal/commands/tfenv.go rename to internal/commands/gen_tfenv.go 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/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..d07f0238 100644 --- a/internal/config/app.go +++ b/internal/config/app.go @@ -16,7 +16,7 @@ type Ecs struct { DependsOn []string `mapstructure:"depends_on,omitempty"` } -type K8s struct { +type Helm struct { Name string `mapstructure:",omitempty"` Path string `mapstructure:",omitempty"` Image string `mapstructure:",omitempty"` diff --git a/internal/config/config.go b/internal/config/config.go index 036d3426..609a64a2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -27,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 @@ -38,41 +40,23 @@ 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) } @@ -82,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 @@ -170,17 +152,17 @@ 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 } @@ -195,7 +177,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 { @@ -232,7 +214,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") == "" { @@ -244,6 +226,7 @@ func SetTag() { } else { viper.SetDefault("TAG", strings.Trim(string(out), "\n")) } + return err } func InitConfig() { @@ -253,6 +236,27 @@ 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") @@ -265,29 +269,36 @@ func InitConfig() { viper.SetDefault("PREFER_RUNTIME", "native") viper.SetDefault("CUSTOM_PROMPT", false) viper.SetDefault("PLAIN_TEXT_OUTPUT", false) + viper.SetDefault("LOCALSTACK", false) 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 (and final) option `apps` + appsPath = filepath.Join(cwd, "apps") } + 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) } @@ -299,7 +310,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 { @@ -309,6 +320,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")) @@ -399,13 +411,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) { @@ -448,13 +480,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 { @@ -472,7 +516,7 @@ func readConfigFile(path string) (*Config, error) { return &cfg, nil } -func ConvertApps() error { +func MigrateAppsConfig() error { ecs := map[string]interface{}{} serverless := map[string]interface{}{} @@ -514,7 +558,7 @@ func ConvertApps() error { return nil } -func ConvertInfra() error { +func MigrateInfraConfig() error { tf := viper.GetStringMap("infra.terraform") version, ok := tf["terraform_version"] @@ -533,7 +577,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 6911e5b9..0ccfb695 100644 --- a/internal/config/project.go +++ b/internal/config/project.go @@ -24,19 +24,19 @@ 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"` EndpointUrl string `mapstructure:"endpoint_url,omitempty"` LocalStack bool `mapstructure:"localstack,omitempty"` SshPublicKey string `mapstructure:"ssh_public_key,omitempty"` - Home string `mapstructure:",omitempty"` + Home string `mapstructure:"home,omitempty"` RootDir string `mapstructure:"root_dir,omitempty"` InfraDir string `mapstructure:"ize_dir,omitempty"` EnvDir string `mapstructure:"env_dir,omitempty"` diff --git a/internal/manager/helm/helm.go b/internal/manager/helm/helm.go index 16caffd2..479ec30f 100644 --- a/internal/manager/helm/helm.go +++ b/internal/manager/helm/helm.go @@ -10,24 +10,22 @@ import ( "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() { @@ -59,7 +57,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() @@ -86,14 +84,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) }() @@ -106,16 +104,14 @@ 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) - if err != nil { - return fmt.Errorf("unable to deploy app: %w", err) - } + //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() @@ -148,16 +144,13 @@ func (e *Manager) Redeploy(ui terminal.UI) error { defer func() { s.Abort(); time.Sleep(50 * time.Millisecond) }() if e.Project.PreferRuntime == "native" { - err := e.redeployLocal(s.TermOutput()) + //err := e.redeployLocal(s.TermOutput()) pterm.SetDefaultOutput(os.Stdout) - if err != nil { - return fmt.Errorf("unable to redeploy app: %w", err) - } + //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) - } + return fmt.Errorf("runtime not implemented. use native") } s.Done() @@ -317,58 +310,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) diff --git a/internal/schema/ize-spec.json b/internal/schema/ize-spec.json index 9d8a9c85..4ed90d2f 100644 --- a/internal/schema/ize-spec.json +++ b/internal/schema/ize-spec.json @@ -70,8 +70,15 @@ "description": "(optional) AWS Endpoint url (can be used with Localstack)" }, "localstack": { - "type": "boolean", - "description": "(optional) Whether enable Localstack" + "description": "(optional) Whether enable Localstack", + "anyOf": [ + { + "type": "string" + }, + { + "type": "boolean" + } + ] }, "ssh_public_key": { "type": "string", 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" From def412e92b408c1cbb986c386db6a0e1f2e4ab31 Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Mon, 6 May 2024 04:16:41 +0000 Subject: [PATCH 03/30] Progress (will reset-cleanup later) --- internal/commands/console.go | 172 +++++++++++++++++++++++--- internal/commands/deploy.go | 67 ++++++---- internal/commands/status.go | 21 ++-- internal/config/app.go | 26 ++-- internal/config/config.go | 18 ++- internal/config/project.go | 16 +-- internal/manager/ecs/ecs.go | 8 +- internal/manager/helm/helm.go | 90 ++++++++------ internal/requirements/requirements.go | 4 +- internal/schema/ize-spec.json | 67 ++++++++++ internal/template/template.go | 2 + 11 files changed, 370 insertions(+), 121 deletions(-) diff --git a/internal/commands/console.go b/internal/commands/console.go index 9d7ded28..d6be8851 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,123 @@ func (o *ConsoleOptions) Run() error { return nil } + +func getEcsServiceName(o *ConsoleOptions) (string, error) { + ecsServiceCandidates := []string{ + 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 ecsServiceCandidates { + + 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, + }) + + 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..630bf421 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,55 @@ 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. Will try to use apps_provider config.", o.Config.AppsProvider) + //if o.Config.AppsProvider == "helm" { + // logrus.Debugf("Found helm app") + // + + if _, ok := o.Config.Ecs[o.AppName]; o.Config.AppsProvider == "ecs" || ok { + providerUsed = "ecs" + m = &ecs.Manager{ + Project: o.Config, + App: &config.Ecs{ + Name: o.AppName, + TaskDefinitionRevision: o.TaskDefinitionRevision, + Unsafe: o.Unsafe, + SkipDeploy: false, + }, + } } - if app, ok := o.Config.Serverless[o.AppName]; ok { - app.Name = o.AppName - app.Force = o.Force - m = &serverless.Manager{ + if _, ok := o.Config.Helm[o.AppName]; o.Config.AppsProvider == "helm" || ok { + providerUsed = "helm" + m = &helm.Manager{ Project: o.Config, - App: app, + App: &config.Helm{ + Name: o.AppName, + Force: o.Force, + }, } } - if app, ok := o.Config.Alias[o.AppName]; ok { - app.Name = o.AppName - m = &alias.Manager{ + + if _, ok := o.Config.Serverless[o.AppName]; o.Config.AppsProvider == "serverless" || ok { + providerUsed = "serverless" + m = &serverless.Manager{ Project: o.Config, - App: app, + App: &config.Serverless{ + Name: o.AppName, + Force: o.Force, + }, } } - 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 +196,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/status.go b/internal/commands/status.go index 3098d718..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 @@ -92,20 +93,20 @@ func NewDebugCmd(project *config.Project) *cobra.Command { } _ = dt.WithData(pterm.TableData{ - {"AWS PROFILE", project.AwsProfile}, - {"AWS USER", userName}, - {"AWS ACCOUNT", *resp.Account}, + {"AWS_PROFILE", project.AwsProfile}, + {"AWS_USER", userName}, + {"AWS_ACCOUNT", *resp.Account}, }).WithLeftAlignment().Render() if len(project.EndpointUrl) > 0 { _ = dt.WithData(pterm.TableData{ - {"AWS ENDPOINT URL", project.EndpointUrl}, + {"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/config/app.go b/internal/config/app.go index d07f0238..71eb51e8 100644 --- a/internal/config/app.go +++ b/internal/config/app.go @@ -17,19 +17,19 @@ type Ecs struct { } type Helm 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"` + 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 609a64a2..7c58aa8a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -58,7 +58,7 @@ func (p *Project) GetConfig() error { 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()) @@ -114,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 { @@ -169,7 +174,7 @@ func (p *Project) GetTestConfig() error { 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()) @@ -270,6 +275,7 @@ func InitConfig() { 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 { @@ -285,8 +291,12 @@ func InitConfig() { appsPath := filepath.Join(cwd, "projects") _, err = os.Stat(appsPath) if os.IsNotExist(err) { - // Second (and final) option `apps` + // 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) diff --git a/internal/config/project.go b/internal/config/project.go index 0ccfb695..f0005144 100644 --- a/internal/config/project.go +++ b/internal/config/project.go @@ -36,13 +36,14 @@ type Project struct { LocalStack bool `mapstructure:"localstack,omitempty"` SshPublicKey string `mapstructure:"ssh_public_key,omitempty"` - 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"` + 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 @@ -51,6 +52,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/manager/ecs/ecs.go b/internal/manager/ecs/ecs.go index 323df1fd..e7f3bf19 100644 --- a/internal/manager/ecs/ecs.go +++ b/internal/manager/ecs/ecs.go @@ -211,6 +211,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{}) @@ -280,10 +281,15 @@ 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{ @@ -294,8 +300,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" diff --git a/internal/manager/helm/helm.go b/internal/manager/helm/helm.go index 479ec30f..6a05bba1 100644 --- a/internal/manager/helm/helm.go +++ b/internal/manager/helm/helm.go @@ -5,7 +5,10 @@ import ( "encoding/base64" "encoding/json" "fmt" + "github.com/hazelops/ize/pkg/term" + "io" "os" + "os/exec" "path" "path/filepath" "time" @@ -44,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 { @@ -107,9 +110,12 @@ func (e *Manager) Deploy(ui terminal.UI) error { //err := e.deployLocal(s.TermOutput()) pterm.SetDefaultOutput(os.Stdout) - //if err != nil { - // return fmt.Errorf("unable to deploy app: %w", err) - //} + 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 { return fmt.Errorf("runtime not implemented. use native") } @@ -121,43 +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, - EndpointUrl: e.Project.EndpointUrl, - }) - 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) }() +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` + //} - 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 { - return fmt.Errorf("runtime not implemented. use native") - } + logrus.SetOutput(w) + logrus.Debugf("command: %s", command) - s.Done() - s = sg.Add("%s: redeployment completed!", e.App.Name) - s.Done() + cmd := exec.Command("bash", "-c", command) - return nil + return term.New( + term.WithDir(helm.App.Path), + term.WithStdout(w), + term.WithStderr(w), + ).InteractiveRun(cmd) } func (e *Manager) Push(ui terminal.UI) error { @@ -271,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{ @@ -285,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" @@ -369,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/requirements/requirements.go b/internal/requirements/requirements.go index 7640c41b..6d4c380c 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")) } diff --git a/internal/schema/ize-spec.json b/internal/schema/ize-spec.json index 4ed90d2f..5a568ff7 100644 --- a/internal/schema/ize-spec.json +++ b/internal/schema/ize-spec.json @@ -111,6 +111,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": { @@ -160,6 +164,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", @@ -394,6 +409,58 @@ "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": "helm app configuration.", + "additionalProperties": false + }, "serverless": { "id": "#/definitions/serverless", "type": "object", diff --git a/internal/template/template.go b/internal/template/template.go index 76b5dcc2..4569317c 100644 --- a/internal/template/template.go +++ b/internal/template/template.go @@ -135,6 +135,7 @@ func GenerateBackendTf(opts BackendOpts, path string) error { rootBody := f.Body() // AWS Provider block providerBlock := rootBody.AppendNewBlock("provider", []string{"aws"}) + 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")) @@ -194,6 +195,7 @@ func GenerateBackendTf(opts BackendOpts, path string) error { 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{ From a69fd7c79044146a1f905bc6682469ec11ac67ec Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Tue, 14 May 2024 16:32:44 +0000 Subject: [PATCH 04/30] Improvements: - Added container name candidate logic - Added service name candidate logic - Added log group name candidate logic - Added log stream name candidate logic - Added service_name parameter - Improved logging --- internal/commands/console.go | 2 +- internal/commands/deploy.go | 31 +++++----- internal/commands/logs.go | 104 +++++++++++++++++++++++++++++---- internal/config/app.go | 1 + internal/manager/ecs/ecs.go | 6 ++ internal/manager/ecs/native.go | 20 +++---- internal/schema/ize-spec.json | 6 +- 7 files changed, 129 insertions(+), 41 deletions(-) diff --git a/internal/commands/console.go b/internal/commands/console.go index d6be8851..70fd9906 100644 --- a/internal/commands/console.go +++ b/internal/commands/console.go @@ -211,6 +211,7 @@ func (o *ConsoleOptions) Run() error { } func getEcsServiceName(o *ConsoleOptions) (string, error) { + // TODO: Move core logic to a shared function (since it's used in deploy too) ecsServiceCandidates := []string{ o.AppName, fmt.Sprintf("%s-%s", o.Config.Env, o.AppName), @@ -218,7 +219,6 @@ func getEcsServiceName(o *ConsoleOptions) (string, error) { } for _, v := range ecsServiceCandidates { - 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, diff --git a/internal/commands/deploy.go b/internal/commands/deploy.go index 630bf421..2a430ee6 100644 --- a/internal/commands/deploy.go +++ b/internal/commands/deploy.go @@ -135,43 +135,38 @@ func (o *DeployOptions) Run() error { var m manager.Manager 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. Will try to use apps_provider config.", o.Config.AppsProvider) + 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 _, ok := o.Config.Ecs[o.AppName]; o.Config.AppsProvider == "ecs" || ok { + 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: &config.Ecs{ - Name: o.AppName, - TaskDefinitionRevision: o.TaskDefinitionRevision, - Unsafe: o.Unsafe, - SkipDeploy: false, - }, + App: app, } } - if _, ok := o.Config.Helm[o.AppName]; o.Config.AppsProvider == "helm" || 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 = &helm.Manager{ Project: o.Config, - App: &config.Helm{ - Name: o.AppName, - Force: o.Force, - }, + App: app, } } - if _, ok := o.Config.Serverless[o.AppName]; o.Config.AppsProvider == "serverless" || ok { + if app, ok := o.Config.Serverless[o.AppName]; o.Config.AppsProvider == "serverless" || ok { providerUsed = "serverless" + app.Name = o.AppName + app.Force = o.Force + m = &serverless.Manager{ Project: o.Config, - App: &config.Serverless{ - Name: o.AppName, - Force: o.Force, - }, + App: app, } } diff --git a/internal/commands/logs.go b/internal/commands/logs.go index 3936ba5d..afe1bcc2 100644 --- a/internal/commands/logs.go +++ b/internal/commands/logs.go @@ -1,6 +1,7 @@ package commands import ( + "errors" "fmt" "os" "strings" @@ -17,10 +18,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 +85,28 @@ func (o *LogsOptions) Validate() error { } func (o *LogsOptions) Run() error { - logGroup := fmt.Sprintf("%s-%s", o.Config.Env, o.AppName) + var err error + + 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 +114,17 @@ 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) + } - GetLogs(o.Config.AWSClient.CloudWatchLogsClient, logGroup, logStreamName, token) + GetLogs(o.Config.AWSClient.CloudWatchLogsClient, o.LogGroupName, o.LogStreamName, token) return nil } @@ -147,3 +165,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{ + 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 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.Debug("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{ + o.AppName, + fmt.Sprintf("main/%s-%s", o.Config.Namespace, o.AppName), + fmt.Sprintf("main/%s-%s", o.Config.Env, o.AppName), + fmt.Sprintf("main/%s-%s-%s", o.Config.Env, o.Config.Namespace, 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/config/app.go b/internal/config/app.go index 71eb51e8..33c50a20 100644 --- a/internal/config/app.go +++ b/internal/config/app.go @@ -14,6 +14,7 @@ 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 Helm struct { diff --git a/internal/manager/ecs/ecs.go b/internal/manager/ecs/ecs.go index e7f3bf19..44f72908 100644 --- a/internal/manager/ecs/ecs.go +++ b/internal/manager/ecs/ecs.go @@ -59,6 +59,10 @@ func (e *Manager) prepare() { if e.App.Timeout == 0 { e.App.Timeout = 300 } + + if len(e.App.ServiceName) == 0 { + e.App.ServiceName = fmt.Sprintf("%s-%s", e.Project.Env, e.App.Name) + } } // Deploy deploys app container to ECS via ECS deploy @@ -101,6 +105,7 @@ func (e *Manager) Deploy(ui terminal.UI) error { 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), @@ -290,6 +295,7 @@ func (e *Manager) Build(ui terminal.UI) error { "APP_PATH": &relProjectPath, "APP_NAME": &e.App.Name, "CACHE_IMAGE": &cache[0], + "TAG": &e.Project.Tag, } tags := []string{ diff --git a/internal/manager/ecs/native.go b/internal/manager/ecs/native.go index 047b5479..1b625bac 100644 --- a/internal/manager/ecs/native.go +++ b/internal/manager/ecs/native.go @@ -21,18 +21,16 @@ func (e *Manager) deployLocal(w io.Writer) error { 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 +41,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 { @@ -109,22 +107,22 @@ func (e *Manager) deployLocal(w io.Writer) error { pterm.Printfln("Successfully created revision: %s:%d", *rtdo.TaskDefinition.Family, *rtdo.TaskDefinition.Revision) - 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)) + if err = e.updateTaskDefinition(&newTaskDef, &oldTaskDef, e.App.ServiceName, "Deploying new task definition"); err != nil { + err := e.getLastContainerLogs(fmt.Sprintf("%s", e.App.ServiceName)) if err != nil { pterm.Println("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.Printfln("Container %s couldn't start: %s", e.App.ServiceName, sr) 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 { + if err = e.updateTaskDefinition(&oldTaskDef, &newTaskDef, e.App.ServiceName, "Deploying previous task definition"); err != nil { return fmt.Errorf("unable to rollback to old task definition: %w", err) } @@ -197,7 +195,7 @@ 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)) + err := e.getLastContainerLogs(fmt.Sprintf("%s", e.App.ServiceName)) if err != nil { pterm.Println("Failed to get logs:", err) } diff --git a/internal/schema/ize-spec.json b/internal/schema/ize-spec.json index 5a568ff7..8b12b7b5 100644 --- a/internal/schema/ize-spec.json +++ b/internal/schema/ize-spec.json @@ -404,9 +404,13 @@ "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.", + "description": "ECS app configuration.", "additionalProperties": false }, "helm": { From 8b7a5654037da0be19da718e27b1f194c63e47da Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Tue, 14 May 2024 16:33:54 +0000 Subject: [PATCH 05/30] Run dev builds on v2.x from now on --- .github/workflows/release-dev.create-release-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From d16bfde01c0029c60ddb2f4e0bd52524641b7918 Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Tue, 14 May 2024 19:30:51 +0000 Subject: [PATCH 06/30] Improvements: - Reorder service name inference - Add service name infrerence to ecs - Upgrade pterm and go - Add spinner to logs - Logs: reorder log stream prefix inference - ECS deployment: add debug task definition logs, improve messages - ize.toml in the directory is considered a valid ize directory (no warning) - Handle case when there is no terraform infra in the ize.toml - Update Status message language --- go.mod | 35 ++++++++-------- go.sum | 43 ++++++++++++++++++++ internal/commands/console.go | 6 ++- internal/commands/logs.go | 14 ++++--- internal/commands/up_apps.go | 19 ++++++--- internal/manager/ecs/ecs.go | 58 ++++++++++++++++++++++++--- internal/manager/ecs/native.go | 19 ++++++++- internal/requirements/requirements.go | 5 +++ 8 files changed, 163 insertions(+), 36 deletions(-) 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..343b6b10 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,7 @@ 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/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 +1046,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 +1056,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 +1137,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 +1153,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 +1180,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 +1243,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 +1272,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 +1382,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 +1395,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 +1408,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 +1487,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/commands/console.go b/internal/commands/console.go index 70fd9906..327f20c1 100644 --- a/internal/commands/console.go +++ b/internal/commands/console.go @@ -213,12 +213,13 @@ func (o *ConsoleOptions) Run() error { func getEcsServiceName(o *ConsoleOptions) (string, error) { // TODO: Move core logic to a shared function (since it's used in deploy too) ecsServiceCandidates := []string{ - o.AppName, - fmt.Sprintf("%s-%s", o.Config.Env, o.AppName), 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, @@ -226,6 +227,7 @@ func getEcsServiceName(o *ConsoleOptions) (string, error) { ServiceName: &v, }) + // If during the search we encounter exceptions, handle them. var aerr awserr.Error if errors.As(err, &aerr) { switch aerr.Code() { diff --git a/internal/commands/logs.go b/internal/commands/logs.go index afe1bcc2..d03eee9f 100644 --- a/internal/commands/logs.go +++ b/internal/commands/logs.go @@ -3,6 +3,7 @@ package commands import ( "errors" "fmt" + "github.com/pterm/pterm" "os" "strings" "time" @@ -86,6 +87,7 @@ func (o *LogsOptions) Validate() error { func (o *LogsOptions) Run() error { var err error + s, _ := pterm.DefaultSpinner.WithRemoveWhenDone().Start("Getting access to logs...") if len(o.LogGroupName) == 0 { o.LogGroupName, err = getEcsServiceLogGroupName(o) @@ -124,6 +126,8 @@ func (o *LogsOptions) Run() error { o.LogStreamName = fmt.Sprintf("%s/%s", logStreamPrefix, taskID) } + s.Success() + GetLogs(o.Config.AWSClient.CloudWatchLogsClient, o.LogGroupName, o.LogStreamName, token) return nil @@ -169,9 +173,9 @@ func formatMessage(e *cloudwatchlogs.OutputLogEvent) (t time.Time, m string) { func getEcsServiceLogGroupName(o *LogsOptions) (string, error) { // TODO: Move core logic to a shared function (since it's used in deploy too) ecsServiceLogGroupCandidates := []string{ - o.AppName, - fmt.Sprintf("%s-%s", o.Config.Env, o.AppName), 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 { @@ -201,10 +205,10 @@ func getEcsServiceLogGroupName(o *LogsOptions) (string, error) { func getEcsServiceLogStreamPrefix(o *LogsOptions) (string, error) { ecsServiceLogStreamNameCandidates := []string{ - o.AppName, - fmt.Sprintf("main/%s-%s", o.Config.Namespace, o.AppName), - fmt.Sprintf("main/%s-%s", o.Config.Env, o.AppName), 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 { 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/manager/ecs/ecs.go b/internal/manager/ecs/ecs.go index 44f72908..9c4dd44a 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" @@ -61,7 +63,11 @@ func (e *Manager) prepare() { } if len(e.App.ServiceName) == 0 { - e.App.ServiceName = fmt.Sprintf("%s-%s", e.Project.Env, e.App.Name) + var err error + e.App.ServiceName, err = getEcsServiceName(e) + if err != nil { + + } } } @@ -101,7 +107,7 @@ 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 == "" { @@ -151,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" { @@ -180,7 +186,7 @@ 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 app image...", e.App.Name) defer func() { s.Abort(); time.Sleep(50 * time.Millisecond) }() if len(e.App.Image) != 0 { @@ -268,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 @@ -388,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 1b625bac..bc0200b9 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" @@ -64,6 +66,12 @@ func (e *Manager) deployLocal(w io.Writer) error { oldTaskDef = *dtdo.TaskDefinition } + oldTaskDefJson, err := json.Marshal(oldTaskDef) + if err != nil { + return err + } + + logrus.Debugf("oldTaskDef: %s", string(oldTaskDefJson)) pterm.Printfln("Deploying based on task definition: %s:%d", *oldTaskDef.Family, *oldTaskDef.Revision) var image string @@ -105,6 +113,12 @@ func (e *Manager) deployLocal(w io.Writer) error { newTaskDef = *rtdo.TaskDefinition + newTaskDefJson, err := json.Marshal(newTaskDef) + if err != nil { + return err + } + + logrus.Debugf("newTaskDef: %s", string(newTaskDefJson)) pterm.Printfln("Successfully created revision: %s:%d", *rtdo.TaskDefinition.Family, *rtdo.TaskDefinition.Revision) if err = e.updateTaskDefinition(&newTaskDef, &oldTaskDef, e.App.ServiceName, "Deploying new task definition"); err != nil { @@ -121,7 +135,10 @@ func (e *Manager) deployLocal(w io.Writer) error { pterm.Printfln("Container %s couldn't start: %s", e.App.ServiceName, sr) pterm.Printfln("Rolling back to old task definition: %s:%d", *oldTaskDef.Family, *oldTaskDef.Revision) + e.App.Timeout = 600 + logrus.Debugf("Setting timeout to %d seconds", e.App.Timeout) + if err = e.updateTaskDefinition(&oldTaskDef, &newTaskDef, e.App.ServiceName, "Deploying previous task definition"); err != nil { return fmt.Errorf("unable to rollback to old task definition: %w", err) } @@ -221,7 +238,7 @@ func getService(name string, cluster string, svc ecsiface.ECSAPI) (*ecs.Describe } func (e *Manager) updateTaskDefinition(newTD *ecs.TaskDefinition, oldTD *ecs.TaskDefinition, serviceName string, title string) error { - pterm.Println("Updating service") + pterm.Printfln("Updating ECS service: %s (timeout: %d)", e.App.ServiceName, e.App.Timeout) svc := e.Project.AWSClient.ECSClient diff --git a/internal/requirements/requirements.go b/internal/requirements/requirements.go index 6d4c380c..0222f044 100644 --- a/internal/requirements/requirements.go +++ b/internal/requirements/requirements.go @@ -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 } From 6322a6e1f0879df4e96ce0f0cc7549af2612164d Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Tue, 14 May 2024 20:16:42 +0000 Subject: [PATCH 07/30] Updated dev build to goreleaser v5 --- .github/workflows/release-dev.build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-dev.build-and-publish.yml b/.github/workflows/release-dev.build-and-publish.yml index b22fee37..e3ca2829 100644 --- a/.github/workflows/release-dev.build-and-publish.yml +++ b/.github/workflows/release-dev.build-and-publish.yml @@ -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 From 62d7ec7f6b39680369d8c88509deb2989c05b026 Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Tue, 14 May 2024 20:17:42 +0000 Subject: [PATCH 08/30] Upgrade go in the workflow --- .github/workflows/release-dev.build-and-publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-dev.build-and-publish.yml b/.github/workflows/release-dev.build-and-publish.yml index e3ca2829..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 From 4c7faa2059ce49eb94ab44e3eef1e681e9e7eb79 Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Wed, 15 May 2024 05:25:59 +0000 Subject: [PATCH 09/30] Update go version in github action for tests --- .github/workflows/run.e2e-tests.yml | 2 +- .github/workflows/run.unit-tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run.e2e-tests.yml b/.github/workflows/run.e2e-tests.yml index 039d02e9..f0ebbc3c 100644 --- a/.github/workflows/run.e2e-tests.yml +++ b/.github/workflows/run.e2e-tests.yml @@ -26,7 +26,7 @@ jobs: - name: Install Go uses: actions/setup-go@v2 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..5afe6bde 100644 --- a/.github/workflows/run.unit-tests.yml +++ b/.github/workflows/run.unit-tests.yml @@ -29,7 +29,7 @@ jobs: - name: Install Go uses: actions/setup-go@v2 with: - go-version: 1.18.x + go-version: 1.22.x - name: Checkout Code uses: actions/checkout@v2 From 2aa093c0816f022219e9b1db55e533a31540ad92 Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Wed, 15 May 2024 08:03:57 +0000 Subject: [PATCH 10/30] Updates & Fixes: - Fixes race condition with multiple app terminals and pterm - Minor language and formatting changes --- internal/commands/up.go | 2 +- internal/manager/ecs/ecs.go | 6 ++-- internal/manager/ecs/native.go | 57 +++++++++++++++++----------------- internal/manager/helm/helm.go | 4 +-- 4 files changed, 34 insertions(+), 35 deletions(-) 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/manager/ecs/ecs.go b/internal/manager/ecs/ecs.go index 9c4dd44a..b893dd0b 100644 --- a/internal/manager/ecs/ecs.go +++ b/internal/manager/ecs/ecs.go @@ -92,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 @@ -186,11 +186,11 @@ func (e *Manager) Push(ui terminal.UI) error { sg := ui.StepGroup() defer sg.Wait() - s := sg.Add("%s: pushing 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 diff --git a/internal/manager/ecs/native.go b/internal/manager/ecs/native.go index bc0200b9..b30241f7 100644 --- a/internal/manager/ecs/native.go +++ b/internal/manager/ecs/native.go @@ -19,7 +19,6 @@ import ( ) func (e *Manager) deployLocal(w io.Writer) error { - pterm.SetDefaultOutput(w) svc := e.Project.AWSClient.ECSClient @@ -72,7 +71,7 @@ func (e *Manager) deployLocal(w io.Writer) error { } logrus.Debugf("oldTaskDef: %s", string(oldTaskDefJson)) - pterm.Printfln("Deploying based on task definition: %s:%d", *oldTaskDef.Family, *oldTaskDef.Revision) + pterm.Fprintln(w, fmt.Sprintf("Deploying based on task definition: %s:%d", *oldTaskDef.Family, *oldTaskDef.Revision)) var image string @@ -88,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, @@ -119,12 +118,12 @@ func (e *Manager) deployLocal(w io.Writer) error { } logrus.Debugf("newTaskDef: %s", string(newTaskDefJson)) - pterm.Printfln("Successfully created revision: %s:%d", *rtdo.TaskDefinition.Family, *rtdo.TaskDefinition.Revision) + pterm.Fprintln(w, fmt.Sprintf("Successfully created revision: %s:%d", *rtdo.TaskDefinition.Family, *rtdo.TaskDefinition.Revision)) - if err = e.updateTaskDefinition(&newTaskDef, &oldTaskDef, e.App.ServiceName, "Deploying new task definition"); err != nil { - err := e.getLastContainerLogs(fmt.Sprintf("%s", e.App.ServiceName)) + 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, e.App.ServiceName, svc) @@ -132,18 +131,18 @@ func (e *Manager) deployLocal(w io.Writer) error { return err } - pterm.Printfln("Container %s couldn't start: %s", e.App.ServiceName, sr) + pterm.Fprintln(w, fmt.Sprintf("Container %s couldn't start: %s", e.App.ServiceName, sr)) - pterm.Printfln("Rolling back to old task definition: %s:%d", *oldTaskDef.Family, *oldTaskDef.Revision) + pterm.Fprintln(w, fmt.Sprintf("Rolling back to old task definition: %s:%d", *oldTaskDef.Family, *oldTaskDef.Revision)) e.App.Timeout = 600 logrus.Debugf("Setting timeout to %d seconds", e.App.Timeout) - if err = e.updateTaskDefinition(&oldTaskDef, &newTaskDef, e.App.ServiceName, "Deploying previous task definition"); err != nil { + 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) } @@ -210,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", e.App.ServiceName)) + 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") } @@ -237,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.Printfln("Updating ECS service: %s (timeout: %d)", e.App.ServiceName, e.App.Timeout) +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 @@ -274,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 @@ -294,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") } @@ -312,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 } } @@ -404,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, @@ -414,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, @@ -435,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{ @@ -447,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/helm/helm.go b/internal/manager/helm/helm.go index 6a05bba1..ca8e2732 100644 --- a/internal/manager/helm/helm.go +++ b/internal/manager/helm/helm.go @@ -166,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 From e8be40c323e133d37906dab21fd648b754d467bf Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Wed, 15 May 2024 08:12:03 +0000 Subject: [PATCH 11/30] Update go version in unit tests workflows --- .github/workflows/run.e2e-tests.yml | 6 +++--- .github/workflows/run.unit-tests.yml | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/run.e2e-tests.yml b/.github/workflows/run.e2e-tests.yml index f0ebbc3c..b0c233ff 100644 --- a/.github/workflows/run.e2e-tests.yml +++ b/.github/workflows/run.e2e-tests.yml @@ -71,7 +71,7 @@ jobs: - name: Install Go uses: actions/setup-go@v2 with: - go-version: 1.18.x + go-version: 1.22.x - name: Checkout Code uses: actions/checkout@v2 @@ -135,7 +135,7 @@ jobs: - name: Install Go uses: actions/setup-go@v2 with: - go-version: 1.18.x + go-version: 1.22.x - name: Checkout Code uses: actions/checkout@v2 @@ -206,7 +206,7 @@ jobs: - name: Install Go uses: actions/setup-go@v2 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 5afe6bde..aba6b4ac 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: @@ -56,7 +57,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.18 + go-version: 1.22.x - name: Generate run: | From b3301a58cf6d54c96ca3abd65afd05494ba3c87a Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Wed, 15 May 2024 08:33:50 +0000 Subject: [PATCH 12/30] Update go version in unit tests workflows --- .github/workflows/release-prod.build-and-publish.yml | 2 +- .github/workflows/run.e2e-tests.yml | 8 ++++---- .github/workflows/run.unit-tests.yml | 4 ++-- go.sum | 1 + 4 files changed, 8 insertions(+), 7 deletions(-) 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.e2e-tests.yml b/.github/workflows/run.e2e-tests.yml index b0c233ff..a50129f6 100644 --- a/.github/workflows/run.e2e-tests.yml +++ b/.github/workflows/run.e2e-tests.yml @@ -24,7 +24,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: go-version: 1.22.x @@ -69,7 +69,7 @@ jobs: aws-region: ${{ env.AWS_REGION }} - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: go-version: 1.22.x @@ -133,7 +133,7 @@ jobs: aws-region: ${{ env.AWS_REGION }} - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: go-version: 1.22.x @@ -204,7 +204,7 @@ jobs: aws-region: ${{ env.AWS_REGION }} - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: go-version: 1.22.x diff --git a/.github/workflows/run.unit-tests.yml b/.github/workflows/run.unit-tests.yml index aba6b4ac..191dfc4d 100644 --- a/.github/workflows/run.unit-tests.yml +++ b/.github/workflows/run.unit-tests.yml @@ -28,7 +28,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: go-version: 1.22.x @@ -55,7 +55,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version: 1.22.x diff --git a/go.sum b/go.sum index 343b6b10..babe6f9c 100644 --- a/go.sum +++ b/go.sum @@ -1000,6 +1000,7 @@ 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= From 4952d16acb3ab40e2d133204e0f0fe6c6461262a Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Wed, 15 May 2024 08:39:00 +0000 Subject: [PATCH 13/30] Fix minor tests --- internal/commands/console.go | 2 +- internal/commands/logs.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/commands/console.go b/internal/commands/console.go index 327f20c1..86e43f31 100644 --- a/internal/commands/console.go +++ b/internal/commands/console.go @@ -296,7 +296,7 @@ func getEcsContainerName(o *ConsoleOptions) (string, error) { } } - return "", errors.New(fmt.Sprintf("Can't find a container for %s in %s", o.AppName, task.TaskDefinitionArn)) + return "", errors.New(fmt.Sprintf("Can't find a container for %s in %s", o.AppName, *task.TaskDefinitionArn)) } } else { fmt.Println("No tasks found.") diff --git a/internal/commands/logs.go b/internal/commands/logs.go index d03eee9f..98688aae 100644 --- a/internal/commands/logs.go +++ b/internal/commands/logs.go @@ -186,7 +186,7 @@ func getEcsServiceLogGroupName(o *LogsOptions) (string, error) { }) if len(resp.LogGroups) == 0 { - logrus.Debug("No log groups with prefix %s. Trying other options", v) + logrus.Debugf("No log groups with prefix %s. Trying other options", v) continue } From 552141ec97b86ef8c735dcf9564ef2b692cf9eee Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Wed, 15 May 2024 11:59:56 +0000 Subject: [PATCH 14/30] Updates: - Updated workflow - Updated tests --- .github/workflows/run.e2e-tests.yml | 16 ++++-- internal/commands/build_test.go | 87 ++++++++++++++++++++++++----- 2 files changed, 85 insertions(+), 18 deletions(-) diff --git a/.github/workflows/run.e2e-tests.yml b/.github/workflows/run.e2e-tests.yml index a50129f6..d5501e95 100644 --- a/.github/workflows/run.e2e-tests.yml +++ b/.github/workflows/run.e2e-tests.yml @@ -62,11 +62,14 @@ 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 }} + 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@v5 @@ -126,11 +129,14 @@ 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@v5 @@ -197,11 +203,13 @@ 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@v5 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 +} From d9d847a2683de139d53a73cabf9b6294ab6f3c9a Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Wed, 15 May 2024 12:05:28 +0000 Subject: [PATCH 15/30] Fix typo --- .github/workflows/run.e2e-tests.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/run.e2e-tests.yml b/.github/workflows/run.e2e-tests.yml index d5501e95..3be6c21a 100644 --- a/.github/workflows/run.e2e-tests.yml +++ b/.github/workflows/run.e2e-tests.yml @@ -66,7 +66,7 @@ jobs: 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 } + aws-region: ${{ env.AWS_REGION }} env: AWS_PROFILE: # This is required due to a bug https://stackoverflow.com/a/77731682 @@ -137,7 +137,6 @@ jobs: env: AWS_PROFILE: # This is required due to a bug https://stackoverflow.com/a/77731682 - - name: Install Go uses: actions/setup-go@v5 with: From f8849bd90c253421dbc3912752a7808f80d0a89f Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Wed, 15 May 2024 14:41:04 +0000 Subject: [PATCH 16/30] remove an empty multistate example, upgrade vpcs terraform modules to 5.0 --- .../.ize/env/testnut/main.tf | 2 +- .../.ize/env/testnut/main.tf | 2 +- examples/multistack-monorepo/vpc/main.tf | 2 +- .../.ize/env/testnut/api/main.tf | 0 .../.ize/env/testnut/api/variables.tf | 11 ---- .../.ize/env/testnut/ize.toml | 60 ------------------- .../.ize/env/testnut/main.tf | 0 .../.ize/env/testnut/variables.tf | 11 ---- .../.ize/env/testnut/vpc/main.tf | 0 .../.ize/env/testnut/vpc/variables.tf | 11 ---- 10 files changed, 3 insertions(+), 96 deletions(-) delete mode 100644 examples/multistate-monorepo/.ize/env/testnut/api/main.tf delete mode 100644 examples/multistate-monorepo/.ize/env/testnut/api/variables.tf delete mode 100644 examples/multistate-monorepo/.ize/env/testnut/ize.toml delete mode 100644 examples/multistate-monorepo/.ize/env/testnut/main.tf delete mode 100644 examples/multistate-monorepo/.ize/env/testnut/variables.tf delete mode 100644 examples/multistate-monorepo/.ize/env/testnut/vpc/main.tf delete mode 100644 examples/multistate-monorepo/.ize/env/testnut/vpc/variables.tf diff --git a/examples/bastion-tunnel-monorepo/.ize/env/testnut/main.tf b/examples/bastion-tunnel-monorepo/.ize/env/testnut/main.tf index e92cfd8e..5ad35c63 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" 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/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 -} From f73a848c23bb84eb6fe89757b789ed2dc29cfac3 Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Wed, 15 May 2024 14:50:51 +0000 Subject: [PATCH 17/30] Upgrade lower limit of terraform provider and bastion module --- examples/bastion-tunnel-monorepo/.ize/env/testnut/main.tf | 2 +- examples/bastion-tunnel-monorepo/.ize/env/testnut/versions.tf | 2 +- examples/ecs-apps-monorepo/.ize/env/testnut/versions.tf | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/bastion-tunnel-monorepo/.ize/env/testnut/main.tf b/examples/bastion-tunnel-monorepo/.ize/env/testnut/main.tf index 5ad35c63..766013d9 100644 --- a/examples/bastion-tunnel-monorepo/.ize/env/testnut/main.tf +++ b/examples/bastion-tunnel-monorepo/.ize/env/testnut/main.tf @@ -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..6793a836 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 = ">= 4.42.0" } } required_version = ">= 0.13" 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" From 391b92621ad4371e504d9cadb65f4d1d057cbdea Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Wed, 15 May 2024 15:00:44 +0000 Subject: [PATCH 18/30] Lower the limit of AWS provider to allow nat instance module to work --- examples/bastion-tunnel-monorepo/.ize/env/testnut/versions.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/bastion-tunnel-monorepo/.ize/env/testnut/versions.tf b/examples/bastion-tunnel-monorepo/.ize/env/testnut/versions.tf index 6793a836..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 = ">= 4.42.0" + version = ">= 3.0" } } required_version = ">= 0.13" From a3a9c8ec6ceeae016ea00b20cfbf44a0d418e161 Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Thu, 6 Jun 2024 15:20:35 +0000 Subject: [PATCH 19/30] Generate localstack profile if IZE_LOCALSTACK is set to true --- internal/commands/gen_aws_profile.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/internal/commands/gen_aws_profile.go b/internal/commands/gen_aws_profile.go index 116db944..e9e48f2c 100644 --- a/internal/commands/gen_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)) } From 89220de2bb7e166f2a02ab3ecf0c049adf7c1aa6 Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Mon, 10 Jun 2024 05:41:59 +0000 Subject: [PATCH 20/30] Fix E2E: WIP --- .../workflows/run.brew-and-apt-install-tests.yml | 1 + .github/workflows/run.e2e-tests.yml | 2 +- .github/workflows/run.unit-tests.yml | 16 ++++++++++++++-- internal/commands/gen_tfenv.go | 2 +- 4 files changed, 17 insertions(+), 4 deletions(-) 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 3be6c21a..998f4597 100644 --- a/.github/workflows/run.e2e-tests.yml +++ b/.github/workflows/run.e2e-tests.yml @@ -156,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 diff --git a/.github/workflows/run.unit-tests.yml b/.github/workflows/run.unit-tests.yml index 191dfc4d..d11a1f2b 100644 --- a/.github/workflows/run.unit-tests.yml +++ b/.github/workflows/run.unit-tests.yml @@ -82,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 @@ -90,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/internal/commands/gen_tfenv.go b/internal/commands/gen_tfenv.go index 581d6d1f..c3f880d8 100644 --- a/internal/commands/gen_tfenv.go +++ b/internal/commands/gen_tfenv.go @@ -145,7 +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) + return fmt.Errorf("can't generate backend.tf: %s", err) } varsOpts := template.VarsOpts{ From 1f5fd2da8d8c523777684f83254d79882a3514ce Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Wed, 7 Aug 2024 19:38:25 +0000 Subject: [PATCH 21/30] Don't nvm silence usage --- internal/commands/nvm.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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") From 1b20ee88c9f73c9c7c9988d543604b4e73408f43 Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Wed, 7 Aug 2024 19:59:03 +0000 Subject: [PATCH 22/30] Add better error from nvm --- internal/manager/serverless/native.go | 21 ++++++++++++++++++--- internal/manager/serverless/serverless.go | 2 +- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/internal/manager/serverless/native.go b/internal/manager/serverless/native.go index 79890d6e..da001a86 100644 --- a/internal/manager/serverless/native.go +++ b/internal/manager/serverless/native.go @@ -1,6 +1,7 @@ package serverless import ( + "bytes" "fmt" "io" "os" @@ -49,15 +50,28 @@ func (sls *Manager) nvm(w io.Writer, command string) error { return err } + logrus.Debugf("Running: bash -c source %s/nvm.sh && nvm install %s && %s", nvmDir, sls.App.NodeVersion, command) cmd := exec.Command("bash", "-c", 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 + cmd.Stderr = &stderr + + 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, stderr: %s", err, stderr.String()) + } + + return nil } func (sls *Manager) readNvmrc() error { @@ -70,6 +84,7 @@ func (sls *Manager) readNvmrc() error { } sls.App.NodeVersion = strings.TrimSpace(string(file)) } + return 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() From 900765949685d2390ba7438db9fa4d21ba899b03 Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Wed, 7 Aug 2024 20:02:31 +0000 Subject: [PATCH 23/30] Add better error from nvm --- internal/manager/serverless/native.go | 90 ++++++++++++++++++++++----- 1 file changed, 75 insertions(+), 15 deletions(-) diff --git a/internal/manager/serverless/native.go b/internal/manager/serverless/native.go index da001a86..52a92b9a 100644 --- a/internal/manager/serverless/native.go +++ b/internal/manager/serverless/native.go @@ -106,11 +106,23 @@ 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 + cmd.Stderr = &stderr + + 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, stderr: %s", err, stderr.String()) + } + + return nil } func (sls *Manager) runDeploy(w io.Writer) error { @@ -165,11 +177,23 @@ 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 + cmd.Stderr = &stderr + + 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, stderr: %s", err, stderr.String()) + } + + return nil } func (sls *Manager) runRemove(w io.Writer) error { @@ -221,11 +245,23 @@ 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 + cmd.Stderr = &stderr + + 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, stderr: %s", err, stderr.String()) + } + + return nil } func (sls *Manager) runCreateDomain(w io.Writer) error { @@ -254,11 +290,23 @@ 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 + cmd.Stderr = &stderr + + 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, stderr: %s", err, stderr.String()) + } + + return nil } func (sls *Manager) runRemoveDomain(w io.Writer) error { @@ -287,11 +335,23 @@ 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 + cmd.Stderr = &stderr + + 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, stderr: %s", err, stderr.String()) + } + + return nil } func npmToYarn(cmd string) string { From 697bcbd3b16adfc33de947ba851075c350407f1d Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Thu, 8 Aug 2024 05:34:24 +0000 Subject: [PATCH 24/30] Better nvm logging --- internal/manager/serverless/native.go | 28 +++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/internal/manager/serverless/native.go b/internal/manager/serverless/native.go index 52a92b9a..117913f8 100644 --- a/internal/manager/serverless/native.go +++ b/internal/manager/serverless/native.go @@ -45,6 +45,10 @@ func (sls *Manager) nvm(w io.Writer, command string) error { if len(nvmDir) == 0 { nvmDir = "$HOME/.nvm" } + + // 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 + err := sls.readNvmrc() if err != nil { return err @@ -57,7 +61,9 @@ func (sls *Manager) nvm(w io.Writer, command string) error { // 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), @@ -68,7 +74,7 @@ func (sls *Manager) nvm(w io.Writer, command string) error { err = t.InteractiveRun(cmd) if err != nil { // Return the error along with stderr output - return fmt.Errorf("command failed with error: %w, stderr: %s", err, stderr.String()) + return fmt.Errorf("command failed with error: %w, %s %s", err, stderr.String(), stdout.String()) } return nil @@ -108,7 +114,9 @@ func (sls *Manager) runNvm(w io.Writer) error { // 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), @@ -119,7 +127,7 @@ func (sls *Manager) runNvm(w io.Writer) error { err = t.InteractiveRun(cmd) if err != nil { // Return the error along with stderr output - return fmt.Errorf("command failed with error: %w, stderr: %s", err, stderr.String()) + return fmt.Errorf("command failed with error: %w, %s %s", err, stderr.String(), stdout.String()) } return nil @@ -179,7 +187,9 @@ func (sls *Manager) runDeploy(w io.Writer) error { // 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), @@ -190,7 +200,7 @@ func (sls *Manager) runDeploy(w io.Writer) error { err := t.InteractiveRun(cmd) if err != nil { // Return the error along with stderr output - return fmt.Errorf("command failed with error: %w, stderr: %s", err, stderr.String()) + return fmt.Errorf("command failed with error: %w, %s %s", err, stderr.String(), stdout.String()) } return nil @@ -247,7 +257,9 @@ func (sls *Manager) runRemove(w io.Writer) error { // 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), @@ -258,7 +270,7 @@ func (sls *Manager) runRemove(w io.Writer) error { err := t.InteractiveRun(cmd) if err != nil { // Return the error along with stderr output - return fmt.Errorf("command failed with error: %w, stderr: %s", err, stderr.String()) + return fmt.Errorf("command failed with error: %w, %s %s", err, stderr.String(), stdout.String()) } return nil @@ -292,7 +304,9 @@ func (sls *Manager) runCreateDomain(w io.Writer) error { // 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), @@ -303,7 +317,7 @@ func (sls *Manager) runCreateDomain(w io.Writer) error { err := t.InteractiveRun(cmd) if err != nil { // Return the error along with stderr output - return fmt.Errorf("command failed with error: %w, stderr: %s", err, stderr.String()) + return fmt.Errorf("command failed with error: %w, %s %s", err, stderr.String(), stdout.String()) } return nil @@ -337,7 +351,9 @@ func (sls *Manager) runRemoveDomain(w io.Writer) error { // 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), @@ -348,7 +364,7 @@ func (sls *Manager) runRemoveDomain(w io.Writer) error { err := t.InteractiveRun(cmd) if err != nil { // Return the error along with stderr output - return fmt.Errorf("command failed with error: %w, stderr: %s", err, stderr.String()) + return fmt.Errorf("command failed with error: %w, %s %s", err, stderr.String(), stdout.String()) } return nil From ba0a6c0908ded133f34f430aeadaa62f3eb395fd Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Thu, 8 Aug 2024 06:50:13 +0000 Subject: [PATCH 25/30] Add bash debug --- .github/workflows/run.e2e-tests.yml | 2 +- internal/manager/serverless/native.go | 32 ++++++++++++++++++--------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/.github/workflows/run.e2e-tests.yml b/.github/workflows/run.e2e-tests.yml index 998f4597..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: diff --git a/internal/manager/serverless/native.go b/internal/manager/serverless/native.go index 117913f8..8f7afff3 100644 --- a/internal/manager/serverless/native.go +++ b/internal/manager/serverless/native.go @@ -19,7 +19,7 @@ func (sls *Manager) runNpmInstall(w io.Writer) error { nvmDir = "$HOME/.nvm" } - command := fmt.Sprintf("source %s/nvm.sh && nvm use %s && npm install --save-dev", nvmDir, sls.App.NodeVersion) + command := fmt.Sprintf("source --debug %s/nvm.sh && nvm use %s && npm install --save-dev", nvmDir, sls.App.NodeVersion) if sls.App.UseYarn { command = npmToYarn(command) @@ -46,6 +46,16 @@ func (sls *Manager) nvm(w io.Writer, command string) error { nvmDir = "$HOME/.nvm" } + // Check if nvm.sh exists in the nvmDir + nvmShPath := filepath.Join(nvmDir, "nvm.sh") + if _, err := os.Stat(nvmShPath); os.IsNotExist(err) { + logrus.Debugf("nvm.sh does not exist in the directory:", nvmDir) + return err + } else if err != nil { + logrus.Debugf("Error checking nvm.sh:", 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 @@ -54,9 +64,9 @@ func (sls *Manager) nvm(w io.Writer, command string) error { return err } - logrus.Debugf("Running: bash -c source %s/nvm.sh && nvm install %s && %s", nvmDir, sls.App.NodeVersion, command) - cmd := exec.Command("bash", "-c", - fmt.Sprintf("source %s/nvm.sh && nvm install %s && %s", nvmDir, sls.App.NodeVersion, command), + logrus.Debugf("Running: bash -c source --debug %s/nvm.sh && nvm install %s && %s", nvmDir, sls.App.NodeVersion, command) + cmd := exec.Command("bash", "-xv", "-c", + fmt.Sprintf("source --debug %s/nvm.sh && nvm install %s && %s", nvmDir, sls.App.NodeVersion, command), ) // Capture stderr in a buffer @@ -105,7 +115,7 @@ func (sls *Manager) runNvm(w io.Writer) error { return err } - command := fmt.Sprintf("source %s/nvm.sh && nvm install %s", nvmDir, sls.App.NodeVersion) + command := fmt.Sprintf("source --debug %s/nvm.sh && nvm install %s", nvmDir, sls.App.NodeVersion) logrus.SetOutput(w) logrus.Debugf("command: %s", command) @@ -143,7 +153,7 @@ func (sls *Manager) runDeploy(w io.Writer) error { // SLS v3 has breaking changes in syntax if sls.App.ServerlessVersion == "3" { command = fmt.Sprintf( - `source %s/nvm.sh && + `source --debug %s/nvm.sh && nvm use %s && npx serverless deploy \ --config=%s \ @@ -157,7 +167,7 @@ func (sls *Manager) runDeploy(w io.Writer) error { sls.App.AwsProfile, sls.Project.Env) } else { command = fmt.Sprintf( - `source %s/nvm.sh && + `source --debug %s/nvm.sh && nvm use %s && npx serverless deploy \ --config %s \ @@ -218,7 +228,7 @@ func (sls *Manager) runRemove(w io.Writer) error { // SLS v3 has breaking changes in syntax if sls.App.ServerlessVersion == "3" { command = fmt.Sprintf( - `source %s/nvm.sh && \ + `source --debug %s/nvm.sh && \ nvm use %s && \ npx serverless remove \ --config=%s \ @@ -232,7 +242,7 @@ func (sls *Manager) runRemove(w io.Writer) error { sls.App.AwsProfile, sls.Project.Env) } else { command = fmt.Sprintf( - `source %s/nvm.sh && \ + `source --debug %s/nvm.sh && \ nvm use %s && \ npx serverless remove \ --config %s \ @@ -283,7 +293,7 @@ func (sls *Manager) runCreateDomain(w io.Writer) error { } command := fmt.Sprintf( - `source %s/nvm.sh && \ + `source --debug %s/nvm.sh && \ nvm use %s && \ npx serverless create_domain \ --verbose \ @@ -330,7 +340,7 @@ func (sls *Manager) runRemoveDomain(w io.Writer) error { } command := fmt.Sprintf( - `source %s/nvm.sh && \ + `source --debug %s/nvm.sh && \ nvm use %s && \ npx serverless delete_domain \ --verbose \ From 078028c2f3780546bbcc47e146a14e07e8c5d4de Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Thu, 8 Aug 2024 09:47:08 +0000 Subject: [PATCH 26/30] Add bash debug --- internal/manager/serverless/native.go | 29 +++++++++++++++++---------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/internal/manager/serverless/native.go b/internal/manager/serverless/native.go index 8f7afff3..e6037dd6 100644 --- a/internal/manager/serverless/native.go +++ b/internal/manager/serverless/native.go @@ -19,7 +19,7 @@ func (sls *Manager) runNpmInstall(w io.Writer) error { nvmDir = "$HOME/.nvm" } - command := fmt.Sprintf("source --debug %s/nvm.sh && nvm use %s && npm install --save-dev", nvmDir, sls.App.NodeVersion) + command := fmt.Sprintf("source %s/nvm.sh && nvm use %s && npm install --save-dev", nvmDir, sls.App.NodeVersion) if sls.App.UseYarn { command = npmToYarn(command) @@ -64,9 +64,16 @@ func (sls *Manager) nvm(w io.Writer, command string) error { return err } - logrus.Debugf("Running: bash -c source --debug %s/nvm.sh && nvm install %s && %s", nvmDir, sls.App.NodeVersion, command) - cmd := exec.Command("bash", "-xv", "-c", - fmt.Sprintf("source --debug %s/nvm.sh && nvm install %s && %s", nvmDir, sls.App.NodeVersion, command), + 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), ) // Capture stderr in a buffer @@ -115,7 +122,7 @@ func (sls *Manager) runNvm(w io.Writer) error { return err } - command := fmt.Sprintf("source --debug %s/nvm.sh && nvm install %s", nvmDir, sls.App.NodeVersion) + command := fmt.Sprintf("source %s/nvm.sh && nvm install %s", nvmDir, sls.App.NodeVersion) logrus.SetOutput(w) logrus.Debugf("command: %s", command) @@ -153,7 +160,7 @@ func (sls *Manager) runDeploy(w io.Writer) error { // SLS v3 has breaking changes in syntax if sls.App.ServerlessVersion == "3" { command = fmt.Sprintf( - `source --debug %s/nvm.sh && + `source %s/nvm.sh && nvm use %s && npx serverless deploy \ --config=%s \ @@ -167,7 +174,7 @@ func (sls *Manager) runDeploy(w io.Writer) error { sls.App.AwsProfile, sls.Project.Env) } else { command = fmt.Sprintf( - `source --debug %s/nvm.sh && + `source %s/nvm.sh && nvm use %s && npx serverless deploy \ --config %s \ @@ -228,7 +235,7 @@ func (sls *Manager) runRemove(w io.Writer) error { // SLS v3 has breaking changes in syntax if sls.App.ServerlessVersion == "3" { command = fmt.Sprintf( - `source --debug %s/nvm.sh && \ + `source %s/nvm.sh && \ nvm use %s && \ npx serverless remove \ --config=%s \ @@ -242,7 +249,7 @@ func (sls *Manager) runRemove(w io.Writer) error { sls.App.AwsProfile, sls.Project.Env) } else { command = fmt.Sprintf( - `source --debug %s/nvm.sh && \ + `source %s/nvm.sh && \ nvm use %s && \ npx serverless remove \ --config %s \ @@ -293,7 +300,7 @@ func (sls *Manager) runCreateDomain(w io.Writer) error { } command := fmt.Sprintf( - `source --debug %s/nvm.sh && \ + `source %s/nvm.sh && \ nvm use %s && \ npx serverless create_domain \ --verbose \ @@ -340,7 +347,7 @@ func (sls *Manager) runRemoveDomain(w io.Writer) error { } command := fmt.Sprintf( - `source --debug %s/nvm.sh && \ + `source %s/nvm.sh && \ nvm use %s && \ npx serverless delete_domain \ --verbose \ From d676a58c48022762d85e667a79c21dd82864db9a Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Thu, 8 Aug 2024 10:44:14 +0000 Subject: [PATCH 27/30] Add nvm version lock/set --- internal/config/config.go | 1 + internal/config/project.go | 1 + internal/manager/serverless/native.go | 67 +++++++++++++++++++++------ internal/schema/ize-spec.json | 4 ++ 4 files changed, 58 insertions(+), 15 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 7c58aa8a..bbc5a925 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -271,6 +271,7 @@ func InitConfig() { // 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) diff --git a/internal/config/project.go b/internal/config/project.go index f0005144..3331fb30 100644 --- a/internal/config/project.go +++ b/internal/config/project.go @@ -35,6 +35,7 @@ type Project struct { 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"` diff --git a/internal/manager/serverless/native.go b/internal/manager/serverless/native.go index e6037dd6..0200d4f0 100644 --- a/internal/manager/serverless/native.go +++ b/internal/manager/serverless/native.go @@ -41,25 +41,13 @@ 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" - } - // Check if nvm.sh exists in the nvmDir - nvmShPath := filepath.Join(nvmDir, "nvm.sh") - if _, err := os.Stat(nvmShPath); os.IsNotExist(err) { - logrus.Debugf("nvm.sh does not exist in the directory:", nvmDir) - return err - } else if err != nil { - logrus.Debugf("Error checking nvm.sh:", err) + nvmDir, err := sls.installNvm() + if err != nil { 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 - - err := sls.readNvmrc() + err = sls.readNvmrc() if err != nil { return err } @@ -391,3 +379,52 @@ 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.Debugf("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/schema/ize-spec.json b/internal/schema/ize-spec.json index 8b12b7b5..00702527 100644 --- a/internal/schema/ize-spec.json +++ b/internal/schema/ize-spec.json @@ -65,6 +65,10 @@ "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)" From 7d1665b14604cae6756440d84ce61a1f43ee533c Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Thu, 8 Aug 2024 10:55:44 +0000 Subject: [PATCH 28/30] Add nvm version lock/set --- internal/manager/serverless/native.go | 32 +++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/internal/manager/serverless/native.go b/internal/manager/serverless/native.go index 0200d4f0..6e007194 100644 --- a/internal/manager/serverless/native.go +++ b/internal/manager/serverless/native.go @@ -100,12 +100,12 @@ func (sls *Manager) readNvmrc() error { } 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 } @@ -139,9 +139,9 @@ func (sls *Manager) runNvm(w io.Writer) error { } 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 @@ -213,9 +213,9 @@ func (sls *Manager) runDeploy(w io.Writer) error { 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 @@ -282,9 +282,9 @@ func (sls *Manager) runRemove(w io.Writer) error { } 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( @@ -329,9 +329,9 @@ func (sls *Manager) runCreateDomain(w io.Writer) error { } 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( From f15f2c7603cba73a8a43ad3b341f899b708fbf26 Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Thu, 8 Aug 2024 10:58:11 +0000 Subject: [PATCH 29/30] Fix err assignment --- internal/manager/serverless/native.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/manager/serverless/native.go b/internal/manager/serverless/native.go index 6e007194..8a0acc6f 100644 --- a/internal/manager/serverless/native.go +++ b/internal/manager/serverless/native.go @@ -202,7 +202,7 @@ func (sls *Manager) runDeploy(w io.Writer) error { term.WithStderr(&stderr), ) - err := t.InteractiveRun(cmd) + 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()) @@ -272,7 +272,7 @@ func (sls *Manager) runRemove(w io.Writer) error { term.WithStderr(&stderr), ) - err := t.InteractiveRun(cmd) + 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()) @@ -319,7 +319,7 @@ func (sls *Manager) runCreateDomain(w io.Writer) error { term.WithStderr(&stderr), ) - err := t.InteractiveRun(cmd) + 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()) @@ -366,7 +366,7 @@ func (sls *Manager) runRemoveDomain(w io.Writer) error { term.WithStderr(&stderr), ) - err := t.InteractiveRun(cmd) + 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()) From 00e186e9be7bb9bd25632d3b9792a91faf255321 Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Thu, 8 Aug 2024 11:00:56 +0000 Subject: [PATCH 30/30] WIP --- internal/manager/serverless/native.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/manager/serverless/native.go b/internal/manager/serverless/native.go index 8a0acc6f..054a9dab 100644 --- a/internal/manager/serverless/native.go +++ b/internal/manager/serverless/native.go @@ -412,7 +412,7 @@ func (sls *Manager) installNvm() (string, error) { } } - logrus.Debugf("No correct nvm version found is incorrect, (re)installing nvm") + 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))