Skip to content

Commit a8d33b0

Browse files
committed
split connection secret from group CEL validation
1 parent c7ac6dc commit a8d33b0

File tree

6 files changed

+239
-72
lines changed

6 files changed

+239
-72
lines changed

tools/openapi2crd/config.yaml

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ spec:
2525
- major_version
2626
- parameters
2727
- connection_secret
28+
- mutual_exclusive_group
2829
- entry
2930
- status
3031
- sensitive_properties
@@ -61,17 +62,6 @@ spec:
6162
skipProperties:
6263
- $.apiKey
6364
- $.organization.links
64-
- majorVersion: v20250312
65-
openAPIRef:
66-
name: v20250312
67-
entry:
68-
schema: "CreateOrganizationRequest"
69-
status:
70-
schema: "CreateOrganizationResponse"
71-
filters:
72-
skipProperties:
73-
- $.apiKey
74-
- $.organization.links
7565
- gvk:
7666
version: v1
7767
kind: Group

tools/openapi2crd/pkg/plugins/catalog.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -136,12 +136,13 @@ func NewCatalog() *Catalog {
136136
"mutual_exclusive_major_versions": &MutualExclusiveMajorVersions{},
137137
},
138138
mapping: map[string]MappingPlugin{
139-
"major_version": &MajorVersion{},
140-
"parameters": &Parameters{},
141-
"entry": &Entry{},
142-
"status": &Status{},
143-
"references": &References{},
144-
"connection_secret": &ConnectionSecret{},
139+
"major_version": &MajorVersion{},
140+
"parameters": &Parameters{},
141+
"entry": &Entry{},
142+
"status": &Status{},
143+
"references": &References{},
144+
"connection_secret": &ConnectionSecret{},
145+
"mutual_exclusive_group": &MutualExclusiveGroup{},
145146
},
146147
property: map[string]PropertyPlugin{
147148
"sensitive_properties": &SensitiveProperties{},

tools/openapi2crd/pkg/plugins/connection_secret.go

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ import (
2525
// It requires the parameters and references plugins to be run first.
2626
type ConnectionSecret struct{}
2727

28-
func (s *ConnectionSecret) Name() string {
28+
func (p *ConnectionSecret) Name() string {
2929
return "connection_secret"
3030
}
3131

32-
func (s *ConnectionSecret) Process(req *MappingProcessorRequest) error {
32+
func (p *ConnectionSecret) Process(req *MappingProcessorRequest) error {
3333
specProps := req.CRD.Spec.Validation.OpenAPIV3Schema.Properties["spec"]
3434

3535
if _, exists := specProps.Properties["connectionSecretRef"]; !exists {
@@ -45,37 +45,22 @@ func (s *ConnectionSecret) Process(req *MappingProcessorRequest) error {
4545
}
4646
}
4747

48-
if specProps.XValidations == nil {
49-
specProps.XValidations = apiextensions.ValidationRules{}
50-
}
51-
5248
version := req.MappingConfig.MajorVersion
5349
versionProps, ok := specProps.Properties[version]
5450
if !ok {
5551
return fmt.Errorf("version %s not found in spec", version)
5652
}
5753

58-
_, gIDExists := versionProps.Properties["groupId"]
59-
if gIDExists {
54+
_, groupIDExists := versionProps.Properties["groupId"]
55+
if groupIDExists {
56+
if specProps.XValidations == nil {
57+
specProps.XValidations = apiextensions.ValidationRules{}
58+
}
6059
specProps.XValidations = append(specProps.XValidations, apiextensions.ValidationRule{
6160
Rule: "(has(self." + version + ".groupId) && has(self.connectionSecretRef)) || (!has(self." + version + ".groupId))",
62-
Message: "connectionSecretRef must be set if groupId is set for version " + version,
61+
Message: fmt.Sprintf("spec.connectionSecretRef must be set if spec.%v.groupId is set.", version),
6362
})
6463
}
65-
66-
if versionProps.XValidations == nil {
67-
versionProps.XValidations = apiextensions.ValidationRules{}
68-
}
69-
70-
_, gRefExists := versionProps.Properties["groupRef"]
71-
if gIDExists || gRefExists {
72-
versionProps.XValidations = append(versionProps.XValidations, apiextensions.ValidationRule{
73-
Rule: "(has(self.groupId) && !has(self.groupRef)) || (!has(self.groupId) && has(self.groupRef))",
74-
Message: "groupId and groupRef are mutually exclusive; only one of them can be set",
75-
})
76-
}
77-
78-
specProps.Properties[version] = versionProps
7964
req.CRD.Spec.Validation.OpenAPIV3Schema.Properties["spec"] = specProps
8065

8166
return nil

tools/openapi2crd/pkg/plugins/connection_secret_test.go

Lines changed: 141 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import (
2121

2222
"github.com/stretchr/testify/assert"
2323
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
24+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25+
"k8s.io/utils/ptr"
2426
)
2527

2628
func TestConnectionSecretName(t *testing.T) {
@@ -30,10 +32,9 @@ func TestConnectionSecretName(t *testing.T) {
3032

3133
func TestConnectionSecretProcess(t *testing.T) {
3234
tests := map[string]struct {
33-
request *MappingProcessorRequest
34-
expectedProperty apiextensions.JSONSchemaProps
35-
expectedValidation apiextensions.ValidationRules
36-
expectedErr error
35+
request *MappingProcessorRequest
36+
expectedProperty apiextensions.JSONSchemaProps
37+
expectedErr error
3738
}{
3839
"add connectionSecretRef property and validations": {
3940
request: mappingRequestWithReferences(t, groupBaseCRDWithParameters(t)),
@@ -67,18 +68,12 @@ func TestConnectionSecretProcess(t *testing.T) {
6768
},
6869
},
6970
},
70-
XValidations: apiextensions.ValidationRules{
71-
apiextensions.ValidationRule{
72-
Rule: "(has(self.groupId) && !has(self.groupRef)) || (!has(self.groupId) && has(self.groupRef))",
73-
Message: "groupId and groupRef are mutually exclusive; only one of them can be set",
74-
},
75-
},
7671
},
7772
},
7873
XValidations: apiextensions.ValidationRules{
7974
apiextensions.ValidationRule{
8075
Rule: "(has(self.v20250312.groupId) && has(self.connectionSecretRef)) || (!has(self.v20250312.groupId))",
81-
Message: "connectionSecretRef must be set if groupId is set for version v20250312",
76+
Message: "spec.connectionSecretRef must be set if spec.v20250312.groupId is set.",
8277
},
8378
},
8479
},
@@ -115,12 +110,6 @@ func TestConnectionSecretProcess(t *testing.T) {
115110
},
116111
},
117112
},
118-
XValidations: apiextensions.ValidationRules{
119-
apiextensions.ValidationRule{
120-
Rule: "(has(self.groupId) && !has(self.groupRef)) || (!has(self.groupId) && has(self.groupRef))",
121-
Message: "groupId and groupRef are mutually exclusive; only one of them can be set",
122-
},
123-
},
124113
},
125114
"v20250219": {
126115
Type: "object",
@@ -138,22 +127,44 @@ func TestConnectionSecretProcess(t *testing.T) {
138127
},
139128
},
140129
},
141-
XValidations: apiextensions.ValidationRules{
142-
apiextensions.ValidationRule{
143-
Rule: "(has(self.groupId) && !has(self.groupRef)) || (!has(self.groupId) && has(self.groupRef))",
144-
Message: "groupId and groupRef are mutually exclusive; only one of them can be set",
145-
},
146-
},
147130
},
148131
},
149132
XValidations: apiextensions.ValidationRules{
150133
apiextensions.ValidationRule{
151134
Rule: "(has(self.v20250219.groupId) && has(self.connectionSecretRef)) || (!has(self.v20250219.groupId))",
152-
Message: "connectionSecretRef must be set if groupId is set for version v20250219",
135+
Message: "spec.connectionSecretRef must be set if spec.v20250219.groupId is set.",
153136
},
154137
apiextensions.ValidationRule{
155138
Rule: "(has(self.v20250312.groupId) && has(self.connectionSecretRef)) || (!has(self.v20250312.groupId))",
156-
Message: "connectionSecretRef must be set if groupId is set for version v20250312",
139+
Message: "spec.connectionSecretRef must be set if spec.v20250312.groupId is set.",
140+
},
141+
},
142+
},
143+
},
144+
"version but not groupId in CRD mappings": {
145+
request: mappingRequestWithReferences(t, orgBaseCRDWithParameters(t)),
146+
expectedProperty: apiextensions.JSONSchemaProps{
147+
Type: "object",
148+
Description: "Specification of the organization supporting the following versions:\n\n- v20250312\n\nAt most one versioned spec can be specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status",
149+
Properties: map[string]apiextensions.JSONSchemaProps{
150+
"connectionSecretRef": {
151+
Type: "object",
152+
Description: "SENSITIVE FIELD\n\nReference to a secret containing the credentials to setup the connection to Atlas.",
153+
Properties: map[string]apiextensions.JSONSchemaProps{
154+
"name": {
155+
Type: "string",
156+
Description: "Name of the secret containing the Atlas credentials.",
157+
},
158+
},
159+
},
160+
"v20250312": {
161+
Type: "object",
162+
Description: "The spec of the organization resource for version v20250312.",
163+
Properties: map[string]apiextensions.JSONSchemaProps{
164+
"orgID": {
165+
Type: "string",
166+
},
167+
},
157168
},
158169
},
159170
},
@@ -228,7 +239,7 @@ func groupBaseCRDMultiVersionWithParameters(t *testing.T) *apiextensions.CustomR
228239
spec.XValidations = apiextensions.ValidationRules{
229240
apiextensions.ValidationRule{
230241
Rule: "(has(self.v20250219.groupId) && has(self.connectionSecretRef)) || (!has(self.v20250219.groupId))",
231-
Message: "connectionSecretRef must be set if groupId is set for version v20250219",
242+
Message: "spec.connectionSecretRef must be set if spec.v20250219.groupId is set.",
232243
},
233244
}
234245
spec.Properties["v20250312"] = apiextensions.JSONSchemaProps{
@@ -264,14 +275,112 @@ func groupBaseCRDMultiVersionWithParameters(t *testing.T) *apiextensions.CustomR
264275
},
265276
},
266277
},
267-
XValidations: apiextensions.ValidationRules{
268-
apiextensions.ValidationRule{
269-
Rule: "(has(self.groupId) && !has(self.groupRef)) || (!has(self.groupId) && has(self.groupRef))",
270-
Message: "groupId and groupRef are mutually exclusive; only one of them can be set",
271-
},
272-
},
273278
}
274279
crd.Spec.Validation.OpenAPIV3Schema.Properties["spec"] = spec
275280

