cmd/ruby/functions_framework/main.go (102 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 ruby/functions_framework buildpack.
// The functions_framework buildpack sets up the execution environment to
// run the Ruby Functions Framework. The framework itself, with its converter,
// is always installed as a dependency.
package main
import (
"fmt"
"os"
"github.com/GoogleCloudPlatform/buildpacks/pkg/cloudfunctions"
"github.com/GoogleCloudPlatform/buildpacks/pkg/env"
gcp "github.com/GoogleCloudPlatform/buildpacks/pkg/gcpbuildpack"
"github.com/Masterminds/semver"
)
const (
defaultSource = "app.rb"
layerName = "functions-framework"
)
var (
// assumedVersion is the version of the framework used when we cannot determine a version.
// To avoid breaking users on early versions of the framework when we could not easily
// determine the framework version, this assumes users are on an early version.
assumedVersion = semver.MustParse("0.2.0")
// recommendedVersion is the lowest version for which a deprecation warning will be hidden.
recommendedVersion = semver.MustParse("1.1.0")
// validateTargetVersion is the minimum version that supports validating FUNCTION_TARGET.
validateTargetVersion = semver.MustParse("0.7.0")
)
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 {
// The framework has been installed with the dependencies, so this layer is
// used only for env vars.
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
}
source, err := validateSource(ctx)
if err != nil {
return err
}
version, err := frameworkVersion(ctx)
if err != nil {
return err
}
if version.GreaterThan(validateTargetVersion) || version.Equal(validateTargetVersion) {
if err := validateTarget(ctx, source); err != nil {
return err
}
}
if version.LessThan(recommendedVersion) {
ctx.Warnf("Found a deprecated version of functions-framework (%s); consider updating your Gemfile to use functions_framework %s or later.", version, recommendedVersion)
}
cloudfunctions.AddFrameworkVersionLabel(ctx, &cloudfunctions.FrameworkVersionInfo{
Runtime: "ruby",
Version: version.String(),
Injected: false,
})
ctx.AddWebProcess([]string{"bundle", "exec", "functions-framework-ruby"})
return nil
}
// validateSource validates the existence of and returns the source file
func validateSource(ctx *gcp.Context) (string, error) {
fnSource, sourceEnvFound := os.LookupEnv(env.FunctionSource)
if !sourceEnvFound {
fnSource = defaultSource
}
fnSourceExists, err := ctx.FileExists(fnSource)
if err != nil {
return "", err
}
if fnSourceExists {
return fnSource, nil
}
if sourceEnvFound {
return "", gcp.UserErrorf("%s specified file %q but it does not exist", env.FunctionSource, fnSource)
}
return "", gcp.UserErrorf("expected source file %q does not exist", fnSource)
}
// frameworkVersion validates framework installation and returns the major and minor components of its version
func frameworkVersion(ctx *gcp.Context) (*semver.Version, error) {
cmd := []string{"bundle", "exec", "functions-framework-ruby", "--version"}
result, err := ctx.Exec(cmd)
// Failure to execute the binary at all implies the functions_framework is
// not properly installed in the user's Gemfile.
if result == nil || result.ExitCode == 127 {
return nil, gcp.UserErrorf("unable to execute functions-framework-ruby; please ensure a recent version of the functions_framework gem is in your Gemfile")
}
// Frameworks older than 0.6 do not support the --version flag, signaled by a
// nonzero error code. Respond with a pessimistic guess of the version.
if err != nil {
return assumedVersion, nil
}
version, perr := semver.NewVersion(result.Stdout)
if perr != nil {
return nil, gcp.UserErrorf(`failed to parse %q from "functions-framework-ruby --version": %v; please ensure a recent version of the functions_framework gem is in your Gemfile`, result.Stdout, perr)
}
return version, nil
}
// validateTarget validates that the given target is defined and can be executed
func validateTarget(ctx *gcp.Context, source string) error {
target := os.Getenv(env.FunctionTarget)
cmd := []string{"bundle", "exec", "functions-framework-ruby", "--quiet", "--verify", "--source", source, "--target", target}
if fnSig, ok := os.LookupEnv(env.FunctionSignatureType); ok {
cmd = append(cmd, "--signature-type", fnSig)
}
if result, err := ctx.Exec(cmd, gcp.WithEnv("MALLOC_ARENA_MAX=2", "LANG=C.utf8", "RACK_ENV=production"), gcp.WithUserAttribution); err != nil {
return gcp.UserErrorf("failed to verify function target %q in source %q: %s", target, source, result.Stderr)
}
return nil
}