github.com/hazelops/ize@v1.1.12-0.20230915191306-97d7c0e48f11/internal/manager/ecs/ecs.go (about)

     1  package ecs
     2  
     3  import (
     4  	"context"
     5  	"encoding/base64"
     6  	"encoding/json"
     7  	"fmt"
     8  	"os"
     9  	"path"
    10  	"path/filepath"
    11  	"time"
    12  
    13  	"github.com/hazelops/ize/internal/aws/utils"
    14  	"github.com/hazelops/ize/internal/config"
    15  	"github.com/hazelops/ize/pkg/templates"
    16  
    17  	"github.com/aws/aws-sdk-go/aws"
    18  	"github.com/aws/aws-sdk-go/service/ecr"
    19  	"github.com/aws/aws-sdk-go/service/ecs"
    20  	"github.com/docker/docker/api/types"
    21  	"github.com/hazelops/ize/internal/docker"
    22  	"github.com/hazelops/ize/pkg/terminal"
    23  	"github.com/pterm/pterm"
    24  	"github.com/sirupsen/logrus"
    25  )
    26  
    27  const ecsDeployImage = "hazelops/ecs-deploy:latest"
    28  
    29  type Manager struct {
    30  	Project *config.Project
    31  	App     *config.Ecs
    32  }
    33  
    34  func (e *Manager) prepare() {
    35  	if e.App.Path == "" {
    36  		appsPath := e.Project.AppsPath
    37  		if !filepath.IsAbs(appsPath) {
    38  			appsPath = filepath.Join(os.Getenv("PWD"), appsPath)
    39  		}
    40  
    41  		e.App.Path = filepath.Join(appsPath, e.App.Name)
    42  	} else {
    43  		rootDir := e.Project.RootDir
    44  
    45  		if !filepath.IsAbs(e.App.Path) {
    46  			e.App.Path = filepath.Join(rootDir, e.App.Path)
    47  		}
    48  	}
    49  
    50  	if len(e.App.Cluster) == 0 {
    51  		e.App.Cluster = fmt.Sprintf("%s-%s", e.Project.Env, e.Project.Namespace)
    52  	}
    53  
    54  	if len(e.App.DockerRegistry) == 0 {
    55  		e.App.DockerRegistry = e.Project.DockerRegistry
    56  	}
    57  
    58  	if e.App.Timeout == 0 {
    59  		e.App.Timeout = 300
    60  	}
    61  }
    62  
    63  // Deploy deploys app container to ECS via ECS deploy
    64  func (e *Manager) Deploy(ui terminal.UI) error {
    65  	e.prepare()
    66  
    67  	sg := ui.StepGroup()
    68  	defer sg.Wait()
    69  
    70  	if len(e.App.AwsRegion) != 0 && len(e.App.AwsProfile) != 0 {
    71  		sess, err := utils.GetSession(&utils.SessionConfig{
    72  			Region:  e.App.AwsRegion,
    73  			Profile: e.App.AwsProfile,
    74  		})
    75  		if err != nil {
    76  			return fmt.Errorf("can't get session: %w", err)
    77  		}
    78  
    79  		e.Project.SettingAWSClient(sess)
    80  	}
    81  
    82  	if e.App.SkipDeploy {
    83  		s := sg.Add("%s: deploy will be skipped", e.App.Name)
    84  		defer func() { s.Abort(); time.Sleep(50 * time.Millisecond) }()
    85  		s.Done()
    86  		return nil
    87  	}
    88  
    89  	if e.App.Unsafe && e.Project.PreferRuntime == "native" {
    90  		pterm.Warning.Println(templates.Dedent(`
    91  			deployment will be accelerated (unsafe):
    92  			- Health Check Interval: 5s
    93  			- Health Check Timeout: 2s
    94  			- Healthy Threshold Count: 2
    95  			- Unhealthy Threshold Count: 2`))
    96  	}
    97  
    98  	s := sg.Add("%s: deploying app container...", e.App.Name)
    99  	defer func() { s.Abort(); time.Sleep(50 * time.Millisecond) }()
   100  
   101  	if e.App.Image == "" {
   102  		e.App.Image = fmt.Sprintf("%s/%s:%s",
   103  			e.App.DockerRegistry,
   104  			fmt.Sprintf("%s-%s", e.Project.Namespace, e.App.Name),
   105  			fmt.Sprintf("%s-%s", e.Project.Env, "latest"))
   106  	}
   107  
   108  	if e.Project.PreferRuntime == "native" {
   109  		err := e.deployLocal(s.TermOutput())
   110  		pterm.SetDefaultOutput(os.Stdout)
   111  		if err != nil {
   112  			return fmt.Errorf("unable to deploy app: %w", err)
   113  		}
   114  	} else {
   115  		err := e.deployWithDocker(s.TermOutput())
   116  		if err != nil {
   117  			return fmt.Errorf("unable to deploy app: %w", err)
   118  		}
   119  	}
   120  
   121  	s.Done()
   122  	s = sg.Add("%s: deployment completed!", e.App.Name)
   123  	s.Done()
   124  
   125  	return nil
   126  }
   127  
   128  func (e *Manager) Redeploy(ui terminal.UI) error {
   129  	e.prepare()
   130  
   131  	sg := ui.StepGroup()
   132  	defer sg.Wait()
   133  
   134  	if len(e.App.AwsRegion) != 0 && len(e.App.AwsProfile) != 0 {
   135  		sess, err := utils.GetSession(&utils.SessionConfig{
   136  			Region:  e.App.AwsRegion,
   137  			Profile: e.App.AwsProfile,
   138  		})
   139  		if err != nil {
   140  			return fmt.Errorf("can't get session: %w", err)
   141  		}
   142  
   143  		e.Project.SettingAWSClient(sess)
   144  	}
   145  
   146  	s := sg.Add("%s: redeploying app container...", e.App.Name)
   147  	defer func() { s.Abort(); time.Sleep(50 * time.Millisecond) }()
   148  
   149  	if e.Project.PreferRuntime == "native" {
   150  		err := e.redeployLocal(s.TermOutput())
   151  		pterm.SetDefaultOutput(os.Stdout)
   152  		if err != nil {
   153  			return fmt.Errorf("unable to redeploy app: %w", err)
   154  		}
   155  	} else {
   156  		err := e.redeployWithDocker(s.TermOutput())
   157  		if err != nil {
   158  			return fmt.Errorf("unable to redeploy app: %w", err)
   159  		}
   160  	}
   161  
   162  	s.Done()
   163  	s = sg.Add("%s: redeployment completed!", e.App.Name)
   164  	s.Done()
   165  
   166  	return nil
   167  }
   168  
   169  func (e *Manager) Push(ui terminal.UI) error {
   170  	e.prepare()
   171  
   172  	sg := ui.StepGroup()
   173  	defer sg.Wait()
   174  
   175  	s := sg.Add("%s: push app image...", e.App.Name)
   176  	defer func() { s.Abort(); time.Sleep(50 * time.Millisecond) }()
   177  
   178  	if len(e.App.Image) != 0 {
   179  		s.Update("%s: pushing app image... (skipped, using %s) ", e.App.Name, e.App.Image)
   180  		s.Done()
   181  
   182  		return nil
   183  	}
   184  
   185  	image := fmt.Sprintf("%s-%s", e.Project.Namespace, e.App.Name)
   186  
   187  	svc := e.Project.AWSClient.ECRClient
   188  
   189  	var repository *ecr.Repository
   190  
   191  	dro, err := svc.DescribeRepositories(&ecr.DescribeRepositoriesInput{
   192  		RepositoryNames: []*string{aws.String(image)},
   193  	})
   194  	if err != nil {
   195  		return fmt.Errorf("can't describe repositories: %w", err)
   196  	}
   197  
   198  	if dro == nil || len(dro.Repositories) == 0 {
   199  		logrus.Info("no ECR repository detected, creating", "name", image)
   200  
   201  		out, err := svc.CreateRepository(&ecr.CreateRepositoryInput{
   202  			RepositoryName: aws.String(image),
   203  		})
   204  		if err != nil {
   205  			return fmt.Errorf("unable to create repository: %w", err)
   206  		}
   207  
   208  		repository = out.Repository
   209  	} else {
   210  		repository = dro.Repositories[0]
   211  	}
   212  
   213  	gat, err := svc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{})
   214  	if err != nil {
   215  		return fmt.Errorf("unable to get authorization token: %w", err)
   216  	}
   217  
   218  	if len(gat.AuthorizationData) == 0 {
   219  		return fmt.Errorf("no authorization tokens provided")
   220  	}
   221  
   222  	upToken := *gat.AuthorizationData[0].AuthorizationToken
   223  	data, err := base64.StdEncoding.DecodeString(upToken)
   224  	if err != nil {
   225  		return fmt.Errorf("unable to decode authorization token: %w", err)
   226  	}
   227  
   228  	auth := types.AuthConfig{
   229  		Username: "AWS",
   230  		Password: string(data[4:]),
   231  	}
   232  
   233  	authBytes, _ := json.Marshal(auth)
   234  
   235  	token := base64.URLEncoding.EncodeToString(authBytes)
   236  
   237  	tagLatest := fmt.Sprintf("%s-latest", e.Project.Env)
   238  	imageUri := fmt.Sprintf("%s/%s", e.App.DockerRegistry, image)
   239  	platform := "linux/amd64"
   240  	if e.Project.PreferRuntime == "docker-arm64" {
   241  		platform = "linux/arm64"
   242  	}
   243  
   244  	r := docker.NewRegistry(*repository.RepositoryUri, token, platform)
   245  
   246  	err = r.Push(context.Background(), s.TermOutput(), imageUri, []string{e.Project.Tag, tagLatest})
   247  	if err != nil {
   248  		return fmt.Errorf("can't push image: %w", err)
   249  	}
   250  
   251  	s.Done()
   252  
   253  	return nil
   254  }
   255  
   256  func (e *Manager) Build(ui terminal.UI) error {
   257  	e.prepare()
   258  
   259  	sg := ui.StepGroup()
   260  	defer sg.Wait()
   261  
   262  	s := sg.Add("%s: building app container...", e.App.Name)
   263  	defer func() { s.Abort(); time.Sleep(50 * time.Millisecond) }()
   264  
   265  	if len(e.App.Image) != 0 {
   266  		s.Update("%s: building app container... (skipped, using %s)", e.App.Name, e.App.Image)
   267  
   268  		s.Done()
   269  		return nil
   270  	}
   271  
   272  	image := fmt.Sprintf("%s-%s", e.Project.Namespace, e.App.Name)
   273  	imageUri := fmt.Sprintf("%s/%s", e.App.DockerRegistry, image)
   274  
   275  	relProjectPath, err := filepath.Rel(e.Project.RootDir, e.App.Path)
   276  	if err != nil {
   277  		return fmt.Errorf("unable to get relative path: %w", err)
   278  	}
   279  
   280  	buildArgs := map[string]*string{
   281  		"PROJECT_PATH": &relProjectPath,
   282  		"APP_PATH":     &relProjectPath,
   283  		"APP_NAME":     &e.App.Name,
   284  	}
   285  
   286  	tags := []string{
   287  		image,
   288  		fmt.Sprintf("%s:%s", imageUri, e.Project.Tag),
   289  		fmt.Sprintf("%s:%s", imageUri, fmt.Sprintf("%s-latest", e.Project.Env)),
   290  	}
   291  
   292  	dockerfile := path.Join(e.App.Path, "Dockerfile")
   293  
   294  	cache := []string{fmt.Sprintf("%s:%s", imageUri, fmt.Sprintf("%s-latest", e.Project.Env))}
   295  
   296  	platform := "linux/amd64"
   297  	if e.Project.PreferRuntime == "docker-arm64" {
   298  		platform = "linux/arm64"
   299  	}
   300  
   301  	b := docker.NewBuilder(
   302  		buildArgs,
   303  		tags,
   304  		dockerfile,
   305  		cache,
   306  		platform,
   307  	)
   308  
   309  	err = b.Build(ui, s, e.Project.RootDir)
   310  	if err != nil {
   311  		return fmt.Errorf("unable to build image: %w", err)
   312  	}
   313  
   314  	s.Done()
   315  
   316  	return nil
   317  }
   318  
   319  func definitionsToBulletItems(definitions *ecs.ListTaskDefinitionsOutput) []pterm.BulletListItem {
   320  	var items []pterm.BulletListItem
   321  	for _, arn := range definitions.TaskDefinitionArns {
   322  		items = append(items, pterm.BulletListItem{Level: 0, Text: *arn})
   323  	}
   324  
   325  	return items
   326  }
   327  
   328  func (e *Manager) Destroy(ui terminal.UI, autoApprove bool) error {
   329  	sg := ui.StepGroup()
   330  	defer sg.Wait()
   331  
   332  	s := sg.Add("%s: destroying task defintions...", e.App.Name)
   333  	defer func() { s.Abort(); time.Sleep(time.Millisecond * 200) }()
   334  
   335  	name := fmt.Sprintf("%s-%s", e.Project.Env, e.App.Name)
   336  
   337  	svc := e.Project.AWSClient.ECSClient
   338  
   339  	definitions, err := svc.ListTaskDefinitions(&ecs.ListTaskDefinitionsInput{
   340  		FamilyPrefix: &name,
   341  		Sort:         aws.String(ecs.SortOrderDesc),
   342  	})
   343  	if err != nil {
   344  		return fmt.Errorf("can't get list task definitions of '%s': %v", name, err)
   345  	}
   346  
   347  	if !autoApprove {
   348  		pterm.SetDefaultOutput(s.TermOutput())
   349  
   350  		pterm.Printfln("this will destroy the following:")
   351  		pterm.DefaultBulletList.WithItems(definitionsToBulletItems(definitions)).Render()
   352  
   353  		isContinue, err := pterm.DefaultInteractiveConfirm.WithDefaultText("Continue?").Show()
   354  		if err != nil {
   355  			return err
   356  		}
   357  
   358  		if !isContinue {
   359  			return fmt.Errorf("destroying was canceled")
   360  		}
   361  	}
   362  
   363  	for _, tda := range definitions.TaskDefinitionArns {
   364  		_, err := e.Project.AWSClient.ECSClient.DeregisterTaskDefinition(&ecs.DeregisterTaskDefinitionInput{
   365  			TaskDefinition: tda,
   366  		})
   367  		if err != nil {
   368  			return fmt.Errorf("can't deregister task definition '%s': %v", *tda, err)
   369  		}
   370  	}
   371  
   372  	s.Done()
   373  	s = sg.Add("%s: destroying completed!", e.App.Name)
   374  	s.Done()
   375  
   376  	return nil
   377  }