276281
return crd
277282
}
283+
284+
func orgBaseCRDWithParameters(t *testing.T) *apiextensions.CustomResourceDefinition {
285+
t.Helper()
286+
287+
return &apiextensions.CustomResourceDefinition{
288+
ObjectMeta: metav1.ObjectMeta{
289+
Name: "groups.atlas.generated.mongodb.com",
290+
},
291+
Spec: apiextensions.CustomResourceDefinitionSpec{
292+
Group: "atlas.generated.mongodb.com",
293+
Names: apiextensions.CustomResourceDefinitionNames{
294+
Kind: "Organization",
295+
ListKind: "OrganizationList",
296+
Plural: "Organizations",
297+
Singular: "organization",
298+
ShortNames: []string{"ao"},
299+
Categories: []string{"atlas"},
300+
},
301+
Scope: apiextensions.NamespaceScoped,
302+
Versions: []apiextensions.CustomResourceDefinitionVersion{
303+
{
304+
Name: "v1",
305+
Served: true,
306+
Storage: true,
307+
},
308+
},
309+
PreserveUnknownFields: ptr.To(false),
310+
Validation: &apiextensions.CustomResourceValidation{
311+
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
312+
Type: "object",
313+
Description: "A organization, managed by the MongoDB Kubernetes Atlas Operator.",
314+
Properties: map[string]apiextensions.JSONSchemaProps{
315+
"spec": {
316+
Type: "object",
317+
Description: "Specification of the organization supporting the following versions:\n\n- v20250312\n\nAt most one versioned spec can be specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status",
318+
Properties: map[string]apiextensions.JSONSchemaProps{
319+
"v20250312": {
320+
Type: "object",
321+
Description: "The spec of the organization resource for version v20250312.",
322+
Properties: map[string]apiextensions.JSONSchemaProps{
323+
"orgID": {
324+
Type: "string",
325+
},
326+
},
327+
},
328+
},
329+
},
330+
"status": {
331+
Type: "object",
332+
Description: `Most recently observed read-only status of the organization for the specified resource version. This data may not be up to date and is populated by the system. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status`,
333+
Properties: map[string]apiextensions.JSONSchemaProps{
334+
"conditions": {
335+
Type: "array",
336+
Description: "Represents the latest available observations of a resource's current state.",
337+
Items: &apiextensions.JSONSchemaPropsOrArray{
338+
Schema: &apiextensions.JSONSchemaProps{
339+
Type: "object",
340+
Properties: map[string]apiextensions.JSONSchemaProps{
341+
"type": {
342+
Type: "string",
343+
Description: "Type of condition.",
344+
},
345+
"status": {
346+
Type: "string",
347+
Description: "Status of the condition, one of True, False, Unknown.",
348+
},
349+
"lastTransitionTime": {
350+
Type: "string",
351+
Format: "date-time",
352+
Description: "Last time the condition transitioned from one status to another.",
353+
},
354+
"reason": {
355+
Type: "string",
356+
Description: "The reason for the condition's last transition.",
357+
},
358+
"message": {
359+
Type: "string",
360+
Description: "A human readable message indicating details about the transition.",
361+
},
362+
"observedGeneration": {
363+
Type: "integer",
364+
Description: "observedGeneration represents the .metadata.generation that the condition was set based upon.",
365+
},
366+
},
367+
Required: []string{"type", "status"},
368+
},
369+
},
370+
XListMapKeys: []string{"type"},
371+
XListType: ptr.To("map"),
372+
},
373+
},
374+
},
375+
},
376+
},
377+
},
378+
Subresources: &apiextensions.CustomResourceSubresources{
379+
Status: &apiextensions.CustomResourceSubresourceStatus{},
380+
},
381+
},
382+
Status: apiextensions.CustomResourceDefinitionStatus{
383+
StoredVersions: []string{"v1"},
384+
},
385+
}
386+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package plugins
2+
3+
import (
4+
"fmt"
5+
6+
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
7+
)
8+
9+
type MutualExclusiveGroup struct{}
10+
11+
func (p *MutualExclusiveGroup) Name() string {
12+
return "mutual_exclusive_group"
13+
}
14+
15+
func (p *MutualExclusiveGroup) Process(req *MappingProcessorRequest) error {
16+
version := req.MappingConfig.MajorVersion
17+
if _, ok := req.CRD.Spec.Validation.OpenAPIV3Schema.Properties["spec"].Properties[version]; !ok {
18+
return fmt.Errorf("version %s not found in spec properties", version)
19+
}
20+
21+
versionProps := req.CRD.Spec.Validation.OpenAPIV3Schema.Properties["spec"].Properties[version]
22+
_, groupIDExists := versionProps.Properties["groupId"]
23+
_, groupRefExists := versionProps.Properties["groupRef"]
24+
if groupIDExists || groupRefExists {
25+
versionProps.XValidations = append(versionProps.XValidations, apiextensions.ValidationRule{
26+
Rule: "(has(self.groupId) && !has(self.groupRef)) || (!has(self.groupId) && has(self.groupRef))",
27+
Message: "groupId and groupRef are mutually exclusive; only one of them can be set",
28+
})
29+
}
30+
31+
req.CRD.Spec.Validation.OpenAPIV3Schema.Properties["spec"].Properties[version] = versionProps
32+
33+
return nil
34+
}

0 commit comments

Comments
 (0)