Generate modern namespaced XR CRDs, with spec.crossplane

Signed-off-by: Nic Cope <nicc@rk0n.org>
This commit is contained in:
Nic Cope 2025-02-28 18:05:40 -08:00
parent a467579bf3
commit 6941dc6206
5 changed files with 384 additions and 29 deletions

View File

@ -99,7 +99,7 @@ func (s *ClientSideCompositeSyncer) Sync(ctx context.Context, cm *claim.Unstruct
// 1. Grabbing a map whose keys represent all well-known claim fields.
// 2. Deleting any well-known fields that we want to propagate.
// 3. Using the resulting map keys to filter the claim's spec.
wellKnownClaimFields := xcrd.CompositeResourceClaimSpecProps()
wellKnownClaimFields := xcrd.CompositeResourceClaimSpecProps(nil)
for _, field := range xcrd.PropagateSpecProps {
delete(wellKnownClaimFields, field)
}
@ -171,7 +171,7 @@ func (s *ClientSideCompositeSyncer) Sync(ctx context.Context, cm *claim.Unstruct
// XR status fields overwrite non-empty claim fields.
withMergeOptions(mergo.WithOverride),
// Don't sync XR machinery (i.e. status conditions, connection details).
withSrcFilter(xcrd.GetPropFields(xcrd.CompositeResourceStatusProps())...)); err != nil {
withSrcFilter(xcrd.GetPropFields(xcrd.LegacyCompositeResourceStatusProps())...)); err != nil {
return errors.Wrap(err, errMergeClaimStatus)
}
@ -194,7 +194,7 @@ func (s *ClientSideCompositeSyncer) Sync(ctx context.Context, cm *claim.Unstruct
// 2. Deleting any well-known fields that we want to propagate.
// 3. Filtering OUT the remaining map keys from the XR's spec so that we end
// up adding only the well-known fields to the claim's spec.
wellKnownXRFields := xcrd.CompositeResourceSpecProps()
wellKnownXRFields := xcrd.LegacyCompositeResourceSpecProps(nil)
for _, field := range xcrd.PropagateSpecProps {
delete(wellKnownXRFields, field)
}

View File

@ -216,7 +216,7 @@ func (s *ServerSideCompositeSyncer) Sync(ctx context.Context, cm *claim.Unstruct
// 1. Grabbing a map whose keys represent all well-known claim fields.
// 2. Deleting any well-known fields that we want to propagate.
// 3. Using the resulting map keys to filter the claim's spec.
wellKnownClaimFields := xcrd.CompositeResourceClaimSpecProps()
wellKnownClaimFields := xcrd.CompositeResourceClaimSpecProps(nil)
for _, field := range xcrd.PropagateSpecProps {
delete(wellKnownClaimFields, field)
}
@ -319,7 +319,7 @@ func (s *ServerSideCompositeSyncer) Sync(ctx context.Context, cm *claim.Unstruct
pub := cm.GetConnectionDetailsLastPublishedTime()
// Update the claim's user-defined status fields to match the XRs.
cm.Object["status"] = withoutKeys(xrStatus, xcrd.GetPropFields(xcrd.CompositeResourceStatusProps())...)
cm.Object["status"] = withoutKeys(xrStatus, xcrd.GetPropFields(xcrd.LegacyCompositeResourceStatusProps())...)
if cmcs.Conditions != nil {
cm.SetConditions(cmcs.Conditions...)

View File

@ -24,7 +24,6 @@ package xcrd
import (
"encoding/json"
"fmt"
extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -56,7 +55,6 @@ const (
func ForCompositeResource(xrd *v1.CompositeResourceDefinition) (*extv1.CustomResourceDefinition, error) {
crd := &extv1.CustomResourceDefinition{
Spec: extv1.CustomResourceDefinitionSpec{
Scope: extv1.ClusterScoped,
Group: xrd.Spec.Group,
Names: xrd.Spec.Names,
Versions: make([]extv1.CustomResourceDefinitionVersion, len(xrd.Spec.Versions)),
@ -70,6 +68,16 @@ func ForCompositeResource(xrd *v1.CompositeResourceDefinition) (*extv1.CustomRes
meta.TypedReferenceTo(xrd, v1.CompositeResourceDefinitionGroupVersionKind),
)})
scope := ptr.Deref(xrd.Spec.Scope, v1.CompositeResourceScopeLegacyCluster)
switch scope {
case v1.CompositeResourceScopeNamespaced:
crd.Spec.Scope = extv1.NamespaceScoped
case v1.CompositeResourceScopeCluster:
crd.Spec.Scope = extv1.ClusterScoped
case v1.CompositeResourceScopeLegacyCluster:
crd.Spec.Scope = extv1.ClusterScoped
}
crd.Spec.Names.Categories = append(crd.Spec.Names.Categories, CategoryComposite)
// The composite name is used as a label value, so we must ensure it is not
@ -82,15 +90,20 @@ func ForCompositeResource(xrd *v1.CompositeResourceDefinition) (*extv1.CustomRes
return nil, errors.Wrapf(err, errFmtGenCrd, "Composite Resource", xrd.Name)
}
crdv.AdditionalPrinterColumns = append(crdv.AdditionalPrinterColumns, CompositeResourcePrinterColumns()...)
props := CompositeResourceSpecProps()
if xrd.Spec.DefaultCompositionUpdatePolicy != nil {
cup := props["compositionUpdatePolicy"]
cup.Default = &extv1.JSON{Raw: []byte(fmt.Sprintf("\"%s\"", *xrd.Spec.DefaultCompositionUpdatePolicy))}
props["compositionUpdatePolicy"] = cup
props := CompositeResourceSpecProps(xrd.Spec.DefaultCompositionUpdatePolicy)
if scope == v1.CompositeResourceScopeLegacyCluster {
props = LegacyCompositeResourceSpecProps(xrd.Spec.DefaultCompositionUpdatePolicy)
}
for k, v := range props {
crdv.Schema.OpenAPIV3Schema.Properties["spec"].Properties[k] = v
}
props = CompositeResourceStatusProps()
if scope == v1.CompositeResourceScopeLegacyCluster {
props = LegacyCompositeResourceStatusProps()
}
for k, v := range props {
crdv.Schema.OpenAPIV3Schema.Properties["status"].Properties[k] = v
}
crd.Spec.Versions[i] = *crdv
}
@ -133,15 +146,14 @@ func ForCompositeResourceClaim(xrd *v1.CompositeResourceDefinition) (*extv1.Cust
return nil, errors.Wrapf(err, errFmtGenCrd, "Composite Resource Claim", xrd.Name)
}
crdv.AdditionalPrinterColumns = append(crdv.AdditionalPrinterColumns, CompositeResourceClaimPrinterColumns()...)
props := CompositeResourceClaimSpecProps()
if xrd.Spec.DefaultCompositeDeletePolicy != nil {
cdp := props["compositeDeletePolicy"]
cdp.Default = &extv1.JSON{Raw: []byte(fmt.Sprintf("\"%s\"", *xrd.Spec.DefaultCompositeDeletePolicy))}
props["compositeDeletePolicy"] = cdp
}
props := CompositeResourceClaimSpecProps(xrd.Spec.DefaultCompositeDeletePolicy)
for k, v := range props {
crdv.Schema.OpenAPIV3Schema.Properties["spec"].Properties[k] = v
}
props = LegacyCompositeResourceStatusProps()
for k, v := range props {
crdv.Schema.OpenAPIV3Schema.Properties["status"].Properties[k] = v
}
crd.Spec.Versions[i] = *crdv
}
@ -206,9 +218,6 @@ func genCrdVersion(vr v1.CompositeResourceDefinitionVersion, maxNameLength int64
for k, v := range xStatus.Properties {
cStatus.Properties[k] = v
}
for k, v := range CompositeResourceStatusProps() {
cStatus.Properties[k] = v
}
crdv.Schema.OpenAPIV3Schema.Properties["status"] = cStatus
return &crdv, nil
}

View File

@ -199,8 +199,286 @@ func TestForCompositeResource(t *testing.T) {
args args
want want
}{
"Successful": {
reason: "A CRD should be generated from a CompositeResourceDefinitionVersion.",
"Namespaced": {
reason: "A CRD should be generated from a modern CompositeResourceDefinitionVersion of a namespaced XR.",
args: args{
xrd: &v1.CompositeResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Labels: labels,
Annotations: annotations,
UID: types.UID("you-you-eye-dee"),
},
Spec: v1.CompositeResourceDefinitionSpec{
Scope: ptr.To(v1.CompositeResourceScopeNamespaced),
Group: group,
Names: extv1.CustomResourceDefinitionNames{
Plural: plural,
Singular: singular,
Kind: kind,
ListKind: listKind,
},
Versions: []v1.CompositeResourceDefinitionVersion{{
Name: version,
Referenceable: true,
Served: true,
}},
},
},
v: &v1.CompositeResourceValidation{
OpenAPIV3Schema: runtime.RawExtension{Raw: []byte(schema)},
},
},
want: want{
c: &extv1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Labels: labels,
OwnerReferences: []metav1.OwnerReference{
meta.AsController(meta.TypedReferenceTo(d, v1.CompositeResourceDefinitionGroupVersionKind)),
},
},
Spec: extv1.CustomResourceDefinitionSpec{
Group: group,
Names: extv1.CustomResourceDefinitionNames{
Plural: plural,
Singular: singular,
Kind: kind,
ListKind: listKind,
Categories: []string{CategoryComposite},
},
Scope: extv1.NamespaceScoped,
Versions: []extv1.CustomResourceDefinitionVersion{{
Name: version,
Served: true,
Storage: true,
Subresources: &extv1.CustomResourceSubresources{
Status: &extv1.CustomResourceSubresourceStatus{},
},
AdditionalPrinterColumns: []extv1.CustomResourceColumnDefinition{
{
Name: "SYNCED",
Type: "string",
JSONPath: ".status.conditions[?(@.type=='Synced')].status",
},
{
Name: "READY",
Type: "string",
JSONPath: ".status.conditions[?(@.type=='Ready')].status",
},
{
Name: "COMPOSITION",
Type: "string",
JSONPath: ".spec.compositionRef.name",
},
{
Name: "COMPOSITIONREVISION",
Type: "string",
JSONPath: ".spec.compositionRevisionRef.name",
Priority: 1,
},
{
Name: "AGE",
Type: "date",
JSONPath: ".metadata.creationTimestamp",
},
},
Schema: &extv1.CustomResourceValidation{
OpenAPIV3Schema: &extv1.JSONSchemaProps{
Type: "object",
Description: "What the resource is for.",
Required: []string{"spec"},
Properties: map[string]extv1.JSONSchemaProps{
"apiVersion": {
Type: "string",
},
"kind": {
Type: "string",
},
"metadata": {
// NOTE(muvaf): api-server takes care of validating
// metadata.
Type: "object",
Properties: map[string]extv1.JSONSchemaProps{
"name": {
Type: "string",
MaxLength: ptr.To[int64](63),
},
},
},
"spec": {
Type: "object",
Required: []string{"storageGB", "engineVersion"},
Description: "Specification of the resource.",
Properties: map[string]extv1.JSONSchemaProps{
"storageGB": {Type: "integer", Description: "Pretend this is useful."},
"engineVersion": {
Type: "string",
Enum: []extv1.JSON{
{Raw: []byte(`"5.6"`)},
{Raw: []byte(`"5.7"`)},
},
},
"someField": {Type: "string", Description: "Pretend this is useful."},
"someOtherField": {Type: "string", Description: "Pretend this is useful."},
"crossplane": {
Type: "object",
Description: "Configures how Crossplane will reconcile this composite resource",
Properties: map[string]extv1.JSONSchemaProps{
"compositionRef": {
Type: "object",
Required: []string{"name"},
Properties: map[string]extv1.JSONSchemaProps{
"name": {Type: "string"},
},
},
"compositionSelector": {
Type: "object",
Required: []string{"matchLabels"},
Properties: map[string]extv1.JSONSchemaProps{
"matchLabels": {
Type: "object",
AdditionalProperties: &extv1.JSONSchemaPropsOrBool{
Allows: true,
Schema: &extv1.JSONSchemaProps{Type: "string"},
},
},
},
},
"compositionRevisionRef": {
Type: "object",
Required: []string{"name"},
Properties: map[string]extv1.JSONSchemaProps{
"name": {Type: "string"},
},
},
"compositionRevisionSelector": {
Type: "object",
Required: []string{"matchLabels"},
Properties: map[string]extv1.JSONSchemaProps{
"matchLabels": {
Type: "object",
AdditionalProperties: &extv1.JSONSchemaPropsOrBool{
Allows: true,
Schema: &extv1.JSONSchemaProps{Type: "string"},
},
},
},
},
"compositionUpdatePolicy": {
Type: "string",
Enum: []extv1.JSON{
{Raw: []byte(`"Automatic"`)},
{Raw: []byte(`"Manual"`)},
},
},
"claimRef": {
Type: "object",
Required: []string{"apiVersion", "kind", "namespace", "name"},
Properties: map[string]extv1.JSONSchemaProps{
"apiVersion": {Type: "string"},
"kind": {Type: "string"},
"namespace": {Type: "string"},
"name": {Type: "string"},
},
},
"resourceRefs": {
Type: "array",
Items: &extv1.JSONSchemaPropsOrArray{
Schema: &extv1.JSONSchemaProps{
Type: "object",
Properties: map[string]extv1.JSONSchemaProps{
"apiVersion": {Type: "string"},
"name": {Type: "string"},
"kind": {Type: "string"},
},
Required: []string{"apiVersion", "kind"},
},
},
XListType: ptr.To("atomic"),
},
"writeConnectionSecretToRef": {
Type: "object",
Required: []string{"name", "namespace"},
Properties: map[string]extv1.JSONSchemaProps{
"name": {Type: "string"},
"namespace": {Type: "string"},
},
},
},
},
},
XValidations: extv1.ValidationRules{
{
Message: "Cannot change engine version",
Rule: "self.engineVersion == oldSelf.engineVersion",
},
},
OneOf: []extv1.JSONSchemaProps{
{Required: []string{"someField"}},
{Required: []string{"someOtherField"}},
},
},
"status": {
Type: "object",
Description: "Status of the resource.",
Properties: map[string]extv1.JSONSchemaProps{
"phase": {Type: "string"},
"something": {Type: "string"},
"conditions": {
Description: "Conditions of the resource.",
Type: "array",
XListType: ptr.To("map"),
XListMapKeys: []string{"type"},
Items: &extv1.JSONSchemaPropsOrArray{
Schema: &extv1.JSONSchemaProps{
Type: "object",
Required: []string{"lastTransitionTime", "reason", "status", "type"},
Properties: map[string]extv1.JSONSchemaProps{
"lastTransitionTime": {Type: "string", Format: "date-time"},
"message": {Type: "string"},
"reason": {Type: "string"},
"status": {Type: "string"},
"type": {Type: "string"},
},
},
},
},
"crossplane": {
Description: "Indicates how Crossplane is reconciling this composite resource",
Type: "object",
Properties: map[string]extv1.JSONSchemaProps{
"connectionDetails": {
Type: "object",
Properties: map[string]extv1.JSONSchemaProps{
"lastPublishedTime": {Type: "string", Format: "date-time"},
},
},
},
},
},
XValidations: extv1.ValidationRules{
{
Message: "Phase is required once set",
Rule: "!has(oldSelf.phase) || has(self.phase)",
},
},
OneOf: []extv1.JSONSchemaProps{
{Required: []string{"phase"}},
{Required: []string{"something"}},
},
},
},
},
},
}},
},
},
},
},
"Legacy": {
reason: "A CRD should be generated from a legacy CompositeResourceDefinitionVersion.",
args: args{
v: &v1.CompositeResourceValidation{
OpenAPIV3Schema: runtime.RawExtension{Raw: []byte(schema)},

View File

@ -17,8 +17,12 @@ limitations under the License.
package xcrd
import (
"fmt"
extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/utils/ptr"
v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
)
// Label keys.
@ -68,9 +72,21 @@ func BaseProps() *extv1.JSONSchemaProps {
}
// CompositeResourceSpecProps is a partial OpenAPIV3Schema for the spec fields
// that Crossplane expects to be present for all defined infrastructure
// resources.
func CompositeResourceSpecProps() map[string]extv1.JSONSchemaProps {
// that Crossplane expects to be present for all defined composite resources.
func CompositeResourceSpecProps(defaultPol *v1.UpdatePolicy) map[string]extv1.JSONSchemaProps {
return map[string]extv1.JSONSchemaProps{
"crossplane": {
Type: "object",
Description: "Configures how Crossplane will reconcile this composite resource",
Properties: LegacyCompositeResourceSpecProps(defaultPol),
},
}
}
// LegacyCompositeResourceSpecProps is a partial OpenAPIV3Schema for the spec
// fields that Crossplane expects to be present for all defined composite
// resources. It's used for Crossplane v1 style ClusterLegacy XRs.
func LegacyCompositeResourceSpecProps(defaultPol *v1.UpdatePolicy) map[string]extv1.JSONSchemaProps {
return map[string]extv1.JSONSchemaProps{
"compositionRef": {
Type: "object",
@ -118,6 +134,12 @@ func CompositeResourceSpecProps() map[string]extv1.JSONSchemaProps {
{Raw: []byte(`"Automatic"`)},
{Raw: []byte(`"Manual"`)},
},
Default: func() *extv1.JSON {
if defaultPol == nil {
return nil
}
return &extv1.JSON{Raw: []byte(fmt.Sprintf("\"%s\"", *defaultPol))}
}(),
},
"claimRef": {
Type: "object",
@ -159,7 +181,7 @@ func CompositeResourceSpecProps() map[string]extv1.JSONSchemaProps {
// CompositeResourceClaimSpecProps is a partial OpenAPIV3Schema for the spec
// fields that Crossplane expects to be present for all published infrastructure
// resources.
func CompositeResourceClaimSpecProps() map[string]extv1.JSONSchemaProps {
func CompositeResourceClaimSpecProps(defaultPol *v1.CompositeDeletePolicy) map[string]extv1.JSONSchemaProps {
return map[string]extv1.JSONSchemaProps{
"compositionRef": {
Type: "object",
@ -214,6 +236,12 @@ func CompositeResourceClaimSpecProps() map[string]extv1.JSONSchemaProps {
{Raw: []byte(`"Background"`)},
{Raw: []byte(`"Foreground"`)},
},
Default: func() *extv1.JSON {
if defaultPol == nil {
return nil
}
return &extv1.JSON{Raw: []byte(fmt.Sprintf("\"%s\"", *defaultPol))}
}(),
},
"resourceRef": {
Type: "object",
@ -235,9 +263,49 @@ func CompositeResourceClaimSpecProps() map[string]extv1.JSONSchemaProps {
}
// CompositeResourceStatusProps is a partial OpenAPIV3Schema for the status
// fields that Crossplane expects to be present for all defined or published
// infrastructure resources.
// fields that Crossplane expects to be present for all composite resources.
func CompositeResourceStatusProps() map[string]extv1.JSONSchemaProps {
return map[string]extv1.JSONSchemaProps{
"conditions": {
Description: "Conditions of the resource.",
Type: "array",
XListMapKeys: []string{
"type",
},
XListType: ptr.To("map"),
Items: &extv1.JSONSchemaPropsOrArray{
Schema: &extv1.JSONSchemaProps{
Type: "object",
Required: []string{"lastTransitionTime", "reason", "status", "type"},
Properties: map[string]extv1.JSONSchemaProps{
"lastTransitionTime": {Type: "string", Format: "date-time"},
"message": {Type: "string"},
"reason": {Type: "string"},
"status": {Type: "string"},
"type": {Type: "string"},
},
},
},
},
"crossplane": {
Type: "object",
Description: "Indicates how Crossplane is reconciling this composite resource",
Properties: map[string]extv1.JSONSchemaProps{
"connectionDetails": {
Type: "object",
Properties: map[string]extv1.JSONSchemaProps{
"lastPublishedTime": {Type: "string", Format: "date-time"},
},
},
},
},
}
}
// LegacyCompositeResourceStatusProps is a partial OpenAPIV3Schema for the
// status fields that Crossplane expects to be present for all composite
// resources and claims. It's used for v1 style ClusterLegacy XRs and claims.
func LegacyCompositeResourceStatusProps() map[string]extv1.JSONSchemaProps {
return map[string]extv1.JSONSchemaProps{
"conditions": {
Description: "Conditions of the resource.",