From b08e42eb87ad7c327482e005e597478916b396ab Mon Sep 17 00:00:00 2001 From: Ana-Maria Garlau Date: Wed, 11 Sep 2024 11:48:36 +0200 Subject: [PATCH] feat(mq): Add MQ Configuration resource Signed-off-by: ana --- apis/mq/generator-config.yaml | 17 +- apis/mq/v1alpha1/custom_types.go | 15 + apis/mq/v1alpha1/zz_configuration.go | 111 ++++++ apis/mq/v1alpha1/zz_generated.deepcopy.go | 284 ++++++++++++-- apis/mq/v1alpha1/zz_generated.managed.go | 60 +++ apis/mq/v1alpha1/zz_generated.managedlist.go | 9 + apis/mq/v1alpha1/zz_types.go | 36 +- examples/mq/activemq-config.yaml | 25 ++ .../mq.aws.crossplane.io_configurations.yaml | 362 ++++++++++++++++++ .../mq/configuration/conversions.go | 61 +++ pkg/controller/mq/configuration/setup.go | 247 ++++++++++++ .../mq/configuration/utils/annotations.go | 34 ++ .../mq/configuration/utils/xmlparse.go | 84 ++++ .../mq/configuration/utils/xmlparse_test.go | 118 ++++++ .../mq/configuration/zz_controller.go | 241 ++++++++++++ .../mq/configuration/zz_conversions.go | 142 +++++++ pkg/controller/mwaa/setup.go | 2 + 17 files changed, 1802 insertions(+), 46 deletions(-) create mode 100644 apis/mq/v1alpha1/zz_configuration.go create mode 100644 examples/mq/activemq-config.yaml create mode 100644 package/crds/mq.aws.crossplane.io_configurations.yaml create mode 100644 pkg/controller/mq/configuration/conversions.go create mode 100644 pkg/controller/mq/configuration/setup.go create mode 100644 pkg/controller/mq/configuration/utils/annotations.go create mode 100644 pkg/controller/mq/configuration/utils/xmlparse.go create mode 100644 pkg/controller/mq/configuration/utils/xmlparse_test.go create mode 100644 pkg/controller/mq/configuration/zz_controller.go create mode 100644 pkg/controller/mq/configuration/zz_conversions.go diff --git a/apis/mq/generator-config.yaml b/apis/mq/generator-config.yaml index 610a06d889..5f5f35f582 100644 --- a/apis/mq/generator-config.yaml +++ b/apis/mq/generator-config.yaml @@ -1,4 +1,6 @@ ignore: + operations: + - UpdateConfiguration field_paths: - CreateBrokerRequest.Users - CreateBrokerRequest.BrokerName @@ -7,9 +9,20 @@ ignore: - CreateUserRequest.Username - CreateUserRequest.BrokerId - CreateUserRequest.Password - resource_names: - - Configuration resources: + Configuration: + fields: + LatestRevision.Data: + is_read_only: true + from: + operation: DescribeConfigurationRevision + path: Data + Tags: + is_read_only: true + from: + operation: DescribeConfiguration + path: Tags + Broker: fields: BrokerInstances: diff --git a/apis/mq/v1alpha1/custom_types.go b/apis/mq/v1alpha1/custom_types.go index df70f4fac3..ee103eaa4d 100644 --- a/apis/mq/v1alpha1/custom_types.go +++ b/apis/mq/v1alpha1/custom_types.go @@ -81,3 +81,18 @@ type CustomUserParameters struct { PasswordSecretRef xpv1.SecretKeySelector `json:"passwordSecretRef,omitempty"` } + +// CustomConfigurationParameters contains the additional fields for CustomConfigurationParameters +type CustomConfigurationParameters struct { + Data *string `json:"data,omitempty"` + Description *string `json:"description,omitempty"` +} + +const ( + // LatestUnsanitizedConfiguration is the key in the annotations map of a + // Configuration resource to track the latest unsanitized version + // and stop the update loop if the external update result always results in + // sanitization of the desired configuration.spec.data + // the latest desired config is stored as + LatestUnsanitizedConfiguration = "crossplane.io/latest-unsanitized-desired" +) diff --git a/apis/mq/v1alpha1/zz_configuration.go b/apis/mq/v1alpha1/zz_configuration.go new file mode 100644 index 0000000000..d7fdf2f431 --- /dev/null +++ b/apis/mq/v1alpha1/zz_configuration.go @@ -0,0 +1,111 @@ +/* +Copyright 2021 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 ack-generate. DO NOT EDIT. + +package v1alpha1 + +import ( + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// ConfigurationParameters defines the desired state of Configuration +type ConfigurationParameters struct { + // Region is which region the Configuration will be created. + // +kubebuilder:validation:Required + Region string `json:"region"` + + AuthenticationStrategy *string `json:"authenticationStrategy,omitempty"` + + // +kubebuilder:validation:Required + EngineType *string `json:"engineType"` + + // +kubebuilder:validation:Required + EngineVersion *string `json:"engineVersion"` + + // +kubebuilder:validation:Required + Name *string `json:"name"` + + Tags map[string]*string `json:"tags,omitempty"` + CustomConfigurationParameters `json:",inline"` +} + +// ConfigurationSpec defines the desired state of Configuration +type ConfigurationSpec struct { + xpv1.ResourceSpec `json:",inline"` + ForProvider ConfigurationParameters `json:"forProvider"` +} + +// ConfigurationObservation defines the observed state of Configuration +type ConfigurationObservation struct { + ARN *string `json:"arn,omitempty"` + + Created *metav1.Time `json:"created,omitempty"` + + ID *string `json:"id,omitempty"` + + LatestRevision *ConfigurationRevision `json:"latestRevision,omitempty"` + + LatestRevisionData *string `json:"latestRevisionData,omitempty"` + + Tags map[string]*string `json:"tags,omitempty"` +} + +// ConfigurationStatus defines the observed state of Configuration. +type ConfigurationStatus struct { + xpv1.ResourceStatus `json:",inline"` + AtProvider ConfigurationObservation `json:"atProvider,omitempty"` +} + +// +kubebuilder:object:root=true + +// Configuration is the Schema for the Configurations API +// +kubebuilder:printcolumn:name="READY",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" +// +kubebuilder:printcolumn:name="SYNCED",type="string",JSONPath=".status.conditions[?(@.type=='Synced')].status" +// +kubebuilder:printcolumn:name="EXTERNAL-NAME",type="string",JSONPath=".metadata.annotations.crossplane\\.io/external-name" +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:subresource:status +// +kubebuilder:storageversion +// +kubebuilder:resource:scope=Cluster,categories={crossplane,managed,aws} +type Configuration struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec ConfigurationSpec `json:"spec"` + Status ConfigurationStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ConfigurationList contains a list of Configurations +type ConfigurationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Configuration `json:"items"` +} + +// Repository type metadata. +var ( + ConfigurationKind = "Configuration" + ConfigurationGroupKind = schema.GroupKind{Group: CRDGroup, Kind: ConfigurationKind}.String() + ConfigurationKindAPIVersion = ConfigurationKind + "." + GroupVersion.String() + ConfigurationGroupVersionKind = GroupVersion.WithKind(ConfigurationKind) +) + +func init() { + SchemeBuilder.Register(&Configuration{}, &ConfigurationList{}) +} diff --git a/apis/mq/v1alpha1/zz_generated.deepcopy.go b/apis/mq/v1alpha1/zz_generated.deepcopy.go index afe2b84e76..bc8e3ea382 100644 --- a/apis/mq/v1alpha1/zz_generated.deepcopy.go +++ b/apis/mq/v1alpha1/zz_generated.deepcopy.go @@ -526,13 +526,92 @@ func (in *BrokerSummary) DeepCopy() *BrokerSummary { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Configuration) DeepCopyInto(out *Configuration) { *out = *in - if in.ARN != nil { - in, out := &in.ARN, &out.ARN + 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 Configuration. +func (in *Configuration) DeepCopy() *Configuration { + if in == nil { + return nil + } + out := new(Configuration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Configuration) 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 *ConfigurationID) DeepCopyInto(out *ConfigurationID) { + *out = *in + if in.ID != nil { + in, out := &in.ID, &out.ID *out = new(string) **out = **in } - if in.AuthenticationStrategy != nil { - in, out := &in.AuthenticationStrategy, &out.AuthenticationStrategy + if in.Revision != nil { + in, out := &in.Revision, &out.Revision + *out = new(int64) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigurationID. +func (in *ConfigurationID) DeepCopy() *ConfigurationID { + if in == nil { + return nil + } + out := new(ConfigurationID) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigurationList) DeepCopyInto(out *ConfigurationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Configuration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigurationList. +func (in *ConfigurationList) DeepCopy() *ConfigurationList { + if in == nil { + return nil + } + out := new(ConfigurationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ConfigurationList) 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 *ConfigurationObservation) DeepCopyInto(out *ConfigurationObservation) { + *out = *in + if in.ARN != nil { + in, out := &in.ARN, &out.ARN *out = new(string) **out = **in } @@ -540,8 +619,54 @@ func (in *Configuration) DeepCopyInto(out *Configuration) { in, out := &in.Created, &out.Created *out = (*in).DeepCopy() } - if in.Description != nil { - in, out := &in.Description, &out.Description + if in.ID != nil { + in, out := &in.ID, &out.ID + *out = new(string) + **out = **in + } + if in.LatestRevision != nil { + in, out := &in.LatestRevision, &out.LatestRevision + *out = new(ConfigurationRevision) + (*in).DeepCopyInto(*out) + } + if in.LatestRevisionData != nil { + in, out := &in.LatestRevisionData, &out.LatestRevisionData + *out = new(string) + **out = **in + } + if in.Tags != nil { + in, out := &in.Tags, &out.Tags + *out = make(map[string]*string, len(*in)) + for key, val := range *in { + var outVal *string + if val == nil { + (*out)[key] = nil + } else { + inVal := (*in)[key] + in, out := &inVal, &outVal + *out = new(string) + **out = **in + } + (*out)[key] = outVal + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigurationObservation. +func (in *ConfigurationObservation) DeepCopy() *ConfigurationObservation { + if in == nil { + return nil + } + out := new(ConfigurationObservation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigurationParameters) DeepCopyInto(out *ConfigurationParameters) { + *out = *in + if in.AuthenticationStrategy != nil { + in, out := &in.AuthenticationStrategy, &out.AuthenticationStrategy *out = new(string) **out = **in } @@ -555,11 +680,6 @@ func (in *Configuration) DeepCopyInto(out *Configuration) { *out = new(string) **out = **in } - if in.ID != nil { - in, out := &in.ID, &out.ID - *out = new(string) - **out = **in - } if in.Name != nil { in, out := &in.Name, &out.Name *out = new(string) @@ -581,23 +701,28 @@ func (in *Configuration) DeepCopyInto(out *Configuration) { (*out)[key] = outVal } } + in.CustomConfigurationParameters.DeepCopyInto(&out.CustomConfigurationParameters) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Configuration. -func (in *Configuration) DeepCopy() *Configuration { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigurationParameters. +func (in *ConfigurationParameters) DeepCopy() *ConfigurationParameters { if in == nil { return nil } - out := new(Configuration) + out := new(ConfigurationParameters) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ConfigurationID) DeepCopyInto(out *ConfigurationID) { +func (in *ConfigurationRevision) DeepCopyInto(out *ConfigurationRevision) { *out = *in - if in.ID != nil { - in, out := &in.ID, &out.ID + if in.Created != nil { + in, out := &in.Created, &out.Created + *out = (*in).DeepCopy() + } + if in.Description != nil { + in, out := &in.Description, &out.Description *out = new(string) **out = **in } @@ -608,19 +733,63 @@ func (in *ConfigurationID) DeepCopyInto(out *ConfigurationID) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigurationID. -func (in *ConfigurationID) DeepCopy() *ConfigurationID { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigurationRevision. +func (in *ConfigurationRevision) DeepCopy() *ConfigurationRevision { if in == nil { return nil } - out := new(ConfigurationID) + out := new(ConfigurationRevision) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ConfigurationRevision) DeepCopyInto(out *ConfigurationRevision) { +func (in *ConfigurationSpec) DeepCopyInto(out *ConfigurationSpec) { *out = *in + in.ResourceSpec.DeepCopyInto(&out.ResourceSpec) + in.ForProvider.DeepCopyInto(&out.ForProvider) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigurationSpec. +func (in *ConfigurationSpec) DeepCopy() *ConfigurationSpec { + if in == nil { + return nil + } + out := new(ConfigurationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigurationStatus) DeepCopyInto(out *ConfigurationStatus) { + *out = *in + in.ResourceStatus.DeepCopyInto(&out.ResourceStatus) + in.AtProvider.DeepCopyInto(&out.AtProvider) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigurationStatus. +func (in *ConfigurationStatus) DeepCopy() *ConfigurationStatus { + if in == nil { + return nil + } + out := new(ConfigurationStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Configuration_SDK) DeepCopyInto(out *Configuration_SDK) { + *out = *in + if in.ARN != nil { + in, out := &in.ARN, &out.ARN + *out = new(string) + **out = **in + } + if in.AuthenticationStrategy != nil { + in, out := &in.AuthenticationStrategy, &out.AuthenticationStrategy + *out = new(string) + **out = **in + } if in.Created != nil { in, out := &in.Created, &out.Created *out = (*in).DeepCopy() @@ -630,19 +799,55 @@ func (in *ConfigurationRevision) DeepCopyInto(out *ConfigurationRevision) { *out = new(string) **out = **in } - if in.Revision != nil { - in, out := &in.Revision, &out.Revision - *out = new(int64) + if in.EngineType != nil { + in, out := &in.EngineType, &out.EngineType + *out = new(string) + **out = **in + } + if in.EngineVersion != nil { + in, out := &in.EngineVersion, &out.EngineVersion + *out = new(string) + **out = **in + } + if in.ID != nil { + in, out := &in.ID, &out.ID + *out = new(string) **out = **in } + if in.LatestRevision != nil { + in, out := &in.LatestRevision, &out.LatestRevision + *out = new(ConfigurationRevision) + (*in).DeepCopyInto(*out) + } + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + if in.Tags != nil { + in, out := &in.Tags, &out.Tags + *out = make(map[string]*string, len(*in)) + for key, val := range *in { + var outVal *string + if val == nil { + (*out)[key] = nil + } else { + inVal := (*in)[key] + in, out := &inVal, &outVal + *out = new(string) + **out = **in + } + (*out)[key] = outVal + } + } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigurationRevision. -func (in *ConfigurationRevision) DeepCopy() *ConfigurationRevision { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Configuration_SDK. +func (in *Configuration_SDK) DeepCopy() *Configuration_SDK { if in == nil { return nil } - out := new(ConfigurationRevision) + out := new(Configuration_SDK) in.DeepCopyInto(out) return out } @@ -755,6 +960,31 @@ func (in *CustomBrokerParameters) DeepCopy() *CustomBrokerParameters { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CustomConfigurationParameters) DeepCopyInto(out *CustomConfigurationParameters) { + *out = *in + if in.Data != nil { + in, out := &in.Data, &out.Data + *out = new(string) + **out = **in + } + if in.Description != nil { + in, out := &in.Description, &out.Description + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomConfigurationParameters. +func (in *CustomConfigurationParameters) DeepCopy() *CustomConfigurationParameters { + if in == nil { + return nil + } + out := new(CustomConfigurationParameters) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CustomUser) DeepCopyInto(out *CustomUser) { *out = *in diff --git a/apis/mq/v1alpha1/zz_generated.managed.go b/apis/mq/v1alpha1/zz_generated.managed.go index 1d51fe4362..7f0c5d5c49 100644 --- a/apis/mq/v1alpha1/zz_generated.managed.go +++ b/apis/mq/v1alpha1/zz_generated.managed.go @@ -80,6 +80,66 @@ func (mg *Broker) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { mg.Spec.WriteConnectionSecretToReference = r } +// GetCondition of this Configuration. +func (mg *Configuration) GetCondition(ct xpv1.ConditionType) xpv1.Condition { + return mg.Status.GetCondition(ct) +} + +// GetDeletionPolicy of this Configuration. +func (mg *Configuration) GetDeletionPolicy() xpv1.DeletionPolicy { + return mg.Spec.DeletionPolicy +} + +// GetManagementPolicies of this Configuration. +func (mg *Configuration) GetManagementPolicies() xpv1.ManagementPolicies { + return mg.Spec.ManagementPolicies +} + +// GetProviderConfigReference of this Configuration. +func (mg *Configuration) GetProviderConfigReference() *xpv1.Reference { + return mg.Spec.ProviderConfigReference +} + +// GetPublishConnectionDetailsTo of this Configuration. +func (mg *Configuration) GetPublishConnectionDetailsTo() *xpv1.PublishConnectionDetailsTo { + return mg.Spec.PublishConnectionDetailsTo +} + +// GetWriteConnectionSecretToReference of this Configuration. +func (mg *Configuration) GetWriteConnectionSecretToReference() *xpv1.SecretReference { + return mg.Spec.WriteConnectionSecretToReference +} + +// SetConditions of this Configuration. +func (mg *Configuration) SetConditions(c ...xpv1.Condition) { + mg.Status.SetConditions(c...) +} + +// SetDeletionPolicy of this Configuration. +func (mg *Configuration) SetDeletionPolicy(r xpv1.DeletionPolicy) { + mg.Spec.DeletionPolicy = r +} + +// SetManagementPolicies of this Configuration. +func (mg *Configuration) SetManagementPolicies(r xpv1.ManagementPolicies) { + mg.Spec.ManagementPolicies = r +} + +// SetProviderConfigReference of this Configuration. +func (mg *Configuration) SetProviderConfigReference(r *xpv1.Reference) { + mg.Spec.ProviderConfigReference = r +} + +// SetPublishConnectionDetailsTo of this Configuration. +func (mg *Configuration) SetPublishConnectionDetailsTo(r *xpv1.PublishConnectionDetailsTo) { + mg.Spec.PublishConnectionDetailsTo = r +} + +// SetWriteConnectionSecretToReference of this Configuration. +func (mg *Configuration) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { + mg.Spec.WriteConnectionSecretToReference = r +} + // GetCondition of this User. func (mg *User) GetCondition(ct xpv1.ConditionType) xpv1.Condition { return mg.Status.GetCondition(ct) diff --git a/apis/mq/v1alpha1/zz_generated.managedlist.go b/apis/mq/v1alpha1/zz_generated.managedlist.go index 799595d4ff..fe5c815af3 100644 --- a/apis/mq/v1alpha1/zz_generated.managedlist.go +++ b/apis/mq/v1alpha1/zz_generated.managedlist.go @@ -29,6 +29,15 @@ func (l *BrokerList) GetItems() []resource.Managed { return items } +// GetItems of this ConfigurationList. +func (l *ConfigurationList) GetItems() []resource.Managed { + items := make([]resource.Managed, len(l.Items)) + for i := range l.Items { + items[i] = &l.Items[i] + } + return items +} + // GetItems of this UserList. func (l *UserList) GetItems() []resource.Managed { items := make([]resource.Managed, len(l.Items)) diff --git a/apis/mq/v1alpha1/zz_types.go b/apis/mq/v1alpha1/zz_types.go index 03ae39e91a..3d03cba0b0 100644 --- a/apis/mq/v1alpha1/zz_types.go +++ b/apis/mq/v1alpha1/zz_types.go @@ -88,7 +88,23 @@ type BrokerSummary struct { } // +kubebuilder:skipversion -type Configuration struct { +type ConfigurationID struct { + ID *string `json:"id,omitempty"` + + Revision *int64 `json:"revision,omitempty"` +} + +// +kubebuilder:skipversion +type ConfigurationRevision struct { + Created *metav1.Time `json:"created,omitempty"` + + Description *string `json:"description,omitempty"` + + Revision *int64 `json:"revision,omitempty"` +} + +// +kubebuilder:skipversion +type Configuration_SDK struct { ARN *string `json:"arn,omitempty"` // Optional. The authentication strategy used to secure the broker. The default // is SIMPLE. @@ -103,28 +119,14 @@ type Configuration struct { EngineVersion *string `json:"engineVersion,omitempty"` ID *string `json:"id,omitempty"` + // Returns information about the specified configuration revision. + LatestRevision *ConfigurationRevision `json:"latestRevision,omitempty"` Name *string `json:"name,omitempty"` Tags map[string]*string `json:"tags,omitempty"` } -// +kubebuilder:skipversion -type ConfigurationID struct { - ID *string `json:"id,omitempty"` - - Revision *int64 `json:"revision,omitempty"` -} - -// +kubebuilder:skipversion -type ConfigurationRevision struct { - Created *metav1.Time `json:"created,omitempty"` - - Description *string `json:"description,omitempty"` - - Revision *int64 `json:"revision,omitempty"` -} - // +kubebuilder:skipversion type Configurations struct { // A list of information about the configuration. diff --git a/examples/mq/activemq-config.yaml b/examples/mq/activemq-config.yaml new file mode 100644 index 0000000000..4625402bac --- /dev/null +++ b/examples/mq/activemq-config.yaml @@ -0,0 +1,25 @@ +apiVersion: mq.aws.crossplane.io/v1alpha1 +kind: Configuration +metadata: + name: example-mq-config +spec: + forProvider: + name: example-mq-config + description: example description + data: | + + + + + + + + + + region: eu-central-1 + engineType: ActiveMQ + engineVersion: 5.17.6 + tags: + Test: test + providerConfigRef: + name: default \ No newline at end of file diff --git a/package/crds/mq.aws.crossplane.io_configurations.yaml b/package/crds/mq.aws.crossplane.io_configurations.yaml new file mode 100644 index 0000000000..a64d85a96b --- /dev/null +++ b/package/crds/mq.aws.crossplane.io_configurations.yaml @@ -0,0 +1,362 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: configurations.mq.aws.crossplane.io +spec: + group: mq.aws.crossplane.io + names: + categories: + - crossplane + - managed + - aws + kind: Configuration + listKind: ConfigurationList + plural: configurations + singular: configuration + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=='Ready')].status + name: READY + type: string + - jsonPath: .status.conditions[?(@.type=='Synced')].status + name: SYNCED + type: string + - jsonPath: .metadata.annotations.crossplane\.io/external-name + name: EXTERNAL-NAME + type: string + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: Configuration is the Schema for the Configurations API + 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: ConfigurationSpec defines the desired state of Configuration + properties: + 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: ConfigurationParameters defines the desired state of + Configuration + properties: + authenticationStrategy: + type: string + data: + type: string + description: + type: string + engineType: + type: string + engineVersion: + type: string + name: + type: string + region: + description: Region is which region the Configuration will be + created. + type: string + tags: + additionalProperties: + type: string + type: object + required: + - engineType + - engineVersion + - name + - region + 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 + 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: ConfigurationStatus defines the observed state of Configuration. + properties: + atProvider: + description: ConfigurationObservation defines the observed state of + Configuration + properties: + arn: + type: string + created: + format: date-time + type: string + id: + type: string + latestRevision: + properties: + created: + format: date-time + type: string + description: + type: string + revision: + format: int64 + type: integer + type: object + latestRevisionData: + type: string + tags: + additionalProperties: + type: string + type: object + 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 + observedGeneration: + description: |- + ObservedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + type: integer + 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 + observedGeneration: + description: |- + ObservedGeneration is the latest metadata.generation + which resulted in either a ready state, or stalled due to error + it can not recover from without human intervention. + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/pkg/controller/mq/configuration/conversions.go b/pkg/controller/mq/configuration/conversions.go new file mode 100644 index 0000000000..1d407ba580 --- /dev/null +++ b/pkg/controller/mq/configuration/conversions.go @@ -0,0 +1,61 @@ +package configuration + +import ( + "encoding/base64" + "fmt" + "strconv" + + svcsdk "github.com/aws/aws-sdk-go/service/mq" + cperrors "github.com/crossplane/crossplane-runtime/pkg/errors" + "github.com/crossplane/crossplane-runtime/pkg/meta" + + svcapitypes "github.com/crossplane-contrib/provider-aws/apis/mq/v1alpha1" + "github.com/crossplane-contrib/provider-aws/pkg/utils/pointer" +) + +func generateDescribeConfigurationRevisionInput(id *string, revision *int64) *svcsdk.DescribeConfigurationRevisionInput { + currRevision := strconv.FormatInt(pointer.Int64Value(revision), 10) + res := &svcsdk.DescribeConfigurationRevisionInput{ + ConfigurationId: id, + ConfigurationRevision: &currRevision, + } + + return res +} + +func generateUpdateConfigurationRequest(cr *svcapitypes.Configuration) *svcsdk.UpdateConfigurationRequest { + res := &svcsdk.UpdateConfigurationRequest{ + ConfigurationId: pointer.ToOrNilIfZeroValue(meta.GetExternalName(cr)), + Data: pointer.ToOrNilIfZeroValue(base64.StdEncoding.EncodeToString([]byte(pointer.StringValue(cr.Spec.ForProvider.Data)))), + Description: cr.Spec.ForProvider.Description, + } + return res +} + +const ( + ErrCannotAddTags = "cannot add tags" + ErrCannotRemoveTags = "cannot remove tags" + ErrSanitizedConfig = "The desired configuration has been sanitized, please adjust the data field accordingly:\n" + ErrUnknownSanitization = "An unknown sanitization reason occurred." + ErrDisallowedElement = "The element '%s' was removed because it is disallowed.\n" + ErrDisallowedAttribute = "The attribute '%s' of element '%s' was removed because it is disallowed.\n" + ErrInvalidAttributeValue = "The attribute '%s' of element '%s' was removed because it had an invalid value.\n" +) + +func handleSanitizationWarnings(warnings []*svcsdk.SanitizationWarning) error { + message := ErrSanitizedConfig + for _, w := range warnings { + reason := pointer.StringValue(w.Reason) + switch svcapitypes.SanitizationWarningReason(reason) { + case svcapitypes.SanitizationWarningReason_DISALLOWED_ELEMENT_REMOVED: + message += fmt.Sprintf(ErrDisallowedElement, pointer.StringValue(w.ElementName)) + case svcapitypes.SanitizationWarningReason_DISALLOWED_ATTRIBUTE_REMOVED: + message += fmt.Sprintf(ErrDisallowedAttribute, pointer.StringValue(w.AttributeName), pointer.StringValue(w.ElementName)) + case svcapitypes.SanitizationWarningReason_INVALID_ATTRIBUTE_VALUE_REMOVED: + message += fmt.Sprintf(ErrInvalidAttributeValue, pointer.StringValue(w.AttributeName), pointer.StringValue(w.ElementName)) + default: + message += ErrUnknownSanitization + } + } + return cperrors.New(message) +} diff --git a/pkg/controller/mq/configuration/setup.go b/pkg/controller/mq/configuration/setup.go new file mode 100644 index 0000000000..5d4ab5bfb2 --- /dev/null +++ b/pkg/controller/mq/configuration/setup.go @@ -0,0 +1,247 @@ +package configuration + +import ( + "context" + "encoding/base64" + + svcsdk "github.com/aws/aws-sdk-go/service/mq" + svcsdkapi "github.com/aws/aws-sdk-go/service/mq/mqiface" + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/connection" + "github.com/crossplane/crossplane-runtime/pkg/controller" + "github.com/crossplane/crossplane-runtime/pkg/event" + "github.com/crossplane/crossplane-runtime/pkg/meta" + "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" + "github.com/crossplane/crossplane-runtime/pkg/resource" + cpresource "github.com/crossplane/crossplane-runtime/pkg/resource" + "github.com/pkg/errors" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + svcapitypes "github.com/crossplane-contrib/provider-aws/apis/mq/v1alpha1" + "github.com/crossplane-contrib/provider-aws/apis/v1alpha1" + mqconfutils "github.com/crossplane-contrib/provider-aws/pkg/controller/mq/configuration/utils" + "github.com/crossplane-contrib/provider-aws/pkg/features" + errorutils "github.com/crossplane-contrib/provider-aws/pkg/utils/errors" + "github.com/crossplane-contrib/provider-aws/pkg/utils/pointer" + custommanaged "github.com/crossplane-contrib/provider-aws/pkg/utils/reconciler/managed" + "github.com/crossplane-contrib/provider-aws/pkg/utils/tags" +) + +func SetupConfiguration(mgr ctrl.Manager, o controller.Options) error { + name := managed.ControllerName(svcapitypes.ConfigurationGroupKind) + opts := []option{ + func(e *external) { + c := &custom{client: e.client, kube: e.kube, external: e} + e.isUpToDate = c.isUpToDate + e.postCreate = c.postCreate + e.preObserve = preObserve + e.postObserve = c.postObserve + e.update = c.update + }, + } + + cps := []managed.ConnectionPublisher{managed.NewAPISecretPublisher(mgr.GetClient(), mgr.GetScheme())} + if o.Features.Enabled(features.EnableAlphaExternalSecretStores) { + cps = append(cps, connection.NewDetailsManager(mgr.GetClient(), v1alpha1.StoreConfigGroupVersionKind)) + } + reconcilerOpts := []managed.ReconcilerOption{ + managed.WithInitializers(), + managed.WithCriticalAnnotationUpdater(custommanaged.NewRetryingCriticalAnnotationUpdater(mgr.GetClient())), + managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), opts: opts}), + managed.WithPollInterval(o.PollInterval), + managed.WithLogger(o.Logger.WithValues("controller", name)), + managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))), + managed.WithConnectionPublishers(cps...), + } + + if o.Features.Enabled(features.EnableAlphaManagementPolicies) { + reconcilerOpts = append(reconcilerOpts, managed.WithManagementPolicies()) + } + + r := managed.NewReconciler(mgr, + resource.ManagedKind(svcapitypes.ConfigurationGroupVersionKind), + reconcilerOpts...) + + return ctrl.NewControllerManagedBy(mgr). + Named(name). + WithOptions(o.ForControllerRuntime()). + WithEventFilter(resource.DesiredStateChanged()). + For(&svcapitypes.Configuration{}). + Complete(r) +} + +type custom struct { + kube client.Client + client svcsdkapi.MQAPI + external *external +} + +func preObserve(_ context.Context, cr *svcapitypes.Configuration, obj *svcsdk.DescribeConfigurationInput) error { + obj.ConfigurationId = pointer.ToOrNilIfZeroValue(meta.GetExternalName(cr)) + return nil +} + +func (e *custom) postObserve(ctx context.Context, cr *svcapitypes.Configuration, obj *svcsdk.DescribeConfigurationOutput, obs managed.ExternalObservation, err error) (managed.ExternalObservation, error) { + if err != nil { + return managed.ExternalObservation{}, err + } + switch { + case meta.WasDeleted(cr): + obs.ResourceExists = false + case mqconfutils.HasBeenSanitized(cr): + cr.SetConditions(xpv1.Unavailable()) + default: + cr.SetConditions(xpv1.Available()) + } + + return obs, nil +} + +func (e *custom) postCreate(ctx context.Context, cr *svcapitypes.Configuration, obj *svcsdk.CreateConfigurationResponse, cre managed.ExternalCreation, err error) (managed.ExternalCreation, error) { + if err != nil { + return managed.ExternalCreation{}, err + } + meta.SetExternalName(cr, pointer.StringValue(obj.Id)) + return cre, nil +} + +func (e *custom) isUpToDate(ctx context.Context, cr *svcapitypes.Configuration, describeConfigOutput *svcsdk.DescribeConfigurationOutput) (bool, string, error) { + atProviderConfig, err := setData(ctx, e, cr, describeConfigOutput.Id, describeConfigOutput.LatestRevision.Revision) + if err != nil { + return false, "", err + } + setTags(cr, describeConfigOutput) + lateInitialize(describeConfigOutput, &cr.Spec.ForProvider, atProviderConfig) + // stops the update loop for the data field if the MQ Configuration sanitizes a revision + // any additional pending updates are postponed until the sanitization warnings + // in the error message are applied + if mqconfutils.HasBeenSanitized(cr) { + hasBeenUpdated := mqconfutils.HasBeenUpdatedPostSanitization(cr) + return !hasBeenUpdated, "", nil + } + isRevisionUpToDate, err := isRevisionUpToDate(cr) + if err != nil { + return false, "", err + } + + add, remove := tags.DiffTagsMapPtr(cr.Spec.ForProvider.Tags, describeConfigOutput.Tags) + areTagsUpToDate := len(add) == 0 && len(remove) == 0 + + return isRevisionUpToDate && areTagsUpToDate, "", nil +} + +func (e *custom) update(ctx context.Context, mg cpresource.Managed) (managed.ExternalUpdate, error) { + cr, ok := mg.(*svcapitypes.Configuration) + if !ok { + return managed.ExternalUpdate{}, errors.New(errUnexpectedObject) + } + + if cr.Status.AtProvider.ARN == nil { + return managed.ExternalUpdate{}, nil + } + added, removed := tags.DiffTagsMapPtr(cr.Spec.ForProvider.Tags, cr.Status.AtProvider.Tags) + if len(added) > 0 { + _, err := e.client.CreateTagsWithContext(ctx, &svcsdk.CreateTagsInput{ + ResourceArn: cr.Status.AtProvider.ARN, + Tags: added, + }) + if err != nil { + return managed.ExternalUpdate{}, errorutils.Wrap(err, ErrCannotAddTags) + } + } + if len(removed) > 0 { + _, err := e.client.DeleteTagsWithContext(ctx, &svcsdk.DeleteTagsInput{ + ResourceArn: cr.Status.AtProvider.ARN, + TagKeys: removed, + }) + if err != nil { + return managed.ExternalUpdate{}, errorutils.Wrap(err, ErrCannotRemoveTags) + } + + } + // update configuration only if its latest revision is not up to date + // a revision is up to date if both description and data are the same + // sanitization without warnings of the data field may potentially result in a new revision + // which can be halted in isUpToDate for the following observe + isRevisionUpToDate, err := isRevisionUpToDate(cr) + if err != nil { + return managed.ExternalUpdate{}, errorutils.Wrap(err, "cannot check if revision is up to date") + } + if isRevisionUpToDate { + return managed.ExternalUpdate{}, nil + } + input := generateUpdateConfigurationRequest(cr) + resp, err := e.client.UpdateConfigurationWithContext(ctx, input) + return e.postUpdate(ctx, cr, resp, managed.ExternalUpdate{}, errorutils.Wrap(err, errUpdate)) +} + +func (e *custom) postUpdate(ctx context.Context, cr *svcapitypes.Configuration, obj *svcsdk.UpdateConfigurationResponse, cre managed.ExternalUpdate, err error) (managed.ExternalUpdate, error) { + if err != nil { + return cre, err + } + + if len(obj.Warnings) == 0 && !mqconfutils.HasBeenSanitized(cr) { + return cre, nil + } + + currentObj := cr.DeepCopy() + if len(obj.Warnings) == 0 { + meta.RemoveAnnotations(cr, svcapitypes.LatestUnsanitizedConfiguration) + } else { + err = handleSanitizationWarnings(obj.Warnings) + mqconfutils.SetLatestUnsanitizedConfiguration(cr) + } + patch := client.MergeFrom(currentObj) + if err := e.kube.Patch(ctx, cr, patch); err != nil { + return cre, err + } + return cre, err +} + +func setData(ctx context.Context, e *custom, cr *svcapitypes.Configuration, id *string, revision *int64) (string, error) { + describeConfigRevisionOutput, err := getLatestRevisionData(ctx, e, id, revision) + if err != nil { + return "", err + } + atProviderDataLatestRevision, err := base64.StdEncoding.DecodeString(pointer.StringValue(describeConfigRevisionOutput.Data)) + if err != nil { + return "", err + } + atProviderConfig := string(atProviderDataLatestRevision) + cr.Status.AtProvider.LatestRevisionData = &atProviderConfig + return atProviderConfig, nil +} + +func setTags(cr *svcapitypes.Configuration, describeConfigOutput *svcsdk.DescribeConfigurationOutput) { + cr.Status.AtProvider.Tags = describeConfigOutput.Tags +} + +func getLatestRevisionData(ctx context.Context, e *custom, id *string, revision *int64) (*svcsdk.DescribeConfigurationRevisionResponse, error) { + describeRevisionInput := generateDescribeConfigurationRevisionInput(id, revision) + describeConfigRevisionOutput, err := e.client.DescribeConfigurationRevisionWithContext(ctx, describeRevisionInput) + if err != nil { + return nil, err + } + return describeConfigRevisionOutput, nil +} + +func lateInitialize(describeConfigOutput *svcsdk.DescribeConfigurationOutput, in *svcapitypes.ConfigurationParameters, out string) { + if pointer.StringValue(in.Data) == "" { + in.Data = pointer.ToOrNilIfZeroValue(out) + } + in.Description = pointer.LateInitialize(in.Description, describeConfigOutput.LatestRevision.Description) +} + +func isRevisionUpToDate(cr *svcapitypes.Configuration) (bool, error) { + forProviderConfig := pointer.StringValue(cr.Spec.ForProvider.Data) + atProviderConfig := pointer.StringValue(cr.Status.AtProvider.LatestRevisionData) + diffDataAtAndForProviderConfig, err := mqconfutils.DiffXMLConfigs(atProviderConfig, forProviderConfig) + if err != nil { + return false, err + } + + isDataUpToDate := diffDataAtAndForProviderConfig == "" + isDescriptionUpToDate := pointer.StringValue(cr.Spec.ForProvider.Description) == pointer.StringValue(cr.Status.AtProvider.LatestRevision.Description) + return isDataUpToDate && isDescriptionUpToDate, nil +} diff --git a/pkg/controller/mq/configuration/utils/annotations.go b/pkg/controller/mq/configuration/utils/annotations.go new file mode 100644 index 0000000000..c396b72996 --- /dev/null +++ b/pkg/controller/mq/configuration/utils/annotations.go @@ -0,0 +1,34 @@ +package utils + +import ( + "crypto/sha256" + "encoding/hex" + + "github.com/crossplane/crossplane-runtime/pkg/meta" + + svcapitypes "github.com/crossplane-contrib/provider-aws/apis/mq/v1alpha1" + "github.com/crossplane-contrib/provider-aws/pkg/utils/pointer" +) + +func SetLatestUnsanitizedConfiguration(cr *svcapitypes.Configuration) { + latestDesiredConfig := pointer.StringValue(cr.Spec.ForProvider.Data) + meta.AddAnnotations(cr, map[string]string{svcapitypes.LatestUnsanitizedConfiguration: hashText(latestDesiredConfig)}) +} + +func GetLatestUnsanitizedConfiguration(o *svcapitypes.Configuration) string { + return o.GetAnnotations()[svcapitypes.LatestUnsanitizedConfiguration] +} + +func hashText(s string) string { + hash := sha256.Sum256([]byte(s)) + return hex.EncodeToString(hash[:]) +} + +func HasBeenSanitized(o *svcapitypes.Configuration) bool { + return GetLatestUnsanitizedConfiguration(o) != "" +} + +func HasBeenUpdatedPostSanitization(o *svcapitypes.Configuration) bool { + latestDesiredConfig := pointer.StringValue(o.Spec.ForProvider.Data) + return GetLatestUnsanitizedConfiguration(o) != hashText(latestDesiredConfig) +} diff --git a/pkg/controller/mq/configuration/utils/xmlparse.go b/pkg/controller/mq/configuration/utils/xmlparse.go new file mode 100644 index 0000000000..43be95ed75 --- /dev/null +++ b/pkg/controller/mq/configuration/utils/xmlparse.go @@ -0,0 +1,84 @@ +package utils + +import ( + "bytes" + "encoding/xml" + "sort" + + "github.com/google/go-cmp/cmp" +) + +//nolint:musttag +type Node struct { + XMLName xml.Name + Attr []xml.Attr `xml:",any,attr"` + Content []byte `xml:",innerxml"` + Nodes []Node `xml:",any"` +} + +func (n *Node) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + //nolint:musttag + type node Node + return d.DecodeElement((*node)(n), &start) +} + +func DiffXMLConfigs(c1, c2 string) (string, error) { + var configNode1 Node + if err := decodeString(c1, &configNode1); err != nil { + return "", err + } + var configNode2 Node + if err := decodeString(c2, &configNode2); err != nil { + return "", err + } + return diffNodes(configNode1, configNode2), nil +} + +func decodeString(xmlString string, node *Node) error { + buf := bytes.NewBuffer([]byte(xmlString)) + dec := xml.NewDecoder(buf) + return dec.Decode(node) +} + +func diffNodes(forProvider, atProvider Node) string { + diff := cmp.Diff(forProvider, atProvider, nodeComparerOpt()) + return diff +} + +// recursively check equality of nodes +// based on attributes, subnodes and tag name equality +func nodeComparerOpt() cmp.Option { + return cmp.Comparer(func(x, y Node) bool { + if x.XMLName.Local != y.XMLName.Local { + return false + } + if len(x.Nodes) != len(y.Nodes) { + return false + } + + sort.Slice(x.Attr, func(i, j int) bool { + return x.Attr[i].Name.Local < x.Attr[j].Name.Local + }) + sort.Slice(y.Attr, func(i, j int) bool { + return y.Attr[i].Name.Local < y.Attr[j].Name.Local + }) + + if !cmp.Equal(x.Attr, y.Attr) { + return false + } + + sort.Slice(x.Nodes, func(i, j int) bool { + return x.Nodes[i].XMLName.Local < x.Nodes[j].XMLName.Local + }) + sort.Slice(y.Nodes, func(i, j int) bool { + return y.Nodes[i].XMLName.Local < y.Nodes[j].XMLName.Local + }) + + for i := range x.Nodes { + if !cmp.Equal(x.Nodes[i], y.Nodes[i], nodeComparerOpt()) { + return false + } + } + return true + }) +} diff --git a/pkg/controller/mq/configuration/utils/xmlparse_test.go b/pkg/controller/mq/configuration/utils/xmlparse_test.go new file mode 100644 index 0000000000..fa539fc67b --- /dev/null +++ b/pkg/controller/mq/configuration/utils/xmlparse_test.go @@ -0,0 +1,118 @@ +package utils + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestDiffMQConfiguration(t *testing.T) { + type args struct { + in string + out string + } + + type want struct { + diff string + } + + cases := map[string]struct { + args + want + }{ + "IdenticalConfigs with unsorted attributes": { + args: args{ + in: ` + + + + + + + `, + out: ` + + + + + + + `, + }, + want: want{ + diff: "", + }, + }, + "IdenticalConfigs, ignore comments": { + args: args{ + in: ` + + + + + + + + `, + out: ` + + + + + + + `, + }, + want: want{ + diff: "", + }, + }, + "Same config with diff tag order and wrong attribute order": { + args: args{ + in: ` + + + + + + + `, + out: ` + + + + + + + `, + }, + want: want{ + diff: "", + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + diff, err := DiffXMLConfigs(tc.args.in, tc.args.out) + if diff := cmp.Diff(tc.want.diff, diff); diff != "" || err != nil { + t.Errorf("DiffXMLConfigs() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/pkg/controller/mq/configuration/zz_controller.go b/pkg/controller/mq/configuration/zz_controller.go new file mode 100644 index 0000000000..2fd701c6db --- /dev/null +++ b/pkg/controller/mq/configuration/zz_controller.go @@ -0,0 +1,241 @@ +/* +Copyright 2021 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 ack-generate. DO NOT EDIT. + +package configuration + +import ( + "context" + + svcapi "github.com/aws/aws-sdk-go/service/mq" + svcsdk "github.com/aws/aws-sdk-go/service/mq" + svcsdkapi "github.com/aws/aws-sdk-go/service/mq/mqiface" + "github.com/google/go-cmp/cmp" + "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/meta" + "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" + cpresource "github.com/crossplane/crossplane-runtime/pkg/resource" + + svcapitypes "github.com/crossplane-contrib/provider-aws/apis/mq/v1alpha1" + connectaws "github.com/crossplane-contrib/provider-aws/pkg/utils/connect/aws" + errorutils "github.com/crossplane-contrib/provider-aws/pkg/utils/errors" +) + +const ( + errUnexpectedObject = "managed resource is not an Configuration resource" + + errCreateSession = "cannot create a new session" + errCreate = "cannot create Configuration in AWS" + errUpdate = "cannot update Configuration in AWS" + errDescribe = "failed to describe Configuration" + errDelete = "failed to delete Configuration" +) + +type connector struct { + kube client.Client + opts []option +} + +func (c *connector) Connect(ctx context.Context, mg cpresource.Managed) (managed.ExternalClient, error) { + cr, ok := mg.(*svcapitypes.Configuration) + if !ok { + return nil, errors.New(errUnexpectedObject) + } + sess, err := connectaws.GetConfigV1(ctx, c.kube, mg, cr.Spec.ForProvider.Region) + if err != nil { + return nil, errors.Wrap(err, errCreateSession) + } + return newExternal(c.kube, svcapi.New(sess), c.opts), nil +} + +func (e *external) Observe(ctx context.Context, mg cpresource.Managed) (managed.ExternalObservation, error) { + cr, ok := mg.(*svcapitypes.Configuration) + if !ok { + return managed.ExternalObservation{}, errors.New(errUnexpectedObject) + } + if meta.GetExternalName(cr) == "" { + return managed.ExternalObservation{ + ResourceExists: false, + }, nil + } + input := GenerateDescribeConfigurationInput(cr) + if err := e.preObserve(ctx, cr, input); err != nil { + return managed.ExternalObservation{}, errors.Wrap(err, "pre-observe failed") + } + resp, err := e.client.DescribeConfigurationWithContext(ctx, input) + if err != nil { + return managed.ExternalObservation{ResourceExists: false}, errorutils.Wrap(cpresource.Ignore(IsNotFound, err), errDescribe) + } + currentSpec := cr.Spec.ForProvider.DeepCopy() + if err := e.lateInitialize(&cr.Spec.ForProvider, resp); err != nil { + return managed.ExternalObservation{}, errors.Wrap(err, "late-init failed") + } + GenerateConfiguration(resp).Status.AtProvider.DeepCopyInto(&cr.Status.AtProvider) + upToDate := true + diff := "" + if !meta.WasDeleted(cr) { // There is no need to run isUpToDate if the resource is deleted + upToDate, diff, err = e.isUpToDate(ctx, cr, resp) + if err != nil { + return managed.ExternalObservation{}, errors.Wrap(err, "isUpToDate check failed") + } + } + return e.postObserve(ctx, cr, resp, managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: upToDate, + Diff: diff, + ResourceLateInitialized: !cmp.Equal(&cr.Spec.ForProvider, currentSpec), + }, nil) +} + +func (e *external) Create(ctx context.Context, mg cpresource.Managed) (managed.ExternalCreation, error) { + cr, ok := mg.(*svcapitypes.Configuration) + if !ok { + return managed.ExternalCreation{}, errors.New(errUnexpectedObject) + } + cr.Status.SetConditions(xpv1.Creating()) + input := GenerateCreateConfigurationRequest(cr) + if err := e.preCreate(ctx, cr, input); err != nil { + return managed.ExternalCreation{}, errors.Wrap(err, "pre-create failed") + } + resp, err := e.client.CreateConfigurationWithContext(ctx, input) + if err != nil { + return managed.ExternalCreation{}, errorutils.Wrap(err, errCreate) + } + + if resp.Arn != nil { + cr.Status.AtProvider.ARN = resp.Arn + } else { + cr.Status.AtProvider.ARN = nil + } + if resp.AuthenticationStrategy != nil { + cr.Spec.ForProvider.AuthenticationStrategy = resp.AuthenticationStrategy + } else { + cr.Spec.ForProvider.AuthenticationStrategy = nil + } + if resp.Created != nil { + cr.Status.AtProvider.Created = &metav1.Time{*resp.Created} + } else { + cr.Status.AtProvider.Created = nil + } + if resp.Id != nil { + cr.Status.AtProvider.ID = resp.Id + } else { + cr.Status.AtProvider.ID = nil + } + if resp.LatestRevision != nil { + f4 := &svcapitypes.ConfigurationRevision{} + if resp.LatestRevision.Created != nil { + f4.Created = &metav1.Time{*resp.LatestRevision.Created} + } + if resp.LatestRevision.Description != nil { + f4.Description = resp.LatestRevision.Description + } + if resp.LatestRevision.Revision != nil { + f4.Revision = resp.LatestRevision.Revision + } + cr.Status.AtProvider.LatestRevision = f4 + } else { + cr.Status.AtProvider.LatestRevision = nil + } + if resp.Name != nil { + cr.Spec.ForProvider.Name = resp.Name + } else { + cr.Spec.ForProvider.Name = nil + } + + return e.postCreate(ctx, cr, resp, managed.ExternalCreation{}, err) +} + +func (e *external) Update(ctx context.Context, mg cpresource.Managed) (managed.ExternalUpdate, error) { + return e.update(ctx, mg) + +} + +func (e *external) Delete(ctx context.Context, mg cpresource.Managed) error { + cr, ok := mg.(*svcapitypes.Configuration) + if !ok { + return errors.New(errUnexpectedObject) + } + cr.Status.SetConditions(xpv1.Deleting()) + return e.delete(ctx, mg) + +} + +type option func(*external) + +func newExternal(kube client.Client, client svcsdkapi.MQAPI, opts []option) *external { + e := &external{ + kube: kube, + client: client, + preObserve: nopPreObserve, + postObserve: nopPostObserve, + lateInitialize: nopLateInitialize, + isUpToDate: alwaysUpToDate, + preCreate: nopPreCreate, + postCreate: nopPostCreate, + delete: nopDelete, + update: nopUpdate, + } + for _, f := range opts { + f(e) + } + return e +} + +type external struct { + kube client.Client + client svcsdkapi.MQAPI + preObserve func(context.Context, *svcapitypes.Configuration, *svcsdk.DescribeConfigurationInput) error + postObserve func(context.Context, *svcapitypes.Configuration, *svcsdk.DescribeConfigurationOutput, managed.ExternalObservation, error) (managed.ExternalObservation, error) + lateInitialize func(*svcapitypes.ConfigurationParameters, *svcsdk.DescribeConfigurationOutput) error + isUpToDate func(context.Context, *svcapitypes.Configuration, *svcsdk.DescribeConfigurationOutput) (bool, string, error) + preCreate func(context.Context, *svcapitypes.Configuration, *svcsdk.CreateConfigurationRequest) error + postCreate func(context.Context, *svcapitypes.Configuration, *svcsdk.CreateConfigurationResponse, managed.ExternalCreation, error) (managed.ExternalCreation, error) + delete func(context.Context, cpresource.Managed) error + update func(context.Context, cpresource.Managed) (managed.ExternalUpdate, error) +} + +func nopPreObserve(context.Context, *svcapitypes.Configuration, *svcsdk.DescribeConfigurationInput) error { + return nil +} + +func nopPostObserve(_ context.Context, _ *svcapitypes.Configuration, _ *svcsdk.DescribeConfigurationOutput, obs managed.ExternalObservation, err error) (managed.ExternalObservation, error) { + return obs, err +} +func nopLateInitialize(*svcapitypes.ConfigurationParameters, *svcsdk.DescribeConfigurationOutput) error { + return nil +} +func alwaysUpToDate(context.Context, *svcapitypes.Configuration, *svcsdk.DescribeConfigurationOutput) (bool, string, error) { + return true, "", nil +} + +func nopPreCreate(context.Context, *svcapitypes.Configuration, *svcsdk.CreateConfigurationRequest) error { + return nil +} +func nopPostCreate(_ context.Context, _ *svcapitypes.Configuration, _ *svcsdk.CreateConfigurationResponse, cre managed.ExternalCreation, err error) (managed.ExternalCreation, error) { + return cre, err +} +func nopDelete(context.Context, cpresource.Managed) error { + return nil +} +func nopUpdate(context.Context, cpresource.Managed) (managed.ExternalUpdate, error) { + return managed.ExternalUpdate{}, nil +} diff --git a/pkg/controller/mq/configuration/zz_conversions.go b/pkg/controller/mq/configuration/zz_conversions.go new file mode 100644 index 0000000000..c3ea6b9251 --- /dev/null +++ b/pkg/controller/mq/configuration/zz_conversions.go @@ -0,0 +1,142 @@ +/* +Copyright 2021 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 ack-generate. DO NOT EDIT. + +package configuration + +import ( + "github.com/aws/aws-sdk-go/aws/awserr" + svcsdk "github.com/aws/aws-sdk-go/service/mq" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + svcapitypes "github.com/crossplane-contrib/provider-aws/apis/mq/v1alpha1" +) + +// NOTE(muvaf): We return pointers in case the function needs to start with an +// empty object, hence need to return a new pointer. + +// GenerateDescribeConfigurationInput returns input for read +// operation. +func GenerateDescribeConfigurationInput(cr *svcapitypes.Configuration) *svcsdk.DescribeConfigurationInput { + res := &svcsdk.DescribeConfigurationInput{} + + return res +} + +// GenerateConfiguration returns the current state in the form of *svcapitypes.Configuration. +func GenerateConfiguration(resp *svcsdk.DescribeConfigurationOutput) *svcapitypes.Configuration { + cr := &svcapitypes.Configuration{} + + if resp.Arn != nil { + cr.Status.AtProvider.ARN = resp.Arn + } else { + cr.Status.AtProvider.ARN = nil + } + if resp.AuthenticationStrategy != nil { + cr.Spec.ForProvider.AuthenticationStrategy = resp.AuthenticationStrategy + } else { + cr.Spec.ForProvider.AuthenticationStrategy = nil + } + if resp.Created != nil { + cr.Status.AtProvider.Created = &metav1.Time{*resp.Created} + } else { + cr.Status.AtProvider.Created = nil + } + if resp.EngineType != nil { + cr.Spec.ForProvider.EngineType = resp.EngineType + } else { + cr.Spec.ForProvider.EngineType = nil + } + if resp.EngineVersion != nil { + cr.Spec.ForProvider.EngineVersion = resp.EngineVersion + } else { + cr.Spec.ForProvider.EngineVersion = nil + } + if resp.Id != nil { + cr.Status.AtProvider.ID = resp.Id + } else { + cr.Status.AtProvider.ID = nil + } + if resp.LatestRevision != nil { + f7 := &svcapitypes.ConfigurationRevision{} + if resp.LatestRevision.Created != nil { + f7.Created = &metav1.Time{*resp.LatestRevision.Created} + } + if resp.LatestRevision.Description != nil { + f7.Description = resp.LatestRevision.Description + } + if resp.LatestRevision.Revision != nil { + f7.Revision = resp.LatestRevision.Revision + } + cr.Status.AtProvider.LatestRevision = f7 + } else { + cr.Status.AtProvider.LatestRevision = nil + } + if resp.Name != nil { + cr.Spec.ForProvider.Name = resp.Name + } else { + cr.Spec.ForProvider.Name = nil + } + if resp.Tags != nil { + f9 := map[string]*string{} + for f9key, f9valiter := range resp.Tags { + var f9val string + f9val = *f9valiter + f9[f9key] = &f9val + } + cr.Spec.ForProvider.Tags = f9 + } else { + cr.Spec.ForProvider.Tags = nil + } + + return cr +} + +// GenerateCreateConfigurationRequest returns a create input. +func GenerateCreateConfigurationRequest(cr *svcapitypes.Configuration) *svcsdk.CreateConfigurationRequest { + res := &svcsdk.CreateConfigurationRequest{} + + if cr.Spec.ForProvider.AuthenticationStrategy != nil { + res.SetAuthenticationStrategy(*cr.Spec.ForProvider.AuthenticationStrategy) + } + if cr.Spec.ForProvider.EngineType != nil { + res.SetEngineType(*cr.Spec.ForProvider.EngineType) + } + if cr.Spec.ForProvider.EngineVersion != nil { + res.SetEngineVersion(*cr.Spec.ForProvider.EngineVersion) + } + if cr.Spec.ForProvider.Name != nil { + res.SetName(*cr.Spec.ForProvider.Name) + } + if cr.Spec.ForProvider.Tags != nil { + f4 := map[string]*string{} + for f4key, f4valiter := range cr.Spec.ForProvider.Tags { + var f4val string + f4val = *f4valiter + f4[f4key] = &f4val + } + res.SetTags(f4) + } + + return res +} + +// IsNotFound returns whether the given error is of type NotFound or not. +func IsNotFound(err error) bool { + awsErr, ok := err.(awserr.Error) + return ok && awsErr.Code() == "NotFoundException" +} diff --git a/pkg/controller/mwaa/setup.go b/pkg/controller/mwaa/setup.go index 6f906e5026..bb394024e8 100644 --- a/pkg/controller/mwaa/setup.go +++ b/pkg/controller/mwaa/setup.go @@ -21,6 +21,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "github.com/crossplane-contrib/provider-aws/pkg/controller/mq/broker" + "github.com/crossplane-contrib/provider-aws/pkg/controller/mq/configuration" "github.com/crossplane-contrib/provider-aws/pkg/controller/mq/user" "github.com/crossplane-contrib/provider-aws/pkg/utils/setup" ) @@ -31,5 +32,6 @@ func Setup(mgr ctrl.Manager, o controller.Options) error { mgr, o, broker.SetupBroker, user.SetupUser, + configuration.SetupConfiguration, ) }