cmd/php/webconfig/main.go (202 lines of code) (raw):

// Copyright 2022 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/webconfig buildpack. // The runtime buildpack installs the config needed for PHP runtime. package main import ( "fmt" "os" "os/user" "path/filepath" "github.com/GoogleCloudPlatform/buildpacks/pkg/appyaml" "github.com/GoogleCloudPlatform/buildpacks/pkg/env" gcp "github.com/GoogleCloudPlatform/buildpacks/pkg/gcpbuildpack" "github.com/GoogleCloudPlatform/buildpacks/pkg/nginx" "github.com/GoogleCloudPlatform/buildpacks/pkg/php" "github.com/GoogleCloudPlatform/buildpacks/pkg/runtime" "github.com/GoogleCloudPlatform/buildpacks/pkg/webconfig" "github.com/Masterminds/semver" ) const ( // pid1 appSocket = "app.sock" pid1Log = "pid1.log" defaultFlexAddress = "127.0.0.1:9000" // nginx defaultFrontController = "index.php" defaultNginxBinary = "nginx" defaultNginxPort = 8080 defaultRoot = "/workspace" nginxConf = "nginx.conf" nginxLog = "nginx.log" // php-fpm defaultDynamicWorkers = false defaultFPMBinary = "php-fpm" defaultFPMWorkers = 2 phpFpmPid = "php-fpm.pid" ) var ( overrides = webconfig.OverrideProperties{} ) func main() { gcp.Main(detectFn, buildFn) } func detectFn(ctx *gcp.Context) (gcp.DetectResult, error) { return gcp.OptInAlways(), nil } func buildFn(ctx *gcp.Context) error { // create webconfig layer l, err := ctx.Layer("webconfig", gcp.LaunchLayer) if err != nil { return fmt.Errorf("creating layer: %w", err) } if env.IsFlex() { runtimeConfig, err := appyaml.PhpConfiguration(ctx.ApplicationRoot()) if err != nil { return err } overrides = webconfig.OverriddenProperties(ctx, runtimeConfig) webconfig.SetEnvVariables(l, overrides) } if customNginxConf, present := os.LookupEnv(php.CustomNginxConfig); present { overrides.NginxConfOverride = true overrides.NginxConfOverrideFileName = filepath.Join(defaultRoot, customNginxConf) } nginxServesStaticFiles, err := env.IsPresentAndTrue(php.NginxServesStaticFiles) if err != nil { return err } overrides.NginxServesStaticFiles = nginxServesStaticFiles fpmConfFile, err := writeFpmConfig(ctx, l.Path, overrides) if err != nil { return err } defer fpmConfFile.Close() nginxServerConfFile, err := writeNginxServerConfig(l.Path, overrides) if err != nil { return err } defer nginxServerConfFile.Close() procExists, err := ctx.FileExists("Procfile") if err != nil { return err } _, entrypointExists := os.LookupEnv(env.Entrypoint) if !procExists && !entrypointExists { cmd := []string{ filepath.Join(os.Getenv("PID1_DIR"), "pid1"), "--nginxBinaryPath", defaultNginxBinary, "--nginxErrLogFilePath", filepath.Join(l.Path, nginxLog), "--customAppCmd", fmt.Sprintf("%q", fmt.Sprintf("%s -R --nodaemonize --fpm-config %s", defaultFPMBinary, fpmConfFile.Name())), "--pid1LogFilePath", filepath.Join(l.Path, pid1Log), // Ideally, we should be able to use the path of the nginx layer and not hardcode it here. // This needs some investigation on how to pass values between build steps of buildpacks. "--mimeTypesPath", filepath.Join("/layers/google.utils.nginx/nginx", "conf/mime.types"), } addArgs, err := addNginxConfCmdArgs(l.Path, nginxServerConfFile.Name(), overrides) if err != nil { return err } cmd = append(cmd, addArgs...) ctx.AddProcess(gcp.WebProcess, cmd, gcp.AsDefaultProcess()) } return nil } func getInstalledPhpVersion(ctx *gcp.Context) (string, error) { version, err := php.ExtractVersion(ctx) if err != nil { return "", fmt.Errorf("determining runtime version: %w", err) } resolvedVersion, err := runtime.ResolveVersion(ctx, php.GetInstallableRuntime(ctx), version, runtime.OSForStack(ctx)) if err != nil { return "", fmt.Errorf("resolving runtime version: %w", err) } return resolvedVersion, nil } func supportsDecorateWorkersOutput(ctx *gcp.Context) (bool, error) { v, err := getInstalledPhpVersion(ctx) if err != nil { return false, err } // Only latest php versions post 8.3 version are tested with RC candidate // and will support the below constraint (>= 7.3.0). if runtime.IsReleaseCandidate(v) { return true, nil } c, err := semver.NewConstraint(">= 7.3.0") if err != nil { return false, err } sv, err := semver.NewVersion(v) if err != nil { return false, fmt.Errorf("parsing semver: %w", err) } return c.Check(sv), nil } func writeFpmConfig(ctx *gcp.Context, path string, overrides webconfig.OverrideProperties) (*os.File, error) { // For php >= 7.3.0, the directive decorate_workers_output prevents php from prepending a warning // message to all logged entries. Prior to 7.3.0, decorate_workers_output was not available, and // these warning messages are prepended to all logged entries. Here we choose to set // decorate_workers_output if the runtime version is >= 7.3.0. addNoDecorateWorkers, err := supportsDecorateWorkersOutput(ctx) if err != nil { return nil, err } conf, err := fpmConfig(path, addNoDecorateWorkers, overrides) if err != nil { return nil, err } return nginx.WriteFpmConfigToPath(path, conf) } func fpmConfig(layer string, addNoDecorateWorkers bool, overrides webconfig.OverrideProperties) (nginx.FPMConfig, error) { user, err := user.Current() if err != nil { return nginx.FPMConfig{}, fmt.Errorf("getting current user: %w", err) } fpm := nginx.FPMConfig{ PidPath: filepath.Join(layer, phpFpmPid), NumWorkers: defaultFPMWorkers, ListenAddress: filepath.Join(layer, appSocket), DynamicWorkers: defaultDynamicWorkers, Username: user.Username, AddNoDecorateWorkers: addNoDecorateWorkers, } if env.IsFlex() { fpm.ListenAddress = defaultFlexAddress } if overrides.PHPFPMOverride { fpm.ConfOverride = overrides.PHPFPMOverrideFileName } return fpm, nil } func addNginxConfCmdArgs(path, nginxServerConfFileName string, overrides webconfig.OverrideProperties) ([]string, error) { var args []string if env.IsFlex() { args = []string{"--customAppPort", "9000"} } else { args = []string{"--customAppSocket", filepath.Join(path, appSocket)} } if overrides.NginxConfOverride { return append(args, "--nginxConfigPath", overrides.NginxConfOverrideFileName), nil } args = append(args, "--nginxConfigPath", filepath.Join(path, nginxConf), "--serverConfigPath", nginxServerConfFileName, ) if overrides.NginxHTTPInclude { args = append(args, "--httpIncludeConfigPath", overrides.NginxHTTPIncludeFileName) } return args, nil } func nginxConfig(layer string, overrides webconfig.OverrideProperties) nginx.Config { frontController := defaultFrontController if overrides.FrontController != "" { frontController = overrides.FrontController } root := defaultRoot if overrides.DocumentRoot != "" { root = filepath.Join(defaultRoot, overrides.DocumentRoot) } nginx := nginx.Config{ Port: defaultNginxPort, FrontControllerScript: frontController, Root: root, AppListenAddress: "unix:" + filepath.Join(layer, appSocket), ServesStaticFiles: overrides.NginxServesStaticFiles, } if env.IsFlex() { nginx.AppListenAddress = defaultFlexAddress } if overrides.NginxServerConfInclude { nginx.NginxConfInclude = overrides.NginxServerConfIncludeFileName } return nginx } func writeNginxServerConfig(path string, overrides webconfig.OverrideProperties) (*os.File, error) { conf := nginxConfig(path, overrides) return nginx.WriteNginxConfigToPath(path, conf) }