github.com/in4it/ecs-deploy@v0.0.42-0.20240508120354-ed77ff16df25/cmd/ecs-client/main.go (about) 1 package main 2 3 import ( 4 "bufio" 5 "bytes" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "io/ioutil" 10 "net/http" 11 "os" 12 "path/filepath" 13 "strings" 14 "syscall" 15 "time" 16 17 "github.com/ghodss/yaml" 18 "github.com/in4it/ecs-deploy/service" 19 "github.com/juju/loggo" 20 "github.com/spf13/pflag" 21 "golang.org/x/crypto/ssh/terminal" 22 ) 23 24 var clientLogger = loggo.GetLogger("client") 25 26 type Token struct { 27 Token string `json:"token" binding:"required"` 28 Expire string `json:"expire" binding:"required"` 29 } 30 type Session struct { 31 Token string 32 Url string 33 Expire string 34 } 35 36 type LoginFlags struct { 37 Url string 38 } 39 type DeployFlags struct { 40 ServiceName string 41 Filename string 42 } 43 44 type DeployResponse struct { 45 Errors map[string]string `json:"errors" binding:"required"` 46 Failures int64 `json:"failures" binding:"required"` 47 Messages []service.DeployResult `json:"messages"` 48 } 49 type DeployStatusResponse struct { 50 Service service.DeployResult `json:"service" binding:"required"` 51 } 52 53 func addLoginFlags(f *LoginFlags, fs *pflag.FlagSet) { 54 fs.StringVar(&f.Url, "url", f.Url, "ecs-deploy url, e.g. https://127.0.0.1:8080/ecs-deploy") 55 } 56 func addDeployFlags(f *DeployFlags, fs *pflag.FlagSet) { 57 fs.StringVar(&f.ServiceName, "service-name", f.ServiceName, "Service name to deploy") 58 fs.StringVarP(&f.Filename, "filename", "f", f.Filename, "filename to deploy") 59 } 60 61 func main() { 62 var err error 63 64 // set logging 65 if os.Getenv("DEBUG") == "true" { 66 loggo.ConfigureLoggers(`<root>=DEBUG`) 67 } else { 68 loggo.ConfigureLoggers(`<root>=INFO`) 69 } 70 71 session, err := readSession() 72 if err != nil { 73 fmt.Printf("%v", err.Error()) 74 os.Exit(1) 75 } 76 77 if len(os.Args) > 1 && os.Args[1] == "login" { 78 // login 79 loginFlags := &LoginFlags{} 80 addLoginFlags(loginFlags, pflag.CommandLine) 81 82 if len(os.Args) > 2 && os.Args[2] != "" { 83 pflag.CommandLine.Parse(os.Args[2:]) 84 if loginFlags.Url == "" { 85 fmt.Fprintf(os.Stderr, "Usage of %s login:\n", os.Args[0]) 86 pflag.PrintDefaults() 87 os.Exit(1) 88 } 89 err = login(loginFlags) 90 } else { 91 fmt.Fprintf(os.Stderr, "Usage of %s login:\n", os.Args[0]) 92 pflag.PrintDefaults() 93 } 94 } else if len(os.Args) > 2 && os.Args[1] == "createrepo" && os.Args[2] != "" { 95 // create repo 96 var result string 97 result, err = createRepository(session, os.Args[2]) 98 fmt.Printf("%v\n", result) 99 } else if len(os.Args) > 1 && os.Args[1] == "deploy" { 100 // deploy 101 deployFlags := &DeployFlags{} 102 addDeployFlags(deployFlags, pflag.CommandLine) 103 104 if len(os.Args) > 2 && os.Args[2] != "" { 105 pflag.CommandLine.Parse(os.Args[2:]) 106 failure, err := deploy(session, deployFlags) 107 if failure { 108 if err != nil { 109 fmt.Printf("%v", err.Error()) 110 } 111 os.Exit(1) 112 } 113 } else { 114 fmt.Fprintf(os.Stderr, "Usage of %s deploy:\n", os.Args[0]) 115 pflag.PrintDefaults() 116 } 117 } else if len(os.Args) > 1 && os.Args[1] == "runtask" { 118 deployFlags := &DeployFlags{} 119 addDeployFlags(deployFlags, pflag.CommandLine) 120 121 if len(os.Args) > 2 && os.Args[2] != "" { 122 pflag.CommandLine.Parse(os.Args[2:]) 123 failure, err := runtask(session, deployFlags) 124 if failure { 125 if err != nil { 126 fmt.Printf("%v", err.Error()) 127 } 128 os.Exit(1) 129 } 130 } else { 131 fmt.Fprintf(os.Stderr, "Usage of %s runtask:\n", os.Args[0]) 132 pflag.PrintDefaults() 133 } 134 } else { 135 fmt.Println("Usage: ") 136 fmt.Printf("%v login login\n", os.Args[0]) 137 fmt.Printf("%v createrepo create repository\n", os.Args[0]) 138 fmt.Printf("%v deploy deploy services\n", os.Args[0]) 139 fmt.Printf("%v runtask run task on service\n", os.Args[0]) 140 } 141 if err != nil { 142 fmt.Printf("%v", err.Error()) 143 os.Exit(1) 144 } 145 } 146 147 func runtask(session Session, deployFlags *DeployFlags) (bool, error) { 148 if deployFlags.Filename == "" { 149 // default for ease 150 deployFlags.Filename = "ecs.json" 151 } 152 if fi, err := os.Stat(deployFlags.Filename); err != nil || fi.IsDir() { 153 return true, errors.New("runtask expects a single json file") 154 } 155 content, err := ioutil.ReadFile(deployFlags.Filename) 156 if err != nil { 157 return true, err 158 } 159 resp, err := doRunTaskAPICall(session, deployFlags.ServiceName, string(content)) 160 if err != nil { 161 return true, err 162 } 163 fmt.Printf("Service %v started task: %v\n", deployFlags.ServiceName, string(resp)) 164 return false, nil 165 } 166 167 // deploy with timeouts 168 // if --service-name is set, look for ecs.[json|yaml] and ecs.*.[json|yaml] (if filename is set, use it as directory to look into) 169 // if --service-name is set, with filename, give error 170 // if filename is set but not service name, expect serviceName in json (normal behavior) 171 172 func deploy(session Session, deployFlags *DeployFlags) (bool, error) { 173 deployData, err := getDeployData(session, deployFlags) 174 if err != nil { 175 return true, err 176 } 177 response, err := doDeployAPICall(session, deployData) 178 if err != nil { 179 return true, err 180 } 181 deployed, err := waitForDeploy(session, response) 182 if err != nil { 183 return true, err 184 } 185 fmt.Println("") 186 fmt.Println("---") 187 var failure bool 188 for k, status := range deployed { 189 fmt.Printf("Service %v deployment status: %v\n", k, status) 190 if status != "success" { 191 failure = true 192 } 193 } 194 return failure, nil 195 } 196 func waitForDeploy(session Session, response []byte) (map[string]string, error) { 197 // api call returned info to follow-up on deployment 198 var deploymentsFinished bool 199 var deployResponse DeployResponse 200 var finished int64 201 deployed := make(map[string]string) 202 maxWait := 1200 203 err := json.Unmarshal(response, &deployResponse) 204 if err != nil { 205 return deployed, err 206 } 207 for _, v := range deployResponse.Messages { 208 deployed[v.ServiceName] = "running" 209 } 210 for k, v := range deployResponse.Errors { 211 fmt.Printf("Service %v: %v\n", k, v) 212 deployed[k] = "error" 213 finished++ 214 } 215 if int64(len(deployed)) == finished { 216 deploymentsFinished = true 217 } 218 for i := 0; i < (maxWait/15) && !deploymentsFinished; i++ { 219 time.Sleep(15 * time.Second) 220 for _, v := range deployResponse.Messages { 221 if deployed[v.ServiceName] == "running" { 222 status, err := checkDeployStatus(session, v.ServiceName, v.DeploymentTime.Format("2006-01-02T15:04:05.999999999Z")) 223 if err != nil { 224 return deployed, err 225 } 226 fmt.Printf(".") 227 if status != "running" { 228 deployed[v.ServiceName] = status 229 fmt.Printf("%v=%v", v.ServiceName, status) 230 finished++ 231 } 232 } 233 } 234 if int64(len(deployed)) == finished { 235 deploymentsFinished = true 236 } 237 } 238 return deployed, nil 239 } 240 func checkDeployStatus(session Session, serviceName, deploymentTime string) (string, error) { 241 var status string 242 var deployStatusResponse DeployStatusResponse 243 req, err := http.NewRequest("GET", session.Url+"/api/v1/deploy/status/"+serviceName+"/"+deploymentTime, nil) 244 if err != nil { 245 return status, err 246 } 247 req.Header.Set("Authorization", "Bearer "+session.Token) 248 var client = &http.Client{ 249 Timeout: time.Second * 15, 250 } 251 resp, err := client.Do(req) 252 if err != nil { 253 return status, err 254 } 255 defer resp.Body.Close() 256 body, err := ioutil.ReadAll(resp.Body) 257 if err != nil { 258 return status, err 259 } 260 if resp.StatusCode != 200 { 261 if resp.StatusCode == 401 { 262 return status, fmt.Errorf("Invalid credentials: use %v login --url <url> to login again\n", os.Args[0]) 263 } else { 264 return status, fmt.Errorf("Error %d: %v", resp.StatusCode, string(body)) 265 } 266 } 267 err = json.Unmarshal(body, &deployStatusResponse) 268 if err != nil { 269 return status, err 270 } 271 status = deployStatusResponse.Service.Status 272 return status, nil 273 } 274 func doRunTaskAPICall(session Session, service string, deployData string) ([]byte, error) { 275 url := fmt.Sprintf("service/runtask/%v", service) 276 return doAPICall(session, url, deployData) 277 } 278 func doDeployAPICall(session Session, deployData string) ([]byte, error) { 279 url := "deploy" 280 return doAPICall(session, url, deployData) 281 } 282 func doAPICall(session Session, url string, deployData string) ([]byte, error) { 283 var body []byte 284 clientLogger.Debugf("API Call data: %v", deployData) 285 req, err := http.NewRequest("POST", session.Url+"/api/v1/"+url, bytes.NewBuffer([]byte(deployData))) 286 if err != nil { 287 return body, err 288 } 289 req.Header.Set("Content-Type", "application/json") 290 req.Header.Set("Authorization", "Bearer "+session.Token) 291 var client = &http.Client{ 292 Timeout: time.Second * 120, 293 } 294 resp, err := client.Do(req) 295 if err != nil { 296 return body, err 297 } 298 defer resp.Body.Close() 299 body, err = ioutil.ReadAll(resp.Body) 300 if err != nil { 301 return body, err 302 } 303 if resp.StatusCode != 200 { 304 if resp.StatusCode == 401 { 305 return body, fmt.Errorf("Invalid credentials: use %v login --url <url> to login again\n", os.Args[0]) 306 } else { 307 return body, fmt.Errorf("Error %d: %v", resp.StatusCode, string(body)) 308 } 309 } 310 return body, nil 311 } 312 func getDeployData(session Session, deployFlags *DeployFlags) (string, error) { 313 var deployData string 314 var deployServices service.DeployServices 315 var err error 316 if deployFlags.ServiceName != "" { 317 // serviceName is set 318 deployServices, err = getDeployDataWithService(deployFlags.ServiceName, deployFlags.Filename) 319 if err != nil { 320 return deployData, err 321 } 322 } else if deployFlags.ServiceName == "" && deployFlags.Filename != "" { 323 // serviceName is not set 324 deployServices, err = getDeployDataWithoutService(deployFlags.ServiceName, deployFlags.Filename) 325 if err != nil { 326 return deployData, err 327 } 328 } else { 329 fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) 330 pflag.PrintDefaults() 331 return deployData, errors.New("InvalidFlags") 332 } 333 // convert to JSON 334 deployData, err = convertDeployServiceToJson(deployServices) 335 if err != nil { 336 return deployData, err 337 } 338 return deployData, nil 339 } 340 func convertDeployServiceToJson(deployServices service.DeployServices) (string, error) { 341 var deployData string 342 b, err := json.Marshal(deployServices) 343 if err != nil { 344 return deployData, err 345 } 346 deployData = string(b) 347 if deployData == `{"deploy":null}` { 348 return deployData, errors.New("No deployment data found") 349 } 350 return deployData, nil 351 } 352 func getDeployDataWithoutService(serviceName, filename string) (service.DeployServices, error) { 353 var deployServices service.DeployServices 354 var err error 355 if ok, _ := isDir(filename); ok { 356 return deployServices, fmt.Errorf("%v is a directory. Specify a file or use the --service-name argument\n", filename) 357 } 358 fType := "" 359 if filepath.Ext(filename) == ".json" { 360 fType = "json" 361 } else if filepath.Ext(filename) == ".yaml" || filepath.Ext(filename) == ".yml" { 362 fType = "yaml" 363 } 364 deployService, err := parseFile(filename, fType, "") 365 if err != nil { 366 return deployServices, nil 367 } 368 deployServices.Services = append(deployServices.Services, deployService...) 369 return deployServices, nil 370 } 371 func getDeployDataWithService(serviceName, filename string) (service.DeployServices, error) { 372 var readDir string 373 var deployServices service.DeployServices 374 var err error 375 if filename != "" { 376 if ok, _ := isDir(filename); !ok { 377 return deployServices, fmt.Errorf("%v needs to be a directory if --service-name is specified\n", filename) 378 } 379 readDir = filename 380 } else { 381 readDir = "./" 382 } 383 // parse JSON/YAML files 384 deployServices, err = parseFiles(readDir, serviceName) 385 if err != nil { 386 return deployServices, err 387 } 388 if len(deployServices.Services) == 0 { 389 return deployServices, fmt.Errorf("No json/yaml files found to deploy\n") 390 } 391 return deployServices, nil 392 } 393 func parseFiles(readDir, serviceName string) (service.DeployServices, error) { 394 var deployServices service.DeployServices 395 fs := make(map[string]string) 396 files, err := ioutil.ReadDir(readDir) 397 if err != nil { 398 return deployServices, err 399 } 400 for _, f := range files { 401 if f.Name() == "ecs.json" { 402 fs[f.Name()] = "json" 403 } else if strings.HasPrefix(f.Name(), "ecs.") && strings.HasSuffix(f.Name(), ".json") { 404 fs[f.Name()] = "json" 405 } else if f.Name() == "ecs.yaml" || f.Name() == "ecs.yml" { 406 fs[f.Name()] = "yaml" 407 } else if strings.HasPrefix(f.Name(), "ecs.") && (strings.HasSuffix(f.Name(), ".yaml") || strings.HasSuffix(f.Name(), ".yml")) { 408 fs[f.Name()] = "yaml" 409 } 410 } 411 for f, fType := range fs { 412 deploy, err := parseFile(filepath.Join(readDir, f), fType, serviceName) 413 if err != nil { 414 return deployServices, nil 415 } 416 deployServices.Services = append(deployServices.Services, deploy...) 417 } 418 return deployServices, nil 419 } 420 func unmarshalWithDefaults(fType string, content []byte, deploy *service.DeployServices) error { 421 // set defaults 422 var err error 423 var singleDeploy service.Deploy 424 service.SetDeployDefaults(&singleDeploy) 425 426 y := len(deploy.Services) 427 deploy.Services = []service.Deploy{} 428 for i := 0; i < y; i++ { 429 deploy.Services = append(deploy.Services, singleDeploy) 430 } 431 // unmarshal with defaults 432 if fType == "json" { 433 err = json.Unmarshal(content, &deploy) 434 } else if fType == "yaml" { 435 err = yaml.Unmarshal(content, &deploy) 436 } else { 437 return fmt.Errorf("Wrong file extension (needs to be json, yaml, or yml)\n") 438 } 439 if err != nil { 440 return fmt.Errorf("Could not unmarshal with defaults: %v", err.Error()) 441 } 442 return nil 443 } 444 func parseFile(filename, fType, serviceName string) ([]service.Deploy, error) { 445 var deploy service.DeployServices 446 var singleDeploy service.Deploy 447 fileBase := filepath.Base(filename) 448 // set defaults for singledeploy 449 service.SetDeployDefaults(&singleDeploy) 450 451 content, err := ioutil.ReadFile(filename) 452 if err != nil { 453 return deploy.Services, fmt.Errorf("Could not read file: %v\n", filename) 454 } 455 if fType == "json" { 456 err = json.Unmarshal(content, &deploy) 457 if err != nil { 458 return deploy.Services, fmt.Errorf("json file %v in wrong format: %v", filename, err.Error()) 459 } 460 if len(deploy.Services) == 0 { 461 err = json.Unmarshal(content, &singleDeploy) 462 if err != nil { 463 return deploy.Services, fmt.Errorf("json file %v in wrong format: %v", filename, err.Error()) 464 } 465 deploy.Services = append(deploy.Services, singleDeploy) 466 } else { 467 unmarshalWithDefaults("json", content, &deploy) 468 } 469 } else if fType == "yaml" { 470 err = yaml.Unmarshal(content, &deploy) 471 if err != nil { 472 return deploy.Services, fmt.Errorf("yaml file %v in wrong format: %v", filename, err.Error()) 473 } 474 if len(deploy.Services) == 0 { 475 err = yaml.Unmarshal(content, &singleDeploy) 476 if err != nil { 477 return deploy.Services, fmt.Errorf("yaml file %v in wrong format: %v", filename, err.Error()) 478 } 479 deploy.Services = append(deploy.Services, singleDeploy) 480 } else { 481 unmarshalWithDefaults("yaml", content, &deploy) 482 } 483 } else { 484 return deploy.Services, fmt.Errorf("Wrong file extension (needs to be json, yaml, or yml)\n") 485 } 486 // check whether we have services 487 if len(deploy.Services) == 0 { 488 return deploy.Services, fmt.Errorf("Unable to extract any services from the provided file(s)\n") 489 } 490 // set defaults and serviceName 491 for k, _ := range deploy.Services { 492 if serviceName != "" { 493 if fileBase == "ecs.json" || fileBase == "ecs.yaml" || fileBase == "ecs.yml" { 494 deploy.Services[k].ServiceName = serviceName 495 } else { 496 start := 4 497 filenameWithoutExt := strings.Replace(fileBase, ".yml", "", -1) 498 filenameWithoutExt = strings.Replace(filenameWithoutExt, ".json", "", -1) 499 filenameWithoutExt = strings.Replace(filenameWithoutExt, ".yaml", "", -1) 500 deploy.Services[k].ServiceName = serviceName + "-" + filenameWithoutExt[start:] 501 } 502 } 503 // check whether we not have an empty service 504 if deploy.Services[k].Cluster == "" { 505 return deploy.Services, fmt.Errorf("Service %v has no ClusterName defined\n", deploy.Services[k].ServiceName) 506 } 507 } 508 return deploy.Services, nil 509 } 510 511 func createRepository(session Session, repository string) (string, error) { 512 var res string 513 req, err := http.NewRequest("POST", session.Url+"/api/v1/ecr/create/"+repository, bytes.NewBuffer([]byte(""))) 514 if err != nil { 515 return res, err 516 } 517 req.Header.Set("Content-Type", "application/json") 518 req.Header.Set("Authorization", "Bearer "+session.Token) 519 var client = &http.Client{ 520 Timeout: time.Second * 60, 521 } 522 resp, err := client.Do(req) 523 if err != nil { 524 return res, err 525 } 526 if resp.StatusCode != 200 { 527 if resp.StatusCode == 401 { 528 return res, fmt.Errorf("Invalid credentials: use %v login --url <url> to login again\n", os.Args[0]) 529 } else { 530 return res, fmt.Errorf("ecr create return http error %d", resp.StatusCode) 531 } 532 } 533 defer resp.Body.Close() 534 body, err := ioutil.ReadAll(resp.Body) 535 if err != nil { 536 return res, err 537 } 538 res = string(body) 539 return res, nil 540 } 541 542 func readSession() (Session, error) { 543 var session Session 544 content, err := ioutil.ReadFile(filepath.Join(os.Getenv("HOME"), ".ecsdeploy", "session.json")) 545 if err != nil { 546 // no file present, return empty session 547 return session, nil 548 } 549 err = json.Unmarshal(content, &session) 550 if err != nil { 551 return session, err 552 } 553 return session, nil 554 555 } 556 func login(loginFlags *LoginFlags) error { 557 var session Session 558 var err error 559 var username, password string 560 561 session.Url = loginFlags.Url 562 if os.Getenv("ECS_DEPLOY_LOGIN") != "" && os.Getenv("ECS_DEPLOY_PASSWORD") != "" { 563 username = os.Getenv("ECS_DEPLOY_LOGIN") 564 password = os.Getenv("ECS_DEPLOY_PASSWORD") 565 } else { 566 username, password, err = readCredentials() 567 if err != nil { 568 return err 569 } 570 } 571 token, err := auth(session.Url, username, password) 572 if err != nil { 573 return err 574 } 575 newpath := filepath.Join(os.Getenv("HOME"), ".ecsdeploy") 576 os.MkdirAll(newpath, os.ModePerm) 577 578 session.Token = token.Token 579 session.Expire = token.Expire 580 581 b, err := json.Marshal(session) 582 if err != nil { 583 return err 584 } 585 err = ioutil.WriteFile(filepath.Join(os.Getenv("HOME"), ".ecsdeploy", "session.json"), b, 0600) 586 if err != nil { 587 return err 588 } 589 fmt.Println("Authentication successful") 590 return nil 591 } 592 func readCredentials() (string, string, error) { 593 reader := bufio.NewReader(os.Stdin) 594 595 fmt.Print("Enter Username: ") 596 username, _ := reader.ReadString('\n') 597 598 fmt.Print("Enter Password: ") 599 bytePassword, err := terminal.ReadPassword(int(syscall.Stdin)) 600 if err != nil { 601 return "", "", err 602 } 603 password := string(bytePassword) 604 605 return strings.TrimSpace(username), strings.TrimSpace(password), nil 606 } 607 608 func auth(url, login, password string) (Token, error) { 609 var token Token 610 var jsonStr = []byte("{\"username\":\"" + login + "\",\"password\":\"" + password + "\"}") 611 req, err := http.NewRequest("POST", url+"/login", bytes.NewBuffer(jsonStr)) 612 if err != nil { 613 return token, err 614 } 615 req.Header.Set("Content-Type", "application/json") 616 var client = &http.Client{ 617 Timeout: time.Second * 10, 618 } 619 resp, err := client.Do(req) 620 if err != nil { 621 return token, err 622 } 623 if resp.StatusCode != 200 { 624 return token, errors.New("Authentication failed") 625 } 626 defer resp.Body.Close() 627 body, err := ioutil.ReadAll(resp.Body) 628 if err != nil { 629 return token, err 630 } 631 632 err = json.Unmarshal(body, &token) 633 if err != nil { 634 return token, err 635 } 636 637 return token, nil 638 } 639 func isDir(pth string) (bool, error) { 640 fi, err := os.Stat(pth) 641 if err != nil { 642 return false, err 643 } 644 645 return fi.Mode().IsDir(), nil 646 }