cmd/php/functions_framework/main.go (154 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 php/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/filepath" "github.com/GoogleCloudPlatform/buildpacks/pkg/cloudfunctions" "github.com/GoogleCloudPlatform/buildpacks/pkg/env" gcp "github.com/GoogleCloudPlatform/buildpacks/pkg/gcpbuildpack" "github.com/GoogleCloudPlatform/buildpacks/pkg/php" ) const ( // ffPackage is the name of the functions framework Packagist package. It's also the path // to the functions framework under the vendor directory, so it's used in both senses. ffPackage = "google/cloud-functions-framework" // ffVersion is the default version of functions framework to install in the container. // This value must match the version specified by converter/composer.json ffVersion = "^1.1" // ffPackageWithVersion is the package that we `composer require` when adding the functions // framework to an existing vendor directory. ffPackageWithVersion = ffPackage + ":" + ffVersion ffGitHubURL = "https://github.com/GoogleCloudPlatform/functions-framework-php" ffPackagistURL = "https://packagist.org/packages/google/cloud-functions-framework" // routerScript is the path to the functions framework invoker script. routerScript = "vendor/google/cloud-functions-framework/router.php" // cacheTag is the cache tag for the `composer install` layer. We only cache in one case: There // is no composer.json file and there is no vendor directory (i.e. a dependency-less function). // That's the only case where we create the vendor dir from scratch, so it's cacheable based on // the composer.lock file. Other cases involve modifying an existing vendor directory, whether // created by the composer buildpack or provided by the user. cacheTag = "functions-framework dependencies" ) func main() { gcp.Main(detectFn, buildFn) } func detectFn(ctx *gcp.Context) (gcp.DetectResult, error) { 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 { fnFile := "index.php" if fnSource, ok := os.LookupEnv(env.FunctionSource); ok { fnFile = fnSource } // Syntax check the function code without executing. command := []string{"php", "-l", fnFile} if _, err := ctx.Exec(command, gcp.WithCombinedTail, gcp.WithUserAttribution); err != nil { return err } composerJSONExists, err := ctx.FileExists("composer.json") if err != nil { return err } // Install the functions framework if need be. if composerJSONExists { if err := handleComposerJSON(ctx); err != nil { return err } } else { if err := handleNoComposerJSON(ctx); err != nil { return err } } ctx.AddWebProcess([]string{"/bin/bash", "-c", fmt.Sprintf("php -S 0.0.0.0:${PORT} %s", routerScript)}) l, err := ctx.Layer("functions-framework", gcp.BuildLayer, gcp.LaunchLayer) if err != nil { return fmt.Errorf("creating layer: %w", err) } if err := ctx.SetFunctionsEnvVars(l); err != nil { return err } return nil } // handleComposerJSON installs the functions framework, if required, in the case // that a composer.json file is present. func handleComposerJSON(ctx *gcp.Context) error { cjs, err := php.ReadComposerJSON(ctx.ApplicationRoot()) if err != nil { return fmt.Errorf("reading composer.json: %w", err) } // Determine if the function has a dependency on the functions framework. if version, ok := cjs.Require[ffPackage]; !ok { ctx.Logf("Handling function without dependency on functions framework") if err := cloudfunctions.AssertFrameworkInjectionAllowed(); err != nil { return err } if err := php.ComposerRequire(ctx, []string{ffPackageWithVersion}); err != nil { return err } cloudfunctions.AddFrameworkVersionLabel(ctx, &cloudfunctions.FrameworkVersionInfo{ Runtime: "php", Version: ffVersion, Injected: true, }) } else { ctx.Logf("Handling function with dependency on functions framework (%s:%s)", ffPackage, version) cloudfunctions.AddFrameworkVersionLabel(ctx, &cloudfunctions.FrameworkVersionInfo{ Runtime: "php", Version: version, Injected: false, }) } return nil } // handleNoComposerJSON installs the functions framework, if required, in the case // that there is no composer.json file present. func handleNoComposerJSON(ctx *gcp.Context) error { ctx.Logf("Handling function without composer.json") vendorExists, err := ctx.FileExists(php.Vendor) if err != nil { return err } // Check if there's a vendor directory. If not, this is truly a dependency-less function // so we can `composer install` the framework and cache the vendor dir. if !vendorExists { ctx.Logf("No vendor directory present, installing functions framework") cvt := filepath.Join(ctx.BuildpackRoot(), "converter") if _, err := ctx.Exec([]string{"cp", filepath.Join(cvt, "composer.json"), filepath.Join(cvt, "composer.lock"), "."}); err != nil { return err } if _, err := php.ComposerInstall(ctx, cacheTag); err != nil { return fmt.Errorf("composer install: %w", err) } cloudfunctions.AddFrameworkVersionLabel(ctx, &cloudfunctions.FrameworkVersionInfo{ Runtime: "php", Version: ffVersion, Injected: true, }) return nil } ffPath := filepath.Join(php.Vendor, ffPackage) ffExists, err := ctx.FileExists(ffPath) if err != nil { return err } // Check if the vendor directory contains the functions framework. If so we're done. if ffExists { ctx.Logf("Functions framework is already present in the vendor directory") routerScriptExists, err := ctx.FileExists(routerScript) if err != nil { return err } // Make sure the router script also exists. If the user is vendoring their own deps // you never know how they've structured their vendor directory. if !routerScriptExists { return gcp.UserErrorf("functions framework router script %s is not present", routerScript) } cloudfunctions.AddFrameworkVersionLabel(ctx, &cloudfunctions.FrameworkVersionInfo{ Runtime: "php", Version: "unknown-vendored", Injected: false, }) return nil } if err := cloudfunctions.AssertFrameworkInjectionAllowed(); err != nil { return err } // The user did not vendor the functions framework. Before installing it, let's see if they used // Composer to install their deps. If so we can safely `composer require` the framework even // without composer.json; vendor/composer/installed.json contains the info required to resolve // a working set of dependencies. ctx.Warnf("Functions framework is not present at %s, so automatic injection will be attempted. Please add a dependency on it to avoid unexpected conflicts or breakages that result from this. See %s and %s", ffPath, ffGitHubURL, ffPackagistURL) installed := filepath.Join(php.Vendor, "composer", "installed.json") installedExists, err := ctx.FileExists(installed) if err != nil { return err } if !installedExists { return gcp.UserErrorf("%s is not present, so it appears that Composer was not used to install dependencies.", installed) } // All clear to install the functions framework! We'll do this via `composer require` // because we're adding a package to an already existing vendor directory. ctx.Logf("Installing functions framework %s", ffPackageWithVersion) if err := php.ComposerRequire(ctx, []string{ffPackageWithVersion}); err != nil { return nil } cloudfunctions.AddFrameworkVersionLabel(ctx, &cloudfunctions.FrameworkVersionInfo{ Runtime: "php", Version: ffVersion, Injected: true, }) return nil }