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

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