concourse/atc/exec/load_var_step.go

215 lines
5.3 KiB
Go

package exec
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"code.cloudfoundry.org/lager"
"code.cloudfoundry.org/lager/lagerctx"
"sigs.k8s.io/yaml"
"github.com/concourse/baggageclaim"
"github.com/concourse/concourse/atc"
"github.com/concourse/concourse/atc/exec/artifact"
"github.com/concourse/concourse/atc/exec/build"
"github.com/concourse/concourse/atc/worker"
"github.com/concourse/concourse/tracing"
)
// LoadVarStep loads a value from a file and sets it as a build-local var.
type LoadVarStep struct {
planID atc.PlanID
plan atc.LoadVarPlan
metadata StepMetadata
delegateFactory BuildStepDelegateFactory
artifactStreamer worker.ArtifactStreamer
}
func NewLoadVarStep(
planID atc.PlanID,
plan atc.LoadVarPlan,
metadata StepMetadata,
delegateFactory BuildStepDelegateFactory,
artifactStreamer worker.ArtifactStreamer,
) Step {
return &LoadVarStep{
planID: planID,
plan: plan,
metadata: metadata,
delegateFactory: delegateFactory,
artifactStreamer: artifactStreamer,
}
}
type UnspecifiedLoadVarStepFileError struct {
File string
}
// Error returns a human-friendly error message.
func (err UnspecifiedLoadVarStepFileError) Error() string {
return fmt.Sprintf("file '%s' does not specify where the file lives", err.File)
}
type InvalidLocalVarFile struct {
File string
Format string
Err error
}
func (err InvalidLocalVarFile) Error() string {
return fmt.Sprintf("failed to parse %s in format %s: %s", err.File, err.Format, err.Err.Error())
}
func (step *LoadVarStep) Run(ctx context.Context, state RunState) (bool, error) {
delegate := step.delegateFactory.BuildStepDelegate(state)
ctx, span := delegate.StartSpan(ctx, "load_var", tracing.Attrs{
"name": step.plan.Name,
})
ok, err := step.run(ctx, state, delegate)
tracing.End(span, err)
return ok, err
}
func (step *LoadVarStep) run(ctx context.Context, state RunState, delegate BuildStepDelegate) (bool, error) {
logger := lagerctx.FromContext(ctx)
logger = logger.Session("load-var-step", lager.Data{
"step-name": step.plan.Name,
"job-id": step.metadata.JobID,
})
delegate.Initializing(logger)
stdout := delegate.Stdout()
stderr := delegate.Stderr()
fmt.Fprintln(stderr, "\x1b[1;33mWARNING: the load_var step is experimental and subject to change!\x1b[0m")
fmt.Fprintln(stderr, "")
fmt.Fprintln(stderr, "\x1b[33mfollow RFC #27 for updates: https://github.com/concourse/rfcs/pull/27\x1b[0m")
fmt.Fprintln(stderr, "")
delegate.Starting(logger)
value, err := step.fetchVars(ctx, logger, step.plan.File, state)
if err != nil {
return false, err
}
fmt.Fprintf(stdout, "var %s fetched.\n", step.plan.Name)
state.AddLocalVar(step.plan.Name, value, !step.plan.Reveal)
fmt.Fprintf(stdout, "added var %s to build.\n", step.plan.Name)
delegate.Finished(logger, true)
return true, nil
}
func (step *LoadVarStep) fetchVars(
ctx context.Context,
logger lager.Logger,
file string,
state RunState,
) (interface{}, error) {
segs := strings.SplitN(file, "/", 2)
if len(segs) != 2 {
return nil, UnspecifiedLoadVarStepFileError{file}
}
artifactName := segs[0]
filePath := segs[1]
format, err := step.fileFormat(file)
if err != nil {
return nil, err
}
logger.Debug("figure-out-format", lager.Data{"format": format})
art, found := state.ArtifactRepository().ArtifactFor(build.ArtifactName(artifactName))
if !found {
return nil, artifact.UnknownArtifactSourceError{
Name: artifactName,
Path: filePath,
}
}
stream, err := step.artifactStreamer.StreamFileFromArtifact(lagerctx.NewContext(ctx, logger), art, filePath)
if err != nil {
if err == baggageclaim.ErrFileNotFound {
return nil, artifact.FileNotFoundError{
Name: artifactName,
FilePath: filePath,
}
}
return nil, err
}
fileContent, err := ioutil.ReadAll(stream)
if err != nil {
return nil, err
}
var value interface{}
switch format {
case "json":
decoder := json.NewDecoder(bytes.NewReader(fileContent))
decoder.UseNumber()
err = decoder.Decode(&value)
if err != nil {
return nil, InvalidLocalVarFile{file, "json", err}
}
if decoder.More() {
return nil, InvalidLocalVarFile{file, "json", errors.New("invalid json: characters found after top-level value")}
}
case "yml", "yaml":
err = yaml.Unmarshal(fileContent, &value, useJSONNumber)
if err != nil {
return nil, InvalidLocalVarFile{file, "yaml", err}
}
case "trim":
value = strings.TrimSpace(string(fileContent))
case "raw":
value = string(fileContent)
default:
return nil, fmt.Errorf("unknown format %s, should never happen, ", format)
}
return value, nil
}
func useJSONNumber(decoder *json.Decoder) *json.Decoder {
decoder.UseNumber()
return decoder
}
func (step *LoadVarStep) fileFormat(file string) (string, error) {
if step.isValidFormat(step.plan.Format) {
return step.plan.Format, nil
} else if step.plan.Format != "" {
return "", fmt.Errorf("invalid format %s", step.plan.Format)
}
fileExt := filepath.Ext(file)
format := strings.TrimPrefix(fileExt, ".")
if step.isValidFormat(format) {
return format, nil
}
return "trim", nil
}
func (step *LoadVarStep) isValidFormat(format string) bool {
switch format {
case "raw", "trim", "yml", "yaml", "json":
return true
}
return false
}