diff --git a/cmd/entrypoint/main.go b/cmd/entrypoint/main.go index 44c8fe5de3a..26d44860969 100644 --- a/cmd/entrypoint/main.go +++ b/cmd/entrypoint/main.go @@ -30,13 +30,12 @@ import ( "github.com/containerd/containerd/platforms" "github.com/tektoncd/pipeline/cmd/entrypoint/subcommands" - featureFlags "github.com/tektoncd/pipeline/pkg/apis/config" - "github.com/tektoncd/pipeline/pkg/apis/pipeline" - v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1/types" "github.com/tektoncd/pipeline/pkg/credentials" "github.com/tektoncd/pipeline/pkg/credentials/dockercreds" "github.com/tektoncd/pipeline/pkg/credentials/gitcreds" "github.com/tektoncd/pipeline/pkg/entrypoint" + "github.com/tektoncd/pipeline/pkg/entrypoint/pipeline" "github.com/tektoncd/pipeline/pkg/spire" "github.com/tektoncd/pipeline/pkg/spire/config" "github.com/tektoncd/pipeline/pkg/termination" @@ -61,7 +60,7 @@ var ( stepMetadataDir = flag.String("step_metadata_dir", "", "If specified, create directory to store the step metadata e.g. /tekton/steps//") enableSpire = flag.Bool("enable_spire", false, "If specified by configmap, this enables spire signing and verification") socketPath = flag.String("spire_socket_path", "unix:///spiffe-workload-api/spire-agent.sock", "Experimental: The SPIRE agent socket for SPIFFE workload API.") - resultExtractionMethod = flag.String("result_from", featureFlags.ResultExtractionMethodTerminationMessage, "The method using which to extract results from tasks. Default is using the termination message.") + resultExtractionMethod = flag.String("result_from", entrypoint.ResultExtractionMethodTerminationMessage, "The method using which to extract results from tasks. Default is using the termination message.") ) const ( diff --git a/cmd/entrypoint/runner.go b/cmd/entrypoint/runner.go index 6e137378531..dbcd526480d 100644 --- a/cmd/entrypoint/runner.go +++ b/cmd/entrypoint/runner.go @@ -31,7 +31,11 @@ import ( "syscall" "github.com/tektoncd/pipeline/pkg/entrypoint" - "github.com/tektoncd/pipeline/pkg/pod" + // "github.com/tektoncd/pipeline/pkg/pod" +) + +const ( + TektonHermeticEnvVar = "TEKTON_HERMETIC" ) // TODO(jasonhall): Test that original exit code is propagated and that @@ -111,7 +115,7 @@ func (rr *realRunner) Run(ctx context.Context, args ...string) error { // main process and all children cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} - if os.Getenv("TEKTON_RESOURCE_NAME") == "" && os.Getenv(pod.TektonHermeticEnvVar) == "1" { + if os.Getenv("TEKTON_RESOURCE_NAME") == "" && os.Getenv(TektonHermeticEnvVar) == "1" { dropNetworking(cmd) } diff --git a/pkg/apis/config/feature_flags.go b/pkg/apis/config/feature_flags.go index d0db62052cf..ecf63930c69 100644 --- a/pkg/apis/config/feature_flags.go +++ b/pkg/apis/config/feature_flags.go @@ -17,13 +17,10 @@ limitations under the License. package config import ( - "context" "fmt" "os" "strconv" "strings" - - corev1 "k8s.io/api/core/v1" ) const ( @@ -441,21 +438,6 @@ func setVerificationNoMatchPolicy(cfgMap map[string]string, defaultValue string, return nil } -// NewFeatureFlagsFromConfigMap returns a Config for the given configmap -func NewFeatureFlagsFromConfigMap(config *corev1.ConfigMap) (*FeatureFlags, error) { - return NewFeatureFlagsFromMap(config.Data) -} - -// GetVerificationNoMatchPolicy returns the "trusted-resources-verification-no-match-policy" value -func GetVerificationNoMatchPolicy(ctx context.Context) string { - return FromContextOrDefaults(ctx).FeatureFlags.VerificationNoMatchPolicy -} - -// IsSpireEnabled checks if non-falsifiable provenance is enforced through SPIRE -func IsSpireEnabled(ctx context.Context) bool { - return FromContextOrDefaults(ctx).FeatureFlags.EnforceNonfalsifiability == EnforceNonfalsifiabilityWithSpire -} - type PerFeatureFlag struct { // Name of the feature flag Name string diff --git a/pkg/apis/config/featureflags_validation.go b/pkg/apis/config/featureflags_validation.go index 92ca4f2b0b7..66256a34f7f 100644 --- a/pkg/apis/config/featureflags_validation.go +++ b/pkg/apis/config/featureflags_validation.go @@ -1,3 +1,5 @@ +//go:build !disable_tls + /* Copyright 2021 The Tekton Authors diff --git a/pkg/apis/config/metrics.go b/pkg/apis/config/metrics.go index f86d4a136a2..3941d084119 100644 --- a/pkg/apis/config/metrics.go +++ b/pkg/apis/config/metrics.go @@ -18,7 +18,6 @@ package config import ( corev1 "k8s.io/api/core/v1" - "knative.dev/pkg/metrics" ) const ( @@ -109,12 +108,6 @@ type Metrics struct { ThrottleWithNamespace bool } -// GetMetricsConfigName returns the name of the configmap containing all -// customizations for the storage bucket. -func GetMetricsConfigName() string { - return metrics.ConfigMapName() -} - // Equals returns true if two Configs are identical func (cfg *Metrics) Equals(other *Metrics) bool { if cfg == nil && other == nil { diff --git a/pkg/apis/config/metrics_notls.go b/pkg/apis/config/metrics_notls.go new file mode 100644 index 00000000000..ffab8f5f79e --- /dev/null +++ b/pkg/apis/config/metrics_notls.go @@ -0,0 +1,7 @@ +//go:build disable_tls + +package config + +// GetMetricsConfigName returns the name of the configmap containing all +// customizations for the storage bucket. +func GetMetricsConfigName() string { panic("not supported when tls is disabled") } diff --git a/pkg/apis/config/metrics_tls.go b/pkg/apis/config/metrics_tls.go new file mode 100644 index 00000000000..549ba350d02 --- /dev/null +++ b/pkg/apis/config/metrics_tls.go @@ -0,0 +1,31 @@ +//go:build !disable_tls + +package config + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + "knative.dev/pkg/metrics" +) + +// GetMetricsConfigName returns the name of the configmap containing all +// customizations for the storage bucket. +func GetMetricsConfigName() string { + return metrics.ConfigMapName() +} + +// NewFeatureFlagsFromConfigMap returns a Config for the given configmap +func NewFeatureFlagsFromConfigMap(config *corev1.ConfigMap) (*FeatureFlags, error) { + return NewFeatureFlagsFromMap(config.Data) +} + +// GetVerificationNoMatchPolicy returns the "trusted-resources-verification-no-match-policy" value +func GetVerificationNoMatchPolicy(ctx context.Context) string { + return FromContextOrDefaults(ctx).FeatureFlags.VerificationNoMatchPolicy +} + +// IsSpireEnabled checks if non-falsifiable provenance is enforced through SPIRE +func IsSpireEnabled(ctx context.Context) bool { + return FromContextOrDefaults(ctx).FeatureFlags.EnforceNonfalsifiability == EnforceNonfalsifiabilityWithSpire +} diff --git a/pkg/apis/config/resolver/store.go b/pkg/apis/config/resolver/store.go index 62f14af7540..f26c9073008 100644 --- a/pkg/apis/config/resolver/store.go +++ b/pkg/apis/config/resolver/store.go @@ -1,3 +1,5 @@ +//go:build !disable_tls + /* Copyright 2022 The Tekton Authors diff --git a/pkg/apis/config/spire_config.go b/pkg/apis/config/spire_config.go index 4a293f042d5..af876437e0a 100644 --- a/pkg/apis/config/spire_config.go +++ b/pkg/apis/config/spire_config.go @@ -1,3 +1,5 @@ +//go:build !disable_tls + /* Copyright 2022 The Tekton Authors diff --git a/pkg/apis/config/store.go b/pkg/apis/config/store.go index a46de411414..65e9e99895d 100644 --- a/pkg/apis/config/store.go +++ b/pkg/apis/config/store.go @@ -1,3 +1,5 @@ +//go:build !disable_tls + /* Copyright 2019 The Tekton Authors diff --git a/pkg/apis/pipeline/v1/types/artifact_types.go b/pkg/apis/pipeline/v1/types/artifact_types.go new file mode 100644 index 00000000000..1caf57a4a7e --- /dev/null +++ b/pkg/apis/pipeline/v1/types/artifact_types.go @@ -0,0 +1,135 @@ +/* +Copyright 2024 The Tekton Authors + +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 types + +import ( + "github.com/google/go-cmp/cmp" +) + +// Algorithm Standard cryptographic hash algorithm +type Algorithm string + +// Artifact represents an artifact within a system, potentially containing multiple values +// associated with it. +type Artifact struct { + // The artifact's identifying category name + Name string `json:"name,omitempty"` + // A collection of values related to the artifact + Values []ArtifactValue `json:"values,omitempty"` + // Indicate if the artifact is a build output or a by-product + BuildOutput bool `json:"buildOutput,omitempty"` +} + +// ArtifactValue represents a specific value or data element within an Artifact. +type ArtifactValue struct { + Digest map[Algorithm]string `json:"digest,omitempty"` // Algorithm-specific digests for verifying the content (e.g., SHA256) + Uri string `json:"uri,omitempty"` // Location where the artifact value can be retrieved +} + +// TaskRunStepArtifact represents an artifact produced or used by a step within a task run. +// It directly uses the Artifact type for its structure. +type TaskRunStepArtifact = Artifact + +// Artifacts represents the collection of input and output artifacts associated with +// a task run or a similar process. Artifacts in this context are units of data or resources +// that the process either consumes as input or produces as output. +type Artifacts struct { + Inputs []Artifact `json:"inputs,omitempty"` + Outputs []Artifact `json:"outputs,omitempty"` +} + +func (a *Artifacts) Merge(another *Artifacts) { + inputMap := make(map[string][]ArtifactValue) + var newInputs []Artifact + + for _, v := range a.Inputs { + inputMap[v.Name] = v.Values + } + if another != nil { + for _, v := range another.Inputs { + _, ok := inputMap[v.Name] + if !ok { + inputMap[v.Name] = []ArtifactValue{} + } + for _, vv := range v.Values { + exists := false + for _, av := range inputMap[v.Name] { + if cmp.Equal(vv, av) { + exists = true + break + } + } + if !exists { + inputMap[v.Name] = append(inputMap[v.Name], vv) + } + } + } + } + + for k, v := range inputMap { + newInputs = append(newInputs, Artifact{ + Name: k, + Values: v, + }) + } + + outputMap := make(map[string]Artifact) + var newOutputs []Artifact + for _, v := range a.Outputs { + outputMap[v.Name] = v + } + + if another != nil { + for _, v := range another.Outputs { + _, ok := outputMap[v.Name] + if !ok { + outputMap[v.Name] = Artifact{Name: v.Name, Values: []ArtifactValue{}, BuildOutput: v.BuildOutput} + } + // only update buildOutput to true. + // Do not convert to false if it was true before. + if v.BuildOutput { + art := outputMap[v.Name] + art.BuildOutput = v.BuildOutput + outputMap[v.Name] = art + } + for _, vv := range v.Values { + exists := false + for _, av := range outputMap[v.Name].Values { + if cmp.Equal(vv, av) { + exists = true + break + } + } + if !exists { + art := outputMap[v.Name] + art.Values = append(art.Values, vv) + outputMap[v.Name] = art + } + } + } + } + + for _, v := range outputMap { + newOutputs = append(newOutputs, Artifact{ + Name: v.Name, + Values: v.Values, + BuildOutput: v.BuildOutput, + }) + } + a.Inputs = newInputs + a.Outputs = newOutputs +} diff --git a/pkg/apis/pipeline/v1/types/param_types.go b/pkg/apis/pipeline/v1/types/param_types.go new file mode 100644 index 00000000000..1754c991d38 --- /dev/null +++ b/pkg/apis/pipeline/v1/types/param_types.go @@ -0,0 +1,88 @@ +package types + +import ( + "encoding/json" + "strings" +) + +// ParamType indicates the type of an input parameter; +// Used to distinguish between a single string and an array of strings. +type ParamType string + +// Valid ParamTypes: +const ( + ParamTypeString ParamType = "string" + ParamTypeArray ParamType = "array" + ParamTypeObject ParamType = "object" +) + +// AllParamTypes can be used for ParamType validation. +var AllParamTypes = []ParamType{ParamTypeString, ParamTypeArray, ParamTypeObject} + +// ParamValues is modeled after IntOrString in kubernetes/apimachinery: + +// ParamValue is a type that can hold a single string, string array, or string map. +// Used in JSON unmarshalling so that a single JSON field can accept +// either an individual string or an array of strings. +type ParamValue struct { + Type ParamType // Represents the stored type of ParamValues. + StringVal string + // +listType=atomic + ArrayVal []string + ObjectVal map[string]string +} + +// PropertySpec defines the struct for object keys +type PropertySpec struct { + Type ParamType `json:"type,omitempty"` +} + +// ParamsPrefix is the prefix used in $(...) expressions referring to parameters +const ParamsPrefix = "params" + +// ArrayReference returns the name of the parameter from array parameter reference +// returns arrayParam from $(params.arrayParam[*]) +func ArrayReference(a string) string { + return strings.TrimSuffix(strings.TrimPrefix(a, "$("+ParamsPrefix+"."), "[*])") +} + +// UnmarshalJSON implements the json.Unmarshaller interface. +func (paramValues *ParamValue) UnmarshalJSON(value []byte) error { + // ParamValues is used for Results Value as well, the results can be any kind of + // data so we need to check if it is empty. + if len(value) == 0 { + paramValues.Type = ParamTypeString + return nil + } + if value[0] == '[' { + // We're trying to Unmarshal to []string, but for cases like []int or other types + // of nested array which we don't support yet, we should continue and Unmarshal + // it to String. If the Type being set doesn't match what it actually should be, + // it will be captured by validation in reconciler. + // if failed to unmarshal to array, we will convert the value to string and marshal it to string + var a []string + if err := json.Unmarshal(value, &a); err == nil { + paramValues.Type = ParamTypeArray + paramValues.ArrayVal = a + return nil + } + } + if value[0] == '{' { + // if failed to unmarshal to map, we will convert the value to string and marshal it to string + var m map[string]string + if err := json.Unmarshal(value, &m); err == nil { + paramValues.Type = ParamTypeObject + paramValues.ObjectVal = m + return nil + } + } + + // By default we unmarshal to string + paramValues.Type = ParamTypeString + if err := json.Unmarshal(value, ¶mValues.StringVal); err == nil { + return nil + } + paramValues.StringVal = string(value) + + return nil +} diff --git a/pkg/apis/pipeline/v1/types/result_types.go b/pkg/apis/pipeline/v1/types/result_types.go new file mode 100644 index 00000000000..87a92198ffe --- /dev/null +++ b/pkg/apis/pipeline/v1/types/result_types.go @@ -0,0 +1,101 @@ +/* +Copyright 2022 The Tekton Authors +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 types + +import "strings" + +// TaskResult used to describe the results of a task +type TaskResult struct { + // Name the given name + Name string `json:"name"` + + // Type is the user-specified type of the result. The possible type + // is currently "string" and will support "array" in following work. + // +optional + Type ResultsType `json:"type,omitempty"` + + // Properties is the JSON Schema properties to support key-value pairs results. + // +optional + Properties map[string]PropertySpec `json:"properties,omitempty"` + + // Description is a human-readable description of the result + // +optional + Description string `json:"description,omitempty"` + + // Value the expression used to retrieve the value of the result from an underlying Step. + // +optional + Value *ResultValue `json:"value,omitempty"` +} + +// StepResult used to describe the Results of a Step. +// +// This is field is at an BETA stability level and gated by "enable-step-actions" feature flag. +type StepResult struct { + // Name the given name + Name string `json:"name"` + + // The possible types are 'string', 'array', and 'object', with 'string' as the default. + // +optional + Type ResultsType `json:"type,omitempty"` + + // Properties is the JSON Schema properties to support key-value pairs results. + // +optional + Properties map[string]PropertySpec `json:"properties,omitempty"` + + // Description is a human-readable description of the result + // +optional + Description string `json:"description,omitempty"` +} + +// TaskRunResult used to describe the results of a task +type TaskRunResult struct { + // Name the given name + Name string `json:"name"` + + // Type is the user-specified type of the result. The possible type + // is currently "string" and will support "array" in following work. + // +optional + Type ResultsType `json:"type,omitempty"` + + // Value the given value of the result + Value ResultValue `json:"value"` +} + +// TaskRunStepResult is a type alias of TaskRunResult +type TaskRunStepResult = TaskRunResult + +// ResultValue is a type alias of ParamValue +type ResultValue = ParamValue + +// ResultsType indicates the type of a result; +// Used to distinguish between a single string and an array of strings. +// Note that there is ResultType used to find out whether a +// RunResult is from a task result or not, which is different from +// this ResultsType. +type ResultsType string + +// Valid ResultsType: +const ( + ResultsTypeString ResultsType = "string" + ResultsTypeArray ResultsType = "array" + ResultsTypeObject ResultsType = "object" +) + +// AllResultsTypes can be used for ResultsTypes validation. +var AllResultsTypes = []ResultsType{ResultsTypeString, ResultsTypeArray, ResultsTypeObject} + +// ResultsArrayReference returns the reference of the result. e.g. results.resultname from $(results.resultname[*]) +func ResultsArrayReference(a string) string { + return strings.TrimSuffix(strings.TrimSuffix(strings.TrimPrefix(a, "$("), ")"), "[*]") +} diff --git a/pkg/apis/pipeline/v1/types/resultsref.go b/pkg/apis/pipeline/v1/types/resultsref.go new file mode 100644 index 00000000000..b3eb74e1096 --- /dev/null +++ b/pkg/apis/pipeline/v1/types/resultsref.go @@ -0,0 +1,31 @@ +package types + +import ( + "regexp" + "strings" +) + +const ( + // TODO(#2462) use one regex across all substitutions + // variableSubstitutionFormat matches format like $result.resultname, $result.resultname[int] and $result.resultname[*] + variableSubstitutionFormat = `\$\([_a-zA-Z0-9.-]+(\.[_a-zA-Z0-9.-]+)*(\[([0-9]+|\*)\])?\)` +) + +// VariableSubstitutionRegex is a regex to find all result matching substitutions +var VariableSubstitutionRegex = regexp.MustCompile(variableSubstitutionFormat) + +func stripVarSubExpression(expression string) string { + return strings.TrimSuffix(strings.TrimPrefix(expression, "$("), ")") +} + +func validateString(value string) []string { + expressions := VariableSubstitutionRegex.FindAllString(value, -1) + if expressions == nil { + return nil + } + var result []string + for _, expression := range expressions { + result = append(result, stripVarSubExpression(expression)) + } + return result +} diff --git a/pkg/apis/pipeline/v1/types/when_types.go b/pkg/apis/pipeline/v1/types/when_types.go new file mode 100644 index 00000000000..b74ea2f1dbc --- /dev/null +++ b/pkg/apis/pipeline/v1/types/when_types.go @@ -0,0 +1,125 @@ +/* +Copyright 2022 The Tekton Authors + +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 types + +import ( + "fmt" + + "github.com/tektoncd/pipeline/pkg/substitution" + "k8s.io/apimachinery/pkg/selection" +) + +// WhenExpression allows a PipelineTask to declare expressions to be evaluated before the Task is run +// to determine whether the Task should be executed or skipped +type WhenExpression struct { + // Input is the string for guard checking which can be a static input or an output from a parent Task + Input string `json:"input,omitempty"` + + // Operator that represents an Input's relationship to the values + Operator selection.Operator `json:"operator,omitempty"` + + // Values is an array of strings, which is compared against the input, for guard checking + // It must be non-empty + // +listType=atomic + Values []string `json:"values,omitempty"` + + // CEL is a string of Common Language Expression, which can be used to conditionally execute + // the task based on the result of the expression evaluation + // More info about CEL syntax: https://github.com/google/cel-spec/blob/master/doc/langdef.md + // +optional + CEL string `json:"cel,omitempty"` +} + +func (we *WhenExpression) isInputInValues() bool { + for i := range we.Values { + if we.Values[i] == we.Input { + return true + } + } + return false +} + +func (we *WhenExpression) isTrue() bool { + if we.Operator == selection.In { + return we.isInputInValues() + } + // selection.NotIn + return !we.isInputInValues() +} + +func (we *WhenExpression) applyReplacements(replacements map[string]string, arrayReplacements map[string][]string) WhenExpression { + replacedInput := substitution.ApplyReplacements(we.Input, replacements) + replacedCEL := substitution.ApplyReplacements(we.CEL, replacements) + + var replacedValues []string + for _, val := range we.Values { + // arrayReplacements holds a list of array parameters with a pattern - params.arrayParam1 + // array params are referenced using $(params.arrayParam1[*]) + // array results are referenced using $(results.resultname[*]) + // check if the param exist in the arrayReplacements to replace it with a list of values + if _, ok := arrayReplacements[fmt.Sprintf("%s.%s", ParamsPrefix, ArrayReference(val))]; ok { + replacedValues = append(replacedValues, substitution.ApplyArrayReplacements(val, replacements, arrayReplacements)...) + } else if _, ok := arrayReplacements[ResultsArrayReference(val)]; ok { + replacedValues = append(replacedValues, substitution.ApplyArrayReplacements(val, replacements, arrayReplacements)...) + } else { + replacedValues = append(replacedValues, substitution.ApplyReplacements(val, replacements)) + } + } + + return WhenExpression{Input: replacedInput, Operator: we.Operator, Values: replacedValues, CEL: replacedCEL} +} + +// GetVarSubstitutionExpressions extracts all the values between "$(" and ")" in a When Expression +func (we *WhenExpression) GetVarSubstitutionExpressions() ([]string, bool) { + var allExpressions []string + allExpressions = append(allExpressions, validateString(we.Input)...) + allExpressions = append(allExpressions, validateString(we.CEL)...) + for _, value := range we.Values { + allExpressions = append(allExpressions, validateString(value)...) + } + return allExpressions, len(allExpressions) != 0 +} + +// WhenExpressions are used to specify whether a Task should be executed or skipped +// All of them need to evaluate to True for a guarded Task to be executed. +type WhenExpressions []WhenExpression + +type StepWhenExpressions = WhenExpressions + +// AllowsExecution evaluates an Input's relationship to an array of Values, based on the Operator, +// to determine whether all the When Expressions are True. If they are all True, the guarded Task is +// executed, otherwise it is skipped. +// If CEL expression exists, AllowsExecution will get the evaluated results from evaluatedCEL and determine +// if the Task should be skipped. +func (wes WhenExpressions) AllowsExecution(evaluatedCEL map[string]bool) bool { + for _, we := range wes { + if !we.isTrue() || (we.CEL != "" && !evaluatedCEL[we.CEL]) { + return false + } + } + return true +} + +// ReplaceVariables interpolates variables, such as Parameters and Results, in +// the Input and Values. +func (wes WhenExpressions) ReplaceVariables(replacements map[string]string, arrayReplacements map[string][]string) WhenExpressions { + replaced := wes + for i := range wes { + replaced[i] = wes[i].applyReplacements(replacements, arrayReplacements) + } + return replaced +} diff --git a/pkg/entrypoint/entrypointer.go b/pkg/entrypoint/entrypointer.go index b3913665a75..0354e53c99f 100644 --- a/pkg/entrypoint/entrypointer.go +++ b/pkg/entrypoint/entrypointer.go @@ -21,6 +21,8 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" + "log" "os" "os/exec" @@ -32,17 +34,14 @@ import ( "time" "github.com/tektoncd/pipeline/internal/artifactref" - "github.com/tektoncd/pipeline/pkg/apis/config" - "github.com/tektoncd/pipeline/pkg/apis/pipeline" - v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1/types" + "github.com/tektoncd/pipeline/pkg/entrypoint/pipeline" "github.com/tektoncd/pipeline/pkg/internal/resultref" - "github.com/tektoncd/pipeline/pkg/pod" "github.com/tektoncd/pipeline/pkg/result" "github.com/tektoncd/pipeline/pkg/spire" "github.com/tektoncd/pipeline/pkg/termination" "github.com/google/cel-go/cel" - "go.uber.org/zap" ) // RFC3339 with millisecond @@ -53,10 +52,24 @@ const ( ) const ( - breakpointExitSuffix = ".breakpointexit" - breakpointBeforeStepSuffix = ".beforestepexit" + breakpointExitSuffix = ".breakpointexit" + breakpointBeforeStepSuffix = ".beforestepexit" + ResultExtractionMethodTerminationMessage = "termination-message" + TerminationReasonSkipped = "Skipped" + TerminationReasonCancelled = "Cancelled" + TerminationReasonTimeoutExceeded = "TimeoutExceeded" + // DownwardMountCancelFile is cancellation file mount to step, entrypoint will check this file to cancel the step. + downwardMountPoint = "/tekton/downward" + downwardMountCancelFile = "cancel" + stepPrefix = "step-" ) +var DownwardMountCancelFile string + +func init() { + DownwardMountCancelFile = filepath.Join(downwardMountPoint, downwardMountCancelFile) +} + // DebugBeforeStepError is an error means mark before step breakpoint failure type DebugBeforeStepError string @@ -180,15 +193,11 @@ type PostWriter interface { // Go optionally waits for a file, runs the command, and writes a // post file. func (e Entrypointer) Go() error { - prod, _ := zap.NewProduction() - logger := prod.Sugar() - output := []result.RunResult{} defer func() { if wErr := termination.WriteMessage(e.TerminationPath, output); wErr != nil { - logger.Fatalf("Error while writing message: %s", wErr) + log.Fatalf("Error while writing message: %s", wErr) } - _ = logger.Sync() }() if err := os.MkdirAll(filepath.Join(e.StepMetadataDir, "results"), os.ModePerm); err != nil { @@ -212,7 +221,7 @@ func (e Entrypointer) Go() error { }) if errors.Is(err, ErrSkipPreviousStepFailed) { - output = append(output, e.outputRunResult(pod.TerminationReasonSkipped)) + output = append(output, e.outputRunResult(TerminationReasonSkipped)) } return err @@ -237,10 +246,10 @@ func (e Entrypointer) Go() error { var cancel context.CancelFunc if err == nil { if err := e.applyStepResultSubstitutions(pipeline.StepsDir); err != nil { - logger.Error("Error while substituting step results: ", err) + slog.Error("Error while substituting step results: ", slog.Any("error", err)) } if err := e.applyStepArtifactSubstitutions(pipeline.StepsDir); err != nil { - logger.Error("Error while substituting step artifacts: ", err) + slog.Error("Error while substituting step artifacts: ", slog.Any("error", err)) } ctx, cancel = context.WithCancel(ctx) @@ -251,7 +260,7 @@ func (e Entrypointer) Go() error { // start a goroutine to listen for cancellation file go func() { if err := e.waitingCancellation(ctx, cancel); err != nil && (!IsContextCanceledError(err) && !IsContextDeadlineError(err)) { - logger.Error("Error while waiting for cancellation", zap.Error(err)) + slog.Error("Error while waiting for cancellation", slog.Any("error", err)) } }() allowExec, err1 := e.allowExec() @@ -262,8 +271,8 @@ func (e Entrypointer) Go() error { case allowExec: err = e.Runner.Run(ctx, e.Command...) default: - logger.Info("Step was skipped due to when expressions were evaluated to false.") - output = append(output, e.outputRunResult(pod.TerminationReasonSkipped)) + slog.Info("Step was skipped due to when expressions were evaluated to false.") + output = append(output, e.outputRunResult(TerminationReasonSkipped)) e.WritePostFile(e.PostFile, nil) e.WriteExitCodeFile(e.StepMetadataDir, "0") return nil @@ -275,15 +284,15 @@ func (e Entrypointer) Go() error { case err != nil && errors.Is(err, errDebugBeforeStep): e.WritePostFile(e.PostFile, err) case err != nil && errors.Is(err, ErrContextCanceled): - logger.Info("Step was canceling") - output = append(output, e.outputRunResult(pod.TerminationReasonCancelled)) + slog.Info("Step was canceling") + output = append(output, e.outputRunResult(TerminationReasonCancelled)) e.WritePostFile(e.PostFile, ErrContextCanceled) e.WriteExitCodeFile(e.StepMetadataDir, syscall.SIGKILL.String()) case errors.Is(err, ErrContextDeadlineExceeded): e.WritePostFile(e.PostFile, err) - output = append(output, e.outputRunResult(pod.TerminationReasonTimeoutExceeded)) + output = append(output, e.outputRunResult(TerminationReasonTimeoutExceeded)) case err != nil && e.BreakpointOnFailure: - logger.Info("Skipping writing to PostFile") + slog.Info("Skipping writing to PostFile") case e.OnError == ContinueOnError && errors.As(err, &ee): // with continue on error and an ExitError, write non-zero exit code and a post file exitCode := strconv.Itoa(ee.ExitCode()) @@ -311,7 +320,8 @@ func (e Entrypointer) Go() error { resultPath = e.ResultsDirectory } if err := e.readResultsFromDisk(ctx, resultPath, result.TaskRunResultType); err != nil { - logger.Fatalf("Error while handling results: %s", err) + slog.Error("Error while substituting step artifacts: ", slog.Any("error", err)) + return err } } if len(e.StepResults) >= 1 && e.StepResults[0] != "" { @@ -320,12 +330,13 @@ func (e Entrypointer) Go() error { stepResultPath = e.ResultsDirectory } if err := e.readResultsFromDisk(ctx, stepResultPath, result.StepResultType); err != nil { - logger.Fatalf("Error while handling step results: %s", err) + slog.Error("Error while substituting step artifacts: ", slog.Any("error", err)) + return err } } - if e.ResultExtractionMethod == config.ResultExtractionMethodTerminationMessage { - e.appendArtifactOutputs(&output, logger) + if e.ResultExtractionMethod == ResultExtractionMethodTerminationMessage { + e.appendArtifactOutputs(&output) } return err @@ -342,12 +353,12 @@ func readArtifacts(fp string, resultType result.ResultType) ([]result.RunResult, return []result.RunResult{{Key: fp, Value: string(file), ResultType: resultType}}, nil } -func (e Entrypointer) appendArtifactOutputs(output *[]result.RunResult, logger *zap.SugaredLogger) { +func (e Entrypointer) appendArtifactOutputs(output *[]result.RunResult) { // step artifacts fp := filepath.Join(e.StepMetadataDir, "artifacts", "provenance.json") artifacts, err := readArtifacts(fp, result.StepArtifactsResultType) if err != nil { - logger.Fatalf("Error while handling step artifacts: %s", err) + log.Fatalf("Error while handling step artifacts: %s", err) } *output = append(*output, artifacts...) @@ -359,7 +370,7 @@ func (e Entrypointer) appendArtifactOutputs(output *[]result.RunResult, logger * fp = filepath.Join(artifactsDir, "provenance.json") artifacts, err = readArtifacts(fp, result.TaskRunArtifactsResultType) if err != nil { - logger.Fatalf("Error while handling task artifacts: %s", err) + log.Fatalf("Error while handling task artifacts: %s", err) } *output = append(*output, artifacts...) } @@ -453,7 +464,7 @@ func (e Entrypointer) readResultsFromDisk(ctx context.Context, resultDir string, } // push output to termination path - if e.ResultExtractionMethod == config.ResultExtractionMethodTerminationMessage && len(output) != 0 { + if e.ResultExtractionMethod == ResultExtractionMethodTerminationMessage && len(output) != 0 { if err := termination.WriteMessage(e.TerminationPath, output); err != nil { return err } @@ -491,7 +502,7 @@ func (e Entrypointer) WriteExitCodeFile(stepPath, content string) { // waitingCancellation waiting cancellation file, if no error occurs, call cancelFunc to cancel the context func (e Entrypointer) waitingCancellation(ctx context.Context, cancel context.CancelFunc) error { - if err := e.Waiter.Wait(ctx, pod.DownwardMountCancelFile, true, false); err != nil { + if err := e.Waiter.Wait(ctx, DownwardMountCancelFile, true, false); err != nil { return err } cancel() @@ -520,10 +531,15 @@ func (e Entrypointer) CheckForBreakpointOnFailure() { } } +// GetContainerName prefixes the input name with "step-" +func GetContainerName(name string) string { + return fmt.Sprintf("%s%s", stepPrefix, name) +} + // loadStepResult reads the step result file and returns the string, array or object result value. func loadStepResult(stepDir string, stepName string, resultName string) (v1.ResultValue, error) { v := v1.ResultValue{} - fp := getStepResultPath(stepDir, pod.GetContainerName(stepName), resultName) + fp := getStepResultPath(stepDir, GetContainerName(stepName), resultName) fileContents, err := os.ReadFile(fp) if err != nil { return v, err diff --git a/pkg/entrypoint/entrypointer_test.go b/pkg/entrypoint/entrypointer_test.go index b5c423d1376..ff57de20528 100644 --- a/pkg/entrypoint/entrypointer_test.go +++ b/pkg/entrypoint/entrypointer_test.go @@ -33,7 +33,7 @@ import ( "time" "github.com/tektoncd/pipeline/pkg/apis/config" - v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1/types" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" "github.com/tektoncd/pipeline/pkg/pod" "github.com/tektoncd/pipeline/pkg/result" diff --git a/pkg/entrypoint/pipeline/paths.go b/pkg/entrypoint/pipeline/paths.go new file mode 100644 index 00000000000..c60202d3f46 --- /dev/null +++ b/pkg/entrypoint/pipeline/paths.go @@ -0,0 +1,19 @@ +package pipeline + +const ( + // WorkspaceDir is the root directory used for PipelineResources and (by default) Workspaces + WorkspaceDir = "/workspace" + // DefaultResultPath is the path for a task result to create the result file + DefaultResultPath = "/tekton/results" + // HomeDir is the HOME directory of PipelineResources + HomeDir = "/tekton/home" + // CredsDir is the directory where credentials are placed to meet the legacy credentials + // helpers image (aka "creds-init") contract + CredsDir = "/tekton/creds" // #nosec + // StepsDir is the directory used for a step to store any metadata related to the step + StepsDir = "/tekton/steps" + + ScriptDir = "/tekton/scripts" + + ArtifactsDir = "/tekton/artifacts" +) diff --git a/pkg/substitution/replacements.go b/pkg/substitution/replacements.go new file mode 100644 index 00000000000..538c542cf6c --- /dev/null +++ b/pkg/substitution/replacements.go @@ -0,0 +1,45 @@ +package substitution + +import ( + "fmt" + "strings" +) + +// ApplyReplacements returns a string with references to parameters replaced, +// based on the mapping provided in replacements. +// For example, if the input string is "foo: $(params.foo)", and replacements maps "params.foo" to "bar", +// the output would be "foo: bar". +func ApplyReplacements(in string, replacements map[string]string) string { + replacementsList := []string{} + for k, v := range replacements { + replacementsList = append(replacementsList, fmt.Sprintf("$(%s)", k), v) + } + // strings.Replacer does all replacements in one pass, preventing multiple replacements + // See #2093 for an explanation on why we need to do this. + replacer := strings.NewReplacer(replacementsList...) + return replacer.Replace(in) +} + +// ApplyArrayReplacements takes an input string, and output an array of strings related to possible arrayReplacements. If there aren't any +// areas where the input can be split up via arrayReplacements, then just return an array with a single element, +// which is ApplyReplacements(in, replacements). +func ApplyArrayReplacements(in string, stringReplacements map[string]string, arrayReplacements map[string][]string) []string { + for k, v := range arrayReplacements { + stringToReplace := fmt.Sprintf("$(%s)", k) + + // If the input string matches a replacement's key (without padding characters), return the corresponding array. + // Note that the webhook should prevent all instances where this could evaluate to false. + if (strings.Count(in, stringToReplace) == 1) && len(in) == len(stringToReplace) { + return v + } + + // same replace logic for star array expressions + starStringtoReplace := fmt.Sprintf("$(%s[*])", k) + if (strings.Count(in, starStringtoReplace) == 1) && len(in) == len(starStringtoReplace) { + return v + } + } + + // Otherwise return a size-1 array containing the input string with standard stringReplacements applied. + return []string{ApplyReplacements(in, stringReplacements)} +} diff --git a/pkg/substitution/substitution.go b/pkg/substitution/substitution.go index df428c31bf5..78a17b8fe3a 100644 --- a/pkg/substitution/substitution.go +++ b/pkg/substitution/substitution.go @@ -1,3 +1,5 @@ +//go:build !disable_tls + /* Copyright 2019 The Tekton Authors @@ -306,45 +308,6 @@ func matchGroups(matches []string, pattern *regexp.Regexp) map[string]string { return groups } -// ApplyReplacements returns a string with references to parameters replaced, -// based on the mapping provided in replacements. -// For example, if the input string is "foo: $(params.foo)", and replacements maps "params.foo" to "bar", -// the output would be "foo: bar". -func ApplyReplacements(in string, replacements map[string]string) string { - replacementsList := []string{} - for k, v := range replacements { - replacementsList = append(replacementsList, fmt.Sprintf("$(%s)", k), v) - } - // strings.Replacer does all replacements in one pass, preventing multiple replacements - // See #2093 for an explanation on why we need to do this. - replacer := strings.NewReplacer(replacementsList...) - return replacer.Replace(in) -} - -// ApplyArrayReplacements takes an input string, and output an array of strings related to possible arrayReplacements. If there aren't any -// areas where the input can be split up via arrayReplacements, then just return an array with a single element, -// which is ApplyReplacements(in, replacements). -func ApplyArrayReplacements(in string, stringReplacements map[string]string, arrayReplacements map[string][]string) []string { - for k, v := range arrayReplacements { - stringToReplace := fmt.Sprintf("$(%s)", k) - - // If the input string matches a replacement's key (without padding characters), return the corresponding array. - // Note that the webhook should prevent all instances where this could evaluate to false. - if (strings.Count(in, stringToReplace) == 1) && len(in) == len(stringToReplace) { - return v - } - - // same replace logic for star array expressions - starStringtoReplace := fmt.Sprintf("$(%s[*])", k) - if (strings.Count(in, starStringtoReplace) == 1) && len(in) == len(starStringtoReplace) { - return v - } - } - - // Otherwise return a size-1 array containing the input string with standard stringReplacements applied. - return []string{ApplyReplacements(in, stringReplacements)} -} - // TrimArrayIndex replaces all `[i]` and `[*]` to "". func TrimArrayIndex(s string) string { return arrayIndexingRegex.ReplaceAllString(s, "") diff --git a/pkg/termination/parse.go b/pkg/termination/parse.go index 876869c6475..a76fd6ec810 100644 --- a/pkg/termination/parse.go +++ b/pkg/termination/parse.go @@ -1,3 +1,5 @@ +//go:build !disable_tls + /* Copyright 2019 The Tekton Authors