cmd/nodejs/functions_framework/main.go (222 lines of code) (raw):

// 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. // Implements nodejs/functions_framework buildpack. // The functions_framework buildpack converts a function into an application and sets up the execution environment. package main import ( "fmt" "os" "path" "path/filepath" "strconv" "github.com/GoogleCloudPlatform/buildpacks/pkg/ar" "github.com/GoogleCloudPlatform/buildpacks/pkg/cache" "github.com/GoogleCloudPlatform/buildpacks/pkg/cloudfunctions" "github.com/GoogleCloudPlatform/buildpacks/pkg/env" gcp "github.com/GoogleCloudPlatform/buildpacks/pkg/gcpbuildpack" "github.com/GoogleCloudPlatform/buildpacks/pkg/nodejs" "github.com/buildpacks/libcnb/v2" ) const ( layerName = "functions-framework" functionsFrameworkPackage = "@google-cloud/functions-framework" // nodeJSHeadroomMB is the amount of memory we'll set aside before computing the max memory size. nodeJSHeadroomMB int = 64 ) var functionsFrameworkNodeModulePath = path.Join("node_modules", functionsFrameworkPackage) func main() { gcp.Main(detectFn, buildFn) } func detectFn(ctx *gcp.Context) (gcp.DetectResult, error) { if nodejs.IsNodeJS8Runtime() { return gcp.OptOut("Incompatible with nodejs8"), nil } if _, ok := os.LookupEnv(env.FunctionTarget); ok { return gcp.OptInEnvSet(env.FunctionTarget), nil } return gcp.OptOutEnvNotSet(env.FunctionTarget), nil } // buildFn sets up the execution environment for the function. // For a function that specifies the framework as a dependency, only set // environment variables and define a web process. The framework is // installed in the npm or yarn buildpack with other dependencies. // For a function that does not, also install the framework. func buildFn(ctx *gcp.Context) error { if _, ok := os.LookupEnv(env.FunctionSource); ok { return gcp.UserErrorf("%s is not currently supported for Node.js buildpacks", env.FunctionSource) } indexJSExists, err := ctx.FileExists("index.js") if err != nil { return err } // Function source code should be defined in the "main" field in package.json, index.js or function.js. // https://cloud.google.com/functions/docs/writing#structuring_source_code fnFile := "function.js" if indexJSExists { fnFile = "index.js" } // Determine if the function has dependency on functions-framework. hasFrameworkDependency := false pjs, err := nodejs.ReadPackageJSONIfExists(ctx.ApplicationRoot()) if err != nil { return fmt.Errorf("reading package.json: %w", err) } if pjs != nil { _, hasFrameworkDependency = pjs.Dependencies[functionsFrameworkPackage] if pjs.Main != "" { fnFile = pjs.Main } } fnFileExists, err := ctx.FileExists(fnFile) if err != nil { return err } if !fnFileExists { return gcp.UserErrorf("%s does not exist", fnFile) } yarnPnP, err := usingYarnModuleResolution(ctx) if err != nil { return err } if yarnPnP && !hasFrameworkDependency { return gcp.UserErrorf("This project is using Yarn Plug'n'Play but you have not included the Functions Framework in your dependencies. Please add it by running: 'yarn add @google-cloud/functions-framework'.") } pnpmLockExists, err := ctx.FileExists(nodejs.PNPMLock) if err != nil { return err } if pnpmLockExists && !hasFrameworkDependency { return gcp.UserErrorf("This project is using pnpm but you have not included the Functions Framework in your dependencies. Please add it by running: 'pnpm add @google-cloud/functions-framework'.") } // TODO(mattrobertson) remove this check once Nodejs has backported the fix to v16. More info here: // https://github.com/GoogleCloudPlatform/functions-framework-nodejs/issues/407 if skip, err := nodejs.SkipSyntaxCheck(ctx, fnFile, pjs); err != nil { return err } else if !skip { // Syntax check the function code without executing to prevent run-time errors. if yarnPnP { if _, err := ctx.Exec([]string{"yarn", "node", "--check", fnFile}, gcp.WithUserAttribution); err != nil { return err } } else { if _, err := ctx.Exec([]string{"node", "--check", fnFile}, gcp.WithUserAttribution); err != nil { return err } } } l, err := ctx.Layer(layerName, gcp.BuildLayer, gcp.CacheLayer, gcp.LaunchLayer) if err != nil { return fmt.Errorf("creating %v layer: %w", layerName, err) } // We use the absolute path to the functions-framework executable in order to // avoid having to add its parent directory to PATH which could cause // conflicts with user-specified dependencies in the case where the framework // is not an explicit dependency. // // If the function specifies a framework dependency, the executable will be // in node_modules, where it would have been installed by the preceding // npm/yarn buildpack. Otherwise, it will be in the layer's node_modules, // installed below. ff := filepath.Join(".bin", "functions-framework") if yarnPnP { // In order for node module resolution to work in Yarn Plug'n'Play mode, we must invoke yarn to // start the Functions Framework. ff = "yarn functions-framework" cloudfunctions.AddFrameworkVersionLabel(ctx, &cloudfunctions.FrameworkVersionInfo{ Runtime: "nodejs", Version: "yarn", }) } else if hasFrameworkDependency { ctx.Logf("Handling functions with dependency on functions-framework.") if err := ctx.ClearLayer(l); err != nil { return fmt.Errorf("clearing layer %q: %w", l.Name, err) } ff = filepath.Join("node_modules", ff) addFrameworkVersionLabel(ctx, functionsFrameworkNodeModulePath, false) } else { ctx.Logf("Handling functions without dependency on functions-framework.") if err := cloudfunctions.AssertFrameworkInjectionAllowed(); err != nil { return err } if err := installFunctionsFramework(ctx, l); err != nil { vendorError := "" if nodejs.IsUsingVendoredDependencies() { vendorError = "Vendored dependencies detected, please make sure you have functions-framework installed locally to avoid the installation error by following: https://github.com/GoogleCloudPlatform/functions-framework-nodejs#installation." } return fmt.Errorf("%s installing functions-framework: %w", vendorError, err) } ff = filepath.Join(l.Path, "node_modules", ff) addFrameworkVersionLabel(ctx, filepath.Join(l.Path, functionsFrameworkNodeModulePath), true) nm := filepath.Join(ctx.ApplicationRoot(), "node_modules") nmExists, err := ctx.FileExists(nm) if err != nil { return err } // Add user's node_modules to NODE_PATH so functions-framework can always find user's packages. if nmExists { l.LaunchEnvironment.Prepend("NODE_PATH", string(os.PathListSeparator), nm) } } // Get and set the valid value for --max-old-space-size node_options. // Keep the existing behaviour if the value is not provided or invalid if size, err := getMaxOldSpaceSize(); err != nil { return err } else if size > 0 { l.LaunchEnvironment.Prepend("NODE_OPTIONS", " ", fmt.Sprintf("--max-old-space-size=%d", size)) } if err := ctx.SetFunctionsEnvVars(l); err != nil { return err } ctx.AddWebProcess([]string{"/bin/bash", "-c", ff}) return nil } // installFunctionsFramework downloads the functions-framework package to node_modules in the given layer. func installFunctionsFramework(ctx *gcp.Context, l *libcnb.Layer) error { nodeVersion := os.Getenv(env.Runtime) var subdir string if nodeVersion == "nodejs12" || nodeVersion == "nodejs14" { subdir = "without-framework-compat" } else { subdir = "without-framework" } cvt := filepath.Join(ctx.BuildpackRoot(), "converter", subdir) pjs := filepath.Join(cvt, "package.json") pljs := filepath.Join(cvt, nodejs.PackageLock) cached, err := nodejs.CheckOrClearCache(ctx, l, cache.WithStrings(nodejs.EnvProduction), cache.WithFiles(pjs, pljs)) if err != nil { return fmt.Errorf("checking cache: %w", err) } if cached { return nil } installCmd, err := nodejs.NPMInstallCommand(ctx) if err != nil { return err } // NPM expects package.json and the lock file in the prefix directory. if _, err := ctx.Exec([]string{"cp", "-t", l.Path, pjs, pljs}, gcp.WithUserTimingAttribution); err != nil { return err } if err := ar.GenerateNPMConfig(ctx); err != nil { return fmt.Errorf("generating Artifact Registry credentials: %w", err) } if _, err := ctx.Exec([]string{"npm", installCmd, "--quiet", "--production", "--prefix", l.Path}, gcp.WithUserAttribution); err != nil { return err } return nil } // getMaxOldSpaceSize returns the memory size specified by (GOOGLE_CONTAINER_MEMORY_HINT_MB - nodeJSHeadroomMB), // or 0 if env var is not specified. func getMaxOldSpaceSize() (int, error) { memHintStr, exist := os.LookupEnv(env.ContainerMemoryHintMB) if !exist { return 0, nil } memHint, err := strconv.Atoi(memHintStr) if err != nil { return 0, fmt.Errorf("%s=%q must be an integer: %v", env.ContainerMemoryHintMB, memHintStr, err) } if memHint <= nodeJSHeadroomMB { return 0, fmt.Errorf("%s=%q must be greater than %d", env.ContainerMemoryHintMB, memHintStr, nodeJSHeadroomMB) } return memHint - nodeJSHeadroomMB, nil } // tryAddFrameworkVersionLabel attempts to identify the functions framework // version being used by reading the functions-framework package's manifest. // If the version is detected it is added to the generated image. func addFrameworkVersionLabel(ctx *gcp.Context, ffPackageJSON string, injected bool) { version := "unknown" packageInfo, err := nodejs.ReadPackageJSONIfExists(ffPackageJSON) if err != nil { ctx.Logf("Could not detect installed functions framework version: %v", err) } else { version = packageInfo.Version } cloudfunctions.AddFrameworkVersionLabel(ctx, &cloudfunctions.FrameworkVersionInfo{ Runtime: "nodejs", Version: version, Injected: injected, }) } // usingYarnModuleResolution returns true if this project was built using a new version of Yarn that // does not create the "node_modules" directory. func usingYarnModuleResolution(ctx *gcp.Context) (bool, error) { yarnLockExists, err := ctx.FileExists(nodejs.YarnLock) if err != nil { return false, err } if !yarnLockExists { return false, nil } yarn2, err := nodejs.IsYarn2(ctx.ApplicationRoot()) if err != nil || !yarn2 { return false, nil } result, err := ctx.Exec([]string{"yarn", "config", "get", "nodeLinker"}, gcp.WithUserAttribution) if err != nil { return false, err } linker := result.Stdout return linker == "pnp", nil }