github.com/jenkins-x/jx/v2@v2.1.155/pkg/cloud/gke/gcloud.go (about) 1 package gke 2 3 import ( 4 "bufio" 5 "encoding/json" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "strings" 13 "time" 14 15 osUser "os/user" 16 17 "github.com/jenkins-x/jx-logging/pkg/log" 18 "github.com/jenkins-x/jx/v2/pkg/kube/naming" 19 "github.com/jenkins-x/jx/v2/pkg/util" 20 "github.com/pkg/errors" 21 v1 "k8s.io/api/core/v1" 22 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 "k8s.io/client-go/kubernetes" 24 "sigs.k8s.io/yaml" 25 ) 26 27 // KmsLocation indicates the location used by the Google KMS service 28 const KmsLocation = "global" 29 30 var ( 31 // RequiredServiceAccountRoles the roles required to create a cluster with terraform 32 RequiredServiceAccountRoles = []string{"roles/owner"} 33 34 // KanikoServiceAccountRoles the roles required to run kaniko with GCS 35 KanikoServiceAccountRoles = []string{"roles/storage.admin", 36 "roles/storage.objectAdmin", 37 "roles/storage.objectCreator"} 38 39 // VeleroServiceAccountRoles the roles required to run velero with GCS 40 VeleroServiceAccountRoles = []string{ 41 /* TODO 42 "roles/compute.disks.get", 43 "roles/compute.disks.create", 44 "roles/compute.disks.createSnapshot", 45 "roles/compute.snapshots.get", 46 "roles/compute.snapshots.create", 47 "roles/compute.snapshots.useReadOnly", 48 "roles/compute.snapshots.delete", 49 "roles/compute.zones.get", 50 */ 51 "roles/storage.admin", 52 "roles/storage.objectAdmin", 53 "roles/storage.objectCreator"} 54 ) 55 56 // GCloud real implementation of the gcloud helper 57 type GCloud struct { 58 } 59 60 // Cluster struct to represent a cluster on gcloud 61 type Cluster struct { 62 Name string `json:"name,omitempty"` 63 ResourceLabels map[string]string `json:"resourceLabels,omitempty"` 64 Status string `json:"status,omitempty"` 65 Location string `json:"location,omitempty"` 66 } 67 68 type recordSet struct { 69 Kind string `json:"kind"` 70 Name string `json:"name"` 71 Rrdatas []string `json:"rrdatas"` 72 TTL int `json:"ttl"` 73 Type string `json:"type"` 74 } 75 76 // generateManagedZoneName constructs and returns a managed zone name using the domain value 77 func generateManagedZoneName(domain string) string { 78 79 var managedZoneName string 80 81 if domain != "" { 82 managedZoneName = strings.Replace(domain, ".", "-", -1) 83 return fmt.Sprintf("%s-zone", managedZoneName) 84 } 85 86 return "" 87 88 } 89 90 // getManagedZoneName checks for a given domain zone within the specified project and returns its name 91 func getManagedZoneName(projectID string, domain string) (string, error) { 92 args := []string{"dns", 93 "managed-zones", 94 fmt.Sprintf("--project=%s", projectID), 95 "list", 96 fmt.Sprintf("--filter=%s.", domain), 97 "--format=json", 98 } 99 100 cmd := util.Command{ 101 Name: "gcloud", 102 Args: args, 103 } 104 105 output, err := cmd.RunWithoutRetry() 106 if err != nil { 107 return "", errors.Wrap(err, "executing gcloud dns managed-zones list command ") 108 } 109 110 type managedZone struct { 111 Name string `json:"name"` 112 } 113 114 var managedZones []managedZone 115 116 err = yaml.Unmarshal([]byte(output), &managedZones) 117 if err != nil { 118 return "", errors.Wrap(err, "unmarshalling gcloud response") 119 } 120 121 if len(managedZones) == 1 { 122 return managedZones[0].Name, nil 123 } 124 125 return "", nil 126 } 127 128 // CreateManagedZone creates a managed zone for the given domain in the specified project 129 func (g *GCloud) CreateManagedZone(projectID string, domain string) error { 130 managedZoneName, err := getManagedZoneName(projectID, domain) 131 if err != nil { 132 return errors.Wrap(err, "unable to determine whether managed zone exists") 133 } 134 if managedZoneName == "" { 135 log.Logger().Infof("Managed Zone doesn't exist for %s domain, creating...", domain) 136 managedZoneName := generateManagedZoneName(domain) 137 args := []string{"dns", 138 "managed-zones", 139 fmt.Sprintf("--project=%s", projectID), 140 "create", 141 managedZoneName, 142 "--dns-name", 143 fmt.Sprintf("%s.", domain), 144 "--description=managed-zone utilised by jx", 145 } 146 147 cmd := util.Command{ 148 Name: "gcloud", 149 Args: args, 150 } 151 152 _, err := cmd.RunWithoutRetry() 153 if err != nil { 154 return errors.Wrap(err, "executing gcloud dns managed-zones list command ") 155 } 156 } else { 157 log.Logger().Infof("Managed Zone exists for %s domain.", domain) 158 } 159 return nil 160 } 161 162 // CreateDNSZone creates the DNS zone if it doesn't exist 163 // and returns the list of name servers for the given domain and project 164 func (g *GCloud) CreateDNSZone(projectID string, domain string) (string, []string, error) { 165 var managedZone, nameServers = "", []string{} 166 err := g.CreateManagedZone(projectID, domain) 167 if err != nil { 168 return "", []string{}, errors.Wrap(err, "while trying to creating a CloudDNS managed zone") 169 } 170 managedZone, nameServers, err = g.GetManagedZoneNameServers(projectID, domain) 171 if err != nil { 172 return "", []string{}, errors.Wrap(err, "while trying to retrieve the managed zone name servers") 173 } 174 175 // Update TTL to 60 for managed zone NS record 176 err = updateManagedZoneRecordTTL(projectID, domain, managedZone, "NS", 60) 177 if err != nil { 178 return "", []string{}, errors.Wrap(err, "when trying to update the managed zone NS record-set") 179 } 180 181 // Update TTL to 60 for managed zone SOA record 182 err = updateManagedZoneRecordTTL(projectID, domain, managedZone, "SOA", 60) 183 if err != nil { 184 return "", []string{}, errors.Wrap(err, "when trying to update the managed zone SOA record-set") 185 } 186 187 return managedZone, nameServers, nil 188 } 189 190 // GetManagedZoneNameServers retrieves a list of name servers associated with a zone 191 func (g *GCloud) GetManagedZoneNameServers(projectID string, domain string) (string, []string, error) { 192 var nameServers = []string{} 193 managedZoneName, err := getManagedZoneName(projectID, domain) 194 if err != nil { 195 return "", []string{}, errors.Wrap(err, "unable to determine whether managed zone exists") 196 } 197 if managedZoneName != "" { 198 log.Logger().Infof("Getting nameservers for %s domain", domain) 199 args := []string{"dns", 200 "managed-zones", 201 fmt.Sprintf("--project=%s", projectID), 202 "describe", 203 managedZoneName, 204 "--format=json", 205 } 206 207 cmd := util.Command{ 208 Name: "gcloud", 209 Args: args, 210 } 211 212 type mz struct { 213 Name string `json:"name"` 214 NameServers []string `json:"nameServers"` 215 } 216 217 var managedZone mz 218 219 output, err := cmd.RunWithoutRetry() 220 if err != nil { 221 return "", []string{}, errors.Wrap(err, "executing gcloud dns managed-zones list command ") 222 } 223 224 err = json.Unmarshal([]byte(output), &managedZone) 225 if err != nil { 226 return "", []string{}, errors.Wrap(err, "unmarshalling gcloud response when returning managed-zone nameservers") 227 } 228 nameServers = managedZone.NameServers 229 } else { 230 log.Logger().Infof("Managed Zone doesn't exist for %s domain.", domain) 231 } 232 return managedZoneName, nameServers, nil 233 } 234 235 func getManagedZoneRecordSet(parentProject string, parentZone string, mzRecordSet recordSet) (recordSet, error) { 236 var googleRecordSet recordSet 237 238 args := []string{"dns", 239 "record-sets", 240 fmt.Sprintf("--project=%s", parentProject), 241 "list", 242 fmt.Sprintf("--name=%s", mzRecordSet.Name), 243 fmt.Sprintf("--zone=%s", parentZone), 244 fmt.Sprintf("--filter=type:%s", mzRecordSet.Type), 245 "--format=json", 246 } 247 cmd := util.Command{ 248 Name: "gcloud", 249 Args: args, 250 } 251 252 output, err := cmd.Run() 253 if err != nil { 254 return googleRecordSet, errors.Wrap(err, "executing gcloud dns managed-zones list command") 255 } 256 var recordSets []recordSet 257 err = yaml.Unmarshal([]byte(output), &recordSets) 258 if err != nil { 259 return googleRecordSet, errors.Wrap(err, "unmarshalling gcloud response") 260 } 261 262 if len(recordSets) == 1 { 263 log.Logger().Infof("google dns record-set contains - domain: %s, with nameServers: %s\n", recordSets[0].Name, strings.Join(recordSets[0].Rrdatas, " ")) 264 googleRecordSet = recordSets[0] 265 } else { 266 log.Logger().Debugf("No record-set or more than one record-set found for %s in %s", mzRecordSet.Name, parentZone) 267 } 268 269 return googleRecordSet, nil 270 } 271 272 func applyManagedZoneRecordTTL(parentProject string, parentZone string, googleRecordSet recordSet, ttl int) error { 273 274 if googleRecordSet.Name != "" { 275 // transaction start 276 startArgs := []string{"dns", 277 "record-sets", 278 fmt.Sprintf("--project=%s", parentProject), 279 "transaction", 280 "start", 281 fmt.Sprintf("--zone=%s", parentZone), 282 "--format=json", 283 } 284 startCmd := util.Command{ 285 Name: "gcloud", 286 Args: startArgs, 287 } 288 289 _, err := startCmd.RunWithoutRetry() 290 if err != nil { 291 return errors.Wrap(err, "executing gcloud dns record-sets transaction start command") 292 } 293 294 // remove the previous record as it needs to be updated 295 removeArgs1 := []string{"dns", 296 "record-sets", 297 fmt.Sprintf("--project=%s", parentProject), 298 "transaction", 299 "remove", 300 } 301 removeArgs2 := []string{fmt.Sprintf("--name=%s", googleRecordSet.Name), 302 fmt.Sprintf("--ttl=%d", googleRecordSet.TTL), 303 fmt.Sprintf("--type=%s", googleRecordSet.Type), 304 fmt.Sprintf("--zone=%s", parentZone), 305 "--format=json", 306 } 307 removeArgs := append(removeArgs1, googleRecordSet.Rrdatas...) 308 removeArgs = append(removeArgs, removeArgs2...) 309 310 removeCmd := util.Command{ 311 Name: "gcloud", 312 Args: removeArgs, 313 } 314 315 _, err = removeCmd.RunWithoutRetry() 316 if err != nil { 317 return errors.Wrap(err, "executing gcloud dns record-sets transaction remove command") 318 } 319 320 // transaction add 321 addArgs1 := []string{"dns", 322 "record-sets", 323 fmt.Sprintf("--project=%s", parentProject), 324 "transaction", 325 "add", 326 } 327 addArgs2 := []string{fmt.Sprintf("--name=%s", googleRecordSet.Name), 328 fmt.Sprintf("--ttl=%d", ttl), 329 fmt.Sprintf("--type=%s", googleRecordSet.Type), 330 fmt.Sprintf("--zone=%s", parentZone), 331 "--format=json", 332 } 333 addArgs := append(addArgs1, googleRecordSet.Rrdatas...) 334 addArgs = append(addArgs, addArgs2...) 335 336 addCmd := util.Command{ 337 Name: "gcloud", 338 Args: addArgs, 339 } 340 341 _, err = addCmd.RunWithoutRetry() 342 if err != nil { 343 return errors.Wrap(err, "executing gcloud dns record-sets transaction add command") 344 } 345 346 // transaction execute 347 executeArgs := []string{"dns", 348 "record-sets", 349 fmt.Sprintf("--project=%s", parentProject), 350 "transaction", 351 "execute", 352 fmt.Sprintf("--zone=%s", parentZone), 353 "--format=json", 354 } 355 executeCmd := util.Command{ 356 Name: "gcloud", 357 Args: executeArgs, 358 } 359 360 _, err = executeCmd.RunWithoutRetry() 361 if err != nil { 362 return errors.Wrap(err, "executing gcloud dns record-sets transaction start command") 363 } 364 } 365 return nil 366 } 367 368 func updateManagedZoneRecordTTL(projectID string, domain string, managedZone string, dnsType string, ttl int) error { 369 var managedZoneRecord recordSet 370 managedZoneRecord.Name = addDomainSuffix(domain) 371 managedZoneRecord.Type = dnsType 372 managedZoneRecord.TTL = ttl 373 managedZoneRecordSet, err := getManagedZoneRecordSet(projectID, managedZone, managedZoneRecord) 374 if err != nil { 375 return errors.Wrap(err, fmt.Sprintf("when retrieving the '%s' record-set of type '%s'", domain, dnsType)) 376 } 377 378 err = applyManagedZoneRecordTTL(projectID, managedZone, managedZoneRecordSet, 60) 379 if err != nil { 380 return errors.Wrap(err, fmt.Sprintf("when updating the '%s' record-set of type '%s'", domain, dnsType)) 381 } 382 return nil 383 } 384 385 // ClusterZone retrives the zone of GKE cluster description 386 func (g *GCloud) ClusterZone(cluster string) (string, error) { 387 args := []string{"container", 388 "clusters", 389 "describe", 390 cluster} 391 392 cmd := util.Command{ 393 Name: "gcloud", 394 Args: args, 395 } 396 output, err := cmd.RunWithoutRetry() 397 if err != nil { 398 return "", err 399 } 400 401 zone, err := parseClusterZone(output) 402 if err != nil { 403 return "", err 404 } 405 return zone, nil 406 } 407 408 func parseClusterZone(clusterInfo string) (string, error) { 409 ci := struct { 410 Zone string `json:"zone"` 411 }{} 412 413 err := yaml.Unmarshal([]byte(clusterInfo), &ci) 414 if err != nil { 415 return "", errors.Wrap(err, "extracting cluster zone from cluster info") 416 } 417 return ci.Zone, nil 418 } 419 420 type nodeConfig struct { 421 OauthScopes []string `json:"oauthScopes"` 422 } 423 424 func parseScopes(clusterInfo string) ([]string, error) { 425 426 ci := struct { 427 NodeConfig nodeConfig `json:"nodeConfig"` 428 }{} 429 430 err := yaml.Unmarshal([]byte(clusterInfo), &ci) 431 if err != nil { 432 return nil, errors.Wrap(err, "extracting cluster oauthScopes from cluster info") 433 } 434 return ci.NodeConfig.OauthScopes, nil 435 } 436 437 // BucketExists checks if a Google Storage bucket exists 438 func (g *GCloud) BucketExists(projectID string, bucketName string) (bool, error) { 439 fullBucketName := fmt.Sprintf("gs://%s", bucketName) 440 args := []string{"ls"} 441 442 if projectID != "" { 443 args = append(args, "-p") 444 args = append(args, projectID) 445 } 446 447 cmd := util.Command{ 448 Name: "gsutil", 449 Args: args, 450 } 451 output, err := cmd.Run() 452 if err != nil { 453 log.Logger().Infof("Error checking bucket exists: %s, %s", output, err) 454 return false, err 455 } 456 return strings.Contains(output, fullBucketName), nil 457 } 458 459 // ListObjects checks if a Google Storage bucket exists 460 func (g *GCloud) ListObjects(bucketName string, path string) ([]string, error) { 461 fullBucketName := fmt.Sprintf("gs://%s/%s", bucketName, path) 462 args := []string{"ls", fullBucketName} 463 464 cmd := util.Command{ 465 Name: "gsutil", 466 Args: args, 467 } 468 output, err := cmd.RunWithoutRetry() 469 if err != nil { 470 log.Logger().Infof("Error checking bucket exists: %s, %s", output, err) 471 return []string{}, err 472 } 473 return strings.Split(output, "\n"), nil 474 } 475 476 // CreateBucket creates a new Google Storage bucket 477 func (g *GCloud) CreateBucket(projectID string, bucketName string, location string) error { 478 fullBucketName := fmt.Sprintf("gs://%s", bucketName) 479 args := []string{"mb", "-l", location} 480 481 if projectID != "" { 482 args = append(args, "-p") 483 args = append(args, projectID) 484 } 485 486 args = append(args, fullBucketName) 487 488 cmd := util.Command{ 489 Name: "gsutil", 490 Args: args, 491 } 492 output, err := cmd.RunWithoutRetry() 493 if err != nil { 494 log.Logger().Infof("Error creating bucket: %s, %s", output, err) 495 return err 496 } 497 return nil 498 } 499 500 //AddBucketLabel adds a label to a Google Storage bucket 501 func (g *GCloud) AddBucketLabel(bucketName string, label string) { 502 found := g.FindBucket(bucketName) 503 if found && label != "" { 504 fullBucketName := fmt.Sprintf("gs://%s", bucketName) 505 args := []string{"label", "ch", "-l", label} 506 507 args = append(args, fullBucketName) 508 509 cmd := util.Command{ 510 Name: "gsutil", 511 Args: args, 512 } 513 output, err := cmd.RunWithoutRetry() 514 if err != nil { 515 log.Logger().Infof("Error adding bucket label: %s, %s", output, err) 516 } 517 } 518 } 519 520 // FindBucket finds a Google Storage bucket 521 func (g *GCloud) FindBucket(bucketName string) bool { 522 fullBucketName := fmt.Sprintf("gs://%s", bucketName) 523 args := []string{"list", "-b", fullBucketName} 524 525 cmd := util.Command{ 526 Name: "gsutil", 527 Args: args, 528 } 529 _, err := cmd.RunWithoutRetry() 530 if err != nil { 531 return false 532 } 533 return true 534 } 535 536 // DeleteAllObjectsInBucket deletes all objects in a Google Storage bucket 537 func (g *GCloud) DeleteAllObjectsInBucket(bucketName string) error { 538 found := g.FindBucket(bucketName) 539 if !found { 540 return nil // nothing to delete 541 } 542 fullBucketName := fmt.Sprintf("gs://%s", bucketName) 543 args := []string{"-m", "rm", "-r", fullBucketName} 544 545 cmd := util.Command{ 546 Name: "gsutil", 547 Args: args, 548 } 549 _, err := cmd.RunWithoutRetry() 550 if err != nil { 551 return err 552 } 553 return nil 554 } 555 556 // StreamTransferFileFromBucket will perform a stream transfer from the GCS bucket to stdout and return a scanner 557 // with the piped result 558 func StreamTransferFileFromBucket(fullBucketURL string) (io.ReadCloser, error) { 559 bucketAccessible, err := isBucketAccessible(fullBucketURL) 560 if !bucketAccessible || err != nil { 561 return nil, errors.Wrap(err, "can't access bucket") 562 } 563 564 args := []string{"cp", fullBucketURL, "-"} 565 cmd := exec.Command("gsutil", args...) 566 stdout, err := cmd.StdoutPipe() 567 if err != nil { 568 return nil, errors.Wrap(err, "can't get cmd stdout") 569 } 570 err = cmd.Start() 571 if err != nil { 572 return nil, errors.Wrap(err, "error streaming the logs from bucket") 573 } 574 return stdout, nil 575 } 576 577 func isBucketAccessible(bucketURL string) (bool, error) { 578 args := []string{"stat", bucketURL} 579 cmd := exec.Command("gsutil", args...) 580 581 out, err := cmd.CombinedOutput() 582 if err != nil { 583 return false, errors.New(string(out)) 584 } 585 586 return true, nil 587 } 588 589 // DeleteBucket deletes a Google storage bucket 590 func (g *GCloud) DeleteBucket(bucketName string) error { 591 found := g.FindBucket(bucketName) 592 if !found { 593 return nil // nothing to delete 594 } 595 fullBucketName := fmt.Sprintf("gs://%s", bucketName) 596 args := []string{"rb", fullBucketName} 597 598 cmd := util.Command{ 599 Name: "gsutil", 600 Args: args, 601 } 602 _, err := cmd.RunWithoutRetry() 603 if err != nil { 604 return err 605 } 606 return nil 607 } 608 609 // GetRegionFromZone parses the region from a GCP zone name. TODO: Return an error if the format of the zone is not correct 610 func GetRegionFromZone(zone string) string { 611 firstDash := strings.Index(zone, "-") 612 lastDash := strings.LastIndex(zone, "-") 613 if firstDash == lastDash { // It's a region, not a zone 614 return zone 615 } 616 return zone[0:lastDash] 617 } 618 619 // FindServiceAccount checks if a service account exists 620 func (g *GCloud) FindServiceAccount(serviceAccount string, projectID string) bool { 621 args := []string{"iam", 622 "service-accounts", 623 "list", 624 "--filter", 625 serviceAccount, 626 "--project", 627 projectID} 628 629 cmd := util.Command{ 630 Name: "gcloud", 631 Args: args, 632 } 633 output, err := cmd.Run() 634 if err != nil { 635 return false 636 } 637 638 if output == "Listed 0 items." { 639 return false 640 } 641 return true 642 } 643 644 // GetOrCreateServiceAccount retrieves or creates a GCP service account. It will return the path to the file where the service 645 // account token is stored 646 func (g *GCloud) GetOrCreateServiceAccount(serviceAccount string, projectID string, clusterConfigDir string, roles []string) (string, error) { 647 if projectID == "" { 648 return "", errors.New("cannot get/create a service account without a projectId") 649 } 650 651 found := g.FindServiceAccount(serviceAccount, projectID) 652 if !found { 653 log.Logger().Infof("Unable to find service account %s, checking if we have enough permission to create", util.ColorInfo(serviceAccount)) 654 655 // if it doesn't check to see if we have permissions to create (assign roles) to a service account 656 hasPerm, err := g.CheckPermission("resourcemanager.projects.setIamPolicy", projectID) 657 if err != nil { 658 return "", err 659 } 660 661 if !hasPerm { 662 return "", errors.New("User does not have the required role 'resourcemanager.projects.setIamPolicy' to configure a service account") 663 } 664 665 // create service 666 log.Logger().Infof("Creating service account %s", util.ColorInfo(serviceAccount)) 667 args := []string{"iam", 668 "service-accounts", 669 "create", 670 serviceAccount, 671 "--project", 672 projectID, 673 "--display-name", 674 serviceAccount} 675 676 cmd := util.Command{ 677 Name: "gcloud", 678 Args: args, 679 } 680 _, err = cmd.RunWithoutRetry() 681 if err != nil { 682 return "", err 683 } 684 685 // assign roles to service account 686 for _, role := range roles { 687 log.Logger().Infof("Assigning role %s", role) 688 args = []string{"projects", 689 "add-iam-policy-binding", 690 projectID, 691 "--member", 692 fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", serviceAccount, projectID), 693 "--role", 694 role, 695 "--project", 696 projectID} 697 698 cmd := util.Command{ 699 Name: "gcloud", 700 Args: args, 701 } 702 _, err := cmd.Run() 703 if err != nil { 704 return "", err 705 } 706 } 707 708 } else { 709 log.Logger().Info("Service Account exists") 710 } 711 712 err := os.MkdirAll(clusterConfigDir, os.ModePerm) 713 if err != nil { 714 return "", errors.Wrapf(err, "Failed to create directory: %s", clusterConfigDir) 715 } 716 keyPath := filepath.Join(clusterConfigDir, fmt.Sprintf("%s.key.json", serviceAccount)) 717 718 if _, err := os.Stat(keyPath); os.IsNotExist(err) { 719 log.Logger().Info("Downloading service account key") 720 err := g.CreateServiceAccountKey(serviceAccount, projectID, keyPath) 721 if err != nil { 722 log.Logger().Infof("Exceeds the maximum number of keys on service account %s", 723 util.ColorInfo(serviceAccount)) 724 err := g.CleanupServiceAccountKeys(serviceAccount, projectID) 725 if err != nil { 726 return "", errors.Wrap(err, "cleaning up the service account keys") 727 } 728 err = g.CreateServiceAccountKey(serviceAccount, projectID, keyPath) 729 if err != nil { 730 return "", errors.Wrap(err, "creating service account key") 731 } 732 } 733 } else { 734 log.Logger().Info("Key already exists") 735 } 736 737 return keyPath, nil 738 } 739 740 // ConfigureBucketRoles gives the given roles to the given service account 741 func (g *GCloud) ConfigureBucketRoles(projectID string, serviceAccount string, bucketURL string, roles []string) error { 742 member := fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", serviceAccount, projectID) 743 744 bindings := bucketMemberRoles{} 745 for _, role := range roles { 746 bindings.Bindings = append(bindings.Bindings, memberRole{ 747 Members: []string{member}, 748 Role: role, 749 }) 750 } 751 file, err := ioutil.TempFile("", "gcp-iam-roles-") 752 if err != nil { 753 return errors.Wrapf(err, "failed to create temp file") 754 } 755 fileName := file.Name() 756 757 data, err := json.Marshal(&bindings) 758 if err != nil { 759 return errors.Wrapf(err, "failed to convert bindings %#v to JSON", bindings) 760 } 761 log.Logger().Infof("created json %s", string(data)) 762 err = ioutil.WriteFile(fileName, data, util.DefaultWritePermissions) 763 if err != nil { 764 return errors.Wrapf(err, "failed to save bindings %#v to JSON file %s", bindings, fileName) 765 } 766 log.Logger().Infof("generated IAM bindings file %s", fileName) 767 args := []string{ 768 "-m", 769 "iam", 770 "set", 771 "-a", 772 fileName, 773 bucketURL, 774 } 775 cmd := util.Command{ 776 Name: "gsutil", 777 Args: args, 778 } 779 log.Logger().Infof("running: gsutil %s", strings.Join(args, " ")) 780 _, err = cmd.Run() 781 if err != nil { 782 return err 783 } 784 return nil 785 } 786 787 type bucketMemberRoles struct { 788 Bindings []memberRole `json:"bindings"` 789 } 790 791 type memberRole struct { 792 Members []string `json:"members"` 793 Role string `json:"role"` 794 } 795 796 // CreateServiceAccountKey creates a new service account key and downloads into the given file 797 func (g *GCloud) CreateServiceAccountKey(serviceAccount string, projectID string, keyPath string) error { 798 args := []string{"iam", 799 "service-accounts", 800 "keys", 801 "create", 802 keyPath, 803 "--iam-account", 804 fmt.Sprintf("%s@%s.iam.gserviceaccount.com", serviceAccount, projectID), 805 "--project", 806 projectID} 807 808 cmd := util.Command{ 809 Name: "gcloud", 810 Args: args, 811 } 812 _, err := cmd.RunWithoutRetry() 813 if err != nil { 814 return errors.Wrap(err, "creating a new service account key") 815 } 816 return nil 817 } 818 819 // GetServiceAccountKeys returns all keys of a service account 820 func (g *GCloud) GetServiceAccountKeys(serviceAccount string, projectID string) ([]string, error) { 821 keys := []string{} 822 account := fmt.Sprintf("%s@%s.iam.gserviceaccount.com", serviceAccount, projectID) 823 args := []string{"iam", 824 "service-accounts", 825 "keys", 826 "list", 827 "--iam-account", 828 account, 829 "--project", 830 projectID} 831 cmd := util.Command{ 832 Name: "gcloud", 833 Args: args, 834 } 835 output, err := cmd.RunWithoutRetry() 836 if err != nil { 837 return keys, errors.Wrapf(err, "listing the keys of the service account '%s'", account) 838 } 839 840 scanner := bufio.NewScanner(strings.NewReader(output)) 841 // Skip the first line with the header information 842 scanner.Scan() 843 for scanner.Scan() { 844 keyFields := strings.Fields(scanner.Text()) 845 if len(keyFields) > 0 { 846 keys = append(keys, keyFields[0]) 847 } 848 } 849 return keys, nil 850 } 851 852 // ListClusters returns the clusters in a GKE project 853 func (g *GCloud) ListClusters(region string, projectID string) ([]Cluster, error) { 854 args := []string{"container", "clusters", "list", "--region=" + region, "--format=json", "--quiet"} 855 if projectID != "" { 856 args = append(args, "--project="+projectID) 857 } 858 cmd := util.Command{ 859 Name: "gcloud", 860 Args: args, 861 } 862 output, err := cmd.RunWithoutRetry() 863 if err != nil { 864 return nil, err 865 } 866 867 clusters := make([]Cluster, 0) 868 err = json.Unmarshal([]byte(output), &clusters) 869 if err != nil { 870 return nil, err 871 } 872 return clusters, nil 873 } 874 875 // LoadGkeCluster load a gke cluster from a GKE project 876 func (g *GCloud) LoadGkeCluster(region string, projectID string, clusterName string) (*Cluster, error) { 877 args := []string{"container", "clusters", "describe", clusterName, "--region=" + region, "--format=json", "--quiet"} 878 if projectID != "" { 879 args = append(args, "--project="+projectID) 880 } 881 cmd := util.Command{ 882 Name: "gcloud", 883 Args: args, 884 } 885 output, err := cmd.RunWithoutRetry() 886 if err != nil { 887 return nil, err 888 } 889 890 cluster := &Cluster{} 891 err = json.Unmarshal([]byte(output), cluster) 892 if err != nil { 893 return nil, err 894 } 895 return cluster, nil 896 } 897 898 // UpdateGkeClusterLabels updates labesl for a gke cluster 899 func (g *GCloud) UpdateGkeClusterLabels(region string, projectID string, clusterName string, labels []string) error { 900 args := []string{"container", "clusters", "update", clusterName, "--quiet", "--update-labels=" + strings.Join(labels, ",") + ""} 901 if region != "" { 902 args = append(args, "--region="+region) 903 } 904 if projectID != "" { 905 args = append(args, "--project="+projectID) 906 } 907 cmd := util.Command{ 908 Name: "gcloud", 909 Args: args, 910 } 911 _, err := cmd.RunWithoutRetry() 912 return err 913 } 914 915 // DeleteServiceAccountKey deletes a service account key 916 func (g *GCloud) DeleteServiceAccountKey(serviceAccount string, projectID string, key string) error { 917 account := fmt.Sprintf("%s@%s.iam.gserviceaccount.com", serviceAccount, projectID) 918 args := []string{"iam", 919 "service-accounts", 920 "keys", 921 "delete", 922 key, 923 "--iam-account", 924 account, 925 "--project", 926 projectID, 927 "--quiet"} 928 cmd := util.Command{ 929 Name: "gcloud", 930 Args: args, 931 } 932 _, err := cmd.RunWithoutRetry() 933 if err != nil { 934 return errors.Wrapf(err, "deleting the key '%s'from service account '%s'", key, account) 935 } 936 return nil 937 } 938 939 // CleanupServiceAccountKeys remove all keys from given service account 940 func (g *GCloud) CleanupServiceAccountKeys(serviceAccount string, projectID string) error { 941 keys, err := g.GetServiceAccountKeys(serviceAccount, projectID) 942 if err != nil { 943 return errors.Wrap(err, "retrieving the service account keys") 944 } 945 946 log.Logger().Infof("Cleaning up the keys of the service account %s", util.ColorInfo(serviceAccount)) 947 948 for _, key := range keys { 949 err := g.DeleteServiceAccountKey(serviceAccount, projectID, key) 950 if err != nil { 951 log.Logger().Infof("Cannot delete the key %s from service account %s: %v", 952 util.ColorWarning(key), util.ColorInfo(serviceAccount), err) 953 } else { 954 log.Logger().Infof("Key %s was removed form service account %s", 955 util.ColorInfo(key), util.ColorInfo(serviceAccount)) 956 } 957 } 958 return nil 959 } 960 961 // DeleteServiceAccount deletes a service account and its role bindings 962 func (g *GCloud) DeleteServiceAccount(serviceAccount string, projectID string, roles []string) error { 963 found := g.FindServiceAccount(serviceAccount, projectID) 964 if !found { 965 return nil // nothing to delete 966 } 967 // remove roles to service account 968 for _, role := range roles { 969 log.Logger().Infof("Removing role %s", role) 970 args := []string{"projects", 971 "remove-iam-policy-binding", 972 projectID, 973 "--member", 974 fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", serviceAccount, projectID), 975 "--role", 976 role, 977 "--project", 978 projectID} 979 980 cmd := util.Command{ 981 Name: "gcloud", 982 Args: args, 983 } 984 _, err := cmd.RunWithoutRetry() 985 if err != nil { 986 return err 987 } 988 } 989 args := []string{"iam", 990 "service-accounts", 991 "delete", 992 fmt.Sprintf("%s@%s.iam.gserviceaccount.com", serviceAccount, projectID), 993 "--project", 994 projectID} 995 996 cmd := util.Command{ 997 Name: "gcloud", 998 Args: args, 999 } 1000 _, err := cmd.RunWithoutRetry() 1001 if err != nil { 1002 return err 1003 } 1004 return nil 1005 } 1006 1007 // GetEnabledApis returns which services have the API enabled 1008 func (g *GCloud) GetEnabledApis(projectID string) ([]string, error) { 1009 args := []string{"services", "list", "--enabled"} 1010 1011 if projectID != "" { 1012 args = append(args, "--project") 1013 args = append(args, projectID) 1014 } 1015 1016 apis := []string{} 1017 1018 cmd := util.Command{ 1019 Name: "gcloud", 1020 Args: args, 1021 } 1022 1023 out, err := cmd.Run() 1024 if err != nil { 1025 return nil, err 1026 } 1027 1028 lines := strings.Split(out, "\n") 1029 for _, l := range lines { 1030 if strings.Contains(l, "NAME") { 1031 continue 1032 } 1033 fields := strings.Fields(l) 1034 apis = append(apis, fields[0]) 1035 } 1036 1037 return apis, nil 1038 } 1039 1040 // EnableAPIs enables APIs for the given services 1041 func (g *GCloud) EnableAPIs(projectID string, apis ...string) error { 1042 enabledApis, err := g.GetEnabledApis(projectID) 1043 if err != nil { 1044 return err 1045 } 1046 1047 toEnableArray := []string{} 1048 1049 for _, toEnable := range apis { 1050 fullName := fmt.Sprintf("%s.googleapis.com", toEnable) 1051 if !util.Contains(enabledApis, fullName) { 1052 toEnableArray = append(toEnableArray, fullName) 1053 } 1054 } 1055 1056 if len(toEnableArray) == 0 { 1057 log.Logger().Debugf("No apis need to be enable as they are already enabled: %s", util.ColorInfo(strings.Join(apis, " "))) 1058 return nil 1059 } 1060 1061 args := []string{"services", "enable"} 1062 args = append(args, toEnableArray...) 1063 1064 if projectID != "" { 1065 args = append(args, "--project") 1066 args = append(args, projectID) 1067 } 1068 1069 log.Logger().Debugf("Lets ensure we have %s enabled on your project via: %s", toEnableArray, util.ColorInfo("gcloud "+strings.Join(args, " "))) 1070 1071 cmd := util.Command{ 1072 Name: "gcloud", 1073 Args: args, 1074 } 1075 _, err = cmd.RunWithoutRetry() 1076 if err != nil { 1077 return err 1078 } 1079 return nil 1080 } 1081 1082 // Login login an user into Google account. It skips the interactive login using the 1083 // browser when the skipLogin flag is active 1084 func (g *GCloud) Login(serviceAccountKeyPath string, skipLogin bool) error { 1085 if serviceAccountKeyPath != "" { 1086 log.Logger().Infof("Activating service account %s", util.ColorInfo(serviceAccountKeyPath)) 1087 1088 if _, err := os.Stat(serviceAccountKeyPath); os.IsNotExist(err) { 1089 return errors.New("Unable to locate service account " + serviceAccountKeyPath) 1090 } 1091 1092 cmd := util.Command{ 1093 Name: "gcloud", 1094 Args: []string{"auth", "activate-service-account", "--key-file", serviceAccountKeyPath}, 1095 } 1096 _, err := cmd.RunWithoutRetry() 1097 if err != nil { 1098 return err 1099 } 1100 1101 // GCP IAM changes can take up to 80 seconds to propagate 1102 err = retry(10, 10*time.Second, func() error { 1103 log.Logger().Infof("Checking for readiness...") 1104 1105 projects, err := GetGoogleProjects() 1106 if err != nil { 1107 return err 1108 } 1109 1110 if len(projects) == 0 { 1111 return errors.New("service account not ready yet") 1112 } 1113 1114 return nil 1115 }) 1116 if err != nil { 1117 return err 1118 } 1119 1120 } else if !skipLogin { 1121 cmd := util.Command{ 1122 Name: "gcloud", 1123 Args: []string{"auth", "login", "--brief"}, 1124 } 1125 _, err := cmd.RunWithoutRetry() 1126 if err != nil { 1127 return err 1128 } 1129 } 1130 return nil 1131 } 1132 1133 func retry(attempts int, sleep time.Duration, fn func() error) error { 1134 if err := fn(); err != nil { 1135 if s, ok := err.(stop); ok { 1136 // Return the original error for later checking 1137 return s.error 1138 } 1139 1140 if attempts--; attempts > 0 { 1141 time.Sleep(sleep) 1142 return retry(attempts, 2*sleep, fn) 1143 } 1144 return err 1145 } 1146 return nil 1147 } 1148 1149 type stop struct { 1150 error 1151 } 1152 1153 // CheckPermission checks permission on the given project 1154 func (g *GCloud) CheckPermission(perm string, projectID string) (bool, error) { 1155 if projectID == "" { 1156 return false, errors.New("cannot check permission without a projectId") 1157 } 1158 // if it doesn't check to see if we have permissions to create (assign roles) to a service account 1159 args := []string{"iam", 1160 "list-testable-permissions", 1161 fmt.Sprintf("//cloudresourcemanager.googleapis.com/projects/%s", projectID), 1162 "--filter", 1163 perm} 1164 1165 cmd := util.Command{ 1166 Name: "gcloud", 1167 Args: args, 1168 } 1169 output, err := cmd.RunWithoutRetry() 1170 if err != nil { 1171 return false, err 1172 } 1173 1174 return strings.Contains(output, perm), nil 1175 } 1176 1177 // CreateKmsKeyring creates a new KMS keyring 1178 func (g *GCloud) CreateKmsKeyring(keyringName string, projectID string) error { 1179 if keyringName == "" { 1180 return errors.New("provided keyring name is empty") 1181 } 1182 1183 if g.IsKmsKeyringAvailable(keyringName, projectID) { 1184 log.Logger().Debugf("keyring '%s' already exists", keyringName) 1185 return nil 1186 } 1187 1188 args := []string{"kms", 1189 "keyrings", 1190 "create", 1191 keyringName, 1192 "--location", 1193 KmsLocation, 1194 "--project", 1195 projectID, 1196 } 1197 1198 log.Logger().Debugf("creating keyring '%s' project=%s, location=%s", keyringName, projectID, KmsLocation) 1199 1200 cmd := util.Command{ 1201 Name: "gcloud", 1202 Args: args, 1203 } 1204 _, err := cmd.RunWithoutRetry() 1205 if err != nil { 1206 return errors.Wrap(err, "creating kms keyring") 1207 } 1208 return nil 1209 } 1210 1211 // IsKmsKeyringAvailable checks if the KMS keyring is already available 1212 func (g *GCloud) IsKmsKeyringAvailable(keyringName string, projectID string) bool { 1213 log.Logger().Debugf("IsKmsKeyringAvailable keyring=%s, projectId=%s, location=%s", keyringName, projectID, KmsLocation) 1214 args := []string{"kms", 1215 "keyrings", 1216 "describe", 1217 keyringName, 1218 "--location", 1219 KmsLocation, 1220 "--project", 1221 projectID, 1222 } 1223 1224 cmd := util.Command{ 1225 Name: "gcloud", 1226 Args: args, 1227 } 1228 _, err := cmd.RunWithoutRetry() 1229 if err != nil { 1230 return false 1231 } 1232 return true 1233 } 1234 1235 // CreateKmsKey creates a new KMS key in the given keyring 1236 func (g *GCloud) CreateKmsKey(keyName string, keyringName string, projectID string) error { 1237 if g.IsKmsKeyAvailable(keyName, keyringName, projectID) { 1238 log.Logger().Debugf("key '%s' already exists", keyName) 1239 return nil 1240 } 1241 1242 log.Logger().Debugf("creating key '%s' keyring=%s, project=%s, location=%s", keyName, keyringName, projectID, KmsLocation) 1243 1244 args := []string{"kms", 1245 "keys", 1246 "create", 1247 keyName, 1248 "--location", 1249 KmsLocation, 1250 "--keyring", 1251 keyringName, 1252 "--purpose", 1253 "encryption", 1254 "--project", 1255 projectID, 1256 } 1257 cmd := util.Command{ 1258 Name: "gcloud", 1259 Args: args, 1260 } 1261 _, err := cmd.RunWithoutRetry() 1262 if err != nil { 1263 return errors.Wrapf(err, "creating kms key '%s' into keyring '%s'", keyName, keyringName) 1264 } 1265 return nil 1266 } 1267 1268 // IsKmsKeyAvailable checks if the KMS key is already available 1269 func (g *GCloud) IsKmsKeyAvailable(keyName string, keyringName string, projectID string) bool { 1270 log.Logger().Debugf("IsKmsKeyAvailable keyName=%s, keyring=%s, projectId=%s, location=%s", keyName, keyringName, projectID, KmsLocation) 1271 1272 args := []string{"kms", 1273 "keys", 1274 "describe", 1275 keyName, 1276 "--location", 1277 KmsLocation, 1278 "--keyring", 1279 keyringName, 1280 "--project", 1281 projectID, 1282 } 1283 1284 cmd := util.Command{ 1285 Name: "gcloud", 1286 Args: args, 1287 } 1288 _, err := cmd.RunWithoutRetry() 1289 if err != nil { 1290 return false 1291 } 1292 return true 1293 } 1294 1295 // IsGCSWriteRoleEnabled will check if the devstorage.full_control scope is enabled in the cluster in order to use GCS 1296 func (g *GCloud) IsGCSWriteRoleEnabled(cluster string, zone string) (bool, error) { 1297 args := []string{"container", 1298 "clusters", 1299 "describe", 1300 cluster, 1301 "--zone", 1302 zone} 1303 1304 cmd := util.Command{ 1305 Name: "gcloud", 1306 Args: args, 1307 } 1308 output, err := cmd.RunWithoutRetry() 1309 if err != nil { 1310 return false, err 1311 } 1312 1313 oauthScopes, err := parseScopes(output) 1314 if err != nil { 1315 return false, err 1316 } 1317 1318 for _, s := range oauthScopes { 1319 if strings.Contains(s, "devstorage.full_control") { 1320 return true, nil 1321 } 1322 } 1323 return false, nil 1324 } 1325 1326 // ConnectToCluster connects to the specified cluster 1327 func (g *GCloud) ConnectToCluster(projectID, zone, clusterName string) error { 1328 args := []string{"container", 1329 "clusters", 1330 "get-credentials", 1331 clusterName, 1332 "--zone", 1333 zone, 1334 "--project", projectID} 1335 1336 cmd := util.Command{ 1337 Name: "gcloud", 1338 Args: args, 1339 } 1340 _, err := cmd.RunWithoutRetry() 1341 if err != nil { 1342 return errors.Wrapf(err, "failed to connect to cluster %s", clusterName) 1343 } 1344 return nil 1345 } 1346 1347 // ConnectToRegionCluster connects to the specified regional cluster 1348 func (g *GCloud) ConnectToRegionCluster(projectID, region, clusterName string) error { 1349 args := []string{"container", 1350 "clusters", 1351 "get-credentials", 1352 clusterName, 1353 "--region", 1354 region, 1355 "--project", projectID} 1356 1357 cmd := util.Command{ 1358 Name: "gcloud", 1359 Args: args, 1360 } 1361 _, err := cmd.RunWithoutRetry() 1362 if err != nil { 1363 return errors.Wrapf(err, "failed to connect to region cluster %s", clusterName) 1364 } 1365 return nil 1366 } 1367 1368 // UserLabel returns a string identifying current user that can be used as a label 1369 func (g *GCloud) UserLabel() string { 1370 user, err := osUser.Current() 1371 if err == nil && user != nil && user.Username != "" { 1372 userLabel := util.SanitizeLabel(user.Username) 1373 return fmt.Sprintf("created-by:%s", userLabel) 1374 } 1375 return "" 1376 } 1377 1378 // CreateGCPServiceAccount creates a service account in GCP for a service using the account roles specified 1379 func (g *GCloud) CreateGCPServiceAccount(kubeClient kubernetes.Interface, serviceName, serviceAbbreviation, namespace, clusterName, projectID string, serviceAccountRoles []string, serviceAccountSecretKey string) (string, error) { 1380 serviceAccountDir, err := ioutil.TempDir("", "gke") 1381 if err != nil { 1382 return "", errors.Wrap(err, "creating a temporary folder where the service account will be stored") 1383 } 1384 defer os.RemoveAll(serviceAccountDir) 1385 1386 serviceAccountName := naming.ToValidGCPServiceAccount(ServiceAccountName(clusterName, serviceAbbreviation)) 1387 1388 serviceAccountPath, err := g.GetOrCreateServiceAccount(serviceAccountName, projectID, serviceAccountDir, serviceAccountRoles) 1389 if err != nil { 1390 return "", errors.Wrap(err, "creating the service account") 1391 } 1392 1393 secretName, err := g.storeGCPServiceAccountIntoSecret(kubeClient, serviceAccountPath, serviceName, namespace, serviceAccountSecretKey) 1394 if err != nil { 1395 return "", errors.Wrap(err, "storing the service account into a secret") 1396 } 1397 return secretName, nil 1398 } 1399 1400 func (g *GCloud) storeGCPServiceAccountIntoSecret(client kubernetes.Interface, serviceAccountPath, serviceName, namespace string, serviceAccountSecretKey string) (string, error) { 1401 serviceAccount, err := ioutil.ReadFile(serviceAccountPath) 1402 if err != nil { 1403 return "", errors.Wrapf(err, "reading the service account from file '%s'", serviceAccountPath) 1404 } 1405 1406 secretName := GcpServiceAccountSecretName(serviceName) 1407 secret := &v1.Secret{ 1408 ObjectMeta: metav1.ObjectMeta{ 1409 Name: secretName, 1410 }, 1411 Data: map[string][]byte{ 1412 serviceAccountSecretKey: serviceAccount, 1413 }, 1414 } 1415 1416 secrets := client.CoreV1().Secrets(namespace) 1417 _, err = secrets.Get(secretName, metav1.GetOptions{}) 1418 if err != nil { 1419 _, err = secrets.Create(secret) 1420 } else { 1421 _, err = secrets.Update(secret) 1422 } 1423 return secretName, nil 1424 } 1425 1426 // CurrentProject returns the current GKE project name if it can be detected 1427 func (g *GCloud) CurrentProject() (string, error) { 1428 args := []string{"config", 1429 "list", 1430 "--format", 1431 "value(core.project)", 1432 } 1433 1434 cmd := util.Command{ 1435 Name: "gcloud", 1436 Args: args, 1437 } 1438 text, err := cmd.RunWithoutRetry() 1439 if err != nil { 1440 return text, errors.Wrap(err, "failed to detect the current GCP project") 1441 } 1442 return strings.TrimSpace(text), nil 1443 } 1444 1445 func (g *GCloud) GetProjectNumber(projectID string) (string, error) { 1446 args := []string{ 1447 "projects", 1448 "describe", 1449 projectID, 1450 "--format=json", 1451 } 1452 cmd := util.Command{ 1453 Name: "gcloud", 1454 Args: args, 1455 } 1456 1457 log.Logger().Infof("running: gcloud %s", strings.Join(args, " ")) 1458 output, err := cmd.Run() 1459 if err != nil { 1460 return "", err 1461 } 1462 1463 var project project 1464 err = json.Unmarshal([]byte(output), &project) 1465 if err != nil { 1466 return "", errors.Wrapf(err, "failed to unmarshal %s", output) 1467 } 1468 return project.ProjectNumber, nil 1469 } 1470 1471 type project struct { 1472 ProjectNumber string `json:"projectNumber"` 1473 } 1474 1475 func addDomainSuffix(domain string) string { 1476 if domain[len(domain)-1:] != "." { 1477 return fmt.Sprintf("%s.", domain) 1478 } 1479 return domain 1480 }