diff --git a/config/300-crds/300-pipelinerun.yaml b/config/300-crds/300-pipelinerun.yaml index 6fb4a179738..ebbacb060d5 100644 --- a/config/300-crds/300-pipelinerun.yaml +++ b/config/300-crds/300-pipelinerun.yaml @@ -2364,6 +2364,8 @@ spec: type: boolean enableKubernetesSidecar: type: boolean + enableNotices: + type: boolean enableParamEnum: type: boolean enableProvenanceInStatus: @@ -2744,6 +2746,8 @@ spec: type: boolean enableKubernetesSidecar: type: boolean + enableNotices: + type: boolean enableParamEnum: type: boolean enableProvenanceInStatus: @@ -3017,6 +3021,8 @@ spec: type: boolean enableKubernetesSidecar: type: boolean + enableNotices: + type: boolean enableParamEnum: type: boolean enableProvenanceInStatus: @@ -5463,6 +5469,8 @@ spec: type: boolean enableKubernetesSidecar: type: boolean + enableNotices: + type: boolean enableParamEnum: type: boolean enableProvenanceInStatus: diff --git a/config/300-crds/300-taskrun.yaml b/config/300-crds/300-taskrun.yaml index 1aa0dc32834..22c55532c69 100644 --- a/config/300-crds/300-taskrun.yaml +++ b/config/300-crds/300-taskrun.yaml @@ -1879,6 +1879,8 @@ spec: type: boolean enableKubernetesSidecar: type: boolean + enableNotices: + type: boolean enableParamEnum: type: boolean enableProvenanceInStatus: @@ -2152,6 +2154,8 @@ spec: type: boolean enableKubernetesSidecar: type: boolean + enableNotices: + type: boolean enableParamEnum: type: boolean enableProvenanceInStatus: @@ -3996,6 +4000,48 @@ spec: type: description: Type of condition. type: string + notices: + description: Notices are structured messages emitted by controllers that do not affect the task's success/failure status. + type: array + items: + description: Notice represents a structured message emitted by a controller that does not affect the run's success/failure status. + type: object + required: + - level + - message + properties: + file: + description: |- + File is the source file path related to this notice. + Used by VCS integrations to create inline annotations. + Maximum length: 256 characters. + type: string + maxLength: 256 + level: + description: |- + Level indicates the severity of the notice. + Valid values: "info", "warning". + type: string + enum: + - info + - warning + message: + description: |- + Message is the human-readable notice text. + Maximum length: 1024 characters. + type: string + maxLength: 1024 + startLine: + description: |- + StartLine is the starting line number in the source file (1-based). + Pointer type so that absence (nil) is distinguishable from line 0. + type: integer + step: + description: |- + Step is the name of the step that emitted this notice. + Empty for controller-emitted notices. + type: string + x-kubernetes-list-type: atomic observedGeneration: description: |- ObservedGeneration is the 'Generation' of the Service that @@ -4033,6 +4079,8 @@ spec: type: boolean enableKubernetesSidecar: type: boolean + enableNotices: + type: boolean enableParamEnum: type: boolean enableProvenanceInStatus: @@ -4298,6 +4346,8 @@ spec: type: boolean enableKubernetesSidecar: type: boolean + enableNotices: + type: boolean enableParamEnum: type: boolean enableProvenanceInStatus: diff --git a/config/config-feature-flags.yaml b/config/config-feature-flags.yaml index 0faa1e55702..aeea74cb2ba 100644 --- a/config/config-feature-flags.yaml +++ b/config/config-feature-flags.yaml @@ -118,6 +118,8 @@ data: # Setting this flag to "true" will enable the use of Artifacts in Steps # This feature is in preview mode and not implemented yet. Please check #7693 for updates. enable-artifacts: "false" + # Setting this flag to "true" will enable controller notices in TaskRun status. + enable-notices: "false" # Setting this flag to "true" will enable the built-in param input validation via param enum. enable-param-enum: "false" # Setting this flag to "pipeline,pipelinerun,taskrun" will prevent users from creating diff --git a/docs/pipeline-api.md b/docs/pipeline-api.md index 2444f3e737c..f20d864e480 100644 --- a/docs/pipeline-api.md +++ b/docs/pipeline-api.md @@ -424,6 +424,44 @@ _Appears in:_ | `params` _[Params](#params)_ | Params is a list of parameters used to fan out the pipelineTask
Params takes only `Parameters` of type `"array"`
Each array element is supplied to the `PipelineTask` by substituting `params` of type `"string"` in the underlying `Task`.
The names of the `params` in the `Matrix` must match the names of the `params` in the underlying `Task` that they will be substituting. | | | +#### Notice + + + +Notice represents a structured message emitted by a controller that does not affect the run's success/failure status. + + + +_Appears in:_ +- [TaskRunStatus](#taskrunstatus) +- [TaskRunStatusFields](#taskrunstatusfields) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `level` _[NoticeLevel](#noticelevel)_ | Level indicates the severity of the notice.
Valid values: "info", "warning". | | Enum: [info warning]
| +| `message` _string_ | Message is the human-readable notice text.
Maximum length: 1024 characters. | | MaxLength: 1024
| +| `step` _string_ | Step is the name of the step that emitted this notice.
Empty for controller-emitted notices. | | Optional: \{\}
| +| `file` _string_ | File is the source file path related to this notice.
Used by VCS integrations to create inline annotations.
Maximum length: 256 characters. | | MaxLength: 256
Optional: \{\}
| +| `startLine` _integer_ | StartLine is the starting line number in the source file (1-based).
Pointer type so that absence (nil) is distinguishable from line 0. | | Optional: \{\}
| + + +#### NoticeLevel + +_Underlying type:_ _string_ + +NoticeLevel indicates the severity of a notice. + + + +_Appears in:_ +- [Notice](#notice) + +| Field | Description | +| --- | --- | +| `info` | NoticeLevelInfo represents an informational message, no action needed.
| +| `warning` | NoticeLevelWarning represents something to address, but not blocking.
| + + #### OnErrorType _Underlying type:_ _string_ @@ -1578,6 +1616,7 @@ _Appears in:_ | `retriesStatus` _[RetriesStatus](#retriesstatus)_ | RetriesStatus contains the history of TaskRunStatus in case of a retry in order to keep record of failures.
All TaskRunStatus stored in RetriesStatus will have no date within the RetriesStatus as is redundant. | | Schemaless: \{\}
Optional: \{\}
| | `results` _[TaskRunResult](#taskrunresult) array_ | Results are the list of results written out by the task's containers | | Optional: \{\}
| | `artifacts` _[Artifacts](#artifacts)_ | Artifacts are the list of artifacts written out by the task's containers | | Optional: \{\}
| +| `notices` _[Notice](#notice) array_ | Notices are structured messages emitted by controllers that do not affect the task's success/failure status. | | Optional: \{\}
| | `sidecars` _[SidecarState](#sidecarstate) array_ | The list has one entry per sidecar in the manifest. Each entry is
represents the imageid of the corresponding sidecar. | | | | `taskSpec` _[TaskSpec](#taskspec)_ | TaskSpec contains the Spec from the dereferenced Task definition used to instantiate this TaskRun. | | | | `provenance` _[Provenance](#provenance)_ | Provenance contains some key authenticated metadata about how a software artifact was built (what sources, what inputs/outputs, etc.). | | Optional: \{\}
| @@ -1606,6 +1645,7 @@ _Appears in:_ | `retriesStatus` _[RetriesStatus](#retriesstatus)_ | RetriesStatus contains the history of TaskRunStatus in case of a retry in order to keep record of failures.
All TaskRunStatus stored in RetriesStatus will have no date within the RetriesStatus as is redundant. | | Schemaless: \{\}
Optional: \{\}
| | `results` _[TaskRunResult](#taskrunresult) array_ | Results are the list of results written out by the task's containers | | Optional: \{\}
| | `artifacts` _[Artifacts](#artifacts)_ | Artifacts are the list of artifacts written out by the task's containers | | Optional: \{\}
| +| `notices` _[Notice](#notice) array_ | Notices are structured messages emitted by controllers that do not affect the task's success/failure status. | | Optional: \{\}
| | `sidecars` _[SidecarState](#sidecarstate) array_ | The list has one entry per sidecar in the manifest. Each entry is
represents the imageid of the corresponding sidecar. | | | | `taskSpec` _[TaskSpec](#taskspec)_ | TaskSpec contains the Spec from the dereferenced Task definition used to instantiate this TaskRun. | | | | `provenance` _[Provenance](#provenance)_ | Provenance contains some key authenticated metadata about how a software artifact was built (what sources, what inputs/outputs, etc.). | | Optional: \{\}
| diff --git a/pkg/apis/config/feature_flags.go b/pkg/apis/config/feature_flags.go index c2de63f06f8..026086b0bfd 100644 --- a/pkg/apis/config/feature_flags.go +++ b/pkg/apis/config/feature_flags.go @@ -101,6 +101,8 @@ const ( EnableCELInWhenExpression = "enable-cel-in-whenexpression" // EnableArtifacts is the flag to enable the use of Artifacts in Steps EnableArtifacts = "enable-artifacts" + // EnableNotices is the flag to enable controller notices in TaskRun status + EnableNotices = "enable-notices" // EnableParamEnum is the flag to enabled enum in params EnableParamEnum = "enable-param-enum" // EnableConciseResolverSyntax is the flag to enable concise resolver syntax @@ -172,6 +174,13 @@ var ( Enabled: DefaultAlphaFeatureEnabled, } + // DefaultEnableNotices is the default PerFeatureFlag value for EnableNotices + DefaultEnableNotices = PerFeatureFlag{ + Name: EnableNotices, + Stability: AlphaAPIFields, + Enabled: DefaultAlphaFeatureEnabled, + } + // DefaultEnableParamEnum is the default PerFeatureFlag value for EnableParamEnum DefaultEnableParamEnum = PerFeatureFlag{ Name: EnableParamEnum, @@ -230,6 +239,7 @@ type FeatureFlags struct { EnableStepActions bool `json:"enableStepActions,omitempty"` EnableParamEnum bool `json:"enableParamEnum,omitempty"` EnableArtifacts bool `json:"enableArtifacts,omitempty"` + EnableNotices bool `json:"enableNotices,omitempty"` DisableInlineSpec string `json:"disableInlineSpec,omitempty"` EnableConciseResolverSyntax bool `json:"enableConciseResolverSyntax,omitempty"` EnableKubernetesSidecar bool `json:"enableKubernetesSidecar,omitempty"` @@ -335,6 +345,9 @@ func NewFeatureFlagsFromMap(cfgMap map[string]string) (*FeatureFlags, error) { if err := setPerFeatureFlag(EnableArtifacts, DefaultEnableArtifacts, &tc.EnableArtifacts); err != nil { return nil, err } + if err := setPerFeatureFlag(EnableNotices, DefaultEnableNotices, &tc.EnableNotices); err != nil { + return nil, err + } if err := setFeatureInlineSpec(cfgMap, DisableInlineSpec, DefaultDisableInlineSpec, &tc.DisableInlineSpec); err != nil { return nil, err diff --git a/pkg/apis/config/feature_flags_test.go b/pkg/apis/config/feature_flags_test.go index 14079efd734..22b57e3ad5b 100644 --- a/pkg/apis/config/feature_flags_test.go +++ b/pkg/apis/config/feature_flags_test.go @@ -76,6 +76,7 @@ func TestNewFeatureFlagsFromConfigMap(t *testing.T) { Coschedule: config.CoscheduleDisabled, EnableCELInWhenExpression: true, EnableArtifacts: true, + EnableNotices: true, EnableParamEnum: true, DisableInlineSpec: "pipeline,pipelinerun,taskrun", EnableConciseResolverSyntax: true, @@ -294,6 +295,9 @@ func TestNewFeatureFlagsConfigMapErrors(t *testing.T) { }, { fileName: "feature-flags-invalid-enable-artifacts", want: `failed parsing feature flags config "invalid": strconv.ParseBool: parsing "invalid": invalid syntax for feature enable-artifacts`, + }, { + fileName: "feature-flags-invalid-enable-notices", + want: `failed parsing feature flags config "invalid": strconv.ParseBool: parsing "invalid": invalid syntax for feature enable-notices`, }, { fileName: "feature-flags-invalid-enable-concise-resolver-syntax", want: `failed parsing feature flags config "invalid": strconv.ParseBool: parsing "invalid": invalid syntax for feature enable-concise-resolver-syntax`, diff --git a/pkg/apis/config/testdata/feature-flags-all-flags-set.yaml b/pkg/apis/config/testdata/feature-flags-all-flags-set.yaml index 67f0e2db80c..ca448ac6fc8 100644 --- a/pkg/apis/config/testdata/feature-flags-all-flags-set.yaml +++ b/pkg/apis/config/testdata/feature-flags-all-flags-set.yaml @@ -35,6 +35,7 @@ data: enable-cel-in-whenexpression: "true" enable-param-enum: "true" enable-artifacts: "true" + enable-notices: "true" disable-inline-spec: "pipeline,pipelinerun,taskrun" enable-concise-resolver-syntax: "true" enable-kubernetes-sidecar: "true" diff --git a/pkg/apis/config/testdata/feature-flags-invalid-enable-notices.yaml b/pkg/apis/config/testdata/feature-flags-invalid-enable-notices.yaml new file mode 100644 index 00000000000..ecd402fee01 --- /dev/null +++ b/pkg/apis/config/testdata/feature-flags-invalid-enable-notices.yaml @@ -0,0 +1,21 @@ +# Copyright 2026 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 +# +# 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. + +apiVersion: v1 +kind: ConfigMap +metadata: + name: feature-flags + namespace: tekton-pipelines +data: + enable-notices: "invalid" diff --git a/pkg/apis/pipeline/v1/notice_types.go b/pkg/apis/pipeline/v1/notice_types.go new file mode 100644 index 00000000000..a49ada2666c --- /dev/null +++ b/pkg/apis/pipeline/v1/notice_types.go @@ -0,0 +1,94 @@ +/* +Copyright 2026 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 v1 + +const ( + maxControllerNotices = 10 + maxNoticeMessageLen = 1024 + maxNoticeFileLen = 256 +) + +// NoticeLevel indicates the severity of a notice. +type NoticeLevel string + +const ( + // NoticeLevelInfo represents an informational message, no action needed. + NoticeLevelInfo NoticeLevel = "info" + // NoticeLevelWarning represents something to address, but not blocking. + NoticeLevelWarning NoticeLevel = "warning" +) + +// AllNoticeLevels can be used for NoticeLevel validation. +var AllNoticeLevels = []NoticeLevel{NoticeLevelInfo, NoticeLevelWarning} + +// Notice represents a structured message emitted by a controller that does not affect the run's success/failure status. +type Notice struct { + // Level indicates the severity of the notice. + // Valid values: "info", "warning". + // +kubebuilder:validation:Enum=info;warning + Level NoticeLevel `json:"level"` + + // Message is the human-readable notice text. + // Maximum length: 1024 characters. + // +kubebuilder:validation:MaxLength=1024 + Message string `json:"message"` + + // Step is the name of the step that emitted this notice. + // Empty for controller-emitted notices. + // +optional + Step string `json:"step,omitempty"` + + // File is the source file path related to this notice. + // Used by VCS integrations to create inline annotations. + // Maximum length: 256 characters. + // +optional + // +kubebuilder:validation:MaxLength=256 + File string `json:"file,omitempty"` + + // StartLine is the starting line number in the source file (1-based). + // Pointer type so that absence (nil) is distinguishable from line 0. + // +optional + StartLine *int `json:"startLine,omitempty"` +} + +// AddControllerNotice appends a bounded, deduplicated controller notice to TaskRun status. +func (trs *TaskRunStatus) AddControllerNotice(notice Notice) { + if notice.Level != NoticeLevelInfo && notice.Level != NoticeLevelWarning { + return + } + notice.Message = truncateString(notice.Message, maxNoticeMessageLen) + notice.File = truncateString(notice.File, maxNoticeFileLen) + notice.Step = "" + + for _, existing := range trs.Notices { + if existing.Level == notice.Level && existing.Message == notice.Message { + return + } + } + if len(trs.Notices) >= maxControllerNotices { + return + } + trs.Notices = append(trs.Notices, notice) +} + +func truncateString(value string, limit int) string { + runes := []rune(value) + if len(runes) <= limit { + return value + } + return string(runes[:limit]) +} diff --git a/pkg/apis/pipeline/v1/notice_types_test.go b/pkg/apis/pipeline/v1/notice_types_test.go new file mode 100644 index 00000000000..ae2f537e016 --- /dev/null +++ b/pkg/apis/pipeline/v1/notice_types_test.go @@ -0,0 +1,58 @@ +/* +Copyright 2026 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 v1 + +import ( + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/tektoncd/pipeline/test/diff" +) + +func TestTaskRunStatusAddControllerNotice(t *testing.T) { + longMessage := strings.Repeat("m", 1100) + longFile := strings.Repeat("f", 300) + trs := &TaskRunStatus{} + + trs.AddControllerNotice(Notice{Level: NoticeLevelWarning, Message: "warn", Step: "ignored"}) + trs.AddControllerNotice(Notice{Level: NoticeLevelWarning, Message: "warn"}) + trs.AddControllerNotice(Notice{Level: NoticeLevelInfo, Message: longMessage, File: longFile}) + trs.AddControllerNotice(Notice{Level: NoticeLevel("bad"), Message: "drop"}) + + want := []Notice{{ + Level: NoticeLevelWarning, + Message: "warn", + }, { + Level: NoticeLevelInfo, + Message: strings.Repeat("m", 1024), + File: strings.Repeat("f", 256), + }} + if d := cmp.Diff(want, trs.Notices); d != "" { + t.Fatal(diff.PrintWantGot(d)) + } +} + +func TestTaskRunStatusAddControllerNoticeCaps(t *testing.T) { + trs := &TaskRunStatus{} + for i := range maxControllerNotices + 1 { + trs.AddControllerNotice(Notice{Level: NoticeLevelWarning, Message: string(rune('a' + i))}) + } + if got := len(trs.Notices); got != maxControllerNotices { + t.Fatalf("notices length = %d, want %d", got, maxControllerNotices) + } +} diff --git a/pkg/apis/pipeline/v1/openapi_generated.go b/pkg/apis/pipeline/v1/openapi_generated.go index 6cbcb68b5e7..036c09f53c5 100644 --- a/pkg/apis/pipeline/v1/openapi_generated.go +++ b/pkg/apis/pipeline/v1/openapi_generated.go @@ -37,6 +37,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.EmbeddedTask": schema_pkg_apis_pipeline_v1_EmbeddedTask(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.IncludeParams": schema_pkg_apis_pipeline_v1_IncludeParams(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Matrix": schema_pkg_apis_pipeline_v1_Matrix(ref), + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Notice": schema_pkg_apis_pipeline_v1_Notice(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Param": schema_pkg_apis_pipeline_v1_Param(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.ParamSpec": schema_pkg_apis_pipeline_v1_ParamSpec(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.ParamValue": schema_pkg_apis_pipeline_v1_ParamValue(ref), @@ -848,6 +849,57 @@ func schema_pkg_apis_pipeline_v1_Matrix(ref common.ReferenceCallback) common.Ope } } +func schema_pkg_apis_pipeline_v1_Notice(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Notice represents a structured message emitted by a controller that does not affect the run's success/failure status.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "level": { + SchemaProps: spec.SchemaProps{ + Description: "Level indicates the severity of the notice. Valid values: \"info\", \"warning\".", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "message": { + SchemaProps: spec.SchemaProps{ + Description: "Message is the human-readable notice text. Maximum length: 1024 characters.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "step": { + SchemaProps: spec.SchemaProps{ + Description: "Step is the name of the step that emitted this notice. Empty for controller-emitted notices.", + Type: []string{"string"}, + Format: "", + }, + }, + "file": { + SchemaProps: spec.SchemaProps{ + Description: "File is the source file path related to this notice. Used by VCS integrations to create inline annotations. Maximum length: 256 characters.", + Type: []string{"string"}, + Format: "", + }, + }, + "startLine": { + SchemaProps: spec.SchemaProps{ + Description: "StartLine is the starting line number in the source file (1-based). Pointer type so that absence (nil) is distinguishable from line 0.", + Type: []string{"integer"}, + Format: "int32", + }, + }, + }, + Required: []string{"level", "message"}, + }, + }, + } +} + func schema_pkg_apis_pipeline_v1_Param(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -4233,6 +4285,25 @@ func schema_pkg_apis_pipeline_v1_TaskRunStatus(ref common.ReferenceCallback) com Ref: ref("github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Artifacts"), }, }, + "notices": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "Notices are structured messages emitted by controllers that do not affect the task's success/failure status.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Notice"), + }, + }, + }, + }, + }, "sidecars": { VendorExtensible: spec.VendorExtensible{ Extensions: spec.Extensions{ @@ -4285,7 +4356,7 @@ func schema_pkg_apis_pipeline_v1_TaskRunStatus(ref common.ReferenceCallback) com }, }, Dependencies: []string{ - "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Artifacts", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Provenance", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.SidecarState", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.StepState", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.TaskRunResult", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.TaskRunStatus", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.TaskSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.Time", "knative.dev/pkg/apis.Condition"}, + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Artifacts", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Notice", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Provenance", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.SidecarState", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.StepState", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.TaskRunResult", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.TaskRunStatus", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.TaskSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.Time", "knative.dev/pkg/apis.Condition"}, } } @@ -4374,6 +4445,25 @@ func schema_pkg_apis_pipeline_v1_TaskRunStatusFields(ref common.ReferenceCallbac Ref: ref("github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Artifacts"), }, }, + "notices": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "Notices are structured messages emitted by controllers that do not affect the task's success/failure status.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Notice"), + }, + }, + }, + }, + }, "sidecars": { VendorExtensible: spec.VendorExtensible{ Extensions: spec.Extensions{ @@ -4426,7 +4516,7 @@ func schema_pkg_apis_pipeline_v1_TaskRunStatusFields(ref common.ReferenceCallbac }, }, Dependencies: []string{ - "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Artifacts", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Provenance", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.SidecarState", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.StepState", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.TaskRunResult", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.TaskRunStatus", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.TaskSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.Time"}, + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Artifacts", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Notice", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Provenance", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.SidecarState", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.StepState", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.TaskRunResult", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.TaskRunStatus", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.TaskSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.Time"}, } } diff --git a/pkg/apis/pipeline/v1/swagger.json b/pkg/apis/pipeline/v1/swagger.json index a0964b1d49b..4cb8ce3d394 100644 --- a/pkg/apis/pipeline/v1/swagger.json +++ b/pkg/apis/pipeline/v1/swagger.json @@ -384,6 +384,39 @@ } } }, + "v1.Notice": { + "description": "Notice represents a structured message emitted by a controller that does not affect the run's success/failure status.", + "type": "object", + "required": [ + "level", + "message" + ], + "properties": { + "file": { + "description": "File is the source file path related to this notice. Used by VCS integrations to create inline annotations. Maximum length: 256 characters.", + "type": "string" + }, + "level": { + "description": "Level indicates the severity of the notice. Valid values: \"info\", \"warning\".", + "type": "string", + "default": "" + }, + "message": { + "description": "Message is the human-readable notice text. Maximum length: 1024 characters.", + "type": "string", + "default": "" + }, + "startLine": { + "description": "StartLine is the starting line number in the source file (1-based). Pointer type so that absence (nil) is distinguishable from line 0.", + "type": "integer", + "format": "int32" + }, + "step": { + "description": "Step is the name of the step that emitted this notice. Empty for controller-emitted notices.", + "type": "string" + } + } + }, "v1.Param": { "description": "Param declares an ParamValues to use for the parameter called name.", "type": "object", @@ -2174,6 +2207,15 @@ "x-kubernetes-patch-merge-key": "type", "x-kubernetes-patch-strategy": "merge" }, + "notices": { + "description": "Notices are structured messages emitted by controllers that do not affect the task's success/failure status.", + "type": "array", + "items": { + "default": {}, + "$ref": "#/definitions/v1.Notice" + }, + "x-kubernetes-list-type": "atomic" + }, "observedGeneration": { "description": "ObservedGeneration is the 'Generation' of the Service that was last processed by the controller.", "type": "integer", @@ -2256,6 +2298,15 @@ "description": "CompletionTime is the time the build completed.", "$ref": "#/definitions/v1.Time" }, + "notices": { + "description": "Notices are structured messages emitted by controllers that do not affect the task's success/failure status.", + "type": "array", + "items": { + "default": {}, + "$ref": "#/definitions/v1.Notice" + }, + "x-kubernetes-list-type": "atomic" + }, "podName": { "description": "PodName is the name of the pod responsible for executing this task's steps.", "type": "string", diff --git a/pkg/apis/pipeline/v1/taskrun_types.go b/pkg/apis/pipeline/v1/taskrun_types.go index d6f92c72f89..dd0c7154d5a 100644 --- a/pkg/apis/pipeline/v1/taskrun_types.go +++ b/pkg/apis/pipeline/v1/taskrun_types.go @@ -345,6 +345,11 @@ type TaskRunStatusFields struct { // +optional Artifacts *Artifacts `json:"artifacts,omitempty"` + // Notices are structured messages emitted by controllers that do not affect the task's success/failure status. + // +optional + // +listType=atomic + Notices []Notice `json:"notices,omitempty"` + // The list has one entry per sidecar in the manifest. Each entry is // represents the imageid of the corresponding sidecar. // +listType=atomic diff --git a/pkg/apis/pipeline/v1/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1/zz_generated.deepcopy.go index 3cddb19a5dc..fb27e6a8b44 100644 --- a/pkg/apis/pipeline/v1/zz_generated.deepcopy.go +++ b/pkg/apis/pipeline/v1/zz_generated.deepcopy.go @@ -275,6 +275,27 @@ func (in *Matrix) DeepCopy() *Matrix { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Notice) DeepCopyInto(out *Notice) { + *out = *in + if in.StartLine != nil { + in, out := &in.StartLine, &out.StartLine + *out = new(int) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Notice. +func (in *Notice) DeepCopy() *Notice { + if in == nil { + return nil + } + out := new(Notice) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Param) DeepCopyInto(out *Param) { *out = *in @@ -2025,6 +2046,13 @@ func (in *TaskRunStatusFields) DeepCopyInto(out *TaskRunStatusFields) { *out = new(Artifacts) (*in).DeepCopyInto(*out) } + if in.Notices != nil { + in, out := &in.Notices, &out.Notices + *out = make([]Notice, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.Sidecars != nil { in, out := &in.Sidecars, &out.Sidecars *out = make([]SidecarState, len(*in)) diff --git a/pkg/reconciler/taskrun/taskrun.go b/pkg/reconciler/taskrun/taskrun.go index db18f0f9070..14d0d3a03eb 100644 --- a/pkg/reconciler/taskrun/taskrun.go +++ b/pkg/reconciler/taskrun/taskrun.go @@ -576,6 +576,12 @@ func (c *Reconciler) prepare(ctx context.Context, tr *v1.TaskRun) (*v1.TaskSpec, Status: corev1.ConditionFalse, Message: taskMeta.VerificationResult.Err.Error(), }) + if config.FromContextOrDefaults(ctx).FeatureFlags.EnableNotices { + tr.Status.AddControllerNotice(v1.Notice{ + Level: v1.NoticeLevelWarning, + Message: taskMeta.VerificationResult.Err.Error(), + }) + } case trustedresources.VerificationPass: tr.Status.SetCondition(&apis.Condition{ Type: trustedresources.ConditionTrustedResourcesVerified, diff --git a/pkg/reconciler/taskrun/taskrun_test.go b/pkg/reconciler/taskrun/taskrun_test.go index ff6c1bca65b..472a165fcf7 100644 --- a/pkg/reconciler/taskrun/taskrun_test.go +++ b/pkg/reconciler/taskrun/taskrun_test.go @@ -7278,8 +7278,10 @@ status: name string task []*v1.Task noMatchPolicy string + enableNotices bool verificationPolicies []*v1alpha1.VerificationPolicy wantTrustedResourcesCondition *apis.Condition + wantNotices []v1.Notice }{ { name: "ignore no match policy", @@ -7291,6 +7293,16 @@ status: noMatchPolicy: config.WarnNoMatchPolicy, verificationPolicies: noMatchPolicy, wantTrustedResourcesCondition: failNoMatchCondition, + }, { + name: "warn no match policy with notices enabled", + noMatchPolicy: config.WarnNoMatchPolicy, + enableNotices: true, + verificationPolicies: noMatchPolicy, + wantTrustedResourcesCondition: failNoMatchCondition, + wantNotices: []v1.Notice{{ + Level: v1.NoticeLevelWarning, + Message: failNoMatchCondition.Message, + }}, }, { name: "pass enforce policy", noMatchPolicy: config.FailNoMatchPolicy, @@ -7313,9 +7325,12 @@ status: }, }, } + if tc.enableNotices { + cms[0].Data[config.EnableNotices] = "true" + } rr := getResolvedResolutionRequest(t, resolverName, signedTaskBytes, tr.Namespace, tr.Name) d := test.Data{ - TaskRuns: []*v1.TaskRun{tr}, + TaskRuns: []*v1.TaskRun{tr.DeepCopy()}, ConfigMaps: cms, VerificationPolicies: tc.verificationPolicies, ResolutionRequests: []*resolutionv1beta1.ResolutionRequest{&rr}, @@ -7344,6 +7359,9 @@ status: if d := cmp.Diff(tc.wantTrustedResourcesCondition, gotVerificationCondition, ignoreLastTransitionTime); d != "" { t.Error(diff.PrintWantGot(d)) } + if d := cmp.Diff(tc.wantNotices, reconciledRun.Status.Notices); d != "" { + t.Error(diff.PrintWantGot(d)) + } }) } } diff --git a/test/featureflags.go b/test/featureflags.go index a9356b9dbd8..0cd7f28fafe 100644 --- a/test/featureflags.go +++ b/test/featureflags.go @@ -117,6 +117,7 @@ func getFeatureFlagsBaseOnAPIFlag(t *testing.T) *config.FeatureFlags { "enable-cel-in-whenexpression": "true", "enable-param-enum": "true", "enable-artifacts": "true", + "enable-notices": "true", "enable-concise-resolver-syntax": "true", "enable-kubernetes-sidecar": "true", "keep-pod-on-cancel": "true", diff --git a/test/per_feature_flags_test.go b/test/per_feature_flags_test.go index 0a927e87ced..e64453cbb5e 100644 --- a/test/per_feature_flags_test.go +++ b/test/per_feature_flags_test.go @@ -44,7 +44,7 @@ const ( ) var ( - alphaFeatureFlags = []string{"enable-param-enum", "keep-pod-enabled-cancel", "enable-cel-in-whenexpression", "enable-artifacts"} + alphaFeatureFlags = []string{"enable-param-enum", "keep-pod-enabled-cancel", "enable-cel-in-whenexpression", "enable-artifacts", "enable-notices"} betaFeatureFlags = []string{} perFeatureFlags = map[string][]string{ "alpha": alphaFeatureFlags,