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 }