pkg/gcpbuildpack/builderoutput.go (170 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.
package gcpbuildpack
import (
"errors"
"fmt"
"io/ioutil"
"math/rand"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/GoogleCloudPlatform/buildpacks/pkg/buildererror"
"github.com/GoogleCloudPlatform/buildpacks/pkg/buildermetadata"
"github.com/GoogleCloudPlatform/buildpacks/pkg/buildermetrics"
"github.com/GoogleCloudPlatform/buildpacks/pkg/builderoutput"
)
const (
builderOutputEnv = "BUILDER_OUTPUT"
builderOutputFilename = "output"
expectedBuilderOutputEnv = "EXPECTED_BUILDER_OUTPUT"
)
var (
// maxMessageBytes limits the size of the exported BuilderOutputs
maxMessageBytes = 49000
// InternalErrorf constructs an Error with status StatusInternal (Google-attributed SLO).
InternalErrorf = buildererror.InternalErrorf
// UserErrorf constructs an Error with status StatusUnknown (user-attributed SLO).
UserErrorf = buildererror.UserErrorf
)
// MessageProducer is a function that produces a useful message from the result.
type MessageProducer func(result *ExecResult) string
// KeepCombinedTail returns the tail of the combined stdout/stderr from the result.
var KeepCombinedTail = func(result *ExecResult) string { return keepTail(result.Combined) }
// KeepCombinedHead returns the head of the combined stdout/stderr from the result.
var KeepCombinedHead = func(result *ExecResult) string { return keepHead(result.Combined) }
// KeepStderrTail returns the tail of stderr from the result.
var KeepStderrTail = func(result *ExecResult) string { return keepTail(result.Stderr) }
// KeepStderrHead returns the head of stderr from the result.
var KeepStderrHead = func(result *ExecResult) string { return keepHead(result.Stderr) }
// KeepStdoutTail returns the tail of stdout from the result.
var KeepStdoutTail = func(result *ExecResult) string { return keepTail(result.Stdout) }
// KeepStdoutHead returns the head of stdout from the result.
var KeepStdoutHead = func(result *ExecResult) string { return keepHead(result.Stdout) }
// saveErrorOutput saves to the builder output file, if appropriate.
func (ctx *Context) saveErrorOutput(err error) {
var be *buildererror.Error
if !errors.As(err, &be) {
be = buildererror.Errorf(buildererror.StatusInternal, "%s", err.Error())
}
outputDir := os.Getenv(builderOutputEnv)
if outputDir == "" {
return
}
if len(be.Message) > maxMessageBytes {
be.Message = keepTail(be.Message)
}
be.BuildpackID, be.BuildpackVersion = ctx.BuildpackID(), ctx.BuildpackVersion()
bo := builderoutput.BuilderOutput{Error: *be}
bm := buildermetrics.GlobalBuilderMetrics()
bmd := buildermetadata.GlobalBuilderMetadata()
bo.Metrics = *bm
bo.Metadata = *bmd
data, err := bo.JSON()
if err != nil {
ctx.Warnf("Failed to marshal, skipping structured error output: %v", err)
return
}
if err := os.MkdirAll(outputDir, 0755); err != nil {
ctx.Warnf("Failed to create dir %s, skipping structured error output: %v", outputDir, err)
return
}
// /bin/detect steps run in parallel, so they might compete over the output file. To eliminate
// this competition, write to temp file, then `mv -f` to final location (last one in wins).
tname := filepath.Join(outputDir, fmt.Sprintf("%s-%d", builderOutputFilename, rand.Int()))
if err := ioutil.WriteFile(tname, data, 0644); err != nil {
ctx.Warnf("Failed to write %s, skipping structured error output: %v", tname, err)
return
}
fname := filepath.Join(outputDir, builderOutputFilename)
if _, err := ctx.Exec([]string{"mv", "-f", tname, fname}); err != nil {
ctx.Warnf("Failed to move %s to %s, skipping structured error output: %v", tname, fname, err)
return
}
if expected := os.Getenv(expectedBuilderOutputEnv); expected != "" {
// This logic is for acceptance tests. Ideally they would examine $BUILDER_OUTPUT themselves, but as
// currently constructed that is difficult. So instead they delegate the task of checking whether
// $BUILDER_OUTPUT contains a certain expected error-message pattern to this code.
r, err := regexp.Compile(expected)
if err == nil {
ctx.Logf("Expected pattern included in error output: %t", r.MatchString(be.Message))
} else {
ctx.Warnf("Bad regexp %q: %v", expectedBuilderOutputEnv, err)
}
}
return
}
func keepTail(message string) string {
message = strings.TrimSpace(message)
if len(message) <= maxMessageBytes {
return message
}
return "..." + message[len(message)-maxMessageBytes+3:]
}
func keepHead(message string) string {
message = strings.TrimSpace(message)
if len(message) <= maxMessageBytes {
return message
}
return message[:maxMessageBytes-3] + "..."
}
// saveSuccessOutput saves information from the context into BUILDER_OUTPUT.
func (ctx *Context) saveSuccessOutput(duration time.Duration) {
outputDir := os.Getenv(builderOutputEnv)
if outputDir == "" {
return
}
bo := builderoutput.New()
fname := filepath.Join(outputDir, builderOutputFilename)
fnameExists, err := ctx.FileExists(fname)
if err != nil {
ctx.Warnf("Failed to determine if %s exists, skipping statistics: %v", fname, err)
return
}
// Previous buildpacks may have already written to the builder output file.
if fnameExists {
content, err := ioutil.ReadFile(fname)
if err != nil {
ctx.Warnf("Failed to read %s, skipping statistics: %v", fname, err)
return
}
bofj, err := builderoutput.FromJSON(content)
bo = &bofj
if err != nil {
ctx.Warnf("Failed to unmarshal %s, skipping statistics: %v", fname, err)
return
}
}
if len(ctx.InstalledRuntimeVersions()) > 0 {
bo.InstalledRuntimeVersions = append(bo.InstalledRuntimeVersions, ctx.InstalledRuntimeVersions()...)
}
bo.Stats = append(bo.Stats, builderoutput.BuilderStat{
BuildpackID: ctx.BuildpackID(),
BuildpackVersion: ctx.BuildpackVersion(),
DurationMs: duration.Milliseconds(),
UserDurationMs: ctx.stats.user.Milliseconds(),
})
bo.Warnings = append(bo.Warnings, ctx.warnings...)
bm := buildermetrics.GlobalBuilderMetrics()
bm.ForEachCounter(func(id buildermetrics.MetricID, c *buildermetrics.Counter) {
count := bo.Metrics.GetCounter(id)
count.Increment(c.Value())
})
bmd := buildermetadata.GlobalBuilderMetadata()
bmd.ForEachValue(func(id buildermetadata.MetadataID, m buildermetadata.MetadataValue) {
(&bo.Metadata).SetValue(id, m)
})
var content []byte
// Make sure the message is smaller than the maximum allowed size.
for {
var err error
content, err = bo.JSON()
if err != nil {
ctx.Warnf("Failed to marshal stats, skipping statistics: %v", err)
return
}
if len(content) <= maxMessageBytes {
break
}
// This is a defensive check; if there are no warnings, the message should be small enough.
// In either case, skip this stat.
if len(bo.Warnings) == 0 {
ctx.Warnf("The builder output is too large and there are no warnings, skipping statistics")
return
}
diff := len(content) - maxMessageBytes
last := len(bo.Warnings) - 1
// If the last warning is too long, only trim it. Otherwise, drop it.
// Also drop the last warning if it is shorter than three characters.
if len(bo.Warnings[last]) > diff+3 {
bo.Warnings[last] = bo.Warnings[last][:len(bo.Warnings[last])-diff-3] + "..."
} else {
bo.Warnings = bo.Warnings[:last]
}
}
if err := os.MkdirAll(outputDir, 0755); err != nil {
ctx.Warnf("Failed to create dir %s, skipping statistics: %v", outputDir, err)
return
}
if err := ioutil.WriteFile(fname, content, 0644); err != nil {
ctx.Warnf("Failed to write %s, skipping statistics: %v", fname, err)
return
}
}