Skip to content

Commit 73e73e8

Browse files
committed
feat(STONEINTG-1659): define NudgeConfig CRD with stateless CEL valid
- Add NudgeConfig CRD types in api/v1beta2 (NudgeConfig, NudgeRelationship,NudgeConfigSpec, NudgeConfigStatus) with NudgeModeType enum (immediate/validated) - Enforce singleton naming via CEL rule (must be named "nudge-config") - Reject self-nudges (from == to) and duplicate (from, to) pairs via CEL - Cap nudges at maxItems=256 and component names at maxLength=63 to stay within the API server's CEL cost budget for the O(n²) duplicate rule - Default mode to "immediate" via kubebuilder schema defaulting - Bump ENVTEST_K8S_VERSION 1.23 → 1.29 (CEL validation GA) - Generate CRD YAML, deepcopy, RBAC roles, and sample manifests - Add pure Go struct tests (7 cases) and envtest CEL validation tests (8 cases) Assicted_By: Claude, Opus 4.6 Signed-off-by: Kasem Alem <kalem@redhat.com>
1 parent f8ebf94 commit 73e73e8

15 files changed

Lines changed: 1304 additions & 57 deletions

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ endif
5252
# Image URL to use all building/pushing image targets
5353
IMG ?= $(IMAGE_TAG_BASE):$(TAG_NAME)
5454
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
55-
ENVTEST_K8S_VERSION = 1.23
55+
ENVTEST_K8S_VERSION = 1.29
5656

5757
# container engine to use. Defaults to docker
5858
CONT_ENGINE ?= docker

api/v1beta2/integrationtestscenario_suite_test.go

Lines changed: 1 addition & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,16 @@ package v1beta2
1818

1919
import (
2020
"context"
21-
"crypto/tls"
22-
"fmt"
2321
"go/build"
24-
"net"
2522
"path/filepath"
2623
"testing"
27-
"time"
28-
29-
"sigs.k8s.io/controller-runtime/pkg/metrics/server"
30-
crwebhook "sigs.k8s.io/controller-runtime/pkg/webhook"
3124

3225
. "github.com/onsi/ginkgo/v2"
3326
. "github.com/onsi/gomega"
3427

3528
applicationapiv1alpha1 "github.com/konflux-ci/application-api/api/v1alpha1"
3629
toolkit "github.com/konflux-ci/operator-toolkit/test"
37-
admissionv1 "k8s.io/api/admission/v1"
3830
"k8s.io/apimachinery/pkg/runtime"
39-
"k8s.io/client-go/rest"
40-
ctrl "sigs.k8s.io/controller-runtime"
4131
"sigs.k8s.io/controller-runtime/pkg/client"
4232
"sigs.k8s.io/controller-runtime/pkg/envtest"
4333
logf "sigs.k8s.io/controller-runtime/pkg/log"
@@ -49,7 +39,6 @@ var (
4939
testEnv *envtest.Environment
5040
ctx context.Context
5141
cancel context.CancelFunc
52-
cfg *rest.Config
5342
)
5443

5544
func TestIntegrationAPIs(t *testing.T) {
@@ -72,23 +61,16 @@ var _ = BeforeSuite(func() {
7261
),
7362
},
7463
ErrorIfCRDPathMissing: false,
75-
WebhookInstallOptions: envtest.WebhookInstallOptions{
76-
Paths: []string{filepath.Join("..", "..", "config", "webhook")},
77-
},
7864
}
7965

8066
var err error
81-
// cfg is defined in this file globally.
82-
cfg, err = testEnv.Start()
67+
cfg, err := testEnv.Start()
8368
Expect(err).NotTo(HaveOccurred())
8469
Expect(cfg).NotTo(BeNil())
8570

8671
scheme := runtime.NewScheme()
8772
Expect(AddToScheme(scheme)).To(Succeed())
8873

89-
err = admissionv1.AddToScheme(scheme)
90-
Expect(err).NotTo(HaveOccurred())
91-
9274
err = applicationapiv1alpha1.AddToScheme(scheme)
9375
Expect(err).NotTo(HaveOccurred())
9476

@@ -97,43 +79,6 @@ var _ = BeforeSuite(func() {
9779
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme})
9880
Expect(err).NotTo(HaveOccurred())
9981
Expect(k8sClient).NotTo(BeNil())
100-
101-
// start webhook server using Manager
102-
webhookInstallOptions := &testEnv.WebhookInstallOptions
103-
mgr, err := ctrl.NewManager(cfg, ctrl.Options{
104-
Scheme: scheme,
105-
WebhookServer: crwebhook.NewServer(crwebhook.Options{
106-
CertDir: webhookInstallOptions.LocalServingCertDir,
107-
Host: webhookInstallOptions.LocalServingHost,
108-
Port: webhookInstallOptions.LocalServingPort,
109-
}),
110-
Metrics: server.Options{
111-
BindAddress: "0", // disables metrics
112-
},
113-
LeaderElection: false,
114-
})
115-
Expect(err).NotTo(HaveOccurred())
116-
117-
//+kubebuilder:scaffold:webhook
118-
119-
go func() {
120-
defer GinkgoRecover()
121-
err = mgr.Start(ctx)
122-
Expect(err).NotTo(HaveOccurred())
123-
}()
124-
125-
// wait for the webhook server to get ready
126-
dialer := &net.Dialer{Timeout: time.Second}
127-
addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort)
128-
Eventually(func() error {
129-
conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true})
130-
if err != nil {
131-
return err
132-
}
133-
conn.Close()
134-
return nil
135-
}).Should(Succeed())
136-
13782
})
13883

