diff --git a/.gitignore b/.gitignore index 22634d71..ccfd4107 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ /bin /vendor /.vendor-new -.vscode \ No newline at end of file +.vscode +/.idea + diff --git a/Makefile b/Makefile index bc8c3c2e..5534faec 100644 --- a/Makefile +++ b/Makefile @@ -125,3 +125,25 @@ manifests: @$(INFO) Deprecated. Run make generate instead. .PHONY: cobertura submodules fallthrough test-integration run manifests + +generate.run: go.generate kustomize.gen + +generate.done: kustomize.clean + +# This hack is needed because we want to inject the conversion webhook +# configuration into the Object CRD. This is not possible with the CRD +# generation through controller-gen, and actually kubebuilder does +# something similar, so we are following suit. Can be removed once we +# drop support for v1alpha1. +kustomize.gen: $(KUBECTL) + @$(INFO) Generating CRDs with kustomize + @$(KUBECTL) kustomize cluster/kustomize/ > cluster/kustomize/kubernetes.crossplane.io_objects.yaml + @mv cluster/kustomize/kubernetes.crossplane.io_objects.yaml cluster/kustomize/crds/kubernetes.crossplane.io_objects.yaml + @mv cluster/kustomize/crds package/crds + @$(OK) Generated CRDs with kustomize + +kustomize.clean: + @$(INFO) Cleaning up kustomize generated CRDs + @rm -rf cluster/kustomize/crds + @rm -f cluster/kustomize/kubernetes.crossplane.io_objects.yaml + @$(OK) Cleaned up kustomize generated CRDs diff --git a/apis/generate.go b/apis/generate.go index 8dc4f86a..5a4b0a8f 100644 --- a/apis/generate.go +++ b/apis/generate.go @@ -24,7 +24,7 @@ limitations under the License. //go:generate rm -rf ../package/crds // Generate deepcopy methodsets and CRD manifests -//go:generate go run -tags generate sigs.k8s.io/controller-tools/cmd/controller-gen object:headerFile=../hack/boilerplate.go.txt paths=./... crd:crdVersions=v1 output:artifacts:config=../package/crds +//go:generate go run -tags generate sigs.k8s.io/controller-tools/cmd/controller-gen object:headerFile=../hack/boilerplate.go.txt paths=./... crd:crdVersions=v1 output:artifacts:config=../cluster/kustomize/crds // Generate crossplane-runtime methodsets (resource.Claim, etc) //go:generate go run -tags generate github.com/crossplane/crossplane-tools/cmd/angryjet generate-methodsets --header-file=../hack/boilerplate.go.txt ./... diff --git a/apis/kubernetes.go b/apis/kubernetes.go index 6db35f6d..f49b7515 100644 --- a/apis/kubernetes.go +++ b/apis/kubernetes.go @@ -21,6 +21,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" objectv1alpha1 "github.com/crossplane-contrib/provider-kubernetes/apis/object/v1alpha1" + objectv1alhpa2 "github.com/crossplane-contrib/provider-kubernetes/apis/object/v1alpha2" templatev1alpha1 "github.com/crossplane-contrib/provider-kubernetes/apis/v1alpha1" ) @@ -29,6 +30,7 @@ func init() { AddToSchemes = append(AddToSchemes, templatev1alpha1.SchemeBuilder.AddToScheme, objectv1alpha1.SchemeBuilder.AddToScheme, + objectv1alhpa2.SchemeBuilder.AddToScheme, ) } diff --git a/apis/object/v1alpha1/conversion.go b/apis/object/v1alpha1/conversion.go new file mode 100644 index 00000000..3c374ff1 --- /dev/null +++ b/apis/object/v1alpha1/conversion.go @@ -0,0 +1,195 @@ +/* +Copyright 2023 The Crossplane 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 ( + "errors" + + "k8s.io/apimachinery/pkg/util/sets" + "sigs.k8s.io/controller-runtime/pkg/conversion" + + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + + "github.com/crossplane-contrib/provider-kubernetes/apis/object/v1alpha2" +) + +// ConvertTo converts this Object to the Hub version (v1alpha2). +func (src *Object) ConvertTo(dstRaw conversion.Hub) error { // nolint:golint // We want to use different names for receiver parameter to be more clear. + dst := dstRaw.(*v1alpha2.Object) + + // copy identical fields + dst.ObjectMeta = src.ObjectMeta + + dst.Status = v1alpha2.ObjectStatus{ + ResourceStatus: src.Status.ResourceStatus, + AtProvider: v1alpha2.ObjectObservation{ + Manifest: src.Status.AtProvider.Manifest, + }, + } + + connectionDetails := []v1alpha2.ConnectionDetail{} + for _, cd := range src.Spec.ConnectionDetails { + connectionDetails = append(connectionDetails, v1alpha2.ConnectionDetail{ + ObjectReference: cd.ObjectReference, + }) + } + + references := []v1alpha2.Reference{} + for _, r := range src.Spec.References { + ref := v1alpha2.Reference{} + if r.DependsOn != nil { + ref.DependsOn = &v1alpha2.DependsOn{ + APIVersion: r.DependsOn.APIVersion, + Kind: r.DependsOn.Kind, + Name: r.DependsOn.Name, + Namespace: r.DependsOn.Namespace, + } + } + if r.PatchesFrom != nil { + ref.PatchesFrom = &v1alpha2.PatchesFrom{ + DependsOn: v1alpha2.DependsOn{ + APIVersion: r.PatchesFrom.APIVersion, + Kind: r.PatchesFrom.Kind, + Name: r.PatchesFrom.Name, + Namespace: r.PatchesFrom.Namespace, + }, + FieldPath: r.PatchesFrom.FieldPath, + } + } + references = append(references, ref) + } + + dst.Spec = v1alpha2.ObjectSpec{ + ResourceSpec: xpv1.ResourceSpec{ + WriteConnectionSecretToReference: src.GetWriteConnectionSecretToReference(), + PublishConnectionDetailsTo: src.GetPublishConnectionDetailsTo(), + ProviderConfigReference: src.GetProviderConfigReference(), + DeletionPolicy: src.GetDeletionPolicy(), + }, + ConnectionDetails: connectionDetails, + ForProvider: v1alpha2.ObjectParameters{ + Manifest: src.Spec.ForProvider.Manifest, + }, + References: references, + Readiness: v1alpha2.Readiness{ + Policy: v1alpha2.ReadinessPolicy(src.Spec.Readiness.Policy), + }, + } + + // handle management policies migration + switch src.Spec.ManagementPolicy { + case Default, "": + dst.Spec.ManagementPolicies = xpv1.ManagementPolicies{xpv1.ManagementActionAll} + case ObserveCreateUpdate: + dst.Spec.ManagementPolicies = xpv1.ManagementPolicies{xpv1.ManagementActionObserve, xpv1.ManagementActionCreate, xpv1.ManagementActionUpdate} + case ObserveDelete: + dst.Spec.ManagementPolicies = xpv1.ManagementPolicies{xpv1.ManagementActionObserve, xpv1.ManagementActionDelete} + case Observe: + dst.Spec.ManagementPolicies = xpv1.ManagementPolicies{xpv1.ManagementActionObserve} + default: + return errors.New("unknown management policy") + } + + return nil +} + +// ConvertFrom converts from the Hub version (v1alpha2) to this version. +func (dst *Object) ConvertFrom(srcRaw conversion.Hub) error { // nolint:golint, gocyclo // We want to use different names for receiver parameter to be more clear. + src := srcRaw.(*v1alpha2.Object) + + // copy identical fields + dst.ObjectMeta = src.ObjectMeta + dst.Status = ObjectStatus{ + ResourceStatus: src.Status.ResourceStatus, + AtProvider: ObjectObservation{ + Manifest: src.Status.AtProvider.Manifest, + }, + } + + connectionDetails := []ConnectionDetail{} + for _, cd := range src.Spec.ConnectionDetails { + connectionDetails = append(connectionDetails, ConnectionDetail{ + ObjectReference: cd.ObjectReference, + }) + } + + references := []Reference{} + for _, r := range src.Spec.References { + ref := Reference{} + if r.DependsOn != nil { + ref.DependsOn = &DependsOn{ + APIVersion: r.DependsOn.APIVersion, + Kind: r.DependsOn.Kind, + Name: r.DependsOn.Name, + Namespace: r.DependsOn.Namespace, + } + } + if r.PatchesFrom != nil { + ref.PatchesFrom = &PatchesFrom{ + DependsOn: DependsOn{ + APIVersion: r.PatchesFrom.APIVersion, + Kind: r.PatchesFrom.Kind, + Name: r.PatchesFrom.Name, + Namespace: r.PatchesFrom.Namespace, + }, + FieldPath: r.PatchesFrom.FieldPath, + } + } + references = append(references, ref) + } + + dst.Spec = ObjectSpec{ + ResourceSpec: ResourceSpec{ + WriteConnectionSecretToReference: src.GetWriteConnectionSecretToReference(), + PublishConnectionDetailsTo: src.GetPublishConnectionDetailsTo(), + ProviderConfigReference: src.GetProviderConfigReference(), + DeletionPolicy: src.GetDeletionPolicy(), + }, + ConnectionDetails: connectionDetails, + ForProvider: ObjectParameters{ + Manifest: src.Spec.ForProvider.Manifest, + }, + References: references, + Readiness: Readiness{ + Policy: ReadinessPolicy(src.Spec.Readiness.Policy), + }, + } + + // handle management policies migration + policySet := sets.New[xpv1.ManagementAction](src.GetManagementPolicies()...) + + switch { + case policySet.Has(xpv1.ManagementActionAll): + dst.Spec.ManagementPolicy = Default + case policySet.HasAll(xpv1.ManagementActionObserve, xpv1.ManagementActionCreate, xpv1.ManagementActionUpdate, xpv1.ManagementActionDelete): + dst.Spec.ManagementPolicy = Default + case policySet.HasAll(xpv1.ManagementActionObserve, xpv1.ManagementActionCreate, xpv1.ManagementActionUpdate) && + !policySet.Has(xpv1.ManagementActionDelete): + dst.Spec.ManagementPolicy = ObserveCreateUpdate + case policySet.HasAll(xpv1.ManagementActionObserve, xpv1.ManagementActionDelete) && + !policySet.HasAny(xpv1.ManagementActionCreate, xpv1.ManagementActionUpdate): + dst.Spec.ManagementPolicy = ObserveDelete + case policySet.Has(xpv1.ManagementActionObserve) && + !policySet.HasAny(xpv1.ManagementActionCreate, xpv1.ManagementActionUpdate, xpv1.ManagementActionDelete): + dst.Spec.ManagementPolicy = Observe + default: + // TODO(turkenh): Should we default to something here instead of erroring out? + return errors.New("unsupported management policy") + } + + return nil +} diff --git a/apis/object/v1alpha1/conversion_test.go b/apis/object/v1alpha1/conversion_test.go new file mode 100644 index 00000000..bc68c3a3 --- /dev/null +++ b/apis/object/v1alpha1/conversion_test.go @@ -0,0 +1,489 @@ +/* +Copyright 2023 The Crossplane 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_test + +import ( + "errors" + "testing" + + "github.com/google/go-cmp/cmp" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/pointer" + + v1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/test" + + "github.com/crossplane-contrib/provider-kubernetes/apis/object/v1alpha1" + "github.com/crossplane-contrib/provider-kubernetes/apis/object/v1alpha2" +) + +func TestConvertTo(t *testing.T) { + type args struct { + src *v1alpha1.Object + } + type want struct { + err error + dst *v1alpha2.Object + } + + tests := []struct { + name string + args args + want want + }{ + { + name: "converts to v1alpha2", + args: args{ + src: &v1alpha1.Object{ + ObjectMeta: metav1.ObjectMeta{ + Name: "coolobject", + }, + Spec: v1alpha1.ObjectSpec{ + ResourceSpec: v1alpha1.ResourceSpec{ + DeletionPolicy: v1.DeletionDelete, + }, + ConnectionDetails: []v1alpha1.ConnectionDetail{ + { + ObjectReference: corev1.ObjectReference{ + APIVersion: "v1", + Kind: "Secret", + Name: "topsecret", + }, + }, + }, + ForProvider: v1alpha1.ObjectParameters{ + Manifest: runtime.RawExtension{Raw: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: topsecret\n")}, + }, + ManagementPolicy: v1alpha1.Observe, + References: []v1alpha1.Reference{ + { + DependsOn: &v1alpha1.DependsOn{ + APIVersion: "v1", + Kind: "Secret", + Name: "topsecret", + Namespace: "coolns", + }, + PatchesFrom: &v1alpha1.PatchesFrom{ + DependsOn: v1alpha1.DependsOn{ + APIVersion: "v1", + Kind: "Secret", + Name: "topsecret", + Namespace: "coolns", + }, + FieldPath: pointer.String("data.password"), + }, + }, + }, + Readiness: v1alpha1.Readiness{Policy: v1alpha1.ReadinessPolicySuccessfulCreate}, + }, + }, + }, + want: want{ + dst: &v1alpha2.Object{ + ObjectMeta: metav1.ObjectMeta{ + Name: "coolobject", + }, + Spec: v1alpha2.ObjectSpec{ + ResourceSpec: v1.ResourceSpec{ + DeletionPolicy: v1.DeletionDelete, + ManagementPolicies: []v1.ManagementAction{v1.ManagementActionObserve}, + }, + ConnectionDetails: []v1alpha2.ConnectionDetail{ + { + ObjectReference: corev1.ObjectReference{ + APIVersion: "v1", + Kind: "Secret", + Name: "topsecret", + }, + }, + }, + ForProvider: v1alpha2.ObjectParameters{ + Manifest: runtime.RawExtension{Raw: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: topsecret\n")}, + }, + References: []v1alpha2.Reference{ + { + DependsOn: &v1alpha2.DependsOn{ + APIVersion: "v1", + Kind: "Secret", + Name: "topsecret", + Namespace: "coolns", + }, + PatchesFrom: &v1alpha2.PatchesFrom{ + DependsOn: v1alpha2.DependsOn{ + APIVersion: "v1", + Kind: "Secret", + Name: "topsecret", + Namespace: "coolns", + }, + FieldPath: pointer.String("data.password"), + }, + }, + }, + Readiness: v1alpha2.Readiness{Policy: v1alpha2.ReadinessPolicySuccessfulCreate}, + }, + }, + }, + }, + { + name: "converts to v1alpha2 - empty policy", + args: args{ + src: &v1alpha1.Object{ + ObjectMeta: metav1.ObjectMeta{ + Name: "coolobject", + }, + Spec: v1alpha1.ObjectSpec{ + ResourceSpec: v1alpha1.ResourceSpec{ + DeletionPolicy: v1.DeletionDelete, + }, + ForProvider: v1alpha1.ObjectParameters{ + Manifest: runtime.RawExtension{Raw: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: topsecret\n")}, + }, + ManagementPolicy: "", + }, + }, + }, + want: want{ + dst: &v1alpha2.Object{ + ObjectMeta: metav1.ObjectMeta{ + Name: "coolobject", + }, + Spec: v1alpha2.ObjectSpec{ + ResourceSpec: v1.ResourceSpec{ + DeletionPolicy: v1.DeletionDelete, + ManagementPolicies: []v1.ManagementAction{v1.ManagementActionAll}, + }, + ForProvider: v1alpha2.ObjectParameters{ + Manifest: runtime.RawExtension{Raw: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: topsecret\n")}, + }, + ConnectionDetails: []v1alpha2.ConnectionDetail{}, + References: []v1alpha2.Reference{}, + }, + }, + }, + }, + { + name: "converts to v1alpha2 - nil checks", + args: args{ + src: &v1alpha1.Object{ + ObjectMeta: metav1.ObjectMeta{ + Name: "coolobject", + }, + Spec: v1alpha1.ObjectSpec{ + ResourceSpec: v1alpha1.ResourceSpec{ + DeletionPolicy: v1.DeletionDelete, + }, + ConnectionDetails: []v1alpha1.ConnectionDetail{ + { + ObjectReference: corev1.ObjectReference{ + APIVersion: "v1", + Kind: "Secret", + Name: "topsecret", + }, + }, + }, + ForProvider: v1alpha1.ObjectParameters{ + Manifest: runtime.RawExtension{Raw: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: topsecret\n")}, + }, + ManagementPolicy: v1alpha1.Observe, + References: []v1alpha1.Reference{ + { + DependsOn: nil, + PatchesFrom: nil, + }, + }, + Readiness: v1alpha1.Readiness{Policy: v1alpha1.ReadinessPolicySuccessfulCreate}, + }, + }, + }, + want: want{ + dst: &v1alpha2.Object{ + ObjectMeta: metav1.ObjectMeta{ + Name: "coolobject", + }, + Spec: v1alpha2.ObjectSpec{ + ResourceSpec: v1.ResourceSpec{ + DeletionPolicy: v1.DeletionDelete, + ManagementPolicies: []v1.ManagementAction{v1.ManagementActionObserve}, + }, + ConnectionDetails: []v1alpha2.ConnectionDetail{ + { + ObjectReference: corev1.ObjectReference{ + APIVersion: "v1", + Kind: "Secret", + Name: "topsecret", + }, + }, + }, + ForProvider: v1alpha2.ObjectParameters{ + Manifest: runtime.RawExtension{Raw: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: topsecret\n")}, + }, + References: []v1alpha2.Reference{ + { + DependsOn: nil, + PatchesFrom: nil, + }, + }, + Readiness: v1alpha2.Readiness{Policy: v1alpha2.ReadinessPolicySuccessfulCreate}, + }, + }, + }, + }, + { + name: "errors if management policy is unknown", + args: args{ + src: &v1alpha1.Object{ + ObjectMeta: metav1.ObjectMeta{ + Name: "coolobject", + }, + Spec: v1alpha1.ObjectSpec{ + ManagementPolicy: v1alpha1.ManagementPolicy("unknown"), + }, + }, + }, + want: want{ + err: errors.New("unknown management policy"), + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + beta := &v1alpha2.Object{} + err := tc.args.src.ConvertTo(beta) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\nr.ConvertTo(...): -want error, +got error:\n%s", diff) + } + if err != nil { + return + } + if diff := cmp.Diff(tc.want.dst, beta); diff != "" { + t.Errorf("\nr.ConvertTo(...): -want converted, +got converted:\n%s", diff) + } + }) + } +} + +func TestConvertFrom(t *testing.T) { + type args struct { + src *v1alpha2.Object + } + type want struct { + err error + dst *v1alpha1.Object + } + + tests := []struct { + name string + args args + want want + }{ + { + name: "converts to v1alpha2", + args: args{ + src: &v1alpha2.Object{ + ObjectMeta: metav1.ObjectMeta{ + Name: "coolobject", + }, + Spec: v1alpha2.ObjectSpec{ + ResourceSpec: v1.ResourceSpec{ + DeletionPolicy: v1.DeletionDelete, + ManagementPolicies: []v1.ManagementAction{v1.ManagementActionObserve}, + }, + ConnectionDetails: []v1alpha2.ConnectionDetail{ + { + ObjectReference: corev1.ObjectReference{ + APIVersion: "v1", + Kind: "Secret", + Name: "topsecret", + }, + }, + }, + ForProvider: v1alpha2.ObjectParameters{ + Manifest: runtime.RawExtension{Raw: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: topsecret\n")}, + }, + References: []v1alpha2.Reference{ + { + DependsOn: &v1alpha2.DependsOn{ + APIVersion: "v1", + Kind: "Secret", + Name: "topsecret", + Namespace: "coolns", + }, + PatchesFrom: &v1alpha2.PatchesFrom{ + DependsOn: v1alpha2.DependsOn{ + APIVersion: "v1", + Kind: "Secret", + Name: "topsecret", + Namespace: "coolns", + }, + FieldPath: pointer.String("data.password"), + }, + }, + }, + Readiness: v1alpha2.Readiness{Policy: v1alpha2.ReadinessPolicySuccessfulCreate}, + }, + }, + }, + want: want{ + dst: &v1alpha1.Object{ + ObjectMeta: metav1.ObjectMeta{ + Name: "coolobject", + }, + Spec: v1alpha1.ObjectSpec{ + ResourceSpec: v1alpha1.ResourceSpec{ + DeletionPolicy: v1.DeletionDelete, + }, + ConnectionDetails: []v1alpha1.ConnectionDetail{ + { + ObjectReference: corev1.ObjectReference{ + APIVersion: "v1", + Kind: "Secret", + Name: "topsecret", + }, + }, + }, + ForProvider: v1alpha1.ObjectParameters{ + Manifest: runtime.RawExtension{Raw: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: topsecret\n")}, + }, + ManagementPolicy: v1alpha1.Observe, + References: []v1alpha1.Reference{ + { + DependsOn: &v1alpha1.DependsOn{ + APIVersion: "v1", + Kind: "Secret", + Name: "topsecret", + Namespace: "coolns", + }, + PatchesFrom: &v1alpha1.PatchesFrom{ + DependsOn: v1alpha1.DependsOn{ + APIVersion: "v1", + Kind: "Secret", + Name: "topsecret", + Namespace: "coolns", + }, + FieldPath: pointer.String("data.password"), + }, + }, + }, + Readiness: v1alpha1.Readiness{Policy: v1alpha1.ReadinessPolicySuccessfulCreate}, + }, + }, + }, + }, + { + name: "converts to v1alpha2 - nil checks", + args: args{ + src: &v1alpha2.Object{ + ObjectMeta: metav1.ObjectMeta{ + Name: "coolobject", + }, + Spec: v1alpha2.ObjectSpec{ + ResourceSpec: v1.ResourceSpec{ + DeletionPolicy: v1.DeletionDelete, + ManagementPolicies: []v1.ManagementAction{v1.ManagementActionObserve}, + }, + ConnectionDetails: []v1alpha2.ConnectionDetail{ + { + ObjectReference: corev1.ObjectReference{ + APIVersion: "v1", + Kind: "Secret", + Name: "topsecret", + }, + }, + }, + ForProvider: v1alpha2.ObjectParameters{ + Manifest: runtime.RawExtension{Raw: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: topsecret\n")}, + }, + References: []v1alpha2.Reference{ + { + DependsOn: nil, + PatchesFrom: nil, + }, + }, + Readiness: v1alpha2.Readiness{Policy: v1alpha2.ReadinessPolicySuccessfulCreate}, + }, + }, + }, + want: want{ + dst: &v1alpha1.Object{ + ObjectMeta: metav1.ObjectMeta{ + Name: "coolobject", + }, + Spec: v1alpha1.ObjectSpec{ + ResourceSpec: v1alpha1.ResourceSpec{ + DeletionPolicy: v1.DeletionDelete, + }, + ConnectionDetails: []v1alpha1.ConnectionDetail{ + { + ObjectReference: corev1.ObjectReference{ + APIVersion: "v1", + Kind: "Secret", + Name: "topsecret", + }, + }, + }, + ForProvider: v1alpha1.ObjectParameters{ + Manifest: runtime.RawExtension{Raw: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: topsecret\n")}, + }, + ManagementPolicy: v1alpha1.Observe, + References: []v1alpha1.Reference{ + { + DependsOn: nil, + PatchesFrom: nil, + }, + }, + Readiness: v1alpha1.Readiness{Policy: v1alpha1.ReadinessPolicySuccessfulCreate}, + }, + }, + }, + }, + { + name: "errors if management policy is unknown", + args: args{ + src: &v1alpha2.Object{ + ObjectMeta: metav1.ObjectMeta{ + Name: "coolobject", + }, + Spec: v1alpha2.ObjectSpec{ + ResourceSpec: v1.ResourceSpec{ + ManagementPolicies: []v1.ManagementAction{}, + }, + }, + }, + }, + want: want{ + err: errors.New("unsupported management policy"), + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + alpha := &v1alpha1.Object{} + err := alpha.ConvertFrom(tc.args.src) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\nr.ConvertFrom(...): -want error, +got error:\n%s", diff) + } + if err != nil { + return + } + if diff := cmp.Diff(tc.want.dst, alpha); diff != "" { + t.Errorf("\nr.ConvertTo(...): -want converted, +got converted:\n%s", diff) + } + }) + } +} diff --git a/apis/object/v1alpha1/types.go b/apis/object/v1alpha1/types.go index 44ede7cb..d44eca82 100644 --- a/apis/object/v1alpha1/types.go +++ b/apis/object/v1alpha1/types.go @@ -178,6 +178,8 @@ type ObjectStatus struct { // +kubebuilder:printcolumn:name="READY",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" // +kubebuilder:resource:scope=Cluster,categories={crossplane,managed,kubernetes} +// +kubebuilder:deprecatedversion +// Deprecated: v1alpha1.Object is deprecated in favor of v1alpha2.Object type Object struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` diff --git a/apis/object/v1alpha2/conversion.go b/apis/object/v1alpha2/conversion.go new file mode 100644 index 00000000..0f284409 --- /dev/null +++ b/apis/object/v1alpha2/conversion.go @@ -0,0 +1,22 @@ +/* +Copyright 2023 The Crossplane 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 v1alpha2 + +// Hub marks this type as a conversion hub. +func (g *Object) Hub() { + +} diff --git a/apis/object/v1alpha2/doc.go b/apis/object/v1alpha2/doc.go new file mode 100644 index 00000000..986868a9 --- /dev/null +++ b/apis/object/v1alpha2/doc.go @@ -0,0 +1,21 @@ +/* +Copyright 2023 The Crossplane 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 v1alpha2 contains the v1alpha2 group Object resources of the Kubernetes provider. +// +kubebuilder:object:generate=true +// +groupName=kubernetes.crossplane.io +// +versionName=v1alpha2 +package v1alpha2 diff --git a/apis/object/v1alpha2/register.go b/apis/object/v1alpha2/register.go new file mode 100644 index 00000000..c6b6ca22 --- /dev/null +++ b/apis/object/v1alpha2/register.go @@ -0,0 +1,50 @@ +/* +Copyright 2023 The Crossplane 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 v1alpha2 + +import ( + "reflect" + + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +// Package type metadata. +const ( + Group = "kubernetes.crossplane.io" + Version = "v1alpha2" +) + +var ( + // SchemeGroupVersion is group version used to register these objects + SchemeGroupVersion = schema.GroupVersion{Group: Group, Version: Version} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion} +) + +// Object type metadata. +var ( + ObjectKind = reflect.TypeOf(Object{}).Name() + ObjectGroupKind = schema.GroupKind{Group: Group, Kind: ObjectKind}.String() + ObjectKindAPIVersion = ObjectKind + "." + SchemeGroupVersion.String() + ObjectGroupVersionKind = SchemeGroupVersion.WithKind(ObjectKind) +) + +func init() { + SchemeBuilder.Register(&Object{}, &ObjectList{}) +} diff --git a/apis/object/v1alpha2/types.go b/apis/object/v1alpha2/types.go new file mode 100644 index 00000000..75210ed8 --- /dev/null +++ b/apis/object/v1alpha2/types.go @@ -0,0 +1,207 @@ +/* +Copyright 2023 The Crossplane 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 v1alpha2 + +import ( + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/fieldpath" +) + +// ObjectAction defines actions applicable to Object +type ObjectAction string + +// DependsOn refers to an object by Name, Kind, APIVersion, etc. It is used to +// reference other Object or arbitrary Kubernetes resource which is either +// cluster or namespace scoped. +type DependsOn struct { + // APIVersion of the referenced object. + // +kubebuilder:default=kubernetes.crossplane.io/v1alpha1 + // +optional + APIVersion string `json:"apiVersion,omitempty"` + // Kind of the referenced object. + // +kubebuilder:default=Object + // +optional + Kind string `json:"kind,omitempty"` + // Name of the referenced object. + Name string `json:"name"` + // Namespace of the referenced object. + // +optional + Namespace string `json:"namespace,omitempty"` +} + +// PatchesFrom refers to an object by Name, Kind, APIVersion, etc., and patch +// fields from this object. +type PatchesFrom struct { + DependsOn `json:",inline"` + // FieldPath is the path of the field on the resource whose value is to be + // used as input. + FieldPath *string `json:"fieldPath"` +} + +// Reference refers to an Object or arbitrary Kubernetes resource and optionally +// patch values from that resource to the current Object. +type Reference struct { + // DependsOn is used to declare dependency on other Object or arbitrary + // Kubernetes resource. + // +optional + *DependsOn `json:"dependsOn,omitempty"` + // PatchesFrom is used to declare dependency on other Object or arbitrary + // Kubernetes resource, and also patch fields from this object. + // +optional + *PatchesFrom `json:"patchesFrom,omitempty"` + // ToFieldPath is the path of the field on the resource whose value will + // be changed with the result of transforms. Leave empty if you'd like to + // propagate to the same path as patchesFrom.fieldPath. + // +optional + ToFieldPath *string `json:"toFieldPath,omitempty"` +} + +// ObjectParameters are the configurable fields of a Object. +type ObjectParameters struct { + // Raw JSON representation of the kubernetes object to be created. + // +kubebuilder:validation:EmbeddedResource + // +kubebuilder:pruning:PreserveUnknownFields + Manifest runtime.RawExtension `json:"manifest"` +} + +// ObjectObservation are the observable fields of a Object. +type ObjectObservation struct { + // Raw JSON representation of the remote object. + // +kubebuilder:validation:EmbeddedResource + // +kubebuilder:pruning:PreserveUnknownFields + Manifest runtime.RawExtension `json:"manifest,omitempty"` +} + +// A ObjectSpec defines the desired state of a Object. +type ObjectSpec struct { + xpv1.ResourceSpec `json:",inline"` + ConnectionDetails []ConnectionDetail `json:"connectionDetails,omitempty"` + ForProvider ObjectParameters `json:"forProvider"` + References []Reference `json:"references,omitempty"` + Readiness Readiness `json:"readiness,omitempty"` +} + +// ReadinessPolicy defines how the Object's readiness condition should be computed. +type ReadinessPolicy string + +const ( + // ReadinessPolicySuccessfulCreate means the object is marked as ready when the + // underlying external resource is successfully created. + ReadinessPolicySuccessfulCreate ReadinessPolicy = "SuccessfulCreate" + // ReadinessPolicyDeriveFromObject means the object is marked as ready if and only if the underlying + // external resource is considered ready. + ReadinessPolicyDeriveFromObject ReadinessPolicy = "DeriveFromObject" + // ReadinessPolicyAllTrue means that all conditions have status true on the object. + // There must be at least one condition. + ReadinessPolicyAllTrue ReadinessPolicy = "AllTrue" +) + +// Readiness defines how the object's readiness condition should be computed, +// if not specified it will be considered ready as soon as the underlying external +// resource is considered up-to-date. +type Readiness struct { + // Policy defines how the Object's readiness condition should be computed. + // +optional + // +kubebuilder:validation:Enum=SuccessfulCreate;DeriveFromObject;AllTrue + // +kubebuilder:default=SuccessfulCreate + Policy ReadinessPolicy `json:"policy,omitempty"` +} + +// ConnectionDetail represents an entry in the connection secret for an Object +type ConnectionDetail struct { + v1.ObjectReference `json:",inline"` + ToConnectionSecretKey string `json:"toConnectionSecretKey,omitempty"` +} + +// A ObjectStatus represents the observed state of a Object. +type ObjectStatus struct { + xpv1.ResourceStatus `json:",inline"` + AtProvider ObjectObservation `json:"atProvider,omitempty"` +} + +// +kubebuilder:object:root=true + +// A Object is an provider Kubernetes API type +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="KIND",type="string",JSONPath=".spec.forProvider.manifest.kind" +// +kubebuilder:printcolumn:name="APIVERSION",type="string",JSONPath=".spec.forProvider.manifest.apiVersion",priority=1 +// +kubebuilder:printcolumn:name="METANAME",type="string",JSONPath=".spec.forProvider.manifest.metadata.name",priority=1 +// +kubebuilder:printcolumn:name="METANAMESPACE",type="string",JSONPath=".spec.forProvider.manifest.metadata.namespace",priority=1 +// +kubebuilder:printcolumn:name="PROVIDERCONFIG",type="string",JSONPath=".spec.providerConfigRef.name" +// +kubebuilder:printcolumn:name="SYNCED",type="string",JSONPath=".status.conditions[?(@.type=='Synced')].status" +// +kubebuilder:printcolumn:name="READY",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:resource:scope=Cluster,categories={crossplane,managed,kubernetes} +// +kubebuilder:storageversion +type Object struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ObjectSpec `json:"spec"` + Status ObjectStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ObjectList contains a list of Object +type ObjectList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Object `json:"items"` +} + +// ApplyFromFieldPathPatch patches the "to" resource, using a source field +// on the "from" resource. +func (r *Reference) ApplyFromFieldPathPatch(from, to runtime.Object) error { + // Default to patch the same field on the "to" resource. + if r.ToFieldPath == nil { + r.ToFieldPath = r.PatchesFrom.FieldPath + } + + paved, err := fieldpath.PaveObject(from) + if err != nil { + return err + } + + out, err := paved.GetValue(*r.PatchesFrom.FieldPath) + if err != nil { + return err + } + + return patchFieldValueToObject(*r.ToFieldPath, out, to) +} + +// patchFieldValueToObject, given a path, value and "to" object, will +// apply the value to the "to" object at the given path, returning +// any errors as they occur. +func patchFieldValueToObject(path string, value interface{}, to runtime.Object) error { + paved, err := fieldpath.PaveObject(to) + if err != nil { + return err + } + + err = paved.SetValue("spec.forProvider.manifest."+path, value) + if err != nil { + return err + } + + return runtime.DefaultUnstructuredConverter.FromUnstructured(paved.UnstructuredContent(), to) +} diff --git a/apis/object/v1alpha2/zz_generated.deepcopy.go b/apis/object/v1alpha2/zz_generated.deepcopy.go new file mode 100644 index 00000000..c0d78570 --- /dev/null +++ b/apis/object/v1alpha2/zz_generated.deepcopy.go @@ -0,0 +1,260 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2020 The Crossplane 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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConnectionDetail) DeepCopyInto(out *ConnectionDetail) { + *out = *in + out.ObjectReference = in.ObjectReference +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionDetail. +func (in *ConnectionDetail) DeepCopy() *ConnectionDetail { + if in == nil { + return nil + } + out := new(ConnectionDetail) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DependsOn) DeepCopyInto(out *DependsOn) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DependsOn. +func (in *DependsOn) DeepCopy() *DependsOn { + if in == nil { + return nil + } + out := new(DependsOn) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Object) DeepCopyInto(out *Object) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Object. +func (in *Object) DeepCopy() *Object { + if in == nil { + return nil + } + out := new(Object) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Object) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ObjectList) DeepCopyInto(out *ObjectList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Object, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectList. +func (in *ObjectList) DeepCopy() *ObjectList { + if in == nil { + return nil + } + out := new(ObjectList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ObjectList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ObjectObservation) DeepCopyInto(out *ObjectObservation) { + *out = *in + in.Manifest.DeepCopyInto(&out.Manifest) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectObservation. +func (in *ObjectObservation) DeepCopy() *ObjectObservation { + if in == nil { + return nil + } + out := new(ObjectObservation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ObjectParameters) DeepCopyInto(out *ObjectParameters) { + *out = *in + in.Manifest.DeepCopyInto(&out.Manifest) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectParameters. +func (in *ObjectParameters) DeepCopy() *ObjectParameters { + if in == nil { + return nil + } + out := new(ObjectParameters) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ObjectSpec) DeepCopyInto(out *ObjectSpec) { + *out = *in + in.ResourceSpec.DeepCopyInto(&out.ResourceSpec) + if in.ConnectionDetails != nil { + in, out := &in.ConnectionDetails, &out.ConnectionDetails + *out = make([]ConnectionDetail, len(*in)) + copy(*out, *in) + } + in.ForProvider.DeepCopyInto(&out.ForProvider) + if in.References != nil { + in, out := &in.References, &out.References + *out = make([]Reference, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + out.Readiness = in.Readiness +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectSpec. +func (in *ObjectSpec) DeepCopy() *ObjectSpec { + if in == nil { + return nil + } + out := new(ObjectSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ObjectStatus) DeepCopyInto(out *ObjectStatus) { + *out = *in + in.ResourceStatus.DeepCopyInto(&out.ResourceStatus) + in.AtProvider.DeepCopyInto(&out.AtProvider) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectStatus. +func (in *ObjectStatus) DeepCopy() *ObjectStatus { + if in == nil { + return nil + } + out := new(ObjectStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PatchesFrom) DeepCopyInto(out *PatchesFrom) { + *out = *in + out.DependsOn = in.DependsOn + if in.FieldPath != nil { + in, out := &in.FieldPath, &out.FieldPath + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PatchesFrom. +func (in *PatchesFrom) DeepCopy() *PatchesFrom { + if in == nil { + return nil + } + out := new(PatchesFrom) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Readiness) DeepCopyInto(out *Readiness) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Readiness. +func (in *Readiness) DeepCopy() *Readiness { + if in == nil { + return nil + } + out := new(Readiness) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Reference) DeepCopyInto(out *Reference) { + *out = *in + if in.DependsOn != nil { + in, out := &in.DependsOn, &out.DependsOn + *out = new(DependsOn) + **out = **in + } + if in.PatchesFrom != nil { + in, out := &in.PatchesFrom, &out.PatchesFrom + *out = new(PatchesFrom) + (*in).DeepCopyInto(*out) + } + if in.ToFieldPath != nil { + in, out := &in.ToFieldPath, &out.ToFieldPath + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Reference. +func (in *Reference) DeepCopy() *Reference { + if in == nil { + return nil + } + out := new(Reference) + in.DeepCopyInto(out) + return out +} diff --git a/apis/object/v1alpha2/zz_generated.managed.go b/apis/object/v1alpha2/zz_generated.managed.go new file mode 100644 index 00000000..9ed35007 --- /dev/null +++ b/apis/object/v1alpha2/zz_generated.managed.go @@ -0,0 +1,80 @@ +/* +Copyright 2020 The Crossplane 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. +*/ +// Code generated by angryjet. DO NOT EDIT. + +package v1alpha2 + +import xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + +// GetCondition of this Object. +func (mg *Object) GetCondition(ct xpv1.ConditionType) xpv1.Condition { + return mg.Status.GetCondition(ct) +} + +// GetDeletionPolicy of this Object. +func (mg *Object) GetDeletionPolicy() xpv1.DeletionPolicy { + return mg.Spec.DeletionPolicy +} + +// GetManagementPolicies of this Object. +func (mg *Object) GetManagementPolicies() xpv1.ManagementPolicies { + return mg.Spec.ManagementPolicies +} + +// GetProviderConfigReference of this Object. +func (mg *Object) GetProviderConfigReference() *xpv1.Reference { + return mg.Spec.ProviderConfigReference +} + +// GetPublishConnectionDetailsTo of this Object. +func (mg *Object) GetPublishConnectionDetailsTo() *xpv1.PublishConnectionDetailsTo { + return mg.Spec.PublishConnectionDetailsTo +} + +// GetWriteConnectionSecretToReference of this Object. +func (mg *Object) GetWriteConnectionSecretToReference() *xpv1.SecretReference { + return mg.Spec.WriteConnectionSecretToReference +} + +// SetConditions of this Object. +func (mg *Object) SetConditions(c ...xpv1.Condition) { + mg.Status.SetConditions(c...) +} + +// SetDeletionPolicy of this Object. +func (mg *Object) SetDeletionPolicy(r xpv1.DeletionPolicy) { + mg.Spec.DeletionPolicy = r +} + +// SetManagementPolicies of this Object. +func (mg *Object) SetManagementPolicies(r xpv1.ManagementPolicies) { + mg.Spec.ManagementPolicies = r +} + +// SetProviderConfigReference of this Object. +func (mg *Object) SetProviderConfigReference(r *xpv1.Reference) { + mg.Spec.ProviderConfigReference = r +} + +// SetPublishConnectionDetailsTo of this Object. +func (mg *Object) SetPublishConnectionDetailsTo(r *xpv1.PublishConnectionDetailsTo) { + mg.Spec.PublishConnectionDetailsTo = r +} + +// SetWriteConnectionSecretToReference of this Object. +func (mg *Object) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { + mg.Spec.WriteConnectionSecretToReference = r +} diff --git a/apis/object/v1alpha2/zz_generated.managedlist.go b/apis/object/v1alpha2/zz_generated.managedlist.go new file mode 100644 index 00000000..b5000727 --- /dev/null +++ b/apis/object/v1alpha2/zz_generated.managedlist.go @@ -0,0 +1,29 @@ +/* +Copyright 2020 The Crossplane 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. +*/ +// Code generated by angryjet. DO NOT EDIT. + +package v1alpha2 + +import resource "github.com/crossplane/crossplane-runtime/pkg/resource" + +// GetItems of this ObjectList. +func (l *ObjectList) GetItems() []resource.Managed { + items := make([]resource.Managed, len(l.Items)) + for i := range l.Items { + items[i] = &l.Items[i] + } + return items +} diff --git a/cluster/kustomize/kustomization.yaml b/cluster/kustomize/kustomization.yaml new file mode 100644 index 00000000..ac63720b --- /dev/null +++ b/cluster/kustomize/kustomization.yaml @@ -0,0 +1,4 @@ +resources: + - crds/kubernetes.crossplane.io_objects.yaml +patches: + - path: webhook/webhook.patch.yaml \ No newline at end of file diff --git a/cluster/kustomize/webhook/webhook.patch.yaml b/cluster/kustomize/webhook/webhook.patch.yaml new file mode 100644 index 00000000..b7d321ec --- /dev/null +++ b/cluster/kustomize/webhook/webhook.patch.yaml @@ -0,0 +1,13 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: objects.kubernetes.crossplane.io +spec: + conversion: + strategy: Webhook + webhook: + conversionReviewVersions: + - v1 + clientConfig: + service: + path: /convert \ No newline at end of file diff --git a/cmd/provider/main.go b/cmd/provider/main.go index 2fdc6445..7d077d2f 100644 --- a/cmd/provider/main.go +++ b/cmd/provider/main.go @@ -28,6 +28,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/crossplane/crossplane-runtime/pkg/controller" "github.com/crossplane/crossplane-runtime/pkg/feature" @@ -35,19 +36,27 @@ import ( "github.com/crossplane/crossplane-runtime/pkg/ratelimiter" "github.com/crossplane-contrib/provider-kubernetes/apis" + "github.com/crossplane-contrib/provider-kubernetes/apis/object/v1alpha1" object "github.com/crossplane-contrib/provider-kubernetes/internal/controller" _ "k8s.io/client-go/plugin/pkg/client/auth" ) +const ( + webhookTLSCertDirEnvVar = "WEBHOOK_TLS_CERT_DIR" + tlsServerCertDirEnvVar = "TLS_SERVER_CERTS_DIR" + tlsServerCertDir = "/tls/server" +) + func main() { var ( - app = kingpin.New(filepath.Base(os.Args[0]), "Template support for Crossplane.").DefaultEnvars() - debug = app.Flag("debug", "Run with debug logging.").Short('d').Bool() - syncInterval = app.Flag("sync", "Controller manager sync period such as 300ms, 1.5h, or 2h45m").Short('s').Default("1h").Duration() - pollInterval = app.Flag("poll", "Poll interval controls how often an individual resource should be checked for drift.").Default("1m").Duration() - leaderElection = app.Flag("leader-election", "Use leader election for the controller manager.").Short('l').Default("false").Envar("LEADER_ELECTION").Bool() - maxReconcileRate = app.Flag("max-reconcile-rate", "The number of concurrent reconciliations that may be running at one time.").Default("10").Int() + app = kingpin.New(filepath.Base(os.Args[0]), "Template support for Crossplane.").DefaultEnvars() + debug = app.Flag("debug", "Run with debug logging.").Short('d').Bool() + syncInterval = app.Flag("sync", "Controller manager sync period such as 300ms, 1.5h, or 2h45m").Short('s').Default("1h").Duration() + pollInterval = app.Flag("poll", "Poll interval controls how often an individual resource should be checked for drift.").Default("1m").Duration() + leaderElection = app.Flag("leader-election", "Use leader election for the controller manager.").Short('l').Default("false").Envar("LEADER_ELECTION").Bool() + maxReconcileRate = app.Flag("max-reconcile-rate", "The number of concurrent reconciliations that may be running at one time.").Default("10").Int() + enableManagementPolicies = app.Flag("enable-management-policies", "Enable support for Management Policies.").Default("true").Envar("ENABLE_MANAGEMENT_POLICIES").Bool() ) kingpin.MustParse(app.Parse(os.Args[1:])) @@ -65,6 +74,18 @@ func main() { cfg, err := ctrl.GetConfig() kingpin.FatalIfError(err, "Cannot get API server rest config") + // Get the TLS certs directory from the environment variable if set + // In older XP versions we used WEBHOOK_TLS_CERT_DIR, in newer versions + // we use TLS_SERVER_CERTS_DIR. If neither are set, use the default. + var certDir string + certDir = os.Getenv(webhookTLSCertDirEnvVar) + if certDir == "" { + certDir = os.Getenv(tlsServerCertDirEnvVar) + if certDir == "" { + certDir = tlsServerCertDir + } + } + mgr, err := ctrl.NewManager(ratelimiter.LimitRESTConfig(cfg, *maxReconcileRate), ctrl.Options{ Cache: cache.Options{ SyncPeriod: syncInterval, @@ -82,6 +103,9 @@ func main() { LeaderElectionResourceLock: resourcelock.LeasesResourceLock, LeaseDuration: func() *time.Duration { d := 60 * time.Second; return &d }(), RenewDeadline: func() *time.Duration { d := 50 * time.Second; return &d }(), + WebhookServer: webhook.NewServer(webhook.Options{ + CertDir: certDir, + }), }) kingpin.FatalIfError(err, "Cannot create controller manager") @@ -94,6 +118,17 @@ func main() { Features: &feature.Flags{}, } + if *enableManagementPolicies { + o.Features.Enable(feature.EnableBetaManagementPolicies) + log.Info("Beta feature enabled", "flag", feature.EnableBetaManagementPolicies) + } + + // NOTE(lsviben): We are registering the conversion webhook with v1alpha1 + // Object. As far as I can see and based on some tests, it doesn't matter + // which version we use here. Leaving it as v1alpha1 as it will be easy to + // notice and remove when we drop support for v1alpha1. + kingpin.FatalIfError(ctrl.NewWebhookManagedBy(mgr).For(&v1alpha1.Object{}).Complete(), "Cannot create Object webhook") + kingpin.FatalIfError(object.Setup(mgr, o), "Cannot setup controller") kingpin.FatalIfError(mgr.Start(ctrl.SetupSignalHandler()), "Cannot start controller manager") } diff --git a/examples/in-composition/composition.yaml b/examples/in-composition/composition.yaml index 40b802ee..68cde505 100644 --- a/examples/in-composition/composition.yaml +++ b/examples/in-composition/composition.yaml @@ -184,7 +184,7 @@ spec: string: fmt: "%s-argocd" - base: - apiVersion: kubernetes.crossplane.io/v1alpha1 + apiVersion: kubernetes.crossplane.io/v1beta1 kind: ProviderConfig spec: credentials: @@ -206,7 +206,7 @@ spec: readinessChecks: - type: None - base: - apiVersion: kubernetes.crossplane.io/v1alpha1 + apiVersion: kubernetes.crossplane.io/v1beta1 kind: Object spec: forProvider: diff --git a/examples/object/deprecated/default.yaml b/examples/object/deprecated/default.yaml new file mode 100644 index 00000000..f19d7edb --- /dev/null +++ b/examples/object/deprecated/default.yaml @@ -0,0 +1,17 @@ +apiVersion: kubernetes.crossplane.io/v1alpha1 +kind: Object +metadata: + name: foo +spec: + # Use management policy Default to fully control k8s resource + # It is the default policy that can be omitted + forProvider: + manifest: + apiVersion: v1 + kind: ConfigMap + metadata: + # name in manifest is optional and defaults to Object name + # name: some-other-name + namespace: default + providerConfigRef: + name: kubernetes-provider diff --git a/examples/object/deprecated/observe-create-update.yaml b/examples/object/deprecated/observe-create-update.yaml new file mode 100644 index 00000000..87f83805 --- /dev/null +++ b/examples/object/deprecated/observe-create-update.yaml @@ -0,0 +1,21 @@ +--- +apiVersion: kubernetes.crossplane.io/v1alpha1 +kind: Object +metadata: + name: foo +spec: + # Use management policy ObserveCreateUpdate to observe, create, or update k8s + # resource, but leave to third party to delete the resource + managementPolicy: ObserveCreateUpdate + forProvider: + manifest: + apiVersion: v1 + kind: ConfigMap + metadata: + # name in manifest is optional and defaults to Object name + # name: some-other-name + namespace: default + data: + sample-key: sample-value + providerConfigRef: + name: kubernetes-provider diff --git a/examples/object/deprecated/observe-delete.yaml b/examples/object/deprecated/observe-delete.yaml new file mode 100644 index 00000000..89cd1efe --- /dev/null +++ b/examples/object/deprecated/observe-delete.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: foo +data: + sample-key: sample-value +--- +apiVersion: kubernetes.crossplane.io/v1alpha1 +kind: Object +metadata: + name: foo +spec: + # Use management policy ObserveDelete to observe or delete k8s resource, + # but leave to third party to create or update the resource + managementPolicy: ObserveDelete + forProvider: + manifest: + apiVersion: v1 + kind: ConfigMap + metadata: + # name in manifest is optional and defaults to Object name + # name: some-other-name + namespace: default + providerConfigRef: + name: kubernetes-provider diff --git a/examples/object/deprecated/observe.yaml b/examples/object/deprecated/observe.yaml new file mode 100644 index 00000000..1df0c1d6 --- /dev/null +++ b/examples/object/deprecated/observe.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: foo +data: + sample-key: sample-value +--- +apiVersion: kubernetes.crossplane.io/v1alpha1 +kind: Object +metadata: + name: foo +spec: + # Use management policy Observe to observe k8s resource, + # but leave to third party to create, update, or delete the resource + managementPolicy: Observe + forProvider: + manifest: + apiVersion: v1 + kind: ConfigMap + metadata: + # name in manifest is optional and defaults to Object name + # name: some-other-name + namespace: default + providerConfigRef: + name: kubernetes-provider diff --git a/examples/object/object.yaml b/examples/object/object.yaml index 9c6f8674..255e5b7b 100644 --- a/examples/object/object.yaml +++ b/examples/object/object.yaml @@ -1,4 +1,4 @@ -apiVersion: kubernetes.crossplane.io/v1alpha1 +apiVersion: kubernetes.crossplane.io/v1alpha2 kind: Object metadata: name: sample-namespace diff --git a/examples/object/policy/default.yaml b/examples/object/policy/default.yaml index f19d7edb..936b9390 100644 --- a/examples/object/policy/default.yaml +++ b/examples/object/policy/default.yaml @@ -1,4 +1,4 @@ -apiVersion: kubernetes.crossplane.io/v1alpha1 +apiVersion: kubernetes.crossplane.io/v1alpha2 kind: Object metadata: name: foo diff --git a/examples/object/policy/observe-create-update.yaml b/examples/object/policy/observe-create-update.yaml index 87f83805..16ec29f4 100644 --- a/examples/object/policy/observe-create-update.yaml +++ b/examples/object/policy/observe-create-update.yaml @@ -1,12 +1,12 @@ --- -apiVersion: kubernetes.crossplane.io/v1alpha1 +apiVersion: kubernetes.crossplane.io/v1alpha2 kind: Object metadata: name: foo spec: - # Use management policy ObserveCreateUpdate to observe, create, or update k8s + # Use management policy Observe Create Update to observe, create, or update k8s # resource, but leave to third party to delete the resource - managementPolicy: ObserveCreateUpdate + managementPolicies: ["Observe", "Create", "Update"] forProvider: manifest: apiVersion: v1 diff --git a/examples/object/policy/observe-delete.yaml b/examples/object/policy/observe-delete.yaml index 89cd1efe..e2c23860 100644 --- a/examples/object/policy/observe-delete.yaml +++ b/examples/object/policy/observe-delete.yaml @@ -6,14 +6,14 @@ metadata: data: sample-key: sample-value --- -apiVersion: kubernetes.crossplane.io/v1alpha1 +apiVersion: kubernetes.crossplane.io/v1alpha2 kind: Object metadata: name: foo spec: - # Use management policy ObserveDelete to observe or delete k8s resource, + # Use management policy Observe Delete to observe or delete k8s resource, # but leave to third party to create or update the resource - managementPolicy: ObserveDelete + managementPolicies: ["Observe", "Delete"] forProvider: manifest: apiVersion: v1 diff --git a/examples/object/policy/observe.yaml b/examples/object/policy/observe.yaml index 1df0c1d6..43713523 100644 --- a/examples/object/policy/observe.yaml +++ b/examples/object/policy/observe.yaml @@ -6,14 +6,14 @@ metadata: data: sample-key: sample-value --- -apiVersion: kubernetes.crossplane.io/v1alpha1 +apiVersion: kubernetes.crossplane.io/v1alpha2 kind: Object metadata: name: foo spec: - # Use management policy Observe to observe k8s resource, + # Use management policies Observe to observe k8s resource, # but leave to third party to create, update, or delete the resource - managementPolicy: Observe + managementPolicies: ["Observe"] forProvider: manifest: apiVersion: v1 diff --git a/examples/object/references/depends-on-object.yaml b/examples/object/references/depends-on-object.yaml index 5c70651c..09efa0f2 100644 --- a/examples/object/references/depends-on-object.yaml +++ b/examples/object/references/depends-on-object.yaml @@ -1,5 +1,5 @@ --- -apiVersion: kubernetes.crossplane.io/v1alpha1 +apiVersion: kubernetes.crossplane.io/v1alpha2 kind: Object metadata: name: foo @@ -22,7 +22,7 @@ spec: providerConfigRef: name: kubernetes-provider --- -apiVersion: kubernetes.crossplane.io/v1alpha1 +apiVersion: kubernetes.crossplane.io/v1alpha2 kind: Object metadata: name: bar diff --git a/examples/object/references/depends-on-resource.yaml b/examples/object/references/depends-on-resource.yaml index 84673fe9..62f5bfd3 100644 --- a/examples/object/references/depends-on-resource.yaml +++ b/examples/object/references/depends-on-resource.yaml @@ -1,5 +1,5 @@ --- -apiVersion: kubernetes.crossplane.io/v1alpha1 +apiVersion: kubernetes.crossplane.io/v1alpha2 kind: Object metadata: name: foo diff --git a/examples/object/references/patches-from-multiple-resources.yaml b/examples/object/references/patches-from-multiple-resources.yaml index e7490429..2d6b9e88 100644 --- a/examples/object/references/patches-from-multiple-resources.yaml +++ b/examples/object/references/patches-from-multiple-resources.yaml @@ -1,5 +1,5 @@ --- -apiVersion: kubernetes.crossplane.io/v1alpha1 +apiVersion: kubernetes.crossplane.io/v1alpha2 kind: Object metadata: name: foo diff --git a/examples/object/references/patches-from-resource.yaml b/examples/object/references/patches-from-resource.yaml index c2821c6c..843aaf3d 100644 --- a/examples/object/references/patches-from-resource.yaml +++ b/examples/object/references/patches-from-resource.yaml @@ -1,5 +1,5 @@ --- -apiVersion: kubernetes.crossplane.io/v1alpha1 +apiVersion: kubernetes.crossplane.io/v1alpha2 kind: Object metadata: name: foo diff --git a/internal/controller/object/object.go b/internal/controller/object/object.go index 436d7051..82f25a13 100644 --- a/internal/controller/object/object.go +++ b/internal/controller/object/object.go @@ -29,6 +29,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/json" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -36,6 +37,7 @@ import ( xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" "github.com/crossplane/crossplane-runtime/pkg/controller" "github.com/crossplane/crossplane-runtime/pkg/event" + "github.com/crossplane/crossplane-runtime/pkg/feature" "github.com/crossplane/crossplane-runtime/pkg/fieldpath" "github.com/crossplane/crossplane-runtime/pkg/logging" "github.com/crossplane/crossplane-runtime/pkg/meta" @@ -43,7 +45,7 @@ import ( "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" "github.com/crossplane/crossplane-runtime/pkg/resource" - "github.com/crossplane-contrib/provider-kubernetes/apis/object/v1alpha1" + "github.com/crossplane-contrib/provider-kubernetes/apis/object/v1alpha2" apisv1alpha1 "github.com/crossplane-contrib/provider-kubernetes/apis/v1alpha1" "github.com/crossplane-contrib/provider-kubernetes/internal/clients" "github.com/crossplane-contrib/provider-kubernetes/internal/clients/azure" @@ -89,12 +91,11 @@ const ( // Setup adds a controller that reconciles Object managed resources. func Setup(mgr ctrl.Manager, o controller.Options) error { - name := managed.ControllerName(v1alpha1.ObjectGroupKind) + name := managed.ControllerName(v1alpha2.ObjectGroupKind) cps := []managed.ConnectionPublisher{managed.NewAPISecretPublisher(mgr.GetClient(), mgr.GetScheme())} - r := managed.NewReconciler(mgr, - resource.ManagedKind(v1alpha1.ObjectGroupVersionKind), + reconcilerOptions := []managed.ReconcilerOption{ managed.WithExternalConnecter(&connector{ logger: o.Logger, kube: mgr.GetClient(), @@ -111,12 +112,22 @@ func Setup(mgr ctrl.Manager, o controller.Options) error { managed.WithPollInterval(o.PollInterval), managed.WithLogger(o.Logger.WithValues("controller", name)), managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))), - managed.WithConnectionPublishers(cps...)) + managed.WithConnectionPublishers(cps...), + } + + if o.Features.Enabled(feature.EnableBetaManagementPolicies) { + reconcilerOptions = append(reconcilerOptions, managed.WithManagementPolicies()) + } + + r := managed.NewReconciler(mgr, + resource.ManagedKind(v1alpha2.ObjectGroupVersionKind), + reconcilerOptions..., + ) return ctrl.NewControllerManagedBy(mgr). Named(name). WithOptions(o.ForControllerRuntime()). - For(&v1alpha1.Object{}). + For(&v1alpha2.Object{}). Complete(ratelimiter.NewReconciler(name, r, o.GlobalRateLimiter)) } @@ -138,7 +149,7 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E // This method is currently a little over our complexity goal - be wary // of making it more complex. - cr, ok := mg.(*v1alpha1.Object) + cr, ok := mg.(*v1alpha2.Object) if !ok { return nil, errors.New(errNotKubernetesObject) } @@ -233,7 +244,7 @@ type external struct { } func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) { - cr, ok := mg.(*v1alpha1.Object) + cr, ok := mg.(*v1alpha2.Object) if !ok { return managed.ExternalObservation{}, errors.New(errNotKubernetesObject) } @@ -256,7 +267,7 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex Name: observed.GetName(), }, observed) - if c.isNotFound(cr, err) { + if kerrors.IsNotFound(err) { return managed.ExternalObservation{ResourceExists: false}, nil } @@ -276,18 +287,13 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex } func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.ExternalCreation, error) { - cr, ok := mg.(*v1alpha1.Object) + cr, ok := mg.(*v1alpha2.Object) if !ok { return managed.ExternalCreation{}, errors.New(errNotKubernetesObject) } c.logger.Debug("Creating", "resource", cr) - if !cr.Spec.ManagementPolicy.IsActionAllowed(v1alpha1.ObjectActionCreate) { - c.logger.Debug("External resource should not be created by provider, skip creating.") - return managed.ExternalCreation{}, nil - } - obj, err := getDesired(cr) if err != nil { return managed.ExternalCreation{}, err @@ -305,18 +311,13 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext } func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) { - cr, ok := mg.(*v1alpha1.Object) + cr, ok := mg.(*v1alpha2.Object) if !ok { return managed.ExternalUpdate{}, errors.New(errNotKubernetesObject) } c.logger.Debug("Updating", "resource", cr) - if !cr.Spec.ManagementPolicy.IsActionAllowed(v1alpha1.ObjectActionUpdate) { - c.logger.Debug("External resource should not be updated by provider, skip updating.") - return managed.ExternalUpdate{}, nil - } - obj, err := getDesired(cr) if err != nil { return managed.ExternalUpdate{}, err @@ -334,18 +335,13 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext } func (c *external) Delete(ctx context.Context, mg resource.Managed) error { - cr, ok := mg.(*v1alpha1.Object) + cr, ok := mg.(*v1alpha2.Object) if !ok { return errors.New(errNotKubernetesObject) } c.logger.Debug("Deleting", "resource", cr) - if !cr.Spec.ManagementPolicy.IsActionAllowed(v1alpha1.ObjectActionDelete) { - c.logger.Debug("External resource should not be deleted by provider, skip deleting.") - return nil - } - obj, err := getDesired(cr) if err != nil { return err @@ -354,7 +350,7 @@ func (c *external) Delete(ctx context.Context, mg resource.Managed) error { return errors.Wrap(resource.IgnoreNotFound(c.client.Delete(ctx, obj)), errDeleteObject) } -func getDesired(obj *v1alpha1.Object) (*unstructured.Unstructured, error) { +func getDesired(obj *v1alpha2.Object) (*unstructured.Unstructured, error) { desired := &unstructured.Unstructured{} if err := json.Unmarshal(obj.Spec.ForProvider.Manifest.Raw, desired); err != nil { return nil, errors.Wrap(err, errUnmarshalTemplate) @@ -366,7 +362,7 @@ func getDesired(obj *v1alpha1.Object) (*unstructured.Unstructured, error) { return desired, nil } -func getLastApplied(obj *v1alpha1.Object, observed *unstructured.Unstructured) (*unstructured.Unstructured, error) { +func getLastApplied(obj *v1alpha2.Object, observed *unstructured.Unstructured) (*unstructured.Unstructured, error) { lastApplied, ok := observed.GetAnnotations()[v1.LastAppliedConfigAnnotation] if !ok { return nil, nil @@ -384,7 +380,7 @@ func getLastApplied(obj *v1alpha1.Object, observed *unstructured.Unstructured) ( return last, nil } -func (c *external) setObserved(obj *v1alpha1.Object, observed *unstructured.Unstructured) error { +func (c *external) setObserved(obj *v1alpha2.Object, observed *unstructured.Unstructured) error { var err error if obj.Status.AtProvider.Manifest.Raw, err = observed.MarshalJSON(); err != nil { return errors.Wrap(err, errFailedToMarshalExisting) @@ -396,9 +392,9 @@ func (c *external) setObserved(obj *v1alpha1.Object, observed *unstructured.Unst return nil } -func (c *external) updateConditionFromObserved(obj *v1alpha1.Object, observed *unstructured.Unstructured) error { +func (c *external) updateConditionFromObserved(obj *v1alpha2.Object, observed *unstructured.Unstructured) error { switch obj.Spec.Readiness.Policy { - case v1alpha1.ReadinessPolicyDeriveFromObject: + case v1alpha2.ReadinessPolicyDeriveFromObject: conditioned := xpv1.ConditionedStatus{} err := fieldpath.Pave(observed.Object).GetValueInto("status", &conditioned) if err != nil { @@ -412,7 +408,7 @@ func (c *external) updateConditionFromObserved(obj *v1alpha1.Object, observed *u return nil } obj.SetConditions(xpv1.Available()) - case v1alpha1.ReadinessPolicyAllTrue: + case v1alpha2.ReadinessPolicyAllTrue: conditioned := xpv1.ConditionedStatus{} err := fieldpath.Pave(observed.Object).GetValueInto("status", &conditioned) if err != nil { @@ -432,7 +428,7 @@ func (c *external) updateConditionFromObserved(obj *v1alpha1.Object, observed *u } else { obj.SetConditions(xpv1.Unavailable()) } - case v1alpha1.ReadinessPolicySuccessfulCreate, "": + case v1alpha2.ReadinessPolicySuccessfulCreate, "": // do nothing, will be handled by c.handleLastApplied method // "" should never happen, but just in case we will treat it as SuccessfulCreate for backward compatibility default: @@ -442,7 +438,7 @@ func (c *external) updateConditionFromObserved(obj *v1alpha1.Object, observed *u return nil } -func getReferenceInfo(ref v1alpha1.Reference) (string, string, string, string) { +func getReferenceInfo(ref v1alpha2.Reference) (string, string, string, string) { var apiVersion, kind, namespace, name string if ref.PatchesFrom != nil { @@ -465,7 +461,7 @@ func getReferenceInfo(ref v1alpha1.Reference) (string, string, string, string) { // resolveReferencies resolves references for the current Object. If it fails to // resolve some reference, e.g.: due to reference not ready, it will then return // error and requeue to wait for resolving it next time. -func (c *external) resolveReferencies(ctx context.Context, obj *v1alpha1.Object) error { +func (c *external) resolveReferencies(ctx context.Context, obj *v1alpha2.Object) error { c.logger.Debug("Resolving referencies.") // Loop through references to resolve each referenced resource @@ -499,34 +495,15 @@ func (c *external) resolveReferencies(ctx context.Context, obj *v1alpha1.Object) return nil } -func (c *external) isNotFound(obj *v1alpha1.Object, err error) bool { - isNotFound := false - - if kerrors.IsNotFound(err) { - isNotFound = true - } else if meta.WasDeleted(obj) { - // If the Object resource was being deleted but the external resource is - // not deletable as management policy is specified, we should return the - // external resource not found, so that Object can be deleted by managed - // resource reconciler. Otherwise, the reconciler will try to delete the - // external resource which breaks the management policy. - if !obj.Spec.ManagementPolicy.IsActionAllowed(v1alpha1.ObjectActionDelete) { - c.logger.Debug("Managed resource was deleted but external resource is undeletable.") - isNotFound = true - } - } - - return isNotFound -} - -func (c *external) handleLastApplied(ctx context.Context, obj *v1alpha1.Object, last, desired *unstructured.Unstructured) (managed.ExternalObservation, error) { +func (c *external) handleLastApplied(ctx context.Context, obj *v1alpha2.Object, last, desired *unstructured.Unstructured) (managed.ExternalObservation, error) { isUpToDate := false - if !obj.Spec.ManagementPolicy.IsActionAllowed(v1alpha1.ObjectActionUpdate) { - // Treated as up-to-date to skip last applied annotation update since we - // do not create or update the external resource. + if !sets.New[xpv1.ManagementAction](obj.GetManagementPolicies()...). + HasAny(xpv1.ManagementActionUpdate, xpv1.ManagementActionCreate, xpv1.ManagementActionAll) { + // Treated as up-to-date as we don't update or create the resource isUpToDate = true - } else if last != nil && equality.Semantic.DeepEqual(last, desired) { + } + if last != nil && equality.Semantic.DeepEqual(last, desired) { // Mark as up-to-date since last is equal to desired isUpToDate = true } @@ -534,7 +511,7 @@ func (c *external) handleLastApplied(ctx context.Context, obj *v1alpha1.Object, if isUpToDate { c.logger.Debug("Up to date!") - if p := obj.Spec.Readiness.Policy; p == v1alpha1.ReadinessPolicySuccessfulCreate || p == "" { + if p := obj.Spec.Readiness.Policy; p == v1alpha2.ReadinessPolicySuccessfulCreate || p == "" { obj.Status.SetConditions(xpv1.Available()) } @@ -563,7 +540,7 @@ type objFinalizer struct { type refFinalizerFn func(context.Context, *unstructured.Unstructured, string) error -func (f *objFinalizer) handleRefFinalizer(ctx context.Context, obj *v1alpha1.Object, finalizerFn refFinalizerFn, ignoreNotFound bool) error { +func (f *objFinalizer) handleRefFinalizer(ctx context.Context, obj *v1alpha2.Object, finalizerFn refFinalizerFn, ignoreNotFound bool) error { // Loop through references to resolve each referenced resource for _, ref := range obj.Spec.References { if ref.DependsOn == nil && ref.PatchesFrom == nil { @@ -599,7 +576,7 @@ func (f *objFinalizer) handleRefFinalizer(ctx context.Context, obj *v1alpha1.Obj } func (f *objFinalizer) AddFinalizer(ctx context.Context, res resource.Object) error { - obj, ok := res.(*v1alpha1.Object) + obj, ok := res.(*v1alpha2.Object) if !ok { return errors.New(errNotKubernetesObject) } @@ -629,7 +606,7 @@ func (f *objFinalizer) AddFinalizer(ctx context.Context, res resource.Object) er } func (f *objFinalizer) RemoveFinalizer(ctx context.Context, res resource.Object) error { - obj, ok := res.(*v1alpha1.Object) + obj, ok := res.(*v1alpha2.Object) if !ok { return errors.New(errNotKubernetesObject) } @@ -658,7 +635,7 @@ func (f *objFinalizer) RemoveFinalizer(ctx context.Context, res resource.Object) return errors.Wrap(err, errRemoveFinalizer) } -func connectionDetails(ctx context.Context, kube client.Client, connDetails []v1alpha1.ConnectionDetail) (managed.ConnectionDetails, error) { +func connectionDetails(ctx context.Context, kube client.Client, connDetails []v1alpha2.ConnectionDetail) (managed.ConnectionDetails, error) { mcd := managed.ConnectionDetails{} for _, cd := range connDetails { diff --git a/internal/controller/object/object_test.go b/internal/controller/object/object_test.go index 11db7152..2373366a 100644 --- a/internal/controller/object/object_test.go +++ b/internal/controller/object/object_test.go @@ -42,7 +42,7 @@ import ( "github.com/crossplane/crossplane-runtime/pkg/resource" "github.com/crossplane/crossplane-runtime/pkg/test" - "github.com/crossplane-contrib/provider-kubernetes/apis/object/v1alpha1" + "github.com/crossplane-contrib/provider-kubernetes/apis/object/v1alpha2" kubernetesv1alpha1 "github.com/crossplane-contrib/provider-kubernetes/apis/v1alpha1" ) @@ -80,31 +80,31 @@ type notKubernetesObject struct { resource.Managed } -type kubernetesObjectModifier func(obj *v1alpha1.Object) +type kubernetesObjectModifier func(obj *v1alpha2.Object) type externalResourceModifier func(res *unstructured.Unstructured) -func kubernetesObject(om ...kubernetesObjectModifier) *v1alpha1.Object { - o := &v1alpha1.Object{ +func kubernetesObject(om ...kubernetesObjectModifier) *v1alpha2.Object { + o := &v1alpha2.Object{ TypeMeta: metav1.TypeMeta{ - APIVersion: v1alpha1.SchemeGroupVersion.String(), - Kind: v1alpha1.ObjectKind, + APIVersion: v1alpha2.SchemeGroupVersion.String(), + Kind: v1alpha2.ObjectKind, }, ObjectMeta: metav1.ObjectMeta{ Name: testObjectName, Namespace: testNamespace, }, - Spec: v1alpha1.ObjectSpec{ - ManagementPolicy: v1alpha1.Default, - ResourceSpec: v1alpha1.ResourceSpec{ + Spec: v1alpha2.ObjectSpec{ + ResourceSpec: xpv1.ResourceSpec{ ProviderConfigReference: &xpv1.Reference{ Name: providerName, }, + ManagementPolicies: xpv1.ManagementPolicies{xpv1.ManagementActionAll}, }, - ForProvider: v1alpha1.ObjectParameters{ + ForProvider: v1alpha2.ObjectParameters{ Manifest: runtime.RawExtension{Raw: externalResourceRaw}, }, }, - Status: v1alpha1.ObjectStatus{}, + Status: v1alpha2.ObjectStatus{}, } for _, m := range om { @@ -146,16 +146,16 @@ func upToDateExternalResource() *unstructured.Unstructured { return externalResourceWithLastAppliedConfigAnnotation(string(externalResourceRaw)) } -func objectReferences() []v1alpha1.Reference { - dependsOn := v1alpha1.DependsOn{ - APIVersion: v1alpha1.SchemeGroupVersion.String(), - Kind: v1alpha1.ObjectKind, +func objectReferences() []v1alpha2.Reference { + dependsOn := v1alpha2.DependsOn{ + APIVersion: v1alpha2.SchemeGroupVersion.String(), + Kind: v1alpha2.ObjectKind, Name: testReferenceObjectName, Namespace: testNamespace, } - ref := []v1alpha1.Reference{ + ref := []v1alpha2.Reference{ { - PatchesFrom: &v1alpha1.PatchesFrom{ + PatchesFrom: &v1alpha2.PatchesFrom{ DependsOn: dependsOn, }, }, @@ -169,8 +169,8 @@ func objectReferences() []v1alpha1.Reference { func referenceObject(rm ...externalResourceModifier) *unstructured.Unstructured { obj := &unstructured.Unstructured{ Object: map[string]interface{}{ - "apiVersion": v1alpha1.SchemeGroupVersion.String(), - "kind": v1alpha1.ObjectKind, + "apiVersion": v1alpha2.SchemeGroupVersion.String(), + "kind": v1alpha2.ObjectKind, "metadata": map[string]interface{}{ "name": testReferenceObjectName, "namespace": testNamespace, @@ -708,7 +708,7 @@ func Test_helmExternal_Observe(t *testing.T) { }, "NotAValidManifest": { args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { + mg: kubernetesObject(func(obj *v1alpha2.Object) { obj.Spec.ForProvider.Manifest.Raw = []byte(`{"test": "not-a-valid-manifest"}`) }), }, @@ -768,7 +768,7 @@ func Test_helmExternal_Observe(t *testing.T) { }, "UpToDateNameDefaultsToObjectName": { args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { + mg: kubernetesObject(func(obj *v1alpha2.Object) { obj.Spec.ForProvider.Manifest.Raw = []byte(`{ "apiVersion": "v1", "kind": "Namespace" }`) @@ -815,32 +815,9 @@ func Test_helmExternal_Observe(t *testing.T) { err: nil, }, }, - "UpToDateIfManagementPolicyDefined": { - args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { - obj.Spec.ManagementPolicy = "ObserveDelete" - }), - client: resource.ClientApplicator{ - Client: &test.MockClient{ - MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { - *obj.(*unstructured.Unstructured) = *externalResource() - return nil - }), - }, - }, - }, - want: want{ - out: managed.ExternalObservation{ - ResourceExists: true, - ResourceUpToDate: true, - ConnectionDetails: managed.ConnectionDetails{}, - }, - err: nil, - }, - }, "FailedToPatchFieldFromReferenceObject": { args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { + mg: kubernetesObject(func(obj *v1alpha2.Object) { obj.Spec.References = objectReferences() obj.Spec.References[0].PatchesFrom.FieldPath = pointer.String("nonexistent_field") }), @@ -861,7 +838,7 @@ func Test_helmExternal_Observe(t *testing.T) { }, "NoReferenceObjectExists": { args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { + mg: kubernetesObject(func(obj *v1alpha2.Object) { obj.Spec.References = objectReferences() }), client: resource.ClientApplicator{ @@ -878,7 +855,7 @@ func Test_helmExternal_Observe(t *testing.T) { }, "NoExternalResourceExistsIfObjectWasDeleted": { args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { + mg: kubernetesObject(func(obj *v1alpha2.Object) { obj.ObjectMeta.DeletionTimestamp = &metav1.Time{Time: time.Now()} obj.Spec.References = objectReferences() }), @@ -902,37 +879,9 @@ func Test_helmExternal_Observe(t *testing.T) { err: nil, }, }, - "NoExternalResourceDeletableIfObjectWasDeleted": { - args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { - obj.ObjectMeta.DeletionTimestamp = &metav1.Time{Time: time.Now()} - obj.Spec.ManagementPolicy = "ObserveCreateUpdate" - obj.Spec.References = objectReferences() - }), - client: resource.ClientApplicator{ - Client: &test.MockClient{ - MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { - if key.Name == testReferenceObjectName { - *obj.(*unstructured.Unstructured) = *referenceObject() - return nil - } else if key.Name == externalResourceName { - *obj.(*unstructured.Unstructured) = *externalResource() - return nil - } - return errBoom - }, - MockUpdate: test.NewMockUpdateFn(nil), - }, - }, - }, - want: want{ - out: managed.ExternalObservation{ResourceExists: false}, - err: nil, - }, - }, "ReferenceToObject": { args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { + mg: kubernetesObject(func(obj *v1alpha2.Object) { obj.Spec.References = objectReferences() }), client: resource.ClientApplicator{ @@ -962,8 +911,8 @@ func Test_helmExternal_Observe(t *testing.T) { }, "EmptyReference": { args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { - obj.Spec.References = []v1alpha1.Reference{{}} + mg: kubernetesObject(func(obj *v1alpha2.Object) { + obj.Spec.References = []v1alpha2.Reference{{}} }), client: resource.ClientApplicator{ Client: &test.MockClient{ @@ -978,9 +927,9 @@ func Test_helmExternal_Observe(t *testing.T) { }, "ConnectionDetails": { args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { + mg: kubernetesObject(func(obj *v1alpha2.Object) { obj.Spec.References = objectReferences() - obj.Spec.ConnectionDetails = []v1alpha1.ConnectionDetail{ + obj.Spec.ConnectionDetails = []v1alpha2.ConnectionDetail{ { ObjectReference: corev1.ObjectReference{ Kind: "Secret", @@ -1024,9 +973,9 @@ func Test_helmExternal_Observe(t *testing.T) { }, "FailedToGetConnectionDetails": { args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { + mg: kubernetesObject(func(obj *v1alpha2.Object) { obj.Spec.References = objectReferences() - obj.Spec.ConnectionDetails = []v1alpha1.ConnectionDetail{ + obj.Spec.ConnectionDetails = []v1alpha2.ConnectionDetail{ { ObjectReference: corev1.ObjectReference{ Kind: "Secret", @@ -1057,6 +1006,28 @@ func Test_helmExternal_Observe(t *testing.T) { err: errors.Wrap(errors.Wrap(errBoom, errGetObject), errGetConnectionDetails), }, }, + "Observe Only - up to date by default": { + args: args{ + mg: kubernetesObject(func(obj *v1alpha2.Object) { + obj.Spec.ManagementPolicies = xpv1.ManagementPolicies{xpv1.ManagementActionObserve} + }), + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + *obj.(*unstructured.Unstructured) = + *externalResourceWithLastAppliedConfigAnnotation( + `{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"crossplane-system", "labels": {"old-label":"gone"}}}`, + ) + return nil + }), + }, + }, + }, + want: want{ + out: managed.ExternalObservation{ResourceExists: true, ResourceUpToDate: true, ConnectionDetails: managed.ConnectionDetails{}}, + err: nil, + }, + }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { @@ -1100,7 +1071,7 @@ func Test_helmExternal_Create(t *testing.T) { }, "NotAValidManifest": { args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { + mg: kubernetesObject(func(obj *v1alpha2.Object) { obj.Spec.ForProvider.Manifest.Raw = []byte(`{"test": "not-a-valid-manifest"}`) }), }, @@ -1121,20 +1092,9 @@ func Test_helmExternal_Create(t *testing.T) { err: errors.Wrap(errBoom, errCreateObject), }, }, - "SkipCreateIfManagementPolicyDefined": { - args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { - obj.Spec.ManagementPolicy = "ObserveDelete" - }), - }, - want: want{ - out: managed.ExternalCreation{}, - err: nil, - }, - }, "SuccessDefaultsToObjectName": { args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { + mg: kubernetesObject(func(obj *v1alpha2.Object) { obj.Spec.ForProvider.Manifest.Raw = []byte(`{ "apiVersion": "v1", "kind": "Namespace" }`) @@ -1219,7 +1179,7 @@ func Test_helmExternal_Update(t *testing.T) { }, "NotAValidManifest": { args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { + mg: kubernetesObject(func(obj *v1alpha2.Object) { obj.Spec.ForProvider.Manifest.Raw = []byte(`{"test": "not-a-valid-manifest"}`) }), }, @@ -1240,20 +1200,9 @@ func Test_helmExternal_Update(t *testing.T) { err: errors.Wrap(errBoom, errApplyObject), }, }, - "SkipUpdateIfManagementPolicyDefined": { - args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { - obj.Spec.ManagementPolicy = "ObserveDelete" - }), - }, - want: want{ - out: managed.ExternalUpdate{}, - err: nil, - }, - }, "SuccessDefaultsToObjectName": { args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { + mg: kubernetesObject(func(obj *v1alpha2.Object) { obj.Spec.ForProvider.Manifest.Raw = []byte(`{ "apiVersion": "v1", "kind": "Namespace" }`) @@ -1325,7 +1274,7 @@ func Test_helmExternal_Delete(t *testing.T) { }, "NotAValidManifest": { args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { + mg: kubernetesObject(func(obj *v1alpha2.Object) { obj.Spec.ForProvider.Manifest.Raw = []byte(`{"test": "not-a-valid-manifest"}`) }), }, @@ -1346,19 +1295,9 @@ func Test_helmExternal_Delete(t *testing.T) { err: errors.Wrap(errBoom, errDeleteObject), }, }, - "SkipDeleteIfManagementPolicyDefined": { - args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { - obj.Spec.ManagementPolicy = "ObserveCreateUpdate" - }), - }, - want: want{ - err: nil, - }, - }, "SuccessDefaultsToObjectName": { args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { + mg: kubernetesObject(func(obj *v1alpha2.Object) { obj.Spec.ForProvider.Manifest.Raw = []byte(`{ "apiVersion": "v1", "kind": "Namespace" }`) @@ -1441,7 +1380,7 @@ func Test_objFinalizer_AddFinalizer(t *testing.T) { }, "ObjectFinalizerExists": { args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { + mg: kubernetesObject(func(obj *v1alpha2.Object) { obj.ObjectMeta.Finalizers = append(obj.ObjectMeta.Finalizers, objFinalizerName) }), }, @@ -1451,7 +1390,7 @@ func Test_objFinalizer_AddFinalizer(t *testing.T) { }, "NoReferenceObjectExists": { args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { + mg: kubernetesObject(func(obj *v1alpha2.Object) { obj.Spec.References = objectReferences() }), client: resource.ClientApplicator{ @@ -1469,8 +1408,8 @@ func Test_objFinalizer_AddFinalizer(t *testing.T) { }, "EmptyReference": { args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { - obj.Spec.References = []v1alpha1.Reference{{}} + mg: kubernetesObject(func(obj *v1alpha2.Object) { + obj.Spec.References = []v1alpha2.Reference{{}} }), client: resource.ClientApplicator{ Client: &test.MockClient{ @@ -1484,7 +1423,7 @@ func Test_objFinalizer_AddFinalizer(t *testing.T) { }, "FailedToAddReferenceFinalizer": { args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { + mg: kubernetesObject(func(obj *v1alpha2.Object) { obj.Spec.References = objectReferences() }), client: resource.ClientApplicator{ @@ -1511,7 +1450,7 @@ func Test_objFinalizer_AddFinalizer(t *testing.T) { }, "Success": { args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { + mg: kubernetesObject(func(obj *v1alpha2.Object) { obj.Spec.References = objectReferences() }), client: resource.ClientApplicator{ @@ -1563,7 +1502,7 @@ func Test_objFinalizer_RemoveFinalizer(t *testing.T) { }, "FailedToRemoveObjectFinalizer": { args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { + mg: kubernetesObject(func(obj *v1alpha2.Object) { obj.ObjectMeta.Finalizers = append(obj.ObjectMeta.Finalizers, objFinalizerName) }), client: resource.ClientApplicator{ @@ -1588,7 +1527,7 @@ func Test_objFinalizer_RemoveFinalizer(t *testing.T) { }, "NoReferenceFinalizerExists": { args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { + mg: kubernetesObject(func(obj *v1alpha2.Object) { obj.ObjectMeta.Finalizers = append(obj.ObjectMeta.Finalizers, objFinalizerName) obj.Spec.References = objectReferences() }), @@ -1609,7 +1548,7 @@ func Test_objFinalizer_RemoveFinalizer(t *testing.T) { }, "ReferenceNotFound": { args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { + mg: kubernetesObject(func(obj *v1alpha2.Object) { obj.ObjectMeta.Finalizers = append(obj.ObjectMeta.Finalizers, objFinalizerName) obj.Spec.References = objectReferences() obj.ObjectMeta.UID = someUID @@ -1628,7 +1567,7 @@ func Test_objFinalizer_RemoveFinalizer(t *testing.T) { }, "FailedToRemoveReferenceFinalizer": { args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { + mg: kubernetesObject(func(obj *v1alpha2.Object) { obj.ObjectMeta.Finalizers = append(obj.ObjectMeta.Finalizers, objFinalizerName) obj.Spec.References = objectReferences() obj.ObjectMeta.UID = someUID @@ -1658,7 +1597,7 @@ func Test_objFinalizer_RemoveFinalizer(t *testing.T) { }, "Success": { args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { + mg: kubernetesObject(func(obj *v1alpha2.Object) { obj.ObjectMeta.Finalizers = append(obj.ObjectMeta.Finalizers, objFinalizerName) obj.Spec.References = objectReferences() obj.ObjectMeta.UID = someUID @@ -1690,7 +1629,7 @@ func Test_objFinalizer_RemoveFinalizer(t *testing.T) { t.Errorf("f.RemoveFinalizer(...): -want error, +got error: %s", diff) } - if _, ok := tc.args.mg.(*v1alpha1.Object); ok { + if _, ok := tc.args.mg.(*v1alpha2.Object); ok { sort := cmpopts.SortSlices(func(a, b string) bool { return a < b }) if diff := cmp.Diff(tc.want.finalizers, tc.args.mg.GetFinalizers(), sort); diff != "" { t.Errorf("managed resource finalizers: -want, +got: %s", diff) @@ -1716,7 +1655,7 @@ func Test_connectionDetails(t *testing.T) { } } - connDetail := v1alpha1.ConnectionDetail{ + connDetail := v1alpha2.ConnectionDetail{ ObjectReference: corev1.ObjectReference{ Kind: "Secret", Namespace: testNamespace, @@ -1729,7 +1668,7 @@ func Test_connectionDetails(t *testing.T) { type args struct { kube client.Client - connDetails []v1alpha1.ConnectionDetail + connDetails []v1alpha2.ConnectionDetail } type want struct { out managed.ConnectionDetails @@ -1745,7 +1684,7 @@ func Test_connectionDetails(t *testing.T) { map[string]interface{}{}, kerrors.NewNotFound(schema.GroupResource{Group: "", Resource: "secrets"}, testSecretName), ), - connDetails: []v1alpha1.ConnectionDetail{connDetail}, + connDetails: []v1alpha2.ConnectionDetail{connDetail}, }, want: want{ out: managed.ConnectionDetails{}, @@ -1760,7 +1699,7 @@ func Test_connectionDetails(t *testing.T) { }, nil, ), - connDetails: []v1alpha1.ConnectionDetail{connDetail}, + connDetails: []v1alpha2.ConnectionDetail{connDetail}, }, want: want{ out: managed.ConnectionDetails{}, @@ -1775,7 +1714,7 @@ func Test_connectionDetails(t *testing.T) { }, nil, ), - connDetails: []v1alpha1.ConnectionDetail{connDetail}, + connDetails: []v1alpha2.ConnectionDetail{connDetail}, }, want: want{ out: managed.ConnectionDetails{}, @@ -1790,7 +1729,7 @@ func Test_connectionDetails(t *testing.T) { }, nil, ), - connDetails: []v1alpha1.ConnectionDetail{connDetail}, + connDetails: []v1alpha2.ConnectionDetail{connDetail}, }, want: want{ out: managed.ConnectionDetails{ @@ -1814,7 +1753,7 @@ func Test_connectionDetails(t *testing.T) { func Test_updateConditionFromObserved(t *testing.T) { type args struct { - obj *v1alpha1.Object + obj *v1alpha2.Object observed *unstructured.Unstructured } type want struct { @@ -1827,7 +1766,7 @@ func Test_updateConditionFromObserved(t *testing.T) { }{ "NoopIfNoPolicyDefined": { args: args{ - obj: &v1alpha1.Object{}, + obj: &v1alpha2.Object{}, observed: &unstructured.Unstructured{ Object: map[string]interface{}{ "status": xpv1.ConditionedStatus{}, @@ -1841,10 +1780,10 @@ func Test_updateConditionFromObserved(t *testing.T) { }, "NoopIfSuccessfulCreatePolicyDefined": { args: args{ - obj: &v1alpha1.Object{ - Spec: v1alpha1.ObjectSpec{ - Readiness: v1alpha1.Readiness{ - Policy: v1alpha1.ReadinessPolicySuccessfulCreate, + obj: &v1alpha2.Object{ + Spec: v1alpha2.ObjectSpec{ + Readiness: v1alpha2.Readiness{ + Policy: v1alpha2.ReadinessPolicySuccessfulCreate, }, }, }, @@ -1861,10 +1800,10 @@ func Test_updateConditionFromObserved(t *testing.T) { }, "UnavailableIfDeriveFromObjectAndNotReady": { args: args{ - obj: &v1alpha1.Object{ - Spec: v1alpha1.ObjectSpec{ - Readiness: v1alpha1.Readiness{ - Policy: v1alpha1.ReadinessPolicyDeriveFromObject, + obj: &v1alpha2.Object{ + Spec: v1alpha2.ObjectSpec{ + Readiness: v1alpha2.Readiness{ + Policy: v1alpha2.ReadinessPolicyDeriveFromObject, }, }, }, @@ -1894,10 +1833,10 @@ func Test_updateConditionFromObserved(t *testing.T) { }, "UnavailableIfDerivedFromObjectAndNoCondition": { args: args{ - obj: &v1alpha1.Object{ - Spec: v1alpha1.ObjectSpec{ - Readiness: v1alpha1.Readiness{ - Policy: v1alpha1.ReadinessPolicyDeriveFromObject, + obj: &v1alpha2.Object{ + Spec: v1alpha2.ObjectSpec{ + Readiness: v1alpha2.Readiness{ + Policy: v1alpha2.ReadinessPolicyDeriveFromObject, }, }, }, @@ -1920,10 +1859,10 @@ func Test_updateConditionFromObserved(t *testing.T) { }, "AvailableIfDeriveFromObjectAndReady": { args: args{ - obj: &v1alpha1.Object{ - Spec: v1alpha1.ObjectSpec{ - Readiness: v1alpha1.Readiness{ - Policy: v1alpha1.ReadinessPolicyDeriveFromObject, + obj: &v1alpha2.Object{ + Spec: v1alpha2.ObjectSpec{ + Readiness: v1alpha2.Readiness{ + Policy: v1alpha2.ReadinessPolicyDeriveFromObject, }, }, }, @@ -1953,10 +1892,10 @@ func Test_updateConditionFromObserved(t *testing.T) { }, "UnavailableIfDerivedFromObjectAndCantParse": { args: args{ - obj: &v1alpha1.Object{ - Spec: v1alpha1.ObjectSpec{ - Readiness: v1alpha1.Readiness{ - Policy: v1alpha1.ReadinessPolicyDeriveFromObject, + obj: &v1alpha2.Object{ + Spec: v1alpha2.ObjectSpec{ + Readiness: v1alpha2.Readiness{ + Policy: v1alpha2.ReadinessPolicyDeriveFromObject, }, }, }, @@ -1979,10 +1918,10 @@ func Test_updateConditionFromObserved(t *testing.T) { }, "UnavailableIfAllTrueWithoutConditions": { args: args{ - obj: &v1alpha1.Object{ - Spec: v1alpha1.ObjectSpec{ - Readiness: v1alpha1.Readiness{ - Policy: v1alpha1.ReadinessPolicyAllTrue, + obj: &v1alpha2.Object{ + Spec: v1alpha2.ObjectSpec{ + Readiness: v1alpha2.Readiness{ + Policy: v1alpha2.ReadinessPolicyAllTrue, }, }, }, @@ -2004,10 +1943,10 @@ func Test_updateConditionFromObserved(t *testing.T) { }, "UnavailableIfAllTrueAndCantParse": { args: args{ - obj: &v1alpha1.Object{ - Spec: v1alpha1.ObjectSpec{ - Readiness: v1alpha1.Readiness{ - Policy: v1alpha1.ReadinessPolicyAllTrue, + obj: &v1alpha2.Object{ + Spec: v1alpha2.ObjectSpec{ + Readiness: v1alpha2.Readiness{ + Policy: v1alpha2.ReadinessPolicyAllTrue, }, }, }, @@ -2029,10 +1968,10 @@ func Test_updateConditionFromObserved(t *testing.T) { }, "UnavailableIfAllTrueAndAnyConditionFalse": { args: args{ - obj: &v1alpha1.Object{ - Spec: v1alpha1.ObjectSpec{ - Readiness: v1alpha1.Readiness{ - Policy: v1alpha1.ReadinessPolicyAllTrue, + obj: &v1alpha2.Object{ + Spec: v1alpha2.ObjectSpec{ + Readiness: v1alpha2.Readiness{ + Policy: v1alpha2.ReadinessPolicyAllTrue, }, }, }, @@ -2065,10 +2004,10 @@ func Test_updateConditionFromObserved(t *testing.T) { }, "AvailableIfAllTrueAndAllConditionsTrue": { args: args{ - obj: &v1alpha1.Object{ - Spec: v1alpha1.ObjectSpec{ - Readiness: v1alpha1.Readiness{ - Policy: v1alpha1.ReadinessPolicyAllTrue, + obj: &v1alpha2.Object{ + Spec: v1alpha2.ObjectSpec{ + Readiness: v1alpha2.Readiness{ + Policy: v1alpha2.ReadinessPolicyAllTrue, }, }, }, diff --git a/package/crds/kubernetes.crossplane.io_objects.yaml b/package/crds/kubernetes.crossplane.io_objects.yaml index 3513d299..9e8f2a5f 100644 --- a/package/crds/kubernetes.crossplane.io_objects.yaml +++ b/package/crds/kubernetes.crossplane.io_objects.yaml @@ -1,4 +1,3 @@ ---- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: @@ -6,6 +5,14 @@ metadata: controller-gen.kubebuilder.io/version: v0.13.0 name: objects.kubernetes.crossplane.io spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + path: /convert + conversionReviewVersions: + - v1 group: kubernetes.crossplane.io names: categories: @@ -46,10 +53,12 @@ spec: - jsonPath: .metadata.creationTimestamp name: AGE type: date + deprecated: true name: v1alpha1 schema: openAPIV3Schema: - description: A Object is an provider Kubernetes API type + description: 'A Object is an provider Kubernetes API type Deprecated: v1alpha1.Object + is deprecated in favor of v1alpha2.Object' properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -445,6 +454,414 @@ spec: - spec type: object served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.forProvider.manifest.kind + name: KIND + type: string + - jsonPath: .spec.forProvider.manifest.apiVersion + name: APIVERSION + priority: 1 + type: string + - jsonPath: .spec.forProvider.manifest.metadata.name + name: METANAME + priority: 1 + type: string + - jsonPath: .spec.forProvider.manifest.metadata.namespace + name: METANAMESPACE + priority: 1 + type: string + - jsonPath: .spec.providerConfigRef.name + name: PROVIDERCONFIG + type: string + - jsonPath: .status.conditions[?(@.type=='Synced')].status + name: SYNCED + type: string + - jsonPath: .status.conditions[?(@.type=='Ready')].status + name: READY + type: string + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + name: v1alpha2 + schema: + openAPIV3Schema: + description: A Object is an provider Kubernetes API type + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: A ObjectSpec defines the desired state of a Object. + properties: + connectionDetails: + items: + description: ConnectionDetail represents an entry in the connection + secret for an Object + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + toConnectionSecretKey: + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic + type: array + deletionPolicy: + default: Delete + description: 'DeletionPolicy specifies what will happen to the underlying + external when this managed resource is deleted - either "Delete" + or "Orphan" the external resource. This field is planned to be deprecated + in favor of the ManagementPolicies field in a future release. Currently, + both could be set independently and non-default values would be + honored if the feature flag is enabled. See the design doc for more + information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223' + enum: + - Orphan + - Delete + type: string + forProvider: + description: ObjectParameters are the configurable fields of a Object. + properties: + manifest: + description: Raw JSON representation of the kubernetes object + to be created. + type: object + x-kubernetes-embedded-resource: true + x-kubernetes-preserve-unknown-fields: true + required: + - manifest + type: object + managementPolicies: + default: + - '*' + description: 'THIS IS A BETA FIELD. It is on by default but can be + opted out through a Crossplane feature flag. ManagementPolicies + specify the array of actions Crossplane is allowed to take on the + managed and external resources. This field is planned to replace + the DeletionPolicy field in a future release. Currently, both could + be set independently and non-default values would be honored if + the feature flag is enabled. If both are custom, the DeletionPolicy + field will be ignored. See the design doc for more information: + https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223 + and this one: https://github.com/crossplane/crossplane/blob/444267e84783136daa93568b364a5f01228cacbe/design/one-pager-ignore-changes.md' + items: + description: A ManagementAction represents an action that the Crossplane + controllers can take on an external resource. + enum: + - Observe + - Create + - Update + - Delete + - LateInitialize + - '*' + type: string + type: array + providerConfigRef: + default: + name: default + description: ProviderConfigReference specifies how the provider that + will be used to create, observe, update, and delete this managed + resource should be configured. + properties: + name: + description: Name of the referenced object. + type: string + policy: + description: Policies for referencing. + properties: + resolution: + default: Required + description: Resolution specifies whether resolution of this + reference is required. The default is 'Required', which + means the reconcile will fail if the reference cannot be + resolved. 'Optional' means this reference will be a no-op + if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: Resolve specifies when this reference should + be resolved. The default is 'IfNotPresent', which will attempt + to resolve the reference only when the corresponding field + is not present. Use 'Always' to resolve the reference on + every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + required: + - name + type: object + publishConnectionDetailsTo: + description: PublishConnectionDetailsTo specifies the connection secret + config which contains a name, metadata and a reference to secret + store config to which any connection details for this managed resource + should be written. Connection details frequently include the endpoint, + username, and password required to connect to the managed resource. + properties: + configRef: + default: + name: default + description: SecretStoreConfigRef specifies which secret store + config should be used for this ConnectionSecret. + properties: + name: + description: Name of the referenced object. + type: string + policy: + description: Policies for referencing. + properties: + resolution: + default: Required + description: Resolution specifies whether resolution of + this reference is required. The default is 'Required', + which means the reconcile will fail if the reference + cannot be resolved. 'Optional' means this reference + will be a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: Resolve specifies when this reference should + be resolved. The default is 'IfNotPresent', which will + attempt to resolve the reference only when the corresponding + field is not present. Use 'Always' to resolve the reference + on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + required: + - name + type: object + metadata: + description: Metadata is the metadata for connection secret. + properties: + annotations: + additionalProperties: + type: string + description: Annotations are the annotations to be added to + connection secret. - For Kubernetes secrets, this will be + used as "metadata.annotations". - It is up to Secret Store + implementation for others store types. + type: object + labels: + additionalProperties: + type: string + description: Labels are the labels/tags to be added to connection + secret. - For Kubernetes secrets, this will be used as "metadata.labels". + - It is up to Secret Store implementation for others store + types. + type: object + type: + description: Type is the SecretType for the connection secret. + - Only valid for Kubernetes Secret Stores. + type: string + type: object + name: + description: Name is the name of the connection secret. + type: string + required: + - name + type: object + readiness: + description: Readiness defines how the object's readiness condition + should be computed, if not specified it will be considered ready + as soon as the underlying external resource is considered up-to-date. + properties: + policy: + default: SuccessfulCreate + description: Policy defines how the Object's readiness condition + should be computed. + enum: + - SuccessfulCreate + - DeriveFromObject + - AllTrue + type: string + type: object + references: + items: + description: Reference refers to an Object or arbitrary Kubernetes + resource and optionally patch values from that resource to the + current Object. + properties: + dependsOn: + description: DependsOn is used to declare dependency on other + Object or arbitrary Kubernetes resource. + properties: + apiVersion: + default: kubernetes.crossplane.io/v1alpha1 + description: APIVersion of the referenced object. + type: string + kind: + default: Object + description: Kind of the referenced object. + type: string + name: + description: Name of the referenced object. + type: string + namespace: + description: Namespace of the referenced object. + type: string + required: + - name + type: object + patchesFrom: + description: PatchesFrom is used to declare dependency on other + Object or arbitrary Kubernetes resource, and also patch fields + from this object. + properties: + apiVersion: + default: kubernetes.crossplane.io/v1alpha1 + description: APIVersion of the referenced object. + type: string + fieldPath: + description: FieldPath is the path of the field on the resource + whose value is to be used as input. + type: string + kind: + default: Object + description: Kind of the referenced object. + type: string + name: + description: Name of the referenced object. + type: string + namespace: + description: Namespace of the referenced object. + type: string + required: + - fieldPath + - name + type: object + toFieldPath: + description: ToFieldPath is the path of the field on the resource + whose value will be changed with the result of transforms. + Leave empty if you'd like to propagate to the same path as + patchesFrom.fieldPath. + type: string + type: object + type: array + writeConnectionSecretToRef: + description: WriteConnectionSecretToReference specifies the namespace + and name of a Secret to which any connection details for this managed + resource should be written. Connection details frequently include + the endpoint, username, and password required to connect to the + managed resource. This field is planned to be replaced in a future + release in favor of PublishConnectionDetailsTo. Currently, both + could be set independently and connection details would be published + to both without affecting each other. + properties: + name: + description: Name of the secret. + type: string + namespace: + description: Namespace of the secret. + type: string + required: + - name + - namespace + type: object + required: + - forProvider + type: object + status: + description: A ObjectStatus represents the observed state of a Object. + properties: + atProvider: + description: ObjectObservation are the observable fields of a Object. + properties: + manifest: + description: Raw JSON representation of the remote object. + type: object + x-kubernetes-embedded-resource: true + x-kubernetes-preserve-unknown-fields: true + type: object + conditions: + description: Conditions of the resource. + items: + description: A Condition that may apply to a resource. + properties: + lastTransitionTime: + description: LastTransitionTime is the last time this condition + transitioned from one status to another. + format: date-time + type: string + message: + description: A Message containing details about this condition's + last transition from one status to another, if any. + type: string + reason: + description: A Reason for this condition's last transition from + one status to another. + type: string + status: + description: Status of this condition; is it currently True, + False, or Unknown? + type: string + type: + description: Type of this condition. At most one of each condition + type may apply to a resource at any point in time. + type: string + required: + - lastTransitionTime + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true storage: true subresources: status: {}