pkg/runtime/install.go (285 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. package runtime import ( "fmt" "io/ioutil" "os" "path" "path/filepath" "slices" "strings" "github.com/GoogleCloudPlatform/buildpacks/pkg/env" "github.com/GoogleCloudPlatform/buildpacks/pkg/fetch" gcp "github.com/GoogleCloudPlatform/buildpacks/pkg/gcpbuildpack" "github.com/GoogleCloudPlatform/buildpacks/pkg/golang" "github.com/GoogleCloudPlatform/buildpacks/pkg/version" "github.com/buildpacks/libcnb/v2" "github.com/Masterminds/semver" ) var ( dartSdkURL = "https://storage.googleapis.com/dart-archive/channels/stable/release/%s/sdk/dartsdk-linux-x64-release.zip" googleTarballURL = "https://dl.google.com/runtimes/%s/%[2]s/%[2]s-%s.tar.gz" runtimeVersionsURL = "https://dl.google.com/runtimes/%s/%s/version.json" // goTarballURL is the location from which we download Go. This is different from other runtimes // because the Go team already provides re-built tarballs on the same CDN. goTarballURL = "https://dl.google.com/go/go%s.linux-amd64.tar.gz" runtimeImageARURL = "%s-docker.pkg.dev/gae-runtimes/runtimes-%s/%s:%s" runtimeImageARRepoURL = "%s-docker.pkg.dev/gae-runtimes/runtimes-%s/%s" fallbackRegion = "us" ) // InstallableRuntime is used to hold runtimes information type InstallableRuntime string // All runtimes that can be installed using the InstallTarballIfNotCached function. const ( Nodejs InstallableRuntime = "nodejs" PHP InstallableRuntime = "php" Python InstallableRuntime = "python" Ruby InstallableRuntime = "ruby" Nginx InstallableRuntime = "nginx" Pid1 InstallableRuntime = "pid1" DotnetSDK InstallableRuntime = "dotnetsdk" AspNetCore InstallableRuntime = "aspnetcore" OpenJDK InstallableRuntime = "openjdk" CanonicalJDK InstallableRuntime = "canonicaljdk" Go InstallableRuntime = "go" ubuntu1804 string = "ubuntu1804" ubuntu2204 string = "ubuntu2204" ) // User friendly display name of all runtime (e.g. for use in error message). var runtimeNames = map[InstallableRuntime]string{ Nodejs: "Node.js", PHP: "PHP Runtime", Python: "Python", Ruby: "Ruby Runtime", Nginx: "Nginx Web Server", Pid1: "Pid1", DotnetSDK: ".NET SDK", Go: "Go", } // stackToOS contains the mapping of Stack to OS. var stackToOS = map[string]string{ "google": ubuntu1804, "google.gae.18": ubuntu1804, "google.22": ubuntu2204, "google.gae.22": ubuntu2204, "google.min.22": ubuntu2204, "firebase.apphosting.22": ubuntu2204, } var languageRuntimes = []InstallableRuntime{Nodejs, PHP, Python, Ruby, OpenJDK, CanonicalJDK, Go, DotnetSDK, AspNetCore} const ( versionKey = "version" stackKey = "stack" // gcpUserAgent is required for the Ruby runtime, but used for others for simplicity. gcpUserAgent = "GCPBuildpacks" ) // OSForStack returns the Operating System being used by input stackID. func OSForStack(ctx *gcp.Context) string { os, ok := stackToOS[ctx.StackID()] if !ok { ctx.Warnf("unknown stack ID %q, falling back to Ubuntu 18.04", ctx.StackID()) os = ubuntu1804 } return os } // IsCached returns true if the requested version of a runtime is installed in the given layer. func IsCached(ctx *gcp.Context, layer *libcnb.Layer, version string) bool { metaVersion := ctx.GetMetadata(layer, versionKey) metaStack := ctx.GetMetadata(layer, stackKey) return metaVersion == version && metaStack == ctx.StackID() } // InstallDartSDK downloads a given version of the dart SDK to the specified layer. func InstallDartSDK(ctx *gcp.Context, layer *libcnb.Layer, version string) error { if err := ctx.ClearLayer(layer); err != nil { return fmt.Errorf("clearing layer %q: %w", layer.Name, err) } sdkURL := fmt.Sprintf(dartSdkURL, version) zip, err := ioutil.TempFile(layer.Path, "dart-sdk-*.zip") if err != nil { return err } defer os.Remove(zip.Name()) if err := fetch.GetURL(sdkURL, zip); err != nil { ctx.Warnf("Failed to download Dart SDK from %s. You can specify the verison by setting the GOOGLE_RUNTIME_VERSION environment variable", sdkURL) return err } if _, err := ctx.Exec([]string{"unzip", "-q", zip.Name(), "-d", layer.Path}); err != nil { return fmt.Errorf("extracting Dart SDK: %v", err) } // Once extracted the SDK contents are in a subdirectory called "dart-sdk". We move everything up // one level so "bin" and "lib" end up in the layer path. files, err := ioutil.ReadDir(path.Join(layer.Path, "dart-sdk")) if err != nil { return err } for _, file := range files { op := path.Join(layer.Path, "dart-sdk", file.Name()) np := path.Join(layer.Path, file.Name()) if err := os.Rename(op, np); err != nil { return err } } ctx.SetMetadata(layer, stackKey, ctx.StackID()) ctx.SetMetadata(layer, versionKey, version) return nil } // InstallTarballIfNotCached installs a runtime tarball hosted on dl.google.com into the provided layer // with caching. // Returns true if a cached layer is used. func InstallTarballIfNotCached(ctx *gcp.Context, runtime InstallableRuntime, versionConstraint string, layer *libcnb.Layer) (bool, error) { runtimeName := runtimeNames[runtime] runtimeID := string(runtime) osName := OSForStack(ctx) version, err := ResolveVersion(ctx, runtime, versionConstraint, osName) if err != nil { return false, err } if err = ValidateFlexMinVersion(ctx, runtime, version); err != nil { return false, err } if layer.Cache { if IsCached(ctx, layer, version) { ctx.CacheHit(runtimeID) ctx.Logf("%s v%s cache hit, skipping installation.", runtimeName, version) return true, nil } ctx.CacheMiss(runtimeID) } if err := ctx.ClearLayer(layer); err != nil { return false, gcp.InternalErrorf("clearing layer %q: %w", layer.Name, err) } ctx.Logf("Installing %s v%s.", runtimeName, version) runtimeURL := tarballDownloadURL(runtime, osName, version) stripComponents := 0 if runtime == OpenJDK || runtime == Go { stripComponents = 1 } region, present := os.LookupEnv(env.RuntimeImageRegion) if present && runtime != Go { url := runtimeImageURL(runtime, osName, version, region) fallbackURL := runtimeImageURL(runtime, osName, version, fallbackRegion) if err := fetch.ARImage(url, fallbackURL, layer.Path, stripComponents, ctx); err != nil { ctx.Warnf("Failed to download %s version %s osName %s from artifact registry. You can specify the version by setting the GOOGLE_RUNTIME_VERSION environment variable", runtimeName, version, osName) return false, err } } else { if err := fetch.Tarball(runtimeURL, layer.Path, stripComponents); err != nil { ctx.Warnf("Failed to download %s version %s osName %s from lorry. You can specify the version by setting the GOOGLE_RUNTIME_VERSION environment variable", runtimeName, version, osName) return false, err } } ctx.SetMetadata(layer, stackKey, ctx.StackID()) ctx.SetMetadata(layer, versionKey, version) return false, nil } func runtimeImageURL(runtime InstallableRuntime, osName, version, region string) string { flag, present := os.LookupEnv(env.ServerlessRuntimesTarballs) if present && flag == "true" { newRuntimeImageARURL := "%s-docker.pkg.dev/serverless-runtimes/runtimes-%s/%s:%s" return fmt.Sprintf(newRuntimeImageARURL, region, osName, runtime, version) } return fmt.Sprintf(runtimeImageARURL, region, osName, runtime, version) } func tarballDownloadURL(runtime InstallableRuntime, os, version string) string { if runtime == Go { return fmt.Sprintf(goTarballURL, version) } return fmt.Sprintf(googleTarballURL, os, runtime, strings.ReplaceAll(version, "+", "_")) } // PinGemAndBundlerVersion pins the RubyGems versions for GAE and GCF runtime versions to prevent // unexpected behaviors with new versions. This is only expected to be called if the target // platform is GAE or GCF. func PinGemAndBundlerVersion(ctx *gcp.Context, version string, layer *libcnb.Layer) error { rubygemsVersion := "3.3.15" bundler1Version := "1.17.3" bundler2Version := "2.1.4" installBundler1 := false // Bundler 1 is only installed for older versions of Ruby // Older 2.x Ruby versions have been using RubyGems 3.1.2 on GAE/GCF. if strings.HasPrefix(version, "2.") { rubygemsVersion = "3.1.2" installBundler1 = true } // Ruby 3.0 has been using 3.2.26 on GAE/GCF if strings.HasPrefix(version, "3.0") { rubygemsVersion = "3.2.26" installBundler1 = true } rubyBinPath := filepath.Join(layer.Path, "bin") gemPath := filepath.Join(rubyBinPath, "gem") // Update RubyGems to a fixed version ctx.Logf("Installing RubyGems %s", rubygemsVersion) _, err := ctx.Exec( []string{gemPath, "update", "--no-document", "--system", rubygemsVersion}, gcp.WithUserAttribution) if err != nil { return fmt.Errorf("updating rubygems %s, err: %v", rubygemsVersion, err) } // Remove any existing bundler versions in the Ruby installation command := []string{"rm", "-f", filepath.Join(rubyBinPath, "bundle"), filepath.Join(rubyBinPath, "bundler")} _, err = ctx.Exec(command, gcp.WithUserAttribution) if err != nil { return fmt.Errorf("removing out-of-box bundler: %v", err) } command = []string{gemPath, "install", "--no-document", fmt.Sprintf("bundler:%s", bundler2Version)} if installBundler1 { // Install fixed versions of Bundler1 and Bundler2 for backwards compatibility command = append(command, fmt.Sprintf("bundler:%s", bundler1Version)) ctx.Logf("Installing bundler %s and %s", bundler1Version, bundler2Version) } else { ctx.Logf("Installing bundler %s ", bundler2Version) } _, err = ctx.Exec(command, gcp.WithUserAttribution) if err != nil { return fmt.Errorf("installing bundler %s and %s: %v", bundler1Version, bundler2Version, err) } return nil } // IsReleaseCandidate returns true if given string is a RC candidate version. func IsReleaseCandidate(verConstraint string) bool { return version.IsReleaseCandidate(verConstraint) } // ResolveVersion returns the newest available version of a runtime that satisfies the provided // version constraint. func ResolveVersion(ctx *gcp.Context, runtime InstallableRuntime, verConstraint, osName string) (string, error) { if runtime == Go { // Go provides its own version manifest so it has its own version resolution logic. return golang.ResolveGoVersion(verConstraint) } // Some release candidates do not follow the convention for semver // Specifically php. example - 8.3.0RC4. if IsReleaseCandidate(verConstraint) || version.IsExactSemver(verConstraint) { return verConstraint, nil } var versions []string var err error region, present := os.LookupEnv(env.RuntimeImageRegion) if present { url := fmt.Sprintf(runtimeImageARRepoURL, region, osName, runtime) fallbackURL := fmt.Sprintf(runtimeImageARRepoURL, fallbackRegion, osName, runtime) versions, err = fetch.ARVersions(url, fallbackURL, ctx) } else { url := fmt.Sprintf(runtimeVersionsURL, osName, runtime) err = fetch.JSON(url, &versions) } if err != nil { return "", gcp.InternalErrorf("fetching %s versions %s osName: %v", runtimeNames[runtime], osName, err) } if present && (runtime == OpenJDK || runtime == CanonicalJDK) { for i, v := range versions { // When resolving version openjdk versions should be decoded to align with semver requirement. (eg. 11.0.21_9 -> 11.0.21+9) versions[i] = strings.ReplaceAll(v, "_", "+") } } v, err := version.ResolveVersion(verConstraint, versions) if err != nil { return "", gcp.UserErrorf("invalid %s version specified: %v. You may need to use a different builder. Please check if the language version specified is supported by the os: %v. You can refer to https://cloud.google.com/docs/buildpacks/builders for a list of compatible runtime languages per builder", runtimeNames[runtime], err, osName) } // When downloading from AR the openjdk version should be encoded to align with tag format requirement. (eg. 11.0.21+9 -> 11.0.21_9) if present { if runtime == OpenJDK || runtime == CanonicalJDK { v = strings.ReplaceAll(v, "+", "_") } } return v, nil } // ValidateFlexMinVersion validates the minimum flex version for a given runtime. func ValidateFlexMinVersion(ctx *gcp.Context, runtime InstallableRuntime, version string) error { if !env.IsFlex() || !slices.Contains(languageRuntimes, runtime) { return nil } if !runtimeMatchesInstallableRuntime(runtime) { return nil } minVersionEnv, present := os.LookupEnv(env.FlexMinVersion) if !present { return nil } minVersion, err := semver.NewVersion(minVersionEnv) if err != nil { // Ignore the error if env version is incorrect since it should be set by RCS. return nil } currentVersion, err := semver.NewVersion(version) if err != nil { return err } if currentVersion.LessThan(minVersion) { return gcp.UserErrorf("flex version %s is less than the minimum version %s allowed", version, minVersionEnv) } return nil } // runtimeMatchesInstallableRuntime returns true if the GOOGLE_RUNTIME should match the installable runtime. // This is because PHP might install python and ruby might install nodejs. func runtimeMatchesInstallableRuntime(installableRuntime InstallableRuntime) bool { switch r := os.Getenv(env.Runtime); r { case "java": return installableRuntime == OpenJDK || installableRuntime == CanonicalJDK case "dotnet": return installableRuntime == DotnetSDK || installableRuntime == AspNetCore default: return strings.HasPrefix(r, string(installableRuntime)) } }