templating/base_render_options.go (212 lines of code) (raw):

// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. package templating import ( "context" "encoding/json" "fmt" "regexp" "strings" "time" "github.com/Azure/acr-builder/graph" "github.com/Azure/acr-builder/secretmgmt" "github.com/pkg/errors" ) var shellEscapePattern = regexp.MustCompile(`[^\w_^@=+%,:./-]`) // BaseRenderOptions represents additional information for the composition of the final rendering. type BaseRenderOptions struct { // Path to the task file. TaskFile string // Base64 encoded task file. Base64EncodedTaskFile string // Path to a values file. ValuesFile string // Base64 encoded values file. Base64EncodedValuesFile string // Override values. TemplateValues []string // ID is a unique identifier for the run. ID string // Commit is the SHA the run was triggered against. Commit string // Repository is the repository the run was triggered against. Repository string // Branch is the branch the run was triggered against. Branch string // TriggeredBy is the reason the run was triggered. TriggeredBy string // GitTag is the git tag the run was triggered against. GitTag string // Registry is the container registry being used. Registry string // Date is the UTC date of the run. Date time.Time // SharedVolume is the name of the shared volume. SharedVolume string // OS is the GOOS. OS string // OSVersion is a specific version of the OS. // For example, on Windows it could be 1903. OSVersion string // Architecture is the GOARCH. Architecture string // SecretResolveTimeout is the timeout for resolving a secret during rendering. SecretResolveTimeout time.Duration // TaskName is the name of the Task executing this run TaskName string } // OverrideValuesWithBuildInfo overrides the specified config's values and provides a default set of values. func OverrideValuesWithBuildInfo(c1 *Config, c2 *Config, opts *BaseRenderOptions) (Values, error) { base := map[string]interface{}{ "Build": map[string]interface{}{ "ID": opts.ID, }, "Run": map[string]interface{}{ "ID": opts.ID, "Commit": opts.Commit, "Repository": opts.Repository, "Branch": opts.Branch, "GitTag": opts.GitTag, "TriggeredBy": opts.TriggeredBy, "Registry": opts.Registry, "RegistryName": parseRegistryName(opts.Registry), "Date": opts.Date.Format("20060102-150405z"), // yyyyMMdd-HHmmssz "SharedVolume": opts.SharedVolume, "OS": opts.OS, "OSVersion": opts.OSVersion, "Architecture": opts.Architecture, "TaskName": opts.TaskName, }, } vals, err := OverrideValues(c1, c2) if err != nil { return base, err } valsJSON, err := json.Marshal(vals) if err != nil { return base, errors.Wrap(err, "failed to serialize Values") } runJSON, err := json.Marshal(base["Run"]) if err != nil { return base, errors.Wrap(err, "failed to serialize Run") } base["Values"] = vals base["ValuesJSON"] = shellQuote(string(valsJSON)) base["RunJSON"] = shellQuote(string(runJSON)) return base, nil } // LoadAndRenderBuildSteps loads a template file for build and renders it according to an optional values file, --set values, // and base render options. func LoadAndRenderBuildSteps(_ context.Context, template *Template, opts *BaseRenderOptions) (string, error) { // load steps and override values mergedVals, err := loadSteps(template, opts) if err != nil { return "", fmt.Errorf("error while loading build steps: %v", err) } engine := NewEngine() rendered, err := engine.Render(template, mergedVals) if err != nil { return "", fmt.Errorf("error while rendering templates: %v", err) } if rendered == "" { return "", errors.New("rendered template was empty") } return rendered, nil } // LoadAndRenderSteps loads a template file for exec and renders it according to an optional values file, --set values, // and base render options. func LoadAndRenderSteps(ctx context.Context, template *Template, opts *BaseRenderOptions) (string, error) { // load steps and override values mergedVals, err := loadSteps(template, opts) if err != nil { return "", fmt.Errorf("error while loading exec steps: %v", err) } // return empty rendered string for an empty template. if mergedVals == nil { return "", nil } engine := NewEngine() // we will pass nil for the secret resolve override so as to use the default resolve function. secrets, err := renderAndResolveSecrets(ctx, template, engine, nil, opts, mergedVals) if err != nil { return "", fmt.Errorf("failed to resolve secrets in the task with error: %v", err) } // update the secrets collection with resolved secrets. mergedVals["Secrets"] = secrets rendered, err := engine.Render(template, mergedVals) if err != nil { return "", fmt.Errorf("error while rendering templates: %v", err) } if rendered == "" { return "", errors.New("rendered template was empty") } return rendered, nil } // loadSteps loads a template file and overrides values with build info func loadSteps(template *Template, opts *BaseRenderOptions) (Values, error) { // return empty values list for an empty template. if len(template.GetData()) == 0 { return nil, nil } var err error config := &Config{} if opts.ValuesFile != "" { if config, err = LoadConfig(opts.ValuesFile); err != nil { return nil, err } } else if opts.Base64EncodedValuesFile != "" { if config, err = DecodeConfig(opts.Base64EncodedValuesFile); err != nil { return nil, err } } setConfig := &Config{} if len(opts.TemplateValues) > 0 { var rawVals string rawVals, err = parseValues(opts.TemplateValues) if err != nil { return nil, err } setConfig = &Config{RawValue: rawVals, Values: map[string]*Value{}} } mergedVals, err := OverrideValuesWithBuildInfo(config, setConfig, opts) if err != nil { return nil, fmt.Errorf("failed to override values: %v", err) } return mergedVals, nil } // renderAndResolveSecrets parses the secrets in the template, resolves them using vault providers and returns the resolved secret values. func renderAndResolveSecrets( ctx context.Context, template *Template, templateEngine *Engine, resolveSecretFunc secretmgmt.ResolveSecretFunc, opts *BaseRenderOptions, sourceValues Values) (Values, error) { result := Values{} // Cheap optimization to skip the secrets merging if the task definition file doesn't contain "secrets" string in it. Note that the task can // have the string secrets but may not essentially the secrets section. if !strings.Contains(string(template.Data), "secrets") { return result, nil } // At first render the template with existing values to render templatized values for secrets. sourceValues["Secrets"] = result rendered, err := templateEngine.Render(template, sourceValues) if err != nil { return result, errors.Wrap(err, "failed to render the template") } if rendered == "" { return result, errors.New("rendered template was empty") } // Unmarshall the template to Task and get all secrets defined in the template. task, err := graph.NewTaskFromString(rendered) if err != nil { return result, errors.Wrap(err, "failed to parse template to create task") } // If no secrets found return. if len(task.Secrets) == 0 { return result, nil } secretResolver, err := secretmgmt.NewSecretResolver(resolveSecretFunc, opts.SecretResolveTimeout) if err != nil { return result, errors.Wrap(err, "failed to create secret resolver") } err = secretResolver.ResolveSecrets(ctx, task.Secrets) if err != nil { return result, err } for _, s := range task.Secrets { result[s.ID] = s.ResolvedValue } return result, nil } // parseValues receives a slice of values in key=val format // and serializes them into YAML. If a key is specified more // than once, the key will be overridden. func parseValues(values []string) (string, error) { ret := Values{} for _, v := range values { i := strings.Index(v, "=") if i < 0 { return "", errors.New("failed to parse --set data; invalid format, no = assignment found") } key := v[:i] if key == "" { return "", errors.New("failed to parse --set data; expected a key=val format") } val := v[i+1:] // Skip the = separator ret[key] = val } return ret.ToYAMLString() } // parseRegistryName parses the fully qualified registry name and extracts only the registry name. // NB: This function is currently designed and provided for Azure Container Registry and may not // work as expected for all other registries' formats. func parseRegistryName(fullyQualifiedRegistryName string) string { idx := strings.Index(fullyQualifiedRegistryName, ".") if idx < 0 { return fullyQualifiedRegistryName } return fullyQualifiedRegistryName[:idx] } // shellQuote detects whether or not the string needs to be escaped and escapes double quotes and wraps them // with single quotes if necessary. func shellQuote(str string) string { if str == "" { return "''" } if shellEscapePattern.MatchString(str) { return "'" + strings.Replace(str, "'", "'\"'\"'", -1) + "'" } return str }