client/buildpacks.go (184 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 // // https://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 main import ( "bytes" "context" "fmt" "io/ioutil" "log" "os" "os/exec" "path/filepath" "regexp" "time" pack "github.com/buildpacks/pack/pkg/client" "github.com/buildpacks/pack/pkg/logging" ) const ( image = "conformance-test-func" defaultBuilderURLTemplate = "gcr.io/gae-runtimes/buildpacks/%s/builder:%s" gcfTargetPlatform = "gcf" ) type buildpacksFunctionServer struct { functionOutputFile string source string target string funcType string runtime string runtimeVersion string tag string ctID string logStdout *os.File logStderr *os.File stdoutFile string stderrFile string builderURL string envs []string } func (b *buildpacksFunctionServer) Start(stdoutFile, stderrFile, functionOutputFile string) (func(), error) { b.functionOutputFile = functionOutputFile b.stdoutFile = stdoutFile b.stderrFile = stderrFile typ := *functionSignature if typ == "legacyevent" { typ = "event" } ctx := context.Background() if err := b.build(ctx); err != nil { return nil, fmt.Errorf("building function container: %v", err) } shutdown, err := b.run() if err != nil { return nil, fmt.Errorf("running function container: %v", err) } return shutdown, nil } func (b *buildpacksFunctionServer) OutputFile() ([]byte, error) { cmd := exec.Command("docker", "cp", filepath.Join(fmt.Sprintf("%s:/workspace", b.containerID()), b.functionOutputFile), os.TempDir()) output, err := cmd.CombinedOutput() if err != nil { return nil, fmt.Errorf("failed to copy output file from the container: %v: %s", err, string(output)) } return ioutil.ReadFile(filepath.Join(os.TempDir(), filepath.Base(b.functionOutputFile))) } func (b *buildpacksFunctionServer) build(ctx context.Context) error { builder, err := b.buildpackBuilderImage() if err != nil { return err } cmd := exec.Command("docker", "pull", builder) output, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("failed to pull builder image %s: %v: %s", builder, err, string(output)) } logger := logging.NewLogWithWriters(os.Stdout, os.Stderr, logging.WithVerbose()) packClient, err := pack.NewClient(pack.WithLogger(logger)) if err != nil { return fmt.Errorf("getting pack client: %v", err) } err = packClient.Build(ctx, pack.BuildOptions{ Image: image, Builder: builder, AppPath: b.source, Registry: "", Env: map[string]string{ "GOOGLE_FUNCTION_TARGET": b.target, "GOOGLE_FUNCTION_SIGNATURE_TYPE": b.funcType, "GOOGLE_RUNTIME": b.runtime, "GOOGLE_RUNTIME_VERSION": b.runtimeVersion, "X_GOOGLE_TARGET_PLATFORM": gcfTargetPlatform, }, }) if err != nil { return fmt.Errorf("building function image: %v", err) } return nil } var runtimeLanguageRegexp = regexp.MustCompile(`^[a-zA-Z]+`) func (b *buildpacksFunctionServer) buildpackBuilderImage() (string, error) { if b.builderURL != "" { return b.builderURL, nil } runtimeLanguage := runtimeLanguageRegexp.FindString(b.runtime) if runtimeLanguage == "" { return "", fmt.Errorf("invalid runtime format. Runtime should start with language followed by version. Example: go119, python311. Got %q", b.runtime) } return fmt.Sprintf(defaultBuilderURLTemplate, runtimeLanguage, b.tag), nil } func (b *buildpacksFunctionServer) run() (func(), error) { // Create logs output files. var err error b.logStdout, err = os.Create(b.stdoutFile) if err != nil { return nil, err } b.logStderr, err = os.Create(b.stderrFile) if err != nil { return nil, err } var args = b.getDockerRunCommand() cmd := exec.Command(args[0], args[1:]...) err = cmd.Start() // TODO: figure out why this isn't picking up errors. if err != nil { return nil, err } // Give it some time to do its setup. time.Sleep(time.Duration(*startDelay) * time.Second) log.Printf("Framework container %q started.", b.containerID()) return func() { if err := b.logs(); err != nil { log.Fatalf("getting container logs: %v", err) } log.Printf("Wrote logs to %v and %v.", b.stdoutFile, b.stderrFile) if err := cmd.Process.Kill(); err != nil { log.Fatalf("failed to kill process: %v", err) } if err := b.killContainer(); err != nil { log.Fatalf("failed to kill container: %v", err) } log.Print("Framework server shut down.") }, nil } func (b *buildpacksFunctionServer) getDockerRunCommand() []string { runtimeVars := []string{"docker", "run", "--network=host", // TODO: figure out why these aren't getting set in the buildpack. "--env=FUNCTION_TARGET=" + b.target, "--env=FUNCTION_SIGNATURE_TYPE=" + b.funcType} for _, s := range b.envs { if s != "" { runtimeVars = append(runtimeVars, fmt.Sprintf("--env=%s", s)) } } return append(runtimeVars, image) } func (b *buildpacksFunctionServer) containerID() string { if b.ctID != "" { return b.ctID } cmd := exec.Command("docker", "ps", "--latest", "--format", "{{.ID}}") containerID, err := cmd.Output() if err != nil { log.Fatalf("failed to get container ID: %v", err) } b.ctID = string(bytes.TrimSpace(containerID)) return b.ctID } func (b *buildpacksFunctionServer) killContainer() error { cmd := exec.Command("docker", "kill", b.containerID()) output, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("failed to kill the container %q: %v: %s", b.containerID(), err, string(output)) } return nil } func (b *buildpacksFunctionServer) logs() error { defer b.logStdout.Close() defer b.logStderr.Close() args := []string{"docker", "logs", b.containerID()} logsCmd := exec.Command(args[0], args[1:]...) logsCmd.Stdout = b.logStdout logsCmd.Stderr = b.logStderr err := logsCmd.Run() if err != nil { log.Fatalf("failed to retrieve container logs: %v", err) } return nil }