13984
var _ = AfterSuite(func() {

api/v1beta2/nudgeconfig_types.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
Copyright 2026 Red Hat Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v1beta2
18+
19+
import (
20+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
21+
)
22+
23+
// NudgeModeType defines when a nudge is triggered.
24+
// +kubebuilder:validation:Enum=immediate;validated
25+
type NudgeModeType string
26+
27+
const (
28+
// NudgeModeImmediate triggers the nudge as soon as the source component build succeeds.
29+
NudgeModeImmediate NudgeModeType = "immediate"
30+
31+
// NudgeModeValidated triggers the nudge only after integration tests pass for the source component.
32+
NudgeModeValidated NudgeModeType = "validated"
33+
)
34+
35+
// NudgeConfigSingletonName is the required name for the singleton NudgeConfig per namespace.
36+
const NudgeConfigSingletonName = "nudge-config"
37+
38+
// NudgeRelationship defines a single nudge from one component to another.
39+
type NudgeRelationship struct {
40+
// From is the source component name that triggers the nudge when its build succeeds.
41+
// +kubebuilder:validation:MinLength=1
42+
// +kubebuilder:validation:MaxLength=63
43+
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`
44+
// +required
45+
From string `json:"from"`
46+
47+
// To is the target component name that receives the nudge.
48+
// +kubebuilder:validation:MinLength=1
49+
// +kubebuilder:validation:MaxLength=63
50+
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`
51+
// +required
52+
To string `json:"to"`
53+
54+
// Mode defines when the nudge is triggered.
55+
// "immediate" triggers on build success; "validated" triggers after integration tests pass.
56+
// +kubebuilder:default=immediate
57+
// +optional
58+
Mode NudgeModeType `json:"mode,omitempty"`
59+
60+
// GatingGroup is reserved for Phase 2 group-based gating and is not enforced in Phase 1.
61+
// +kubebuilder:validation:MaxLength=63
62+
// +optional
63+
GatingGroup string `json:"gatingGroup,omitempty"`
64+
}
65+
66+
// NudgeConfigSpec defines the desired nudging relationships between components.
67+
// +kubebuilder:validation:XValidation:rule="!has(self.nudges) || self.nudges.all(n, n.from != n.to)",message="self-nudge not allowed: from and to must be different"
68+
// +kubebuilder:validation:XValidation:rule="!has(self.nudges) || self.nudges.all(i, self.nudges.exists_one(j, i.from == j.from && i.to == j.to))",message="duplicate (from, to) pair not allowed"
69+
type NudgeConfigSpec struct {
70+
// Nudges is the list of component nudge relationships.
71+
// +kubebuilder:validation:MaxItems=256
72+
// +optional
73+
Nudges []NudgeRelationship `json:"nudges,omitempty"`
74+
}
75+
76+
// NudgeConfigStatus defines the observed state of NudgeConfig.
77+
type NudgeConfigStatus struct {
78+
// Conditions represent the latest available observations of the NudgeConfig's state.
79+
// +optional
80+
Conditions []metav1.Condition `json:"conditions,omitempty"`
81+
82+
// LastValidationTime is the timestamp of the last successful validation of the nudge graph.
83+
// +optional
84+
LastValidationTime *metav1.Time `json:"lastValidationTime,omitempty"`
85+
}
86+
87+
// +kubebuilder:object:root=true
88+
// +kubebuilder:resource:shortName=nc
89+
// +kubebuilder:subresource:status
90+
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
91+
// +kubebuilder:storageversion
92+
// +kubebuilder:validation:XValidation:rule="self.metadata.name == 'nudge-config'",message="NudgeConfig must be named 'nudge-config' (singleton per namespace)"
93+
94+
// NudgeConfig is a namespace-scoped singleton CRD that stores component nudging relationships.
95+
// Exactly one NudgeConfig named "nudge-config" may exist per namespace.
96+
// Validation rules are enforced stateless by the API server via CEL expressions with no webhook overhead.
97+
type NudgeConfig struct {
98+
metav1.TypeMeta `json:",inline"`
99+
metav1.ObjectMeta `json:"metadata,omitempty"`
100+
101+
Spec NudgeConfigSpec `json:"spec,omitempty"`
102+
Status NudgeConfigStatus `json:"status,omitempty"`
103+
}
104+
105+
// +kubebuilder:object:root=true
106+
107+
// NudgeConfigList contains a list of NudgeConfigs.
108+
type NudgeConfigList struct {
109+
metav1.TypeMeta `json:",inline"`
110+
metav1.ListMeta `json:"metadata,omitempty"`
111+
Items []NudgeConfig `json:"items"`
112+
}
113+
114+
func init() {
115+
SchemeBuilder.Register(&NudgeConfig{}, &NudgeConfigList{})
116+
}

0 commit comments

Comments
 (0)