diff --git a/README.md b/README.md index 91f46d0..d92c3c9 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,36 @@ Run Serverless Sample Tester by passing in the root directory of the sample you ./sst [target-dir] ``` + +## Build and Deploying Your Sample +This program allows you to specify how you would like it to build and deploy your sample to Cloud Run, fully managed, +in three ways. From highest to lowest, this program uses the following precedence order to find your build and deploy +instructions: + +1. Cloud Build Config file +1. README parsing +1. Defaults + +### Cloud Build Config +If you'd like, you can specify the build and deploy process for your sample in a [Cloud Build config file](https://cloud.google.com/cloud-build/docs/build-config) +with the name `cloudbuild.yaml` located in the root directory of your sample. Make sure to deploy to the Cloud Run fully +managed platform. + +You can specify the substitutions you'd like passed to your Cloud Build config by either setting the +`SST_CLOUD_BUILD_SUBS` environment variable in a JSON format or the `--cloud-build-subs` flag in a comma-separated +format when calling the `sst` executable. For example: + +```bash +export SST_CLOUD_BUILD_SUBS="{\"_FOO\": \"hello\",\"_BAR\": \"world\"}" +sst [sample-dir] # or + +sst [sample-dir] --cloud-build-subs="_FOO=hello,_BAR=world" +``` + +It's required that you use the `_SST_RUN_REGION` [substitution](https://cloud.google.com/cloud-build/docs/configuring-builds/substitute-variable-values) +for the Cloud Run region you deploy your sample to. The substitution will be set as the same region specified by your +local gcloud installation's `run/region` property. + ### README parsing To parse build and deploy commands from your sample's README, include the following comment code tag before each gcloud command: @@ -79,3 +109,12 @@ what the name is, but it may not always be accurate. For example, if your README gcloud run deploy run-mysql --image gcr.io/[YOUR_PROJECT_ID]/run-mysql ``` then `$CLOUD_RUN_SERVICE_NAME` should be set to `run-mysql`. + +### Defaults +If your sample is Java-based (has a `pom.xml` file in its root directory) and has a `Dockerfile` in its root directory, +your sample will be built and pushed the Container Registry using `mvn compile com.google.cloud.tools:jib-maven-plugin:2.0.0:build -Dimage=[image_tag]`. + +Otherwise, your sample will be built and pushed to the Container Registry using `gcloud builds submit --tag=[image_tag]`. + +In both cases, `gcloud run deploy --image=[image_tag] --platform=managed`, will be used to deploy the container image to +Cloud Run. diff --git a/cmd/root.go b/cmd/root.go index c58d09d..4f92be4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -27,11 +27,11 @@ import ( var ( rootCmd = &cobra.Command{ - Use: "sst [sample-dir]", - Short: "An end-to-end tester for GCP samples", - Args: cobra.ExactArgs(1), + Use: "sst [sample-dir]", + Short: "An end-to-end tester for GCP samples", + Args: cobra.ExactArgs(1), SilenceErrors: true, - SilenceUsage: true, + SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { // Parse sample directory from command line argument sampleDir, err := filepath.Abs(filepath.Dir(args[0])) @@ -39,15 +39,19 @@ var ( return err } - log.Println("Setting up configuration values") // Set up config file location + log.Println("Setting up configuration values") viper.SetConfigName("config") viper.SetConfigType("yaml") viper.AddConfigPath(sampleDir) - s, err := sample.NewSample(sampleDir) + + s, cleanup, err := sample.NewSample(sampleDir, viper.GetStringMapString("cloud_build_subs")) if err != nil { return err } + if cleanup != nil { + defer cleanup() + } log.Println("Loading test endpoints") swagger := util.LoadTestEndpoints() @@ -93,7 +97,12 @@ func Execute() error { return rootCmd.Execute() } -// init initializes the tool. +// init binds command line flags and environment variables to viper configuration keys. func init() { - // Initialization goes here + viper.SetEnvPrefix("sst") + + viper.SetDefault("cloud_build_subs", map[string]string{}) + viper.BindEnv("cloud_build_subs") + rootCmd.PersistentFlags().StringToString("cloud-build-subs", map[string]string{}, "") + viper.BindPFlag("cloud_build_subs", rootCmd.PersistentFlags().Lookup("cloud-build-subs")) } diff --git a/go.mod b/go.mod index e1963eb..b264c3e 100644 --- a/go.mod +++ b/go.mod @@ -6,4 +6,5 @@ require ( github.com/getkin/kin-openapi v0.18.0 github.com/spf13/cobra v1.0.0 github.com/spf13/viper v1.7.1 + gopkg.in/yaml.v2 v2.3.0 ) diff --git a/go.sum b/go.sum index 9b799b3..8829327 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,7 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -71,6 +72,7 @@ github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OI github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -103,6 +105,7 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= @@ -159,7 +162,9 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= diff --git a/internal/lifecycle/cloud_build_config.go b/internal/lifecycle/cloud_build_config.go new file mode 100644 index 0000000..479ac89 --- /dev/null +++ b/internal/lifecycle/cloud_build_config.go @@ -0,0 +1,122 @@ +// Copyright 2020 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 +// +// http://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 lifecycle + +import ( + "fmt" + "github.com/GoogleCloudPlatform/serverless-sample-tester/internal/util" + "gopkg.in/yaml.v2" + "io/ioutil" + "log" + "os" + "os/exec" + "strings" +) + +// runRegionSubstitution is the substitution used to specify which Cloud Run region Cloud Build configs will deploy +// samples to. +const runRegionSubstitution = "_SST_RUN_REGION" + +// getCloudBuildConfigLifecycle returns a Lifecycle for the executing the provided Cloud Build config file. It creates +// and uses a temporary copy of the file where it replaces the Cloud Run service names and Container Registry tags with +// the provided inputs. It provides also passes in the provided substitutions as well a runRegionSubstitution with the +// provided region. Also returns a function that removes the temp file created while making Lifecycle. This function +// should be called after Lifecycle is done executing. +func getCloudBuildConfigLifecycle(filename, serviceName, gcrURL, runRegion string, substitutions map[string]string) (Lifecycle, func(), error) { + config := make(map[string]interface{}) + + buildConfigBytes, err := ioutil.ReadFile(filename) + if err != nil { + return nil, nil, fmt.Errorf("ioutil.ReadFile: %w", err) + } + + err = yaml.Unmarshal(buildConfigBytes, &config) + if err != nil { + return nil, nil, fmt.Errorf("yaml.Unmarshal: %s: %w", filename, err) + } + + // Replace Cloud Run service names and Container Registry URLs + for stepIndex := range config["steps"].([]interface{}) { + var args []string + for argIndex := range config["steps"].([]interface{})[stepIndex].(map[interface{}]interface{})["args"].([]interface{}) { + arg := config["steps"].([]interface{})[stepIndex].(map[interface{}]interface{})["args"].([]interface{})[argIndex].(string) + arg = gcrURLRegexp.ReplaceAllString(arg, gcrURL) + + args = append(args, arg) + } + + prog := config["steps"].([]interface{})[stepIndex].(map[interface{}]interface{})["name"].(string) + err := replaceServiceName(prog, args, serviceName) + if err != nil { + return nil, nil, fmt.Errorf("replacing Cloud Run service name in %s step args: %w", filename, err) + } + + config["steps"].([]interface{})[stepIndex].(map[interface{}]interface{})["args"] = args + } + + configMarshalBytes, err := yaml.Marshal(&config) + if err != nil { + return nil, nil, fmt.Errorf("yaml.Marshal: modified %s: %w", filename, err) + } + + tempBuildConfigFile, err := ioutil.TempFile("", "cloudbuild.*.yaml") + if err != nil { + return nil, nil, fmt.Errorf("ioutil.TempFile: %w\n", err) + } + cleanup := func() { + err := os.Remove(tempBuildConfigFile.Name()) + if err != nil { + log.Printf("Error removing Temp File for Cloud Build config: %v\n", err) + } + } + + if _, err := tempBuildConfigFile.Write(configMarshalBytes); err != nil { + cleanup() + return nil, nil, fmt.Errorf("os.File.Write: TempFile %s: %w", tempBuildConfigFile.Name(), err) + } + if err := tempBuildConfigFile.Close(); err != nil { + cleanup() + return nil, nil, fmt.Errorf("os.File.Close: TempFile %s: %w", tempBuildConfigFile.Name(), err) + } + + return buildCloudBuildConfigLifecycle(tempBuildConfigFile.Name(), runRegion, substitutions), cleanup, nil +} + +// buildCloudBuildConfigLifecycle returns a Lifecycle with a single command that calls gcloud builds subit and passes +// in the provided Cloud Build config file. It also adds a `--substitutions` flag according to the substitutions +// provided and adds a substitution for the Cloud Run region with the name runRegionSubstitution and value provided. +func buildCloudBuildConfigLifecycle(buildConfigFilename, runRegion string, substitutions map[string]string) Lifecycle { + a := append(util.GcloudCommonFlags, "builds", "submit", + fmt.Sprintf("--config=%s", buildConfigFilename)) + + subsitutions := substitutionsString(substitutions, runRegion) + a = append(a, fmt.Sprintf("--substitutions=%s", subsitutions)) + + return Lifecycle{exec.Command("gcloud", a...)} +} + +// substitutionsString takes a string to string map and converts it into an argument for the `gcloud builds submit` +// `--config` file. It treats the keys in the map as the substitutions and the values as the substitution values. It +// also adds a substitution for the Cloud Run region with the name runRegionSubstitution and value provided. +func substitutionsString(m map[string]string, runRegion string) string { + var subs []string + subs = append(subs, fmt.Sprintf("%s=%s", runRegionSubstitution, runRegion)) + + for k, v := range m { + subs = append(subs, fmt.Sprintf("%s=%s", k, v)) + } + + return strings.Join(subs, ",") +} diff --git a/internal/lifecycle/lifecycle.go b/internal/lifecycle/lifecycle.go index 9aa8801..ffa7123 100644 --- a/internal/lifecycle/lifecycle.go +++ b/internal/lifecycle/lifecycle.go @@ -23,8 +23,12 @@ import ( "os" "os/exec" "path/filepath" + "regexp" + "strings" ) +var gcrURLRegexp = regexp.MustCompile(`gcr.io/.+/\S+`) + // Lifecycle is a list of ordered exec.Cmd that should be run to execute a certain process. type Lifecycle []*exec.Cmd @@ -46,8 +50,24 @@ func (l Lifecycle) Execute(commandsDir string) error { // NewLifecycle tries to parse the different options provided for build and deploy command configuration. If none of // those options are set up, it falls back to reasonable defaults based on whether the sample is java-based -// (has a pom.xml) that doesn't have a Dockerfile or isn't. -func NewLifecycle(sampleDir, serviceName, gcrURL string) (Lifecycle, error) { +// (has a pom.xml) that doesn't have a Dockerfile or isn't. Also returns a function that cleans up any created local +// resources (e.g. temp files) created while making creating this Lifecycle. This function should be called after this +// Lifecycle is done executing. +func NewLifecycle(sampleDir, serviceName, gcrURL, runRegion string, cloudBuildConfSubs map[string]string) (Lifecycle, func(), error) { + // First try Cloud Build Config file + cloudBuildConfigPath := fmt.Sprintf("%s/cloudbuild.yaml", sampleDir) + + if _, err := os.Stat(cloudBuildConfigPath); err == nil { + lifecycle, cleanup, err := getCloudBuildConfigLifecycle(cloudBuildConfigPath, serviceName, gcrURL, runRegion, cloudBuildConfSubs) + if err == nil { + log.Println("Using cloud build config file") + return lifecycle, cleanup, nil + } + + return nil, nil, fmt.Errorf("lifecycle.getCloudBuildConfigLifecycle: %s: %w\n", cloudBuildConfigPath, err) + } + + // Then try README parsing var readmePath string // Searching for config file if err := viper.ReadInConfig(); err == nil { @@ -64,11 +84,11 @@ func NewLifecycle(sampleDir, serviceName, gcrURL string) (Lifecycle, error) { log.Println("README.md location: " + readmePath) if err == nil { log.Println("Using build and deploy commands found in README.md") - return lifecycle, nil + return lifecycle, nil, nil } if !errors.Is(err, errNoReadmeCodeBlocksFound) { - return nil, fmt.Errorf("lifecycle.parseREADME: %s: %w", readmePath, err) + return nil, nil, fmt.Errorf("lifecycle.parseREADME: %s: %w", readmePath, err) } log.Printf("No code blocks immediately preceded by %s found in README.md\n", codeTag) @@ -76,6 +96,7 @@ func NewLifecycle(sampleDir, serviceName, gcrURL string) (Lifecycle, error) { log.Println("No README.md found") } + // Finally fall back to reasonable defaults pomPath := filepath.Join(sampleDir, "pom.xml") dockerfilePath := filepath.Join(sampleDir, "Dockerfile") @@ -87,11 +108,11 @@ func NewLifecycle(sampleDir, serviceName, gcrURL string) (Lifecycle, error) { if pomE && !dockerfileE { log.Println("Using default build and deploy commands for java samples without a Dockerfile") - return buildDefaultJavaLifecycle(serviceName, gcrURL), nil + return buildDefaultJavaLifecycle(serviceName, gcrURL), nil, nil } log.Println("Using default build and deploy commands for non-java samples or java samples with a Dockerfile") - return buildDefaultLifecycle(serviceName, gcrURL), nil + return buildDefaultLifecycle(serviceName, gcrURL), nil, nil } // buildDefaultLifecycle builds a build and deploy command lifecycle with reasonable defaults for a non-Java @@ -122,3 +143,49 @@ func buildDefaultJavaLifecycle(serviceName, gcrURL string) Lifecycle { return l } + +// replaceServiceName takes a terminal command string as input and replaces the Cloud Run service name, if any. +// If the user specified the service name in $CLOUD_RUN_SERVICE_NAME, it replaces that. Otherwise, as a failsafe, +// it detects whether the command is a gcloud run command and replaces the last argument that isn't a flag +// with the input service name. +func replaceServiceName(name string, args []string, serviceName string) error { + if !strings.Contains(name, "gcloud") { + return nil + } + + var runCmd bool + + // Detects if the user specified the Cloud Run service name in an environment variable + for i := 0; i < len(args); i++ { + if args[i] == os.ExpandEnv("$CLOUD_RUN_SERVICE_NAME") { + args[i] = serviceName + return nil + } + + if args[i] == "run" { + runCmd = true + break + } + } + + if !runCmd { + return nil + } + + // Searches for specific gcloud keywords and takes service name from them + for i := 0; i < len(args)-1; i++ { + if args[i] == "deploy" || args[i] == "update" { + args[i+1] = serviceName + return nil + } + } + + // Provides a failsafe if neither of the above options work + for i := len(args) - 1; i >= 0; i-- { + if !strings.Contains(args[i], "--") { + args[i] = serviceName + break + } + } + return nil +} diff --git a/internal/lifecycle/readme.go b/internal/lifecycle/readme.go index 703420f..a7f2cd1 100644 --- a/internal/lifecycle/readme.go +++ b/internal/lifecycle/readme.go @@ -35,11 +35,6 @@ const ( ) var ( - gcloudCommandRegexp = regexp.MustCompile(`^gcloud\b`) - cloudRunCommandRegexp = regexp.MustCompile(`\brun\b`) - - gcrURLRegexp = regexp.MustCompile(`gcr.io/.+/\S+`) - mdCodeFenceStartRegexp = regexp.MustCompile("^\\w*`{3,}[^`]*$") errNoReadmeCodeBlocksFound = fmt.Errorf("lifecycle.extractCodeBlocks: no code blocks immediately preceded by %s found", codeTag) @@ -85,9 +80,14 @@ func (cb codeBlock) toCommands(serviceName, gcrURL string) ([]*exec.Cmd, error) line = os.ExpandEnv(line) line = gcrURLRegexp.ReplaceAllString(line, gcrURL) - line = replaceServiceName(line, serviceName) + sp := strings.Split(line, " ") + err := replaceServiceName(sp[0], sp[1:], serviceName) + if err != nil { + return nil, fmt.Errorf("lifecycle.replaceServiceName: %s: %w", line, err) + } + var cmd *exec.Cmd if sp[0] == "gcloud" { a := append(util.GcloudCommonFlags, sp[1:]...) @@ -205,40 +205,3 @@ func extractCodeBlocks(scanner *bufio.Scanner) ([]codeBlock, error) { return blocks, nil } - -// replaceServiceName takes a terminal command string as input and replaces the Cloud Run service name, if any. -// If the user specified the service name in $CLOUD_RUN_SERVICE_NAME, it replaces that. Otherwise, as a failsafe, -// it detects whether the command is a gcloud run command and replaces the last argument that isn't a flag -// with the input service name. -func replaceServiceName(command, serviceName string) string { - if !(gcloudCommandRegexp.MatchString(command) && cloudRunCommandRegexp.MatchString(command)) { - return command - } - - sp := strings.Split(command, " ") - - // Detects if the user specified the Cloud Run service name in an environment variable - for i := 0; i < len(sp); i++ { - if sp[i] == os.ExpandEnv("$CLOUD_RUN_SERVICE_NAME") { - sp[i] = serviceName - return strings.Join(sp, " ") - } - } - - // Searches for specific gcloud keywords and takes service name from them - for i := 0; i < len(sp)-1; i++ { - if sp[i] == "deploy" || sp[i] == "update" { - sp[i+1] = serviceName - return strings.Join(sp, " ") - } - } - - // Provides a failsafe if neither of the above options work - for i := len(sp) - 1; i >= 0; i-- { - if !strings.Contains(sp[i], "--") { - sp[i] = serviceName - break - } - } - return strings.Join(sp, " ") -} diff --git a/internal/sample/sample.go b/internal/sample/sample.go index 29d1f09..aac9292 100644 --- a/internal/sample/sample.go +++ b/internal/sample/sample.go @@ -41,34 +41,46 @@ type Sample struct { // The URL location of this sample's build container image in the GCP Container Registry. cloudContainerImageURL string + + // The Cloud Run region this sample will deploy to. + runRegion string } -// NewSample creates a new sample object for the sample located in the provided local directory. -func NewSample(dir string) (*Sample, error) { +// NewSample creates a new sample object for the sample located in the provided local directory. Also returns a function +// that cleans up any created local resources (e.g. temp files) created while making creating this object. This function +// should be called after this sample object is no longer needed. +func NewSample(dir string, cloudBuildConfSubs map[string]string) (*Sample, func(), error) { name := sampleName(dir) containerTag, err := cloudContainerImageTag(name, dir) if err != nil { - return nil, fmt.Errorf("sample.cloudContainerImageTag: %s %s: %w", name, dir, err) + return nil, nil, fmt.Errorf("sample.cloudContainerImageTag: sample name: %s, sample dir: %s: %w", name, dir, err) } a := append(util.GcloudCommonFlags, "config", "get-value", "core/project") projectID, err := util.ExecCommand(exec.Command("gcloud", a...), dir) if err != nil { - return nil, fmt.Errorf("getting gcloud default project: %w", err) + return nil, nil, fmt.Errorf("getting gcloud default project: %w", err) } cloudContainerImageURL := fmt.Sprintf("gcr.io/%s/%s", projectID, containerTag) + a = append(util.GcloudCommonFlags, "config", "get-value", "run/region") + runRegion, err := util.ExecCommand(exec.Command("gcloud", a...), dir) + + if err != nil { + return nil, nil, fmt.Errorf("getting gcloud cloud run default region: %w", err) + } + serviceName, err := gcloud.ServiceName(name) if err != nil { - return nil, fmt.Errorf("gcloud.ServiceName: %s sample: %w", name, err) + return nil, nil, fmt.Errorf("gcloud.ServiceName: sample name: %s: %w", name, err) } service := gcloud.CloudRunService{Name: serviceName} - buildDeployLifecycle, err := lifecycle.NewLifecycle(dir, service.Name, cloudContainerImageURL) + buildDeployLifecycle, cleanup, err := lifecycle.NewLifecycle(dir, service.Name, cloudContainerImageURL, runRegion, cloudBuildConfSubs) if err != nil { - return nil, fmt.Errorf("lifecycle.NewLifecycle: %w", err) + return nil, nil, fmt.Errorf("lifecycle.NewLifecycle: %w", err) } s := &Sample{ @@ -77,8 +89,9 @@ func NewSample(dir string) (*Sample, error) { Service: service, BuildDeployLifecycle: buildDeployLifecycle, cloudContainerImageURL: cloudContainerImageURL, + runRegion: runRegion, } - return s, nil + return s, cleanup, nil } // sampleName computes a sample name for a sample object. Right now, it's defined as a shortened version of the sample's diff --git a/main.go b/main.go index ee2d21a..ee123d2 100644 --- a/main.go +++ b/main.go @@ -16,8 +16,11 @@ package main import ( "github.com/GoogleCloudPlatform/serverless-sample-tester/cmd" + "log" ) func main() { - cmd.Execute() + if err := cmd.Execute(); err != nil { + log.Fatalf("Error ocurred in the execution of this program: %v\n", err) + } }