cmd/cloudshell_open/deploy.go (209 lines of code) (raw):

package main import ( "context" "fmt" "math/rand" "strconv" "strings" "time" "google.golang.org/api/googleapi" runapi "google.golang.org/api/run/v1" ) // parseEnv parses K=V pairs into a map. func parseEnv(envs []string) map[string]string { out := make(map[string]string) for _, v := range envs { p := strings.SplitN(v, "=", 2) out[p[0]] = p[1] } return out } // deploy reimplements the "gcloud run deploy" command, including setting IAM policy and // waiting for Service to be Ready. func deploy(project, name, image, region string, envs []string, options options) (string, error) { envVars := parseEnv(envs) client, err := runClient(region) if err != nil { return "", fmt.Errorf("failed to initialize Run API client: %w", err) } svc, err := getService(project, name, region) if err == nil { // existing service svc = patchService(svc, envVars, image, options) _, err = client.Namespaces.Services.ReplaceService("namespaces/"+project+"/services/"+name, svc).Do() if err != nil { if e, ok := err.(*googleapi.Error); ok { return "", fmt.Errorf("failed to deploy existing Service: code=%d message=%s -- %s", e.Code, e.Message, e.Body) } return "", fmt.Errorf("failed to deploy to existing Service: %w", err) } } else { // new service svc := newService(name, project, image, envVars, options) _, err = client.Namespaces.Services.Create("namespaces/"+project, svc).Do() if err != nil { if e, ok := err.(*googleapi.Error); ok { return "", fmt.Errorf("failed to deploy a new Service: code=%d message=%s -- %s", e.Code, e.Message, e.Body) } return "", fmt.Errorf("failed to deploy a new Service: %w", err) } } if options.AllowUnauthenticated == nil || *options.AllowUnauthenticated { if err := allowUnauthenticated(project, name, region); err != nil { return "", fmt.Errorf("failed to allow unauthenticated requests on the service: %w", err) } } if err := waitReady(project, name, region); err != nil { return "", err } out, err := getService(project, name, region) if err != nil { return "", fmt.Errorf("failed to get service after deploying: %w", err) } return out.Status.Url, nil } func optionsToResourceRequirements(options options) *runapi.ResourceRequirements { limits := make(map[string]string) if options.Memory != "" { limits["memory"] = options.Memory } if options.CPU != "" { limits["cpu"] = options.CPU } return &runapi.ResourceRequirements{Limits: limits} } func optionsToContainerSpec(options options) *runapi.ContainerPort { var containerPortName = "http1" if options.HTTP2 != nil && *options.HTTP2 { containerPortName = "h2c" } var containerPort = 8080 if options.Port > 0 { containerPort = options.Port } return &runapi.ContainerPort{ContainerPort: int64(containerPort), Name: containerPortName} } // newService initializes a new Knative Service object with given properties. func newService(name, project, image string, envs map[string]string, options options) *runapi.Service { var envVars []*runapi.EnvVar for k, v := range envs { envVars = append(envVars, &runapi.EnvVar{Name: k, Value: v}) } svc := &runapi.Service{ ApiVersion: "serving.knative.dev/v1", Kind: "Service", Metadata: &runapi.ObjectMeta{ Annotations: make(map[string]string), Name: name, Namespace: project, }, Spec: &runapi.ServiceSpec{ Template: &runapi.RevisionTemplate{ Metadata: &runapi.ObjectMeta{ Name: generateRevisionName(name, 0), Annotations: make(map[string]string), }, Spec: &runapi.RevisionSpec{ ContainerConcurrency: int64(options.Concurrency), Containers: []*runapi.Container{ { Image: image, Env: envVars, Resources: optionsToResourceRequirements(options), Ports: []*runapi.ContainerPort{optionsToContainerSpec(options)}, }, }, }, ForceSendFields: nil, NullFields: nil, }, }, } applyMeta(svc.Metadata, image) applyMeta(svc.Spec.Template.Metadata, image) applyScaleMeta(svc.Spec.Template.Metadata, "maxScale", options.MaxInstances) return svc } // applyMeta applies optional annotations to the specified Metadata.Annotation fields func applyMeta(meta *runapi.ObjectMeta, userImage string) { if meta.Annotations == nil { meta.Annotations = make(map[string]string) } meta.Annotations["client.knative.dev/user-image"] = userImage meta.Annotations["run.googleapis.com/client-name"] = "cloud-run-button" } // applyScaleMeta optional annotations for scale commands func applyScaleMeta(meta *runapi.ObjectMeta, scaleType string, scaleValue int) { if scaleValue > 0 { meta.Annotations["autoscaling.knative.dev"+scaleType] = strconv.Itoa(scaleValue) } } // generateRevisionName attempts to generate a random revision name that is alphabetically increasing but also has // a random suffix. objectGeneration is the current object generation. func generateRevisionName(name string, objectGeneration int64) string { num := fmt.Sprintf("%05d", objectGeneration+1) out := name + "-" + num + "-" r := rand.New(rand.NewSource(time.Now().UnixNano())) for i := 0; i < 3; i++ { out += string(rune(int('a') + r.Intn(26))) } return out } // patchService modifies an existing Service with requested changes. func patchService(svc *runapi.Service, envs map[string]string, image string, options options) *runapi.Service { // merge env vars svc.Spec.Template.Spec.Containers[0].Env = mergeEnvs(svc.Spec.Template.Spec.Containers[0].Env, envs) // update container image svc.Spec.Template.Spec.Containers[0].Image = image // update container port svc.Spec.Template.Spec.Containers[0].Ports[0] = optionsToContainerSpec(options) // apply metadata annotations applyMeta(svc.Metadata, image) applyMeta(svc.Spec.Template.Metadata, image) // apply scale metadata annotations applyScaleMeta(svc.Spec.Template.Metadata, "maxScale", options.MaxInstances) // update revision name svc.Spec.Template.Metadata.Name = generateRevisionName(svc.Metadata.Name, svc.Metadata.Generation) return svc } // mergeEnvs updates variables in existing, and adds missing ones. func mergeEnvs(existing []*runapi.EnvVar, env map[string]string) []*runapi.EnvVar { for i, ee := range existing { if v, ok := env[ee.Name]; ok { existing[i].Value = v delete(env, ee.Name) } } // add missing ones for k, v := range env { existing = append(existing, &runapi.EnvVar{Name: k, Value: v}) } return existing } // waitReady waits until the specified service reaches Ready status func waitReady(project, name, region string) error { wait := time.Minute * 4 deadline := time.Now().Add(wait) for time.Now().Before(deadline) { svc, err := getService(project, name, region) if err != nil { return fmt.Errorf("failed to query Service for readiness: %w", err) } for _, cond := range svc.Status.Conditions { if cond.Type == "Ready" { if cond.Status == "True" { return nil } else if cond.Status == "False" { return fmt.Errorf("reason=%s message=%s", cond.Reason, cond.Message) } } } time.Sleep(time.Second * 2) } return fmt.Errorf("the service did not become ready in %s, check Cloud Console for logs to see why it failed", wait) } // allowUnauthenticated sets IAM policy on the specified Cloud Run service to give allUsers subject // roles/run.invoker role. func allowUnauthenticated(project, name, region string) error { client, err := runapi.NewService(context.TODO()) if err != nil { return fmt.Errorf("failed to initialize Run API client: %w", err) } res := fmt.Sprintf("projects/%s/locations/%s/services/%s", project, region, name) policy, err := client.Projects.Locations.Services.GetIamPolicy(res).Do() if err != nil { return fmt.Errorf("failed to get IAM policy for Cloud Run Service: %w", err) } policy.Bindings = append(policy.Bindings, &runapi.Binding{ Members: []string{"allUsers"}, Role: "roles/run.invoker", }) _, err = client.Projects.Locations.Services.SetIamPolicy(res, &runapi.SetIamPolicyRequest{Policy: policy}).Do() if err != nil { var extra string e, ok := err.(*googleapi.Error) if ok { extra = fmt.Sprintf("code=%d, message=%s -- %s", e.Code, e.Message, e.Body) } return fmt.Errorf("failed to set IAM policy for Cloud Run Service: %w %s", err, extra) } return nil }