Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ spec:
type: object
spec:
properties:
disabled:
description: enable or disable manual approval gate feature
type: boolean
options:
description: options holds additions fields and these fields will
be updated on the manifests
Expand Down
43 changes: 43 additions & 0 deletions config/base/generated-crds/operator.tekton.dev_tektonconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,49 @@ spec:
type: string
type: object
type: array
required:
- options
type: object
manualApproval:
description: ManualApproval holds the customizable options for the
ManualApprovalGate component
properties:
disabled:
description: enable or disable manual approval gate feature
type: boolean
options:
description: options holds additions fields and these fields will
be updated on the manifests
properties:
configMaps:
x-kubernetes-preserve-unknown-fields: true
deployments:
x-kubernetes-preserve-unknown-fields: true
disabled:
type: boolean
horizontalPodAutoscalers:
x-kubernetes-preserve-unknown-fields: true
statefulSets:
x-kubernetes-preserve-unknown-fields: true
webhookConfigurationOptions:
additionalProperties:
description: WebhookOptions defines options for webhooks
properties:
failurePolicy:
description: FailurePolicyType specifies a failure policy
that defines how unrecognized errors from the admission
endpoint are handled.
type: string
sideEffects:
description: SideEffectClass specifies the types of
side effects a webhook may have.
type: string
timeoutSeconds:
format: int32
type: integer
type: object
type: object
type: object
type: object
multiclusterProxyAAE:
description: MulticlusterProxyAAE holds the customizable options for
Expand Down
20 changes: 20 additions & 0 deletions docs/TektonConfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Other than the above components depending on the platform operator also provides
- On both Kubernetes and OpenShift
- [TektonChain](./TektonChain.md)
- [TektonResult](./TektonResult.md)
- [ManualApprovalGate](./ManualApprovalGate.md)
- On Kubernetes
- [TektonDashboard](./TektonDashboard.md)
- [OpenShiftPipelinesAsCode](./OpenShiftPipelinesAsCode.md) (installed via `spec.platforms.kubernetes.pipelinesAsCode`; same CRD/kind as on OpenShift)
Expand Down Expand Up @@ -47,6 +48,8 @@ spec:
priorityClassName: system-cluster-critical
chain:
disabled: false
manualApproval:
disabled: true
pipeline:
await-sidecar-readiness: true
coschedule: workspaces
Expand Down Expand Up @@ -305,6 +308,23 @@ chain:
transparency.url: #value
```

### Manual Approval Gate

Manual Approval Gate section allows user to enable or disable the [ManualApprovalGate](./ManualApprovalGate.md) component through TektonConfig. When enabled, the operator installs and manages the ManualApprovalGate CR automatically.

Example:

```yaml
manualApproval:
disabled: true # - `disabled` : if the value set as `true`, ManualApprovalGate will not be installed (default: `true`)
options:
disabled: false
deployments: {}
```

- `disabled`: if set to `true`, the ManualApprovalGate component will not be installed. Default is `true` (disabled).
- `options`: allows customizing the ManualApprovalGate deployments and configmaps. See [Additional fields as options](#additional-fields-as-options) for details.

### Result

Result section allows user to customize the Tekton Result component, Refer to [Result Spec](https://github.com/tektoncd/operator/blob/main/docs/TektonResult.md#spec) section in TektonResult for available options.
Expand Down
34 changes: 34 additions & 0 deletions pkg/apis/operator/v1alpha1/manualapprovalgate_defaults.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
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 v1alpha1

import "context"

func (mag *ManualApprovalGate) SetDefaults(_ context.Context) {
mag.Spec.ManualApproval.setDefaults()
}

func (m *ManualApproval) setDefaults() {
if m.Disabled == nil {
disabled := true
m.Disabled = &disabled
}
}

func (m *ManualApproval) IsDisabled() bool {
return m.Disabled == nil || *m.Disabled
}
109 changes: 109 additions & 0 deletions pkg/apis/operator/v1alpha1/manualapprovalgate_defaults_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
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 v1alpha1

import (
"context"
"testing"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestSetDefaultsManualApprovalGate(t *testing.T) {
mag := &ManualApprovalGate{
ObjectMeta: metav1.ObjectMeta{
Name: ManualApprovalGates,
},
Spec: ManualApprovalGateSpec{
CommonSpec: CommonSpec{
TargetNamespace: "tekton-pipelines",
},
},
}

mag.SetDefaults(context.TODO())

if mag.Spec.ManualApproval.Disabled == nil {
t.Error("expected Disabled to be set, got nil")
}
if !*mag.Spec.ManualApproval.Disabled {
t.Error("expected Disabled to default to true, got false")
}
}

func TestSetDefaultsManualApprovalGate_DisabledAlreadySet(t *testing.T) {
disabled := false
mag := &ManualApprovalGate{
ObjectMeta: metav1.ObjectMeta{
Name: ManualApprovalGates,
},
Spec: ManualApprovalGateSpec{
CommonSpec: CommonSpec{
TargetNamespace: "tekton-pipelines",
},
ManualApproval: ManualApproval{
Disabled: &disabled,
},
},
}

mag.SetDefaults(context.TODO())

if mag.Spec.ManualApproval.Disabled == nil {
t.Error("expected Disabled to remain set, got nil")
}
if *mag.Spec.ManualApproval.Disabled {
t.Error("expected Disabled to remain false, got true")
}
}

func TestManualApprovalIsDisabled(t *testing.T) {
tests := []struct {
name string
disabled *bool
want bool
}{
{
name: "nil defaults to disabled",
disabled: nil,
want: true,
},
{
name: "explicitly disabled",
disabled: boolPtr(true),
want: true,
},
{
name: "explicitly enabled",
disabled: boolPtr(false),
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := &ManualApproval{Disabled: tt.disabled}
if got := m.IsDisabled(); got != tt.want {
t.Errorf("IsDisabled() = %v, want %v", got, tt.want)
}
})
}
}

func boolPtr(b bool) *bool {
return &b
}
3 changes: 3 additions & 0 deletions pkg/apis/operator/v1alpha1/manualapprovalgate_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ type ManualApprovalGateSpec struct {
}

type ManualApproval struct {
// enable or disable manual approval gate feature
// +optional
Disabled *bool `json:"disabled,omitempty"`
// options holds additions fields and these fields will be updated on the manifests
// +optional
Options AdditionalOptions `json:"options"`
Expand Down
39 changes: 39 additions & 0 deletions pkg/apis/operator/v1alpha1/manualapprovalgate_validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
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 v1alpha1

import (
"context"
"fmt"

"knative.dev/pkg/apis"
)

func (mag *ManualApprovalGate) Validate(ctx context.Context) (errs *apis.FieldError) {
if apis.IsInDelete(ctx) {
return nil
}

if mag.GetName() != ManualApprovalGates {
errMsg := fmt.Sprintf("metadata.name, Only one instance of ManualApprovalGate is allowed by name, %s", ManualApprovalGates)
errs = errs.Also(apis.ErrInvalidValue(mag.GetName(), errMsg))
}

errs = errs.Also(mag.Spec.CommonSpec.validate("spec"))

return errs
}
85 changes: 85 additions & 0 deletions pkg/apis/operator/v1alpha1/manualapprovalgate_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
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 v1alpha1

import (
"context"
"testing"

"gotest.tools/v3/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"knative.dev/pkg/apis"
)

func TestValidateManualApprovalGate_ValidConfig(t *testing.T) {
mag := &ManualApprovalGate{
ObjectMeta: metav1.ObjectMeta{
Name: ManualApprovalGates,
},
Spec: ManualApprovalGateSpec{
CommonSpec: CommonSpec{
TargetNamespace: "tekton-pipelines",
},
},
}

err := mag.Validate(context.TODO())
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
}

func TestValidateManualApprovalGate_InvalidResourceName(t *testing.T) {
mag := &ManualApprovalGate{
ObjectMeta: metav1.ObjectMeta{
Name: "invalid-name",
},
Spec: ManualApprovalGateSpec{
CommonSpec: CommonSpec{
TargetNamespace: "tekton-pipelines",
},
},
}

err := mag.Validate(context.TODO())
assert.ErrorContains(t, err, "Only one instance of ManualApprovalGate is allowed")
}

func TestValidateManualApprovalGate_MissingTargetNamespace(t *testing.T) {
mag := &ManualApprovalGate{
ObjectMeta: metav1.ObjectMeta{
Name: ManualApprovalGates,
},
Spec: ManualApprovalGateSpec{},
}

err := mag.Validate(context.TODO())
assert.ErrorContains(t, err, "missing field(s): spec.targetNamespace")
}

func TestValidateManualApprovalGate_SkipOnDelete(t *testing.T) {
mag := &ManualApprovalGate{
ObjectMeta: metav1.ObjectMeta{
Name: "invalid-name",
},
}

err := mag.Validate(apis.WithinDelete(context.TODO()))
if err != nil {
t.Errorf("expected no error on delete, got: %v", err)
}
}
1 change: 1 addition & 0 deletions pkg/apis/operator/v1alpha1/tektonconfig_defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func (tc *TektonConfig) SetDefaults(ctx context.Context) {
tc.Spec.Result.setDefaults()
tc.Spec.TektonPruner.SetDefaults()
tc.Spec.Scheduler.SetDefaults()
tc.Spec.ManualApproval.setDefaults()

if IsOpenShiftPlatform() {
// PAC may appear under spec.platforms.kubernetes if the mutating webhook ran without
Expand Down
3 changes: 3 additions & 0 deletions pkg/apis/operator/v1alpha1/tektonconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ type TektonConfigSpec struct {
// Chain holds the customizable option for chains component
// +optional
Chain Chain `json:"chain,omitempty"`
// ManualApproval holds the customizable options for the ManualApprovalGate component
// +optional
ManualApproval ManualApproval `json:"manualApproval,omitempty"`
// Result holds the customize option for results component
// +optional
Result Result `json:"result,omitempty"`
Expand Down
Loading
Loading