cmd/cloudshell_open/cloudrun.go (104 lines of code) (raw):
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"context"
"fmt"
"regexp"
"sort"
"strings"
"unicode"
"github.com/AlecAivazis/survey/v2"
"google.golang.org/api/option"
runapi "google.golang.org/api/run/v1"
)
const (
defaultRunRegion = "us-central1"
defaultRunMemory = "512Mi"
)
func projectRunLocations(ctx context.Context, project string) ([]string, error) {
runSvc, err := runapi.NewService(ctx)
if err != nil {
return nil, fmt.Errorf("failed to initialize Run API client: %+v", err)
}
var locations []string
if err := runapi.NewProjectsLocationsService(runSvc).
List("projects/"+project).Pages(ctx, func(resp *runapi.ListLocationsResponse) error {
for _, v := range resp.Locations {
locations = append(locations, v.LocationId)
}
return nil
}); err != nil {
return nil, fmt.Errorf("request to query Cloud Run locations failed: %+v", err)
}
sort.Strings(locations)
return locations, nil
}
func promptDeploymentRegion(ctx context.Context, project string) (string, error) {
locations, err := projectRunLocations(ctx, project)
if err != nil {
return "", fmt.Errorf("cannot retrieve Cloud Run locations: %+v", err)
}
var choice string
if err := survey.AskOne(&survey.Select{
Message: "Choose a region to deploy this application:",
Options: locations,
Default: defaultRunRegion,
}, &choice,
surveyIconOpts,
survey.WithValidator(survey.Required),
); err != nil {
return choice, fmt.Errorf("could not choose a region: %+v", err)
}
return choice, nil
}
func getService(project, name, region string) (*runapi.Service, error) {
client, err := runClient(region)
if err != nil {
return nil, fmt.Errorf("failed to initialize Run API client: %w", err)
}
return client.Namespaces.Services.Get(fmt.Sprintf("namespaces/%s/services/%s", project, name)).Do()
}
func runClient(region string) (*runapi.APIService, error) {
regionalEndpoint := fmt.Sprintf("https://%s-run.googleapis.com/", region)
return runapi.NewService(context.TODO(), option.WithEndpoint(regionalEndpoint))
}
func serviceURL(project, name, region string) (string, error) {
service, err := getService(project, name, region)
if err != nil {
return "", fmt.Errorf("failed to get Service: %w", err)
}
return service.Status.Address.Url, nil
}
func envVars(project, name, region string) (map[string]struct{}, error) {
service, err := getService(project, name, region)
if err != nil {
return nil, err
}
existing := make(map[string]struct{})
for _, container := range service.Spec.Template.Spec.Containers {
for _, envVar := range container.Env {
existing[envVar.Name] = struct{}{}
}
}
return existing, nil
}
// tryFixServiceName attempts replace the service name with a better one to
// prevent deployment failures due to Cloud Run service naming constraints such
// as:
//
// - names with a leading non-letter (e.g. digit or '-') are prefixed
// - names over 63 characters are truncated
// - names ending with a '-' have the suffix trimmed
func tryFixServiceName(name string) (string, error) {
if name == "" {
return "", fmt.Errorf("service name can't be empty")
}
name = strings.ToLower(name)
reg := regexp.MustCompile("[^a-z0-9-]+")
name = reg.ReplaceAllString(name, "-")
if name[0] == '-' {
name = fmt.Sprintf("svc%s", name)
}
if !unicode.IsLetter([]rune(name)[0]) {
name = fmt.Sprintf("svc-%s", name)
}
if len(name) > 63 {
name = name[:63]
}
for name[len(name)-1] == '-' {
name = name[:len(name)-1]
}
return name, nil
}