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 }