cmd/go/legacy_worker/main.go (197 lines of code) (raw):

// Copyright 2021 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. // Implements the legacy GCF Go 1.11 worker buildpack. // The legacy_worker buildpack converts a function into an application and sets up the execution environment. package main import ( _ "embed" "fmt" "os" "path/filepath" "text/template" "github.com/GoogleCloudPlatform/buildpacks/pkg/env" gcp "github.com/GoogleCloudPlatform/buildpacks/pkg/gcpbuildpack" "github.com/GoogleCloudPlatform/buildpacks/pkg/golang" ) const ( layerName = "legacy-worker" gopathLayerName = "gopath" appModule = "functions.local/app" fnSourceDir = "serverless_function_source_code" ) var ( googleDirs = []string{fnSourceDir, ".googlebuild", ".googleconfig"} //go:embed converter/worker/main.tmpl workerTmplFile string //go:embed converter/worker/gomod.tmpl goModTmplFile string ) type fnInfo struct { Source string Target string Package string } func main() { gcp.Main(detectFn, buildFn) } func detectFn(ctx *gcp.Context) (gcp.DetectResult, error) { if !golang.IsGo111Runtime() { return gcp.OptOut("Only compatible with go111"), nil } if _, ok := os.LookupEnv(env.FunctionTarget); ok { return gcp.OptInEnvSet(env.FunctionTarget), nil } return gcp.OptOutEnvNotSet(env.FunctionTarget), nil } func buildFn(ctx *gcp.Context) error { l, err := ctx.Layer(layerName, gcp.LaunchLayer) if err != nil { return fmt.Errorf("creating %v layer: %w", layerName, err) } if err := ctx.SetFunctionsEnvVars(l); err != nil { return err } ctx.AddWebProcess([]string{golang.OutBin}) fnTarget := os.Getenv(env.FunctionTarget) // Move the function source code into a subdirectory in order to construct the app in the main application root. if err := ctx.RemoveAll(fnSourceDir); err != nil { return err } if err := ctx.MkdirAll(fnSourceDir, 0755); err != nil { return err } // mindepth=1 excludes '.', '+' collects all file names before running the command. // Exclude serverless_function_source_code and .google* dir e.g. .googlebuild, .googleconfig command := fmt.Sprintf("find . -mindepth 1 -not -name %[1]s -prune -not -name %[2]q -prune -exec mv -t %[1]s {} +", fnSourceDir, ".google*") if _, err := ctx.Exec([]string{"bash", "-c", command}, gcp.WithUserTimingAttribution); err != nil { return err } fnSource := filepath.Join(ctx.ApplicationRoot(), fnSourceDir) pkgName, err := extractPackageNameInDir(ctx, fnSource) if err != nil { return fmt.Errorf("extracting package name: %w", err) } fn := fnInfo{ Source: fnSource, Target: fnTarget, Package: pkgName, } l.LaunchEnvironment.Default("X_GOOGLE_ENTRY_POINT", os.Getenv(env.FunctionTarget)) triggerType := os.Getenv(env.FunctionSignatureType) if triggerType == "http" || triggerType == "" { triggerType = "HTTP_TRIGGER" } l.LaunchEnvironment.Default("X_GOOGLE_FUNCTION_TRIGGER_TYPE", triggerType) goMod := filepath.Join(fn.Source, "go.mod") goModExists, err := ctx.FileExists(goMod) if err != nil { return err } if !goModExists { return createMainVendored(ctx, fn) } isWriteable, err := ctx.IsWritable(goMod) if err != nil { return err } if !isWriteable { // Preempt an obscure failure mode: if go.mod is not writable then `go list -m` can fail saying: // go: updates to go.sum needed, disabled by -mod=readonly return gcp.UserErrorf("go.mod exists but is not writable") } return createMainGoMod(ctx, fn) } /* createMainGoMod creates the `main.go` and `go.mod` required to form a module-based Go application that wraps the user function into a server. The main application's Go module depends on the user function's Go module, which is assumed to be a subdirectory of the main application's source code. ctx.ApplicationRoot() ├── go.mod // `module functions.local/app` ├── main.go └── serverless_function_source_code // assumed to aleady exist ├── go.mod // `module <user's module name>` ├── fn.go └── ... */ func createMainGoMod(ctx *gcp.Context, fn fnInfo) error { l, err := ctx.Layer(gopathLayerName, gcp.BuildLayer) if err != nil { return fmt.Errorf("creating %v layer: %w", gopathLayerName, err) } l.BuildEnvironment.Override("GOPATH", l.Path) if err := ctx.Setenv("GOPATH", l.Path); err != nil { return err } fnMod, fnPackage, err := moduleAndPackageNames(ctx, fn) if err != nil { return fmt.Errorf("extracting module and package names: %w", err) } fn.Package = fnPackage if err := createMainGoModFile(ctx, fnMod, filepath.Join(ctx.ApplicationRoot(), "go.mod")); err != nil { return fmt.Errorf("error creating `go.mod` for function application: %w", err) } return createMainGoFile(ctx, fn, filepath.Join(ctx.ApplicationRoot(), "main.go")) } func createMainGoModFile(ctx *gcp.Context, fnMod string, goModPath string) error { f, err := ctx.CreateFile(goModPath) if err != nil { return err } defer f.Close() tmpl, err := template.New("worker_gomod").Parse(goModTmplFile) if err != nil { return err } tmplSubs := struct { AppModule string FnModule string FnSource string }{ AppModule: appModule, FnModule: fnMod, FnSource: fnSourceDir, } return tmpl.Execute(f, tmplSubs) } // moduleAndPackageNames extracts the module name and package name of the function. func moduleAndPackageNames(ctx *gcp.Context, fn fnInfo) (string, string, error) { result, err := ctx.Exec([]string{"go", "list", "-m"}, gcp.WithWorkDir(fn.Source), gcp.WithUserAttribution) if err != nil { return "", "", err } fnMod := result.Stdout // Add the module name to the the package name, such that go build will be able to find it, // if a directory with the package name is not at the app root. Otherwise, assume the package is at the module root. fnPackage := fnMod fnPackageExists, err := ctx.FileExists(ctx.ApplicationRoot(), fn.Package) if err != nil { return "", "", err } if fnPackageExists { fnPackage = fmt.Sprintf("%s/%s", fnMod, fn.Package) } return fnMod, fnPackage, nil } // createMainVendored creates the main.go file for vendored functions. // This should only be run for Go 1.11 and 1.13. // Go 1.11 and 1.13 on GCF allow for vendored go.mod deployments without a go.mod file. // Note that despite the lack of a go.mod file, this does *not* mean that these are GOPATH deployments. // These deployments were created by running `go mod vendor` and then .gcloudignoring the go.mod file, // so that Go versions that don't natively handle gomod vendoring would be able to pick up the vendored deps. // n.b. later versions of Go (1.14+) handle vendored go.mod files natively, and so we just use the go.mod route there. func createMainVendored(ctx *gcp.Context, fn fnInfo) error { l, err := ctx.Layer(gopathLayerName, gcp.BuildLayer) if err != nil { return fmt.Errorf("creating %v layer: %w", gopathLayerName, err) } gopath := ctx.ApplicationRoot() gopathSrc := filepath.Join(gopath, "src") if err := ctx.MkdirAll(gopathSrc, 0755); err != nil { return err } l.BuildEnvironment.Override(env.Buildable, appModule+"/main") l.BuildEnvironment.Override("GOPATH", gopath) l.BuildEnvironment.Override("GO111MODULE", "auto") if err := ctx.Setenv("GOPATH", gopath); err != nil { return err } appPath := filepath.Join(gopathSrc, appModule, "main") if err := ctx.MkdirAll(appPath, 0755); err != nil { return err } // We move the function source (including any vendored deps) into GOPATH. if err := ctx.Rename(fn.Source, filepath.Join(gopathSrc, fn.Package)); err != nil { return err } return createMainGoFile(ctx, fn, filepath.Join(appPath, "main.go")) } func createMainGoFile(ctx *gcp.Context, fn fnInfo, main string) error { f, err := ctx.CreateFile(main) if err != nil { return err } defer f.Close() tmpl, err := template.New("worker_main").Parse(workerTmplFile) if err != nil { return err } return tmpl.Execute(f, fn) } // extractPackageNameInDir builds the script that does the extraction, and then runs it with the // specified source directory. // The parser is dependent on the language version being used, and it's highly likely that the buildpack binary // will be built with a different version of the language than the function deployment. Building this script ensures // that the version of Go used to build the function app will be the same as the version used to parse it. func extractPackageNameInDir(ctx *gcp.Context, source string) (string, error) { script := filepath.Join(ctx.BuildpackRoot(), "converter", "get_package", "main.go") cacheDir, err := ctx.TempDir("app") if err != nil { return "", fmt.Errorf("creating temp directory: %w", err) } result, err := ctx.Exec([]string{"go", "run", script, "-dir", source}, gcp.WithEnv("GOCACHE="+cacheDir), gcp.WithUserAttribution) if err != nil { return "", err } return result.Stdout, nil }