diff --git a/apis/v1alpha1/ack-generate-metadata.yaml b/apis/v1alpha1/ack-generate-metadata.yaml index 00c8af7..4ff4b92 100755 --- a/apis/v1alpha1/ack-generate-metadata.yaml +++ b/apis/v1alpha1/ack-generate-metadata.yaml @@ -1,13 +1,13 @@ ack_generate_info: - build_date: "2025-11-29T03:44:25Z" - build_hash: 23c7074fa310ad1ccb38946775397c203b49f024 + build_date: "2025-12-16T03:02:38Z" + build_hash: 5c8b9050006ef6c7d3a97c279e7b1bc163f20a0a go_version: go1.25.4 - version: v0.56.0 -api_directory_checksum: 90b0d1adcc91f4a1b1f1b436e3ac0c30d9271678 + version: v0.56.0-3-g5c8b905 +api_directory_checksum: 7a1921a5655bc146b350911169d5a0a1bc3299c7 api_version: v1alpha1 aws_sdk_go_version: v1.32.6 generator_config_info: - file_checksum: 23c3c400e5913ebaa0047af70fda453b065ce321 + file_checksum: 7e15405979825405b45c038fbb0e8344d47d0a80 original_file_name: generator.yaml last_modification: reason: API generation diff --git a/apis/v1alpha1/generator.yaml b/apis/v1alpha1/generator.yaml index 3f5af19..a82d4f1 100644 --- a/apis/v1alpha1/generator.yaml +++ b/apis/v1alpha1/generator.yaml @@ -17,7 +17,7 @@ ignore: #- DBSubnetGroup - EventSubscription #- GlobalCluster - - OptionGroup + #- OptionGroup - Integration - DBShardGroup - TenantDatabase @@ -616,3 +616,17 @@ resources: template_path: hooks/db_cluster_endpoint/sdk_update_pre_build_request.go.tpl delta_pre_compare: template_path: hooks/db_cluster_endpoint/delta_pre_compare.go.tpl + OptionGroup: + fields: + OptionGroupName: + is_primary_key: true + Tags: + compare: + is_ignored: true + hooks: + sdk_read_many_post_set_output: + template_path: hooks/option_group/sdk_read_many_post_set_output.go.tpl + sdk_update_pre_build_request: + template_path: hooks/option_group/sdk_update_pre_build_request.go.tpl + delta_pre_compare: + template_path: hooks/option_group/delta_pre_compare.go.tpl diff --git a/apis/v1alpha1/option_group.go b/apis/v1alpha1/option_group.go new file mode 100644 index 0000000..4df4e98 --- /dev/null +++ b/apis/v1alpha1/option_group.go @@ -0,0 +1,144 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file 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 ( + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// OptionGroupSpec defines the desired state of OptionGroup. +// + +type OptionGroupSpec struct { + + // The name of the engine to associate this option group with. + // + // Valid Values: + // + // - db2-ae + // + // - db2-se + // + // - mariadb + // + // - mysql + // + // - oracle-ee + // + // - oracle-ee-cdb + // + // - oracle-se2 + // + // - oracle-se2-cdb + // + // - postgres + // + // - sqlserver-ee + // + // - sqlserver-se + // + // - sqlserver-ex + // + // - sqlserver-web + // + // +kubebuilder:validation:Required + EngineName *string `json:"engineName"` + // Specifies the major version of the engine that this option group should be + // associated with. + // +kubebuilder:validation:Required + MajorEngineVersion *string `json:"majorEngineVersion"` + // The description of the option group. + // +kubebuilder:validation:Required + OptionGroupDescription *string `json:"optionGroupDescription"` + // Specifies the name of the option group to be created. + // + // Constraints: + // + // - Must be 1 to 255 letters, numbers, or hyphens + // + // - First character must be a letter + // + // - Can't end with a hyphen or contain two consecutive hyphens + // + // Example: myoptiongroup + // +kubebuilder:validation:Required + OptionGroupName *string `json:"optionGroupName"` + // Tags to assign to the option group. + Tags []*Tag `json:"tags,omitempty"` +} + +// OptionGroupStatus defines the observed state of OptionGroup +type OptionGroupStatus struct { + // All CRs managed by ACK have a common `Status.ACKResourceMetadata` member + // that is used to contain resource sync state, account ownership, + // constructed ARN for the resource + // +kubebuilder:validation:Optional + ACKResourceMetadata *ackv1alpha1.ResourceMetadata `json:"ackResourceMetadata"` + // All CRs managed by ACK have a common `Status.Conditions` member that + // contains a collection of `ackv1alpha1.Condition` objects that describe + // the various terminal states of the CR and its backend AWS service API + // resource + // +kubebuilder:validation:Optional + Conditions []*ackv1alpha1.Condition `json:"conditions"` + // Indicates whether this option group can be applied to both VPC and non-VPC + // instances. The value true indicates the option group can be applied to both + // VPC and non-VPC instances. + // +kubebuilder:validation:Optional + AllowsVPCAndNonVPCInstanceMemberships *bool `json:"allowsVPCAndNonVPCInstanceMemberships,omitempty"` + // Indicates when the option group was copied. + // +kubebuilder:validation:Optional + CopyTimestamp *metav1.Time `json:"copyTimestamp,omitempty"` + // Indicates what options are available in the option group. + // +kubebuilder:validation:Optional + Options []*Option `json:"options,omitempty"` + // Specifies the Amazon Web Services account ID for the option group from which + // this option group is copied. + // +kubebuilder:validation:Optional + SourceAccountID *string `json:"sourceAccountID,omitempty"` + // Specifies the name of the option group from which this option group is copied. + // +kubebuilder:validation:Optional + SourceOptionGroup *string `json:"sourceOptionGroup,omitempty"` + // If AllowsVpcAndNonVpcInstanceMemberships is false, this field is blank. If + // AllowsVpcAndNonVpcInstanceMemberships is true and this field is blank, then + // this option group can be applied to both VPC and non-VPC instances. If this + // field contains a value, then this option group can only be applied to instances + // that are in the VPC indicated by this field. + // +kubebuilder:validation:Optional + VPCID *string `json:"vpcID,omitempty"` +} + +// OptionGroup is the Schema for the OptionGroups API +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type OptionGroup struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec OptionGroupSpec `json:"spec,omitempty"` + Status OptionGroupStatus `json:"status,omitempty"` +} + +// OptionGroupList contains a list of OptionGroup +// +kubebuilder:object:root=true +type OptionGroupList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []OptionGroup `json:"items"` +} + +func init() { + SchemeBuilder.Register(&OptionGroup{}, &OptionGroupList{}) +} diff --git a/apis/v1alpha1/types.go b/apis/v1alpha1/types.go index b425b34..66e5aea 100644 --- a/apis/v1alpha1/types.go +++ b/apis/v1alpha1/types.go @@ -1261,8 +1261,10 @@ type MinimumEngineVersionPerAllowedValue struct { // The details of an option. type Option struct { + DBSecurityGroupMemberships []*DBSecurityGroupMembership `json:"dbSecurityGroupMemberships,omitempty"` OptionDescription *string `json:"optionDescription,omitempty"` OptionName *string `json:"optionName,omitempty"` + OptionSettings []*OptionSetting `json:"optionSettings,omitempty"` OptionVersion *string `json:"optionVersion,omitempty"` Permanent *bool `json:"permanent,omitempty"` Persistent *bool `json:"persistent,omitempty"` @@ -1272,24 +1274,12 @@ type Option struct { // A list of all available options for an option group. type OptionConfiguration struct { - DBSecurityGroupMemberships []*string `json:"dbSecurityGroupMemberships,omitempty"` - OptionName *string `json:"optionName,omitempty"` - OptionVersion *string `json:"optionVersion,omitempty"` - Port *int64 `json:"port,omitempty"` - VPCSecurityGroupMemberships []*string `json:"vpcSecurityGroupMemberships,omitempty"` -} - -type OptionGroup struct { - AllowsVPCAndNonVPCInstanceMemberships *bool `json:"allowsVPCAndNonVPCInstanceMemberships,omitempty"` - CopyTimestamp *metav1.Time `json:"copyTimestamp,omitempty"` - EngineName *string `json:"engineName,omitempty"` - MajorEngineVersion *string `json:"majorEngineVersion,omitempty"` - OptionGroupARN *string `json:"optionGroupARN,omitempty"` - OptionGroupDescription *string `json:"optionGroupDescription,omitempty"` - OptionGroupName *string `json:"optionGroupName,omitempty"` - SourceAccountID *string `json:"sourceAccountID,omitempty"` - SourceOptionGroup *string `json:"sourceOptionGroup,omitempty"` - VPCID *string `json:"vpcID,omitempty"` + DBSecurityGroupMemberships []*string `json:"dbSecurityGroupMemberships,omitempty"` + OptionName *string `json:"optionName,omitempty"` + OptionSettings []*OptionSetting `json:"optionSettings,omitempty"` + OptionVersion *string `json:"optionVersion,omitempty"` + Port *int64 `json:"port,omitempty"` + VPCSecurityGroupMemberships []*string `json:"vpcSecurityGroupMemberships,omitempty"` } // Provides information on the option groups the DB instance is a member of. @@ -1328,6 +1318,20 @@ type OptionGroupOptionSetting struct { SettingName *string `json:"settingName,omitempty"` } +type OptionGroup_SDK struct { + AllowsVPCAndNonVPCInstanceMemberships *bool `json:"allowsVPCAndNonVPCInstanceMemberships,omitempty"` + CopyTimestamp *metav1.Time `json:"copyTimestamp,omitempty"` + EngineName *string `json:"engineName,omitempty"` + MajorEngineVersion *string `json:"majorEngineVersion,omitempty"` + OptionGroupARN *string `json:"optionGroupARN,omitempty"` + OptionGroupDescription *string `json:"optionGroupDescription,omitempty"` + OptionGroupName *string `json:"optionGroupName,omitempty"` + Options []*Option `json:"options,omitempty"` + SourceAccountID *string `json:"sourceAccountID,omitempty"` + SourceOptionGroup *string `json:"sourceOptionGroup,omitempty"` + VPCID *string `json:"vpcID,omitempty"` +} + // Option settings are the actual settings being applied or configured for that // option. It is used when you modify an option group or describe option groups. // For example, the NATIVE_NETWORK_ENCRYPTION option has a setting called SQLNET.ENCRYPTION_SERVER diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index 4799e6b..14d3749 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -7009,6 +7009,17 @@ func (in *MinimumEngineVersionPerAllowedValue) DeepCopy() *MinimumEngineVersionP // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Option) DeepCopyInto(out *Option) { *out = *in + if in.DBSecurityGroupMemberships != nil { + in, out := &in.DBSecurityGroupMemberships, &out.DBSecurityGroupMemberships + *out = make([]*DBSecurityGroupMembership, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(DBSecurityGroupMembership) + (*in).DeepCopyInto(*out) + } + } + } if in.OptionDescription != nil { in, out := &in.OptionDescription, &out.OptionDescription *out = new(string) @@ -7019,6 +7030,17 @@ func (in *Option) DeepCopyInto(out *Option) { *out = new(string) **out = **in } + if in.OptionSettings != nil { + in, out := &in.OptionSettings, &out.OptionSettings + *out = make([]*OptionSetting, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(OptionSetting) + (*in).DeepCopyInto(*out) + } + } + } if in.OptionVersion != nil { in, out := &in.OptionVersion, &out.OptionVersion *out = new(string) @@ -7081,6 +7103,17 @@ func (in *OptionConfiguration) DeepCopyInto(out *OptionConfiguration) { *out = new(string) **out = **in } + if in.OptionSettings != nil { + in, out := &in.OptionSettings, &out.OptionSettings + *out = make([]*OptionSetting, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(OptionSetting) + (*in).DeepCopyInto(*out) + } + } + } if in.OptionVersion != nil { in, out := &in.OptionVersion, &out.OptionVersion *out = new(string) @@ -7117,55 +7150,10 @@ func (in *OptionConfiguration) DeepCopy() *OptionConfiguration { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OptionGroup) DeepCopyInto(out *OptionGroup) { *out = *in - if in.AllowsVPCAndNonVPCInstanceMemberships != nil { - in, out := &in.AllowsVPCAndNonVPCInstanceMemberships, &out.AllowsVPCAndNonVPCInstanceMemberships - *out = new(bool) - **out = **in - } - if in.CopyTimestamp != nil { - in, out := &in.CopyTimestamp, &out.CopyTimestamp - *out = (*in).DeepCopy() - } - if in.EngineName != nil { - in, out := &in.EngineName, &out.EngineName - *out = new(string) - **out = **in - } - if in.MajorEngineVersion != nil { - in, out := &in.MajorEngineVersion, &out.MajorEngineVersion - *out = new(string) - **out = **in - } - if in.OptionGroupARN != nil { - in, out := &in.OptionGroupARN, &out.OptionGroupARN - *out = new(string) - **out = **in - } - if in.OptionGroupDescription != nil { - in, out := &in.OptionGroupDescription, &out.OptionGroupDescription - *out = new(string) - **out = **in - } - if in.OptionGroupName != nil { - in, out := &in.OptionGroupName, &out.OptionGroupName - *out = new(string) - **out = **in - } - if in.SourceAccountID != nil { - in, out := &in.SourceAccountID, &out.SourceAccountID - *out = new(string) - **out = **in - } - if in.SourceOptionGroup != nil { - in, out := &in.SourceOptionGroup, &out.SourceOptionGroup - *out = new(string) - **out = **in - } - if in.VPCID != nil { - in, out := &in.VPCID, &out.VPCID - *out = new(string) - **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 OptionGroup. @@ -7178,6 +7166,46 @@ func (in *OptionGroup) DeepCopy() *OptionGroup { return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OptionGroup) 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 *OptionGroupList) DeepCopyInto(out *OptionGroupList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]OptionGroup, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OptionGroupList. +func (in *OptionGroupList) DeepCopy() *OptionGroupList { + if in == nil { + return nil + } + out := new(OptionGroupList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OptionGroupList) 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 *OptionGroupMembership) DeepCopyInto(out *OptionGroupMembership) { *out = *in @@ -7333,6 +7361,193 @@ func (in *OptionGroupOptionSetting) DeepCopy() *OptionGroupOptionSetting { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OptionGroupSpec) DeepCopyInto(out *OptionGroupSpec) { + *out = *in + if in.EngineName != nil { + in, out := &in.EngineName, &out.EngineName + *out = new(string) + **out = **in + } + if in.MajorEngineVersion != nil { + in, out := &in.MajorEngineVersion, &out.MajorEngineVersion + *out = new(string) + **out = **in + } + if in.OptionGroupDescription != nil { + in, out := &in.OptionGroupDescription, &out.OptionGroupDescription + *out = new(string) + **out = **in + } + if in.OptionGroupName != nil { + in, out := &in.OptionGroupName, &out.OptionGroupName + *out = new(string) + **out = **in + } + if in.Tags != nil { + in, out := &in.Tags, &out.Tags + *out = make([]*Tag, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Tag) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OptionGroupSpec. +func (in *OptionGroupSpec) DeepCopy() *OptionGroupSpec { + if in == nil { + return nil + } + out := new(OptionGroupSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OptionGroupStatus) DeepCopyInto(out *OptionGroupStatus) { + *out = *in + if in.ACKResourceMetadata != nil { + in, out := &in.ACKResourceMetadata, &out.ACKResourceMetadata + *out = new(corev1alpha1.ResourceMetadata) + (*in).DeepCopyInto(*out) + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]*corev1alpha1.Condition, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(corev1alpha1.Condition) + (*in).DeepCopyInto(*out) + } + } + } + if in.AllowsVPCAndNonVPCInstanceMemberships != nil { + in, out := &in.AllowsVPCAndNonVPCInstanceMemberships, &out.AllowsVPCAndNonVPCInstanceMemberships + *out = new(bool) + **out = **in + } + if in.CopyTimestamp != nil { + in, out := &in.CopyTimestamp, &out.CopyTimestamp + *out = (*in).DeepCopy() + } + if in.Options != nil { + in, out := &in.Options, &out.Options + *out = make([]*Option, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Option) + (*in).DeepCopyInto(*out) + } + } + } + if in.SourceAccountID != nil { + in, out := &in.SourceAccountID, &out.SourceAccountID + *out = new(string) + **out = **in + } + if in.SourceOptionGroup != nil { + in, out := &in.SourceOptionGroup, &out.SourceOptionGroup + *out = new(string) + **out = **in + } + if in.VPCID != nil { + in, out := &in.VPCID, &out.VPCID + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OptionGroupStatus. +func (in *OptionGroupStatus) DeepCopy() *OptionGroupStatus { + if in == nil { + return nil + } + out := new(OptionGroupStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OptionGroup_SDK) DeepCopyInto(out *OptionGroup_SDK) { + *out = *in + if in.AllowsVPCAndNonVPCInstanceMemberships != nil { + in, out := &in.AllowsVPCAndNonVPCInstanceMemberships, &out.AllowsVPCAndNonVPCInstanceMemberships + *out = new(bool) + **out = **in + } + if in.CopyTimestamp != nil { + in, out := &in.CopyTimestamp, &out.CopyTimestamp + *out = (*in).DeepCopy() + } + if in.EngineName != nil { + in, out := &in.EngineName, &out.EngineName + *out = new(string) + **out = **in + } + if in.MajorEngineVersion != nil { + in, out := &in.MajorEngineVersion, &out.MajorEngineVersion + *out = new(string) + **out = **in + } + if in.OptionGroupARN != nil { + in, out := &in.OptionGroupARN, &out.OptionGroupARN + *out = new(string) + **out = **in + } + if in.OptionGroupDescription != nil { + in, out := &in.OptionGroupDescription, &out.OptionGroupDescription + *out = new(string) + **out = **in + } + if in.OptionGroupName != nil { + in, out := &in.OptionGroupName, &out.OptionGroupName + *out = new(string) + **out = **in + } + if in.Options != nil { + in, out := &in.Options, &out.Options + *out = make([]*Option, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Option) + (*in).DeepCopyInto(*out) + } + } + } + if in.SourceAccountID != nil { + in, out := &in.SourceAccountID, &out.SourceAccountID + *out = new(string) + **out = **in + } + if in.SourceOptionGroup != nil { + in, out := &in.SourceOptionGroup, &out.SourceOptionGroup + *out = new(string) + **out = **in + } + if in.VPCID != nil { + in, out := &in.VPCID, &out.VPCID + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OptionGroup_SDK. +func (in *OptionGroup_SDK) DeepCopy() *OptionGroup_SDK { + if in == nil { + return nil + } + out := new(OptionGroup_SDK) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OptionSetting) DeepCopyInto(out *OptionSetting) { *out = *in diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 23e3335..cf61e1f 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -51,6 +51,7 @@ import ( _ "github.com/aws-controllers-k8s/rds-controller/pkg/resource/db_snapshot" _ "github.com/aws-controllers-k8s/rds-controller/pkg/resource/db_subnet_group" _ "github.com/aws-controllers-k8s/rds-controller/pkg/resource/global_cluster" + _ "github.com/aws-controllers-k8s/rds-controller/pkg/resource/option_group" "github.com/aws-controllers-k8s/rds-controller/pkg/version" ) diff --git a/config/crd/bases/rds.services.k8s.aws_optiongroups.yaml b/config/crd/bases/rds.services.k8s.aws_optiongroups.yaml new file mode 100644 index 0000000..ac0a211 --- /dev/null +++ b/config/crd/bases/rds.services.k8s.aws_optiongroups.yaml @@ -0,0 +1,296 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: optiongroups.rds.services.k8s.aws +spec: + group: rds.services.k8s.aws + names: + kind: OptionGroup + listKind: OptionGroupList + plural: optiongroups + singular: optiongroup + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: OptionGroup is the Schema for the OptionGroups 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: + properties: + engineName: + description: |- + The name of the engine to associate this option group with. + + Valid Values: + + * db2-ae + + * db2-se + + * mariadb + + * mysql + + * oracle-ee + + * oracle-ee-cdb + + * oracle-se2 + + * oracle-se2-cdb + + * postgres + + * sqlserver-ee + + * sqlserver-se + + * sqlserver-ex + + * sqlserver-web + type: string + majorEngineVersion: + description: |- + Specifies the major version of the engine that this option group should be + associated with. + type: string + optionGroupDescription: + description: The description of the option group. + type: string + optionGroupName: + description: |- + Specifies the name of the option group to be created. + + Constraints: + + * Must be 1 to 255 letters, numbers, or hyphens + + * First character must be a letter + + * Can't end with a hyphen or contain two consecutive hyphens + + Example: myoptiongroup + type: string + tags: + description: Tags to assign to the option group. + items: + description: |- + Metadata assigned to an Amazon RDS resource consisting of a key-value pair. + + For more information, see Tagging Amazon RDS resources (https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Tagging.html) + in the Amazon RDS User Guide or Tagging Amazon Aurora and Amazon RDS resources + (https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_Tagging.html) + in the Amazon Aurora User Guide. + properties: + key: + type: string + value: + type: string + type: object + type: array + required: + - engineName + - majorEngineVersion + - optionGroupDescription + - optionGroupName + type: object + status: + description: OptionGroupStatus defines the observed state of OptionGroup + properties: + ackResourceMetadata: + description: |- + All CRs managed by ACK have a common `Status.ACKResourceMetadata` member + that is used to contain resource sync state, account ownership, + constructed ARN for the resource + properties: + arn: + description: |- + ARN is the Amazon Resource Name for the resource. This is a + globally-unique identifier and is set only by the ACK service controller + once the controller has orchestrated the creation of the resource OR + when it has verified that an "adopted" resource (a resource where the + ARN annotation was set by the Kubernetes user on the CR) exists and + matches the supplied CR's Spec field values. + https://github.com/aws/aws-controllers-k8s/issues/270 + type: string + ownerAccountID: + description: |- + OwnerAccountID is the AWS Account ID of the account that owns the + backend AWS service API resource. + type: string + region: + description: Region is the AWS region in which the resource exists + or will exist. + type: string + required: + - ownerAccountID + - region + type: object + allowsVPCAndNonVPCInstanceMemberships: + description: |- + Indicates whether this option group can be applied to both VPC and non-VPC + instances. The value true indicates the option group can be applied to both + VPC and non-VPC instances. + type: boolean + conditions: + description: |- + All CRs managed by ACK have a common `Status.Conditions` member that + contains a collection of `ackv1alpha1.Condition` objects that describe + the various terminal states of the CR and its backend AWS service API + resource + items: + description: |- + Condition is the common struct used by all CRDs managed by ACK service + controllers to indicate terminal states of the CR and its backend AWS + service API resource + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type is the type of the Condition + type: string + required: + - status + - type + type: object + type: array + copyTimestamp: + description: Indicates when the option group was copied. + format: date-time + type: string + options: + description: Indicates what options are available in the option group. + items: + description: The details of an option. + properties: + dbSecurityGroupMemberships: + items: + description: |- + This data type is used as a response element in the following actions: + + * ModifyDBInstance + + * RebootDBInstance + + * RestoreDBInstanceFromDBSnapshot + + * RestoreDBInstanceToPointInTime + properties: + dbSecurityGroupName: + type: string + status: + type: string + type: object + type: array + optionDescription: + type: string + optionName: + type: string + optionSettings: + items: + description: |- + Option settings are the actual settings being applied or configured for that + option. It is used when you modify an option group or describe option groups. + For example, the NATIVE_NETWORK_ENCRYPTION option has a setting called SQLNET.ENCRYPTION_SERVER + that can have several different values. + properties: + allowedValues: + type: string + applyType: + type: string + dataType: + type: string + defaultValue: + type: string + description: + type: string + isCollection: + type: boolean + isModifiable: + type: boolean + name: + type: string + value: + type: string + type: object + type: array + optionVersion: + type: string + permanent: + type: boolean + persistent: + type: boolean + port: + format: int64 + type: integer + vpcSecurityGroupMemberships: + items: + description: |- + This data type is used as a response element for queries on VPC security + group membership. + properties: + status: + type: string + vpcSecurityGroupID: + type: string + type: object + type: array + type: object + type: array + sourceAccountID: + description: |- + Specifies the Amazon Web Services account ID for the option group from which + this option group is copied. + type: string + sourceOptionGroup: + description: Specifies the name of the option group from which this + option group is copied. + type: string + vpcID: + description: |- + If AllowsVpcAndNonVpcInstanceMemberships is false, this field is blank. If + AllowsVpcAndNonVpcInstanceMemberships is true and this field is blank, then + this option group can be applied to both VPC and non-VPC instances. If this + field contains a value, then this option group can only be applied to instances + that are in the VPC indicated by this field. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/common/bases/services.k8s.aws_iamroleselectors.yaml b/config/crd/common/bases/services.k8s.aws_iamroleselectors.yaml index 9477c90..803a75c 100644 --- a/config/crd/common/bases/services.k8s.aws_iamroleselectors.yaml +++ b/config/crd/common/bases/services.k8s.aws_iamroleselectors.yaml @@ -63,6 +63,16 @@ spec: required: - names type: object + resourceLabelSelector: + description: LabelSelector is a label query over a set of resources. + properties: + matchLabels: + additionalProperties: + type: string + type: object + required: + - matchLabels + type: object resourceTypeSelector: items: properties: diff --git a/config/crd/common/kustomization.yaml b/config/crd/common/kustomization.yaml index 8165534..65cb01b 100644 --- a/config/crd/common/kustomization.yaml +++ b/config/crd/common/kustomization.yaml @@ -3,5 +3,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - - bases/services.k8s.aws_iamroleselectors.yaml - bases/services.k8s.aws_fieldexports.yaml + - bases/services.k8s.aws_iamroleselectors.yaml diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 806dda4..c349c00 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -12,3 +12,4 @@ resources: - bases/rds.services.k8s.aws_dbsnapshots.yaml - bases/rds.services.k8s.aws_dbsubnetgroups.yaml - bases/rds.services.k8s.aws_globalclusters.yaml + - bases/rds.services.k8s.aws_optiongroups.yaml diff --git a/config/rbac/cluster-role-controller.yaml b/config/rbac/cluster-role-controller.yaml index 8c84ac7..533ac9a 100644 --- a/config/rbac/cluster-role-controller.yaml +++ b/config/rbac/cluster-role-controller.yaml @@ -53,6 +53,7 @@ rules: - dbsnapshots - dbsubnetgroups - globalclusters + - optiongroups verbs: - create - delete @@ -74,6 +75,7 @@ rules: - dbsnapshots/status - dbsubnetgroups/status - globalclusters/status + - optiongroups/status verbs: - get - patch diff --git a/config/rbac/role-reader.yaml b/config/rbac/role-reader.yaml index 8f27698..d4e291a 100644 --- a/config/rbac/role-reader.yaml +++ b/config/rbac/role-reader.yaml @@ -19,6 +19,7 @@ rules: - dbsnapshots - dbsubnetgroups - globalclusters + - optiongroups verbs: - get - list diff --git a/config/rbac/role-writer.yaml b/config/rbac/role-writer.yaml index 33b58f9..663c093 100644 --- a/config/rbac/role-writer.yaml +++ b/config/rbac/role-writer.yaml @@ -19,6 +19,7 @@ rules: - dbsnapshots - dbsubnetgroups - globalclusters + - optiongroups verbs: - create - delete @@ -40,6 +41,7 @@ rules: - dbsnapshots - dbsubnetgroups - globalclusters + - optiongroups verbs: - get - patch diff --git a/generator.yaml b/generator.yaml index 3f5af19..a82d4f1 100644 --- a/generator.yaml +++ b/generator.yaml @@ -17,7 +17,7 @@ ignore: #- DBSubnetGroup - EventSubscription #- GlobalCluster - - OptionGroup + #- OptionGroup - Integration - DBShardGroup - TenantDatabase @@ -616,3 +616,17 @@ resources: template_path: hooks/db_cluster_endpoint/sdk_update_pre_build_request.go.tpl delta_pre_compare: template_path: hooks/db_cluster_endpoint/delta_pre_compare.go.tpl + OptionGroup: + fields: + OptionGroupName: + is_primary_key: true + Tags: + compare: + is_ignored: true + hooks: + sdk_read_many_post_set_output: + template_path: hooks/option_group/sdk_read_many_post_set_output.go.tpl + sdk_update_pre_build_request: + template_path: hooks/option_group/sdk_update_pre_build_request.go.tpl + delta_pre_compare: + template_path: hooks/option_group/delta_pre_compare.go.tpl diff --git a/helm/crds/rds.services.k8s.aws_optiongroups.yaml b/helm/crds/rds.services.k8s.aws_optiongroups.yaml new file mode 100644 index 0000000..8de3b96 --- /dev/null +++ b/helm/crds/rds.services.k8s.aws_optiongroups.yaml @@ -0,0 +1,296 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: optiongroups.rds.services.k8s.aws +spec: + group: rds.services.k8s.aws + names: + kind: OptionGroup + listKind: OptionGroupList + plural: optiongroups + singular: optiongroup + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: OptionGroup is the Schema for the OptionGroups 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: + properties: + engineName: + description: |- + The name of the engine to associate this option group with. + + Valid Values: + + - db2-ae + + - db2-se + + - mariadb + + - mysql + + - oracle-ee + + - oracle-ee-cdb + + - oracle-se2 + + - oracle-se2-cdb + + - postgres + + - sqlserver-ee + + - sqlserver-se + + - sqlserver-ex + + - sqlserver-web + type: string + majorEngineVersion: + description: |- + Specifies the major version of the engine that this option group should be + associated with. + type: string + optionGroupDescription: + description: The description of the option group. + type: string + optionGroupName: + description: |- + Specifies the name of the option group to be created. + + Constraints: + + - Must be 1 to 255 letters, numbers, or hyphens + + - First character must be a letter + + - Can't end with a hyphen or contain two consecutive hyphens + + Example: myoptiongroup + type: string + tags: + description: Tags to assign to the option group. + items: + description: |- + Metadata assigned to an Amazon RDS resource consisting of a key-value pair. + + For more information, see Tagging Amazon RDS resources (https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Tagging.html) + in the Amazon RDS User Guide or Tagging Amazon Aurora and Amazon RDS resources + (https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_Tagging.html) + in the Amazon Aurora User Guide. + properties: + key: + type: string + value: + type: string + type: object + type: array + required: + - engineName + - majorEngineVersion + - optionGroupDescription + - optionGroupName + type: object + status: + description: OptionGroupStatus defines the observed state of OptionGroup + properties: + ackResourceMetadata: + description: |- + All CRs managed by ACK have a common `Status.ACKResourceMetadata` member + that is used to contain resource sync state, account ownership, + constructed ARN for the resource + properties: + arn: + description: |- + ARN is the Amazon Resource Name for the resource. This is a + globally-unique identifier and is set only by the ACK service controller + once the controller has orchestrated the creation of the resource OR + when it has verified that an "adopted" resource (a resource where the + ARN annotation was set by the Kubernetes user on the CR) exists and + matches the supplied CR's Spec field values. + https://github.com/aws/aws-controllers-k8s/issues/270 + type: string + ownerAccountID: + description: |- + OwnerAccountID is the AWS Account ID of the account that owns the + backend AWS service API resource. + type: string + region: + description: Region is the AWS region in which the resource exists + or will exist. + type: string + required: + - ownerAccountID + - region + type: object + allowsVPCAndNonVPCInstanceMemberships: + description: |- + Indicates whether this option group can be applied to both VPC and non-VPC + instances. The value true indicates the option group can be applied to both + VPC and non-VPC instances. + type: boolean + conditions: + description: |- + All CRs managed by ACK have a common `Status.Conditions` member that + contains a collection of `ackv1alpha1.Condition` objects that describe + the various terminal states of the CR and its backend AWS service API + resource + items: + description: |- + Condition is the common struct used by all CRDs managed by ACK service + controllers to indicate terminal states of the CR and its backend AWS + service API resource + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type is the type of the Condition + type: string + required: + - status + - type + type: object + type: array + copyTimestamp: + description: Indicates when the option group was copied. + format: date-time + type: string + options: + description: Indicates what options are available in the option group. + items: + description: The details of an option. + properties: + dbSecurityGroupMemberships: + items: + description: |- + This data type is used as a response element in the following actions: + + - ModifyDBInstance + + - RebootDBInstance + + - RestoreDBInstanceFromDBSnapshot + + - RestoreDBInstanceToPointInTime + properties: + dbSecurityGroupName: + type: string + status: + type: string + type: object + type: array + optionDescription: + type: string + optionName: + type: string + optionSettings: + items: + description: |- + Option settings are the actual settings being applied or configured for that + option. It is used when you modify an option group or describe option groups. + For example, the NATIVE_NETWORK_ENCRYPTION option has a setting called SQLNET.ENCRYPTION_SERVER + that can have several different values. + properties: + allowedValues: + type: string + applyType: + type: string + dataType: + type: string + defaultValue: + type: string + description: + type: string + isCollection: + type: boolean + isModifiable: + type: boolean + name: + type: string + value: + type: string + type: object + type: array + optionVersion: + type: string + permanent: + type: boolean + persistent: + type: boolean + port: + format: int64 + type: integer + vpcSecurityGroupMemberships: + items: + description: |- + This data type is used as a response element for queries on VPC security + group membership. + properties: + status: + type: string + vpcSecurityGroupID: + type: string + type: object + type: array + type: object + type: array + sourceAccountID: + description: |- + Specifies the Amazon Web Services account ID for the option group from which + this option group is copied. + type: string + sourceOptionGroup: + description: Specifies the name of the option group from which this + option group is copied. + type: string + vpcID: + description: |- + If AllowsVpcAndNonVpcInstanceMemberships is false, this field is blank. If + AllowsVpcAndNonVpcInstanceMemberships is true and this field is blank, then + this option group can be applied to both VPC and non-VPC instances. If this + field contains a value, then this option group can only be applied to instances + that are in the VPC indicated by this field. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/helm/crds/services.k8s.aws_iamroleselectors.yaml b/helm/crds/services.k8s.aws_iamroleselectors.yaml index 9477c90..803a75c 100644 --- a/helm/crds/services.k8s.aws_iamroleselectors.yaml +++ b/helm/crds/services.k8s.aws_iamroleselectors.yaml @@ -63,6 +63,16 @@ spec: required: - names type: object + resourceLabelSelector: + description: LabelSelector is a label query over a set of resources. + properties: + matchLabels: + additionalProperties: + type: string + type: object + required: + - matchLabels + type: object resourceTypeSelector: items: properties: diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl index 4753920..99aef7b 100644 --- a/helm/templates/_helpers.tpl +++ b/helm/templates/_helpers.tpl @@ -100,6 +100,7 @@ rules: - dbsnapshots - dbsubnetgroups - globalclusters + - optiongroups verbs: - create - delete @@ -121,6 +122,7 @@ rules: - dbsnapshots/status - dbsubnetgroups/status - globalclusters/status + - optiongroups/status verbs: - get - patch diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 7f692af..afb1a4b 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -51,6 +51,13 @@ spec: - "$(AWS_REGION)" - --aws-endpoint-url - "$(AWS_ENDPOINT_URL)" +{{- if .Values.aws.identity_endpoint_url }} + - --aws-identity-endpoint-url + - "$(AWS_IDENTITY_ENDPOINT_URL)" +{{- end }} +{{- if .Values.aws.allow_unsafe_aws_endpoint_urls }} + - --allow-unsafe-aws-endpoint-urls +{{- end }} {{- if .Values.log.enable_development_logging }} - --enable-development-logging {{- end }} @@ -109,6 +116,8 @@ spec: value: {{ .Values.aws.region }} - name: AWS_ENDPOINT_URL value: {{ .Values.aws.endpoint_url | quote }} + - name: AWS_IDENTITY_ENDPOINT_URL + value: {{ .Values.aws.identity_endpoint_url | quote }} - name: ACK_WATCH_NAMESPACE value: {{ include "ack-rds-controller.watch-namespace" . }} - name: ACK_WATCH_SELECTORS diff --git a/helm/templates/role-reader.yaml b/helm/templates/role-reader.yaml index c733783..4fe33b7 100644 --- a/helm/templates/role-reader.yaml +++ b/helm/templates/role-reader.yaml @@ -26,6 +26,7 @@ rules: - dbsnapshots - dbsubnetgroups - globalclusters + - optiongroups verbs: - get - list diff --git a/helm/templates/role-writer.yaml b/helm/templates/role-writer.yaml index 1fa3ff2..d6de1ed 100644 --- a/helm/templates/role-writer.yaml +++ b/helm/templates/role-writer.yaml @@ -26,6 +26,7 @@ rules: - dbsnapshots - dbsubnetgroups - globalclusters + - optiongroups verbs: - create - delete @@ -47,6 +48,7 @@ rules: - dbsnapshots - dbsubnetgroups - globalclusters + - optiongroups verbs: - get - patch diff --git a/helm/values.schema.json b/helm/values.schema.json index c3f56a0..619cfe3 100644 --- a/helm/values.schema.json +++ b/helm/values.schema.json @@ -171,9 +171,16 @@ "region": { "type": "string" }, - "endpoint": { + "endpoint_url": { "type": "string" }, + "identity_endpoint_url": { + "type": "string" + }, + "allow_unsafe_aws_endpoint_urls": { + "type": "boolean", + "default": false + }, "credentials": { "description": "AWS credentials information", "properties": { diff --git a/helm/values.yaml b/helm/values.yaml index 8f2aea3..5aa72d2 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -90,6 +90,8 @@ aws: # If specified, use the AWS region for AWS API calls region: "" endpoint_url: "" + identity_endpoint_url: "" + allow_unsafe_aws_endpoint_urls: false credentials: # If specified, Secret with shared credentials file to use. secretName: "" @@ -158,6 +160,7 @@ reconcile: - DBSnapshot - DBSubnetGroup - GlobalCluster + - OptionGroup serviceAccount: # Specifies whether a service account should be created diff --git a/pkg/resource/option_group/delta.go b/pkg/resource/option_group/delta.go new file mode 100644 index 0000000..4062ed1 --- /dev/null +++ b/pkg/resource/option_group/delta.go @@ -0,0 +1,77 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file 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 option_group + +import ( + "bytes" + "reflect" + + ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare" + acktags "github.com/aws-controllers-k8s/runtime/pkg/tags" +) + +// Hack to avoid import errors during build... +var ( + _ = &bytes.Buffer{} + _ = &reflect.Method{} + _ = &acktags.Tags{} +) + +// newResourceDelta returns a new `ackcompare.Delta` used to compare two +// resources +func newResourceDelta( + a *resource, + b *resource, +) *ackcompare.Delta { + delta := ackcompare.NewDelta() + if (a == nil && b != nil) || + (a != nil && b == nil) { + delta.Add("", a, b) + return delta + } + compareTags(delta, a, b) + + if ackcompare.HasNilDifference(a.ko.Spec.EngineName, b.ko.Spec.EngineName) { + delta.Add("Spec.EngineName", a.ko.Spec.EngineName, b.ko.Spec.EngineName) + } else if a.ko.Spec.EngineName != nil && b.ko.Spec.EngineName != nil { + if *a.ko.Spec.EngineName != *b.ko.Spec.EngineName { + delta.Add("Spec.EngineName", a.ko.Spec.EngineName, b.ko.Spec.EngineName) + } + } + if ackcompare.HasNilDifference(a.ko.Spec.MajorEngineVersion, b.ko.Spec.MajorEngineVersion) { + delta.Add("Spec.MajorEngineVersion", a.ko.Spec.MajorEngineVersion, b.ko.Spec.MajorEngineVersion) + } else if a.ko.Spec.MajorEngineVersion != nil && b.ko.Spec.MajorEngineVersion != nil { + if *a.ko.Spec.MajorEngineVersion != *b.ko.Spec.MajorEngineVersion { + delta.Add("Spec.MajorEngineVersion", a.ko.Spec.MajorEngineVersion, b.ko.Spec.MajorEngineVersion) + } + } + if ackcompare.HasNilDifference(a.ko.Spec.OptionGroupDescription, b.ko.Spec.OptionGroupDescription) { + delta.Add("Spec.OptionGroupDescription", a.ko.Spec.OptionGroupDescription, b.ko.Spec.OptionGroupDescription) + } else if a.ko.Spec.OptionGroupDescription != nil && b.ko.Spec.OptionGroupDescription != nil { + if *a.ko.Spec.OptionGroupDescription != *b.ko.Spec.OptionGroupDescription { + delta.Add("Spec.OptionGroupDescription", a.ko.Spec.OptionGroupDescription, b.ko.Spec.OptionGroupDescription) + } + } + if ackcompare.HasNilDifference(a.ko.Spec.OptionGroupName, b.ko.Spec.OptionGroupName) { + delta.Add("Spec.OptionGroupName", a.ko.Spec.OptionGroupName, b.ko.Spec.OptionGroupName) + } else if a.ko.Spec.OptionGroupName != nil && b.ko.Spec.OptionGroupName != nil { + if *a.ko.Spec.OptionGroupName != *b.ko.Spec.OptionGroupName { + delta.Add("Spec.OptionGroupName", a.ko.Spec.OptionGroupName, b.ko.Spec.OptionGroupName) + } + } + + return delta +} diff --git a/pkg/resource/option_group/descriptor.go b/pkg/resource/option_group/descriptor.go new file mode 100644 index 0000000..b15ad52 --- /dev/null +++ b/pkg/resource/option_group/descriptor.go @@ -0,0 +1,155 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file 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 option_group + +import ( + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" + ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare" + acktypes "github.com/aws-controllers-k8s/runtime/pkg/types" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + rtclient "sigs.k8s.io/controller-runtime/pkg/client" + k8sctrlutil "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + svcapitypes "github.com/aws-controllers-k8s/rds-controller/apis/v1alpha1" +) + +const ( + FinalizerString = "finalizers.rds.services.k8s.aws/OptionGroup" +) + +var ( + GroupVersionResource = svcapitypes.GroupVersion.WithResource("optiongroups") + GroupKind = metav1.GroupKind{ + Group: "rds.services.k8s.aws", + Kind: "OptionGroup", + } +) + +// resourceDescriptor implements the +// `aws-service-operator-k8s/pkg/types.AWSResourceDescriptor` interface +type resourceDescriptor struct { +} + +// GroupVersionKind returns a Kubernetes schema.GroupVersionKind struct that +// describes the API Group, Version and Kind of CRs described by the descriptor +func (d *resourceDescriptor) GroupVersionKind() schema.GroupVersionKind { + return svcapitypes.GroupVersion.WithKind(GroupKind.Kind) +} + +// EmptyRuntimeObject returns an empty object prototype that may be used in +// apimachinery and k8s client operations +func (d *resourceDescriptor) EmptyRuntimeObject() rtclient.Object { + return &svcapitypes.OptionGroup{} +} + +// ResourceFromRuntimeObject returns an AWSResource that has been initialized +// with the supplied runtime.Object +func (d *resourceDescriptor) ResourceFromRuntimeObject( + obj rtclient.Object, +) acktypes.AWSResource { + return &resource{ + ko: obj.(*svcapitypes.OptionGroup), + } +} + +// Delta returns an `ackcompare.Delta` object containing the difference between +// one `AWSResource` and another. +func (d *resourceDescriptor) Delta(a, b acktypes.AWSResource) *ackcompare.Delta { + return newResourceDelta(a.(*resource), b.(*resource)) +} + +// IsManaged returns true if the supplied AWSResource is under the management +// of an ACK service controller. What this means in practice is that the +// underlying custom resource (CR) in the AWSResource has had a +// resource-specific finalizer associated with it. +func (d *resourceDescriptor) IsManaged( + res acktypes.AWSResource, +) bool { + obj := res.RuntimeObject() + if obj == nil { + // Should not happen. If it does, there is a bug in the code + panic("nil RuntimeMetaObject in AWSResource") + } + // Remove use of custom code once + // https://github.com/kubernetes-sigs/controller-runtime/issues/994 is + // fixed. This should be able to be: + // + // return k8sctrlutil.ContainsFinalizer(obj, FinalizerString) + return containsFinalizer(obj, FinalizerString) +} + +// Remove once https://github.com/kubernetes-sigs/controller-runtime/issues/994 +// is fixed. +func containsFinalizer(obj rtclient.Object, finalizer string) bool { + f := obj.GetFinalizers() + for _, e := range f { + if e == finalizer { + return true + } + } + return false +} + +// MarkManaged places the supplied resource under the management of ACK. What +// this typically means is that the resource manager will decorate the +// underlying custom resource (CR) with a finalizer that indicates ACK is +// managing the resource and the underlying CR may not be deleted until ACK is +// finished cleaning up any backend AWS service resources associated with the +// CR. +func (d *resourceDescriptor) MarkManaged( + res acktypes.AWSResource, +) { + obj := res.RuntimeObject() + if obj == nil { + // Should not happen. If it does, there is a bug in the code + panic("nil RuntimeMetaObject in AWSResource") + } + k8sctrlutil.AddFinalizer(obj, FinalizerString) +} + +// MarkUnmanaged removes the supplied resource from management by ACK. What +// this typically means is that the resource manager will remove a finalizer +// underlying custom resource (CR) that indicates ACK is managing the resource. +// This will allow the Kubernetes API server to delete the underlying CR. +func (d *resourceDescriptor) MarkUnmanaged( + res acktypes.AWSResource, +) { + obj := res.RuntimeObject() + if obj == nil { + // Should not happen. If it does, there is a bug in the code + panic("nil RuntimeMetaObject in AWSResource") + } + k8sctrlutil.RemoveFinalizer(obj, FinalizerString) +} + +// MarkAdopted places descriptors on the custom resource that indicate the +// resource was not created from within ACK. +func (d *resourceDescriptor) MarkAdopted( + res acktypes.AWSResource, +) { + obj := res.RuntimeObject() + if obj == nil { + // Should not happen. If it does, there is a bug in the code + panic("nil RuntimeObject in AWSResource") + } + curr := obj.GetAnnotations() + if curr == nil { + curr = make(map[string]string) + } + curr[ackv1alpha1.AnnotationAdopted] = "true" + obj.SetAnnotations(curr) +} diff --git a/pkg/resource/option_group/hooks.go b/pkg/resource/option_group/hooks.go new file mode 100644 index 0000000..8c5f0fd --- /dev/null +++ b/pkg/resource/option_group/hooks.go @@ -0,0 +1,135 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file 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 option_group + +import ( + "context" + + ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare" + ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log" + + svcapitypes "github.com/aws-controllers-k8s/rds-controller/apis/v1alpha1" + svcsdk "github.com/aws/aws-sdk-go-v2/service/rds" + svcsdktypes "github.com/aws/aws-sdk-go-v2/service/rds/types" + + "github.com/aws-controllers-k8s/rds-controller/pkg/util" +) + +// syncTags keeps the resource's tags in sync +func (rm *resourceManager) syncTags( + ctx context.Context, + desired *resource, + latest *resource, +) (err error) { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("rm.syncTags") + defer func() { exit(err) }() + + arn := (*string)(latest.ko.Status.ACKResourceMetadata.ARN) + + toAdd, toDelete := util.ComputeTagsDelta( + desired.ko.Spec.Tags, latest.ko.Spec.Tags, + ) + + if len(toDelete) > 0 { + rlog.Debug("removing tags from subnet group", "tags", toDelete) + _, err = rm.sdkapi.RemoveTagsFromResource( + ctx, + &svcsdk.RemoveTagsFromResourceInput{ + ResourceName: arn, + TagKeys: toDelete, + }, + ) + rm.metrics.RecordAPICall("UPDATE", "RemoveTagsFromResource", err) + if err != nil { + return err + } + } + + // NOTE(jaypipes): According to the RDS API documentation, adding a tag + // with a new value overwrites any existing tag with the same key. So, we + // don't need to do anything to "update" a Tag. Simply including it in the + // AddTagsToResource call is enough. + if len(toAdd) > 0 { + rlog.Debug("adding tags to subnet group", "tags", toAdd) + _, err = rm.sdkapi.AddTagsToResource( + ctx, + &svcsdk.AddTagsToResourceInput{ + ResourceName: arn, + Tags: sdkTagsFromResourceTags(toAdd), + }, + ) + rm.metrics.RecordAPICall("UPDATE", "AddTagsToResource", err) + if err != nil { + return err + } + } + return nil +} + +// getTags retrieves the resource's associated tags +func (rm *resourceManager) getTags( + ctx context.Context, + resourceARN string, +) ([]*svcapitypes.Tag, error) { + resp, err := rm.sdkapi.ListTagsForResource( + ctx, + &svcsdk.ListTagsForResourceInput{ + ResourceName: &resourceARN, + }, + ) + rm.metrics.RecordAPICall("GET", "ListTagsForResource", err) + if err != nil { + return nil, err + } + tags := make([]*svcapitypes.Tag, 0, len(resp.TagList)) + for _, tag := range resp.TagList { + tags = append(tags, &svcapitypes.Tag{ + Key: tag.Key, + Value: tag.Value, + }) + } + return tags, nil +} + +// compareTags adds a difference to the delta if the supplied resources have +// different tag collections +func compareTags( + delta *ackcompare.Delta, + a *resource, + b *resource, +) { + if len(a.ko.Spec.Tags) != len(b.ko.Spec.Tags) { + delta.Add("Spec.Tags", a.ko.Spec.Tags, b.ko.Spec.Tags) + } else if len(a.ko.Spec.Tags) > 0 { + if !util.EqualTags(a.ko.Spec.Tags, b.ko.Spec.Tags) { + delta.Add("Spec.Tags", a.ko.Spec.Tags, b.ko.Spec.Tags) + } + } +} + +// sdkTagsFromResourceTags transforms a *svcapitypes.Tag array to a *svcsdk.Tag +// array. +func sdkTagsFromResourceTags( + rTags []*svcapitypes.Tag, +) []svcsdktypes.Tag { + tags := make([]svcsdktypes.Tag, len(rTags)) + for i := range rTags { + tags[i] = svcsdktypes.Tag{ + Key: rTags[i].Key, + Value: rTags[i].Value, + } + } + return tags +} diff --git a/pkg/resource/option_group/identifiers.go b/pkg/resource/option_group/identifiers.go new file mode 100644 index 0000000..da3af06 --- /dev/null +++ b/pkg/resource/option_group/identifiers.go @@ -0,0 +1,55 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file 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 option_group + +import ( + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" +) + +// resourceIdentifiers implements the +// `aws-service-operator-k8s/pkg/types.AWSResourceIdentifiers` interface +type resourceIdentifiers struct { + meta *ackv1alpha1.ResourceMetadata +} + +// ARN returns the AWS Resource Name for the backend AWS resource. If nil, +// this means the resource has not yet been created in the backend AWS +// service. +func (ri *resourceIdentifiers) ARN() *ackv1alpha1.AWSResourceName { + if ri.meta != nil { + return ri.meta.ARN + } + return nil +} + +// OwnerAccountID returns the AWS account identifier in which the +// backend AWS resource resides, or nil if this information is not known +// for the resource +func (ri *resourceIdentifiers) OwnerAccountID() *ackv1alpha1.AWSAccountID { + if ri.meta != nil { + return ri.meta.OwnerAccountID + } + return nil +} + +// Region returns the AWS region in which the resource exists, or +// nil if this information is not known. +func (ri *resourceIdentifiers) Region() *ackv1alpha1.AWSRegion { + if ri.meta != nil { + return ri.meta.Region + } + return nil +} diff --git a/pkg/resource/option_group/manager.go b/pkg/resource/option_group/manager.go new file mode 100644 index 0000000..0963420 --- /dev/null +++ b/pkg/resource/option_group/manager.go @@ -0,0 +1,417 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file 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 option_group + +import ( + "context" + "fmt" + "time" + + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" + ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare" + ackcondition "github.com/aws-controllers-k8s/runtime/pkg/condition" + ackcfg "github.com/aws-controllers-k8s/runtime/pkg/config" + ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors" + ackmetrics "github.com/aws-controllers-k8s/runtime/pkg/metrics" + ackrequeue "github.com/aws-controllers-k8s/runtime/pkg/requeue" + ackrt "github.com/aws-controllers-k8s/runtime/pkg/runtime" + ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log" + acktags "github.com/aws-controllers-k8s/runtime/pkg/tags" + acktypes "github.com/aws-controllers-k8s/runtime/pkg/types" + ackutil "github.com/aws-controllers-k8s/runtime/pkg/util" + "github.com/aws/aws-sdk-go-v2/aws" + svcsdk "github.com/aws/aws-sdk-go-v2/service/rds" + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + + svcapitypes "github.com/aws-controllers-k8s/rds-controller/apis/v1alpha1" +) + +var ( + _ = ackutil.InStrings + _ = acktags.NewTags() + _ = ackrt.MissingImageTagValue + _ = svcapitypes.OptionGroup{} +) + +// +kubebuilder:rbac:groups=rds.services.k8s.aws,resources=optiongroups,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=rds.services.k8s.aws,resources=optiongroups/status,verbs=get;update;patch + +var lateInitializeFieldNames = []string{} + +// resourceManager is responsible for providing a consistent way to perform +// CRUD operations in a backend AWS service API for Book custom resources. +type resourceManager struct { + // cfg is a copy of the ackcfg.Config object passed on start of the service + // controller + cfg ackcfg.Config + // clientcfg is a copy of the client configuration passed on start of the + // service controller + clientcfg aws.Config + // log refers to the logr.Logger object handling logging for the service + // controller + log logr.Logger + // metrics contains a collection of Prometheus metric objects that the + // service controller and its reconcilers track + metrics *ackmetrics.Metrics + // rr is the Reconciler which can be used for various utility + // functions such as querying for Secret values given a SecretReference + rr acktypes.Reconciler + // awsAccountID is the AWS account identifier that contains the resources + // managed by this resource manager + awsAccountID ackv1alpha1.AWSAccountID + // The AWS Region that this resource manager targets + awsRegion ackv1alpha1.AWSRegion + // sdk is a pointer to the AWS service API client exposed by the + // aws-sdk-go-v2/services/{alias} package. + sdkapi *svcsdk.Client +} + +// concreteResource returns a pointer to a resource from the supplied +// generic AWSResource interface +func (rm *resourceManager) concreteResource( + res acktypes.AWSResource, +) *resource { + // cast the generic interface into a pointer type specific to the concrete + // implementing resource type managed by this resource manager + return res.(*resource) +} + +// ReadOne returns the currently-observed state of the supplied AWSResource in +// the backend AWS service API. +func (rm *resourceManager) ReadOne( + ctx context.Context, + res acktypes.AWSResource, +) (acktypes.AWSResource, error) { + r := rm.concreteResource(res) + if r.ko == nil { + // Should never happen... if it does, it's buggy code. + panic("resource manager's ReadOne() method received resource with nil CR object") + } + observed, err := rm.sdkFind(ctx, r) + mirrorAWSTags(r, observed) + if err != nil { + if observed != nil { + return rm.onError(observed, err) + } + return rm.onError(r, err) + } + return rm.onSuccess(observed) +} + +// Create attempts to create the supplied AWSResource in the backend AWS +// service API, returning an AWSResource representing the newly-created +// resource +func (rm *resourceManager) Create( + ctx context.Context, + res acktypes.AWSResource, +) (acktypes.AWSResource, error) { + r := rm.concreteResource(res) + if r.ko == nil { + // Should never happen... if it does, it's buggy code. + panic("resource manager's Create() method received resource with nil CR object") + } + created, err := rm.sdkCreate(ctx, r) + if err != nil { + if created != nil { + return rm.onError(created, err) + } + return rm.onError(r, err) + } + return rm.onSuccess(created) +} + +// Update attempts to mutate the supplied desired AWSResource in the backend AWS +// service API, returning an AWSResource representing the newly-mutated +// resource. +// Note for specialized logic implementers can check to see how the latest +// observed resource differs from the supplied desired state. The +// higher-level reonciler determines whether or not the desired differs +// from the latest observed and decides whether to call the resource +// manager's Update method +func (rm *resourceManager) Update( + ctx context.Context, + resDesired acktypes.AWSResource, + resLatest acktypes.AWSResource, + delta *ackcompare.Delta, +) (acktypes.AWSResource, error) { + desired := rm.concreteResource(resDesired) + latest := rm.concreteResource(resLatest) + if desired.ko == nil || latest.ko == nil { + // Should never happen... if it does, it's buggy code. + panic("resource manager's Update() method received resource with nil CR object") + } + updated, err := rm.sdkUpdate(ctx, desired, latest, delta) + if err != nil { + if updated != nil { + return rm.onError(updated, err) + } + return rm.onError(latest, err) + } + return rm.onSuccess(updated) +} + +// Delete attempts to destroy the supplied AWSResource in the backend AWS +// service API, returning an AWSResource representing the +// resource being deleted (if delete is asynchronous and takes time) +func (rm *resourceManager) Delete( + ctx context.Context, + res acktypes.AWSResource, +) (acktypes.AWSResource, error) { + r := rm.concreteResource(res) + if r.ko == nil { + // Should never happen... if it does, it's buggy code. + panic("resource manager's Update() method received resource with nil CR object") + } + observed, err := rm.sdkDelete(ctx, r) + if err != nil { + if observed != nil { + return rm.onError(observed, err) + } + return rm.onError(r, err) + } + + return rm.onSuccess(observed) +} + +// ARNFromName returns an AWS Resource Name from a given string name. This +// is useful for constructing ARNs for APIs that require ARNs in their +// GetAttributes operations but all we have (for new CRs at least) is a +// name for the resource +func (rm *resourceManager) ARNFromName(name string) string { + return fmt.Sprintf( + "arn:aws:rds:%s:%s:%s", + rm.awsRegion, + rm.awsAccountID, + name, + ) +} + +// LateInitialize returns an acktypes.AWSResource after setting the late initialized +// fields from the readOne call. This method will initialize the optional fields +// which were not provided by the k8s user but were defaulted by the AWS service. +// If there are no such fields to be initialized, the returned object is similar to +// object passed in the parameter. +func (rm *resourceManager) LateInitialize( + ctx context.Context, + latest acktypes.AWSResource, +) (acktypes.AWSResource, error) { + rlog := ackrtlog.FromContext(ctx) + // If there are no fields to late initialize, do nothing + if len(lateInitializeFieldNames) == 0 { + rlog.Debug("no late initialization required.") + return latest, nil + } + latestCopy := latest.DeepCopy() + lateInitConditionReason := "" + lateInitConditionMessage := "" + observed, err := rm.ReadOne(ctx, latestCopy) + if err != nil { + lateInitConditionMessage = "Unable to complete Read operation required for late initialization" + lateInitConditionReason = "Late Initialization Failure" + ackcondition.SetLateInitialized(latestCopy, corev1.ConditionFalse, &lateInitConditionMessage, &lateInitConditionReason) + ackcondition.SetSynced(latestCopy, corev1.ConditionFalse, nil, nil) + return latestCopy, err + } + lateInitializedRes := rm.lateInitializeFromReadOneOutput(observed, latestCopy) + incompleteInitialization := rm.incompleteLateInitialization(lateInitializedRes) + if incompleteInitialization { + // Add the condition with LateInitialized=False + lateInitConditionMessage = "Late initialization did not complete, requeuing with delay of 5 seconds" + lateInitConditionReason = "Delayed Late Initialization" + ackcondition.SetLateInitialized(lateInitializedRes, corev1.ConditionFalse, &lateInitConditionMessage, &lateInitConditionReason) + ackcondition.SetSynced(lateInitializedRes, corev1.ConditionFalse, nil, nil) + return lateInitializedRes, ackrequeue.NeededAfter(nil, time.Duration(5)*time.Second) + } + // Set LateInitialized condition to True + lateInitConditionMessage = "Late initialization successful" + lateInitConditionReason = "Late initialization successful" + ackcondition.SetLateInitialized(lateInitializedRes, corev1.ConditionTrue, &lateInitConditionMessage, &lateInitConditionReason) + return lateInitializedRes, nil +} + +// incompleteLateInitialization return true if there are fields which were supposed to be +// late initialized but are not. If all the fields are late initialized, false is returned +func (rm *resourceManager) incompleteLateInitialization( + res acktypes.AWSResource, +) bool { + return false +} + +// lateInitializeFromReadOneOutput late initializes the 'latest' resource from the 'observed' +// resource and returns 'latest' resource +func (rm *resourceManager) lateInitializeFromReadOneOutput( + observed acktypes.AWSResource, + latest acktypes.AWSResource, +) acktypes.AWSResource { + return latest +} + +// IsSynced returns true if the resource is synced. +func (rm *resourceManager) IsSynced(ctx context.Context, res acktypes.AWSResource) (bool, error) { + r := rm.concreteResource(res) + if r.ko == nil { + // Should never happen... if it does, it's buggy code. + panic("resource manager's IsSynced() method received resource with nil CR object") + } + + return true, nil +} + +// EnsureTags ensures that tags are present inside the AWSResource. +// If the AWSResource does not have any existing resource tags, the 'tags' +// field is initialized and the controller tags are added. +// If the AWSResource has existing resource tags, then controller tags are +// added to the existing resource tags without overriding them. +// If the AWSResource does not support tags, only then the controller tags +// will not be added to the AWSResource. +func (rm *resourceManager) EnsureTags( + ctx context.Context, + res acktypes.AWSResource, + md acktypes.ServiceControllerMetadata, +) error { + r := rm.concreteResource(res) + if r.ko == nil { + // Should never happen... if it does, it's buggy code. + panic("resource manager's EnsureTags method received resource with nil CR object") + } + defaultTags := ackrt.GetDefaultTags(&rm.cfg, r.ko, md) + var existingTags []*svcapitypes.Tag + existingTags = r.ko.Spec.Tags + resourceTags, keyOrder := convertToOrderedACKTags(existingTags) + tags := acktags.Merge(resourceTags, defaultTags) + r.ko.Spec.Tags = fromACKTags(tags, keyOrder) + return nil +} + +// FilterSystemTags removes system-managed tags from the resource's tag collection +// to prevent the controller from attempting to manage them. This includes: +// - Tags with keys starting with "aws:" (AWS-managed system tags) +// - Tags specified via the --resource-tags startup flag (controller-level tags) +// - Tags injected by AWS services (e.g., CloudFormation, EKS, etc.) +// +// This filtering is essential because: +// 1. AWS services automatically add system tags that cannot be modified by users +// 2. Attempting to remove these tags would result in API errors +// 3. The controller should only manage user-defined tags, not system tags +// +// Must be called after each Read operation to ensure the resource state +// reflects only manageable tags. This prevents unnecessary update attempts +// and maintains consistency between desired and actual resource state. +// +// Example system tags that are filtered: +// - aws:cloudformation:stack-name (CloudFormation) +// - aws:eks:cluster-name (EKS) +// - services.k8s.aws/* (Kubernetes-managed) +func (rm *resourceManager) FilterSystemTags(res acktypes.AWSResource, systemTags []string) { + r := rm.concreteResource(res) + if r == nil || r.ko == nil { + return + } + var existingTags []*svcapitypes.Tag + existingTags = r.ko.Spec.Tags + resourceTags, tagKeyOrder := convertToOrderedACKTags(existingTags) + ignoreSystemTags(resourceTags, systemTags) + r.ko.Spec.Tags = fromACKTags(resourceTags, tagKeyOrder) +} + +// mirrorAWSTags ensures that AWS tags are included in the desired resource +// if they are present in the latest resource. This will ensure that the +// aws tags are not present in a diff. The logic of the controller will +// ensure these tags aren't patched to the resource in the cluster, and +// will only be present to make sure we don't try to remove these tags. +// +// Although there are a lot of similarities between this function and +// EnsureTags, they are very much different. +// While EnsureTags tries to make sure the resource contains the controller +// tags, mirrowAWSTags tries to make sure tags injected by AWS are mirrored +// from the latest resoruce to the desired resource. +func mirrorAWSTags(a *resource, b *resource) { + if a == nil || a.ko == nil || b == nil || b.ko == nil { + return + } + var existingLatestTags []*svcapitypes.Tag + var existingDesiredTags []*svcapitypes.Tag + existingDesiredTags = a.ko.Spec.Tags + existingLatestTags = b.ko.Spec.Tags + desiredTags, desiredTagKeyOrder := convertToOrderedACKTags(existingDesiredTags) + latestTags, _ := convertToOrderedACKTags(existingLatestTags) + syncAWSTags(desiredTags, latestTags) + a.ko.Spec.Tags = fromACKTags(desiredTags, desiredTagKeyOrder) +} + +// newResourceManager returns a new struct implementing +// acktypes.AWSResourceManager +// This is for AWS-SDK-GO-V2 - Created newResourceManager With AWS sdk-Go-ClientV2 +func newResourceManager( + cfg ackcfg.Config, + clientcfg aws.Config, + log logr.Logger, + metrics *ackmetrics.Metrics, + rr acktypes.Reconciler, + id ackv1alpha1.AWSAccountID, + region ackv1alpha1.AWSRegion, +) (*resourceManager, error) { + return &resourceManager{ + cfg: cfg, + clientcfg: clientcfg, + log: log, + metrics: metrics, + rr: rr, + awsAccountID: id, + awsRegion: region, + sdkapi: svcsdk.NewFromConfig(clientcfg), + }, nil +} + +// onError updates resource conditions and returns updated resource +// it returns nil if no condition is updated. +func (rm *resourceManager) onError( + r *resource, + err error, +) (acktypes.AWSResource, error) { + if r == nil { + return nil, err + } + r1, updated := rm.updateConditions(r, false, err) + if !updated { + return r, err + } + for _, condition := range r1.Conditions() { + if condition.Type == ackv1alpha1.ConditionTypeTerminal && + condition.Status == corev1.ConditionTrue { + // resource is in Terminal condition + // return Terminal error + return r1, ackerr.Terminal + } + } + return r1, err +} + +// onSuccess updates resource conditions and returns updated resource +// it returns the supplied resource if no condition is updated. +func (rm *resourceManager) onSuccess( + r *resource, +) (acktypes.AWSResource, error) { + if r == nil { + return nil, nil + } + r1, updated := rm.updateConditions(r, true, nil) + if !updated { + return r, nil + } + return r1, nil +} diff --git a/pkg/resource/option_group/manager_factory.go b/pkg/resource/option_group/manager_factory.go new file mode 100644 index 0000000..49f186f --- /dev/null +++ b/pkg/resource/option_group/manager_factory.go @@ -0,0 +1,100 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file 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 option_group + +import ( + "fmt" + "sync" + + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" + ackcfg "github.com/aws-controllers-k8s/runtime/pkg/config" + ackmetrics "github.com/aws-controllers-k8s/runtime/pkg/metrics" + acktypes "github.com/aws-controllers-k8s/runtime/pkg/types" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/go-logr/logr" + + svcresource "github.com/aws-controllers-k8s/rds-controller/pkg/resource" +) + +// resourceManagerFactory produces resourceManager objects. It implements the +// `types.AWSResourceManagerFactory` interface. +type resourceManagerFactory struct { + sync.RWMutex + // rmCache contains resource managers for a particular AWS account ID + rmCache map[string]*resourceManager +} + +// ResourcePrototype returns an AWSResource that resource managers produced by +// this factory will handle +func (f *resourceManagerFactory) ResourceDescriptor() acktypes.AWSResourceDescriptor { + return &resourceDescriptor{} +} + +// ManagerFor returns a resource manager object that can manage resources for a +// supplied AWS account +func (f *resourceManagerFactory) ManagerFor( + cfg ackcfg.Config, + clientcfg aws.Config, + log logr.Logger, + metrics *ackmetrics.Metrics, + rr acktypes.Reconciler, + id ackv1alpha1.AWSAccountID, + region ackv1alpha1.AWSRegion, + roleARN ackv1alpha1.AWSResourceName, +) (acktypes.AWSResourceManager, error) { + // We use the account ID, region, and role ARN to uniquely identify a + // resource manager. This helps us to avoid creating multiple resource + // managers for the same account/region/roleARN combination. + rmId := fmt.Sprintf("%s/%s/%s", id, region, roleARN) + f.RLock() + rm, found := f.rmCache[rmId] + f.RUnlock() + + if found { + return rm, nil + } + + f.Lock() + defer f.Unlock() + + rm, err := newResourceManager(cfg, clientcfg, log, metrics, rr, id, region) + if err != nil { + return nil, err + } + f.rmCache[rmId] = rm + return rm, nil +} + +// IsAdoptable returns true if the resource is able to be adopted +func (f *resourceManagerFactory) IsAdoptable() bool { + return true +} + +// RequeueOnSuccessSeconds returns true if the resource should be requeued after specified seconds +// Default is false which means resource will not be requeued after success. +func (f *resourceManagerFactory) RequeueOnSuccessSeconds() int { + return 0 +} + +func newResourceManagerFactory() *resourceManagerFactory { + return &resourceManagerFactory{ + rmCache: map[string]*resourceManager{}, + } +} + +func init() { + svcresource.RegisterManagerFactory(newResourceManagerFactory()) +} diff --git a/pkg/resource/option_group/references.go b/pkg/resource/option_group/references.go new file mode 100644 index 0000000..3f25fad --- /dev/null +++ b/pkg/resource/option_group/references.go @@ -0,0 +1,57 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file 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 option_group + +import ( + "context" + + "sigs.k8s.io/controller-runtime/pkg/client" + + acktypes "github.com/aws-controllers-k8s/runtime/pkg/types" + + svcapitypes "github.com/aws-controllers-k8s/rds-controller/apis/v1alpha1" +) + +// ClearResolvedReferences removes any reference values that were made +// concrete in the spec. It returns a copy of the input AWSResource which +// contains the original *Ref values, but none of their respective concrete +// values. +func (rm *resourceManager) ClearResolvedReferences(res acktypes.AWSResource) acktypes.AWSResource { + ko := rm.concreteResource(res).ko.DeepCopy() + + return &resource{ko} +} + +// ResolveReferences finds if there are any Reference field(s) present +// inside AWSResource passed in the parameter and attempts to resolve those +// reference field(s) into their respective target field(s). It returns a +// copy of the input AWSResource with resolved reference(s), a boolean which +// is set to true if the resource contains any references (regardless of if +// they are resolved successfully) and an error if the passed AWSResource's +// reference field(s) could not be resolved. +func (rm *resourceManager) ResolveReferences( + ctx context.Context, + apiReader client.Reader, + res acktypes.AWSResource, +) (acktypes.AWSResource, bool, error) { + return res, false, nil +} + +// validateReferenceFields validates the reference field and corresponding +// identifier field. +func validateReferenceFields(ko *svcapitypes.OptionGroup) error { + return nil +} diff --git a/pkg/resource/option_group/resource.go b/pkg/resource/option_group/resource.go new file mode 100644 index 0000000..dc2790c --- /dev/null +++ b/pkg/resource/option_group/resource.go @@ -0,0 +1,132 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file 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 option_group + +import ( + "fmt" + + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" + ackerrors "github.com/aws-controllers-k8s/runtime/pkg/errors" + acktypes "github.com/aws-controllers-k8s/runtime/pkg/types" + "github.com/aws/aws-sdk-go-v2/aws" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + rtclient "sigs.k8s.io/controller-runtime/pkg/client" + + svcapitypes "github.com/aws-controllers-k8s/rds-controller/apis/v1alpha1" +) + +// Hack to avoid import errors during build... +var ( + _ = &ackerrors.MissingNameIdentifier +) + +// resource implements the `aws-controller-k8s/runtime/pkg/types.AWSResource` +// interface +type resource struct { + // The Kubernetes-native CR representing the resource + ko *svcapitypes.OptionGroup +} + +// Identifiers returns an AWSResourceIdentifiers object containing various +// identifying information, including the AWS account ID that owns the +// resource, the resource's AWS Resource Name (ARN) +func (r *resource) Identifiers() acktypes.AWSResourceIdentifiers { + return &resourceIdentifiers{r.ko.Status.ACKResourceMetadata} +} + +// IsBeingDeleted returns true if the Kubernetes resource has a non-zero +// deletion timestamp +func (r *resource) IsBeingDeleted() bool { + return !r.ko.DeletionTimestamp.IsZero() +} + +// RuntimeObject returns the Kubernetes apimachinery/runtime representation of +// the AWSResource +func (r *resource) RuntimeObject() rtclient.Object { + return r.ko +} + +// MetaObject returns the Kubernetes apimachinery/apis/meta/v1.Object +// representation of the AWSResource +func (r *resource) MetaObject() metav1.Object { + return r.ko.GetObjectMeta() +} + +// Conditions returns the ACK Conditions collection for the AWSResource +func (r *resource) Conditions() []*ackv1alpha1.Condition { + return r.ko.Status.Conditions +} + +// ReplaceConditions sets the Conditions status field for the resource +func (r *resource) ReplaceConditions(conditions []*ackv1alpha1.Condition) { + r.ko.Status.Conditions = conditions +} + +// SetObjectMeta sets the ObjectMeta field for the resource +func (r *resource) SetObjectMeta(meta metav1.ObjectMeta) { + r.ko.ObjectMeta = meta +} + +// SetStatus will set the Status field for the resource +func (r *resource) SetStatus(desired acktypes.AWSResource) { + r.ko.Status = desired.(*resource).ko.Status +} + +// SetIdentifiers sets the Spec or Status field that is referenced as the unique +// resource identifier +func (r *resource) SetIdentifiers(identifier *ackv1alpha1.AWSIdentifiers) error { + if identifier.NameOrID == "" { + return ackerrors.MissingNameIdentifier + } + r.ko.Spec.OptionGroupName = &identifier.NameOrID + + f0, f0ok := identifier.AdditionalKeys["engineName"] + if f0ok { + r.ko.Spec.EngineName = aws.String(f0) + } + f2, f2ok := identifier.AdditionalKeys["majorEngineVersion"] + if f2ok { + r.ko.Spec.MajorEngineVersion = aws.String(f2) + } + + return nil +} + +// PopulateResourceFromAnnotation populates the fields passed from adoption annotation +func (r *resource) PopulateResourceFromAnnotation(fields map[string]string) error { + primaryKey, ok := fields["optionGroupName"] + if !ok { + return ackerrors.NewTerminalError(fmt.Errorf("required field missing: optionGroupName")) + } + r.ko.Spec.OptionGroupName = &primaryKey + + f0, f0ok := fields["engineName"] + if f0ok { + r.ko.Spec.EngineName = aws.String(f0) + } + f2, f2ok := fields["majorEngineVersion"] + if f2ok { + r.ko.Spec.MajorEngineVersion = aws.String(f2) + } + + return nil +} + +// DeepCopy will return a copy of the resource +func (r *resource) DeepCopy() acktypes.AWSResource { + koCopy := r.ko.DeepCopy() + return &resource{koCopy} +} diff --git a/pkg/resource/option_group/sdk.go b/pkg/resource/option_group/sdk.go new file mode 100644 index 0000000..d9ef9ec --- /dev/null +++ b/pkg/resource/option_group/sdk.go @@ -0,0 +1,843 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file 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 option_group + +import ( + "context" + "errors" + "fmt" + "reflect" + "strings" + + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" + ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare" + ackcondition "github.com/aws-controllers-k8s/runtime/pkg/condition" + ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors" + ackrequeue "github.com/aws-controllers-k8s/runtime/pkg/requeue" + ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log" + "github.com/aws/aws-sdk-go-v2/aws" + svcsdk "github.com/aws/aws-sdk-go-v2/service/rds" + svcsdktypes "github.com/aws/aws-sdk-go-v2/service/rds/types" + smithy "github.com/aws/smithy-go" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + svcapitypes "github.com/aws-controllers-k8s/rds-controller/apis/v1alpha1" +) + +// Hack to avoid import errors during build... +var ( + _ = &metav1.Time{} + _ = strings.ToLower("") + _ = &svcsdk.Client{} + _ = &svcapitypes.OptionGroup{} + _ = ackv1alpha1.AWSAccountID("") + _ = &ackerr.NotFound + _ = &ackcondition.NotManagedMessage + _ = &reflect.Value{} + _ = fmt.Sprintf("") + _ = &ackrequeue.NoRequeue{} + _ = &aws.Config{} +) + +// sdkFind returns SDK-specific information about a supplied resource +func (rm *resourceManager) sdkFind( + ctx context.Context, + r *resource, +) (latest *resource, err error) { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("rm.sdkFind") + defer func() { + exit(err) + }() + // If any required fields in the input shape are missing, AWS resource is + // not created yet. Return NotFound here to indicate to callers that the + // resource isn't yet created. + if rm.requiredFieldsMissingFromReadManyInput(r) { + return nil, ackerr.NotFound + } + + input, err := rm.newListRequestPayload(r) + if err != nil { + return nil, err + } + var resp *svcsdk.DescribeOptionGroupsOutput + resp, err = rm.sdkapi.DescribeOptionGroups(ctx, input) + rm.metrics.RecordAPICall("READ_MANY", "DescribeOptionGroups", err) + if err != nil { + var awsErr smithy.APIError + if errors.As(err, &awsErr) && awsErr.ErrorCode() == "OptionGroupNotFoundFault" { + return nil, ackerr.NotFound + } + return nil, err + } + + // Merge in the information we read from the API call above to the copy of + // the original Kubernetes object we passed to the function + ko := r.ko.DeepCopy() + + found := false + for _, elem := range resp.OptionGroupsList { + if elem.AllowsVpcAndNonVpcInstanceMemberships != nil { + ko.Status.AllowsVPCAndNonVPCInstanceMemberships = elem.AllowsVpcAndNonVpcInstanceMemberships + } else { + ko.Status.AllowsVPCAndNonVPCInstanceMemberships = nil + } + if elem.CopyTimestamp != nil { + ko.Status.CopyTimestamp = &metav1.Time{*elem.CopyTimestamp} + } else { + ko.Status.CopyTimestamp = nil + } + if elem.EngineName != nil { + ko.Spec.EngineName = elem.EngineName + } else { + ko.Spec.EngineName = nil + } + if elem.MajorEngineVersion != nil { + ko.Spec.MajorEngineVersion = elem.MajorEngineVersion + } else { + ko.Spec.MajorEngineVersion = nil + } + if elem.OptionGroupArn != nil { + if ko.Status.ACKResourceMetadata == nil { + ko.Status.ACKResourceMetadata = &ackv1alpha1.ResourceMetadata{} + } + tmpARN := ackv1alpha1.AWSResourceName(*elem.OptionGroupArn) + ko.Status.ACKResourceMetadata.ARN = &tmpARN + } + if elem.OptionGroupDescription != nil { + ko.Spec.OptionGroupDescription = elem.OptionGroupDescription + } else { + ko.Spec.OptionGroupDescription = nil + } + if elem.OptionGroupName != nil { + ko.Spec.OptionGroupName = elem.OptionGroupName + } else { + ko.Spec.OptionGroupName = nil + } + if elem.Options != nil { + f7 := []*svcapitypes.Option{} + for _, f7iter := range elem.Options { + f7elem := &svcapitypes.Option{} + if f7iter.DBSecurityGroupMemberships != nil { + f7elemf0 := []*svcapitypes.DBSecurityGroupMembership{} + for _, f7elemf0iter := range f7iter.DBSecurityGroupMemberships { + f7elemf0elem := &svcapitypes.DBSecurityGroupMembership{} + if f7elemf0iter.DBSecurityGroupName != nil { + f7elemf0elem.DBSecurityGroupName = f7elemf0iter.DBSecurityGroupName + } + if f7elemf0iter.Status != nil { + f7elemf0elem.Status = f7elemf0iter.Status + } + f7elemf0 = append(f7elemf0, f7elemf0elem) + } + f7elem.DBSecurityGroupMemberships = f7elemf0 + } + if f7iter.OptionDescription != nil { + f7elem.OptionDescription = f7iter.OptionDescription + } + if f7iter.OptionName != nil { + f7elem.OptionName = f7iter.OptionName + } + if f7iter.OptionSettings != nil { + f7elemf3 := []*svcapitypes.OptionSetting{} + for _, f7elemf3iter := range f7iter.OptionSettings { + f7elemf3elem := &svcapitypes.OptionSetting{} + if f7elemf3iter.AllowedValues != nil { + f7elemf3elem.AllowedValues = f7elemf3iter.AllowedValues + } + if f7elemf3iter.ApplyType != nil { + f7elemf3elem.ApplyType = f7elemf3iter.ApplyType + } + if f7elemf3iter.DataType != nil { + f7elemf3elem.DataType = f7elemf3iter.DataType + } + if f7elemf3iter.DefaultValue != nil { + f7elemf3elem.DefaultValue = f7elemf3iter.DefaultValue + } + if f7elemf3iter.Description != nil { + f7elemf3elem.Description = f7elemf3iter.Description + } + if f7elemf3iter.IsCollection != nil { + f7elemf3elem.IsCollection = f7elemf3iter.IsCollection + } + if f7elemf3iter.IsModifiable != nil { + f7elemf3elem.IsModifiable = f7elemf3iter.IsModifiable + } + if f7elemf3iter.Name != nil { + f7elemf3elem.Name = f7elemf3iter.Name + } + if f7elemf3iter.Value != nil { + f7elemf3elem.Value = f7elemf3iter.Value + } + f7elemf3 = append(f7elemf3, f7elemf3elem) + } + f7elem.OptionSettings = f7elemf3 + } + if f7iter.OptionVersion != nil { + f7elem.OptionVersion = f7iter.OptionVersion + } + if f7iter.Permanent != nil { + f7elem.Permanent = f7iter.Permanent + } + if f7iter.Persistent != nil { + f7elem.Persistent = f7iter.Persistent + } + if f7iter.Port != nil { + portCopy := int64(*f7iter.Port) + f7elem.Port = &portCopy + } + if f7iter.VpcSecurityGroupMemberships != nil { + f7elemf8 := []*svcapitypes.VPCSecurityGroupMembership{} + for _, f7elemf8iter := range f7iter.VpcSecurityGroupMemberships { + f7elemf8elem := &svcapitypes.VPCSecurityGroupMembership{} + if f7elemf8iter.Status != nil { + f7elemf8elem.Status = f7elemf8iter.Status + } + if f7elemf8iter.VpcSecurityGroupId != nil { + f7elemf8elem.VPCSecurityGroupID = f7elemf8iter.VpcSecurityGroupId + } + f7elemf8 = append(f7elemf8, f7elemf8elem) + } + f7elem.VPCSecurityGroupMemberships = f7elemf8 + } + f7 = append(f7, f7elem) + } + ko.Status.Options = f7 + } else { + ko.Status.Options = nil + } + if elem.SourceAccountId != nil { + ko.Status.SourceAccountID = elem.SourceAccountId + } else { + ko.Status.SourceAccountID = nil + } + if elem.SourceOptionGroup != nil { + ko.Status.SourceOptionGroup = elem.SourceOptionGroup + } else { + ko.Status.SourceOptionGroup = nil + } + if elem.VpcId != nil { + ko.Status.VPCID = elem.VpcId + } else { + ko.Status.VPCID = nil + } + found = true + break + } + if !found { + return nil, ackerr.NotFound + } + + rm.setStatusDefaults(ko) + if ko.Status.ACKResourceMetadata != nil && ko.Status.ACKResourceMetadata.ARN != nil { + resourceARN := (*string)(ko.Status.ACKResourceMetadata.ARN) + tags, err := rm.getTags(ctx, *resourceARN) + if err != nil { + return nil, err + } + ko.Spec.Tags = tags + } + + return &resource{ko}, nil +} + +// requiredFieldsMissingFromReadManyInput returns true if there are any fields +// for the ReadMany Input shape that are required but not present in the +// resource's Spec or Status +func (rm *resourceManager) requiredFieldsMissingFromReadManyInput( + r *resource, +) bool { + return r.ko.Spec.OptionGroupName == nil + +} + +// newListRequestPayload returns SDK-specific struct for the HTTP request +// payload of the List API call for the resource +func (rm *resourceManager) newListRequestPayload( + r *resource, +) (*svcsdk.DescribeOptionGroupsInput, error) { + res := &svcsdk.DescribeOptionGroupsInput{} + + if r.ko.Spec.EngineName != nil { + res.EngineName = r.ko.Spec.EngineName + } + if r.ko.Spec.MajorEngineVersion != nil { + res.MajorEngineVersion = r.ko.Spec.MajorEngineVersion + } + if r.ko.Spec.OptionGroupName != nil { + res.OptionGroupName = r.ko.Spec.OptionGroupName + } + + return res, nil +} + +// sdkCreate creates the supplied resource in the backend AWS service API and +// returns a copy of the resource with resource fields (in both Spec and +// Status) filled in with values from the CREATE API operation's Output shape. +func (rm *resourceManager) sdkCreate( + ctx context.Context, + desired *resource, +) (created *resource, err error) { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("rm.sdkCreate") + defer func() { + exit(err) + }() + input, err := rm.newCreateRequestPayload(ctx, desired) + if err != nil { + return nil, err + } + + var resp *svcsdk.CreateOptionGroupOutput + _ = resp + resp, err = rm.sdkapi.CreateOptionGroup(ctx, input) + rm.metrics.RecordAPICall("CREATE", "CreateOptionGroup", err) + if err != nil { + return nil, err + } + // Merge in the information we read from the API call above to the copy of + // the original Kubernetes object we passed to the function + ko := desired.ko.DeepCopy() + + if resp.OptionGroup.AllowsVpcAndNonVpcInstanceMemberships != nil { + ko.Status.AllowsVPCAndNonVPCInstanceMemberships = resp.OptionGroup.AllowsVpcAndNonVpcInstanceMemberships + } else { + ko.Status.AllowsVPCAndNonVPCInstanceMemberships = nil + } + if resp.OptionGroup.CopyTimestamp != nil { + ko.Status.CopyTimestamp = &metav1.Time{*resp.OptionGroup.CopyTimestamp} + } else { + ko.Status.CopyTimestamp = nil + } + if resp.OptionGroup.EngineName != nil { + ko.Spec.EngineName = resp.OptionGroup.EngineName + } else { + ko.Spec.EngineName = nil + } + if resp.OptionGroup.MajorEngineVersion != nil { + ko.Spec.MajorEngineVersion = resp.OptionGroup.MajorEngineVersion + } else { + ko.Spec.MajorEngineVersion = nil + } + if ko.Status.ACKResourceMetadata == nil { + ko.Status.ACKResourceMetadata = &ackv1alpha1.ResourceMetadata{} + } + if resp.OptionGroup.OptionGroupArn != nil { + arn := ackv1alpha1.AWSResourceName(*resp.OptionGroup.OptionGroupArn) + ko.Status.ACKResourceMetadata.ARN = &arn + } + if resp.OptionGroup.OptionGroupDescription != nil { + ko.Spec.OptionGroupDescription = resp.OptionGroup.OptionGroupDescription + } else { + ko.Spec.OptionGroupDescription = nil + } + if resp.OptionGroup.OptionGroupName != nil { + ko.Spec.OptionGroupName = resp.OptionGroup.OptionGroupName + } else { + ko.Spec.OptionGroupName = nil + } + if resp.OptionGroup.Options != nil { + f7 := []*svcapitypes.Option{} + for _, f7iter := range resp.OptionGroup.Options { + f7elem := &svcapitypes.Option{} + if f7iter.DBSecurityGroupMemberships != nil { + f7elemf0 := []*svcapitypes.DBSecurityGroupMembership{} + for _, f7elemf0iter := range f7iter.DBSecurityGroupMemberships { + f7elemf0elem := &svcapitypes.DBSecurityGroupMembership{} + if f7elemf0iter.DBSecurityGroupName != nil { + f7elemf0elem.DBSecurityGroupName = f7elemf0iter.DBSecurityGroupName + } + if f7elemf0iter.Status != nil { + f7elemf0elem.Status = f7elemf0iter.Status + } + f7elemf0 = append(f7elemf0, f7elemf0elem) + } + f7elem.DBSecurityGroupMemberships = f7elemf0 + } + if f7iter.OptionDescription != nil { + f7elem.OptionDescription = f7iter.OptionDescription + } + if f7iter.OptionName != nil { + f7elem.OptionName = f7iter.OptionName + } + if f7iter.OptionSettings != nil { + f7elemf3 := []*svcapitypes.OptionSetting{} + for _, f7elemf3iter := range f7iter.OptionSettings { + f7elemf3elem := &svcapitypes.OptionSetting{} + if f7elemf3iter.AllowedValues != nil { + f7elemf3elem.AllowedValues = f7elemf3iter.AllowedValues + } + if f7elemf3iter.ApplyType != nil { + f7elemf3elem.ApplyType = f7elemf3iter.ApplyType + } + if f7elemf3iter.DataType != nil { + f7elemf3elem.DataType = f7elemf3iter.DataType + } + if f7elemf3iter.DefaultValue != nil { + f7elemf3elem.DefaultValue = f7elemf3iter.DefaultValue + } + if f7elemf3iter.Description != nil { + f7elemf3elem.Description = f7elemf3iter.Description + } + if f7elemf3iter.IsCollection != nil { + f7elemf3elem.IsCollection = f7elemf3iter.IsCollection + } + if f7elemf3iter.IsModifiable != nil { + f7elemf3elem.IsModifiable = f7elemf3iter.IsModifiable + } + if f7elemf3iter.Name != nil { + f7elemf3elem.Name = f7elemf3iter.Name + } + if f7elemf3iter.Value != nil { + f7elemf3elem.Value = f7elemf3iter.Value + } + f7elemf3 = append(f7elemf3, f7elemf3elem) + } + f7elem.OptionSettings = f7elemf3 + } + if f7iter.OptionVersion != nil { + f7elem.OptionVersion = f7iter.OptionVersion + } + if f7iter.Permanent != nil { + f7elem.Permanent = f7iter.Permanent + } + if f7iter.Persistent != nil { + f7elem.Persistent = f7iter.Persistent + } + if f7iter.Port != nil { + portCopy := int64(*f7iter.Port) + f7elem.Port = &portCopy + } + if f7iter.VpcSecurityGroupMemberships != nil { + f7elemf8 := []*svcapitypes.VPCSecurityGroupMembership{} + for _, f7elemf8iter := range f7iter.VpcSecurityGroupMemberships { + f7elemf8elem := &svcapitypes.VPCSecurityGroupMembership{} + if f7elemf8iter.Status != nil { + f7elemf8elem.Status = f7elemf8iter.Status + } + if f7elemf8iter.VpcSecurityGroupId != nil { + f7elemf8elem.VPCSecurityGroupID = f7elemf8iter.VpcSecurityGroupId + } + f7elemf8 = append(f7elemf8, f7elemf8elem) + } + f7elem.VPCSecurityGroupMemberships = f7elemf8 + } + f7 = append(f7, f7elem) + } + ko.Status.Options = f7 + } else { + ko.Status.Options = nil + } + if resp.OptionGroup.SourceAccountId != nil { + ko.Status.SourceAccountID = resp.OptionGroup.SourceAccountId + } else { + ko.Status.SourceAccountID = nil + } + if resp.OptionGroup.SourceOptionGroup != nil { + ko.Status.SourceOptionGroup = resp.OptionGroup.SourceOptionGroup + } else { + ko.Status.SourceOptionGroup = nil + } + if resp.OptionGroup.VpcId != nil { + ko.Status.VPCID = resp.OptionGroup.VpcId + } else { + ko.Status.VPCID = nil + } + + rm.setStatusDefaults(ko) + return &resource{ko}, nil +} + +// newCreateRequestPayload returns an SDK-specific struct for the HTTP request +// payload of the Create API call for the resource +func (rm *resourceManager) newCreateRequestPayload( + ctx context.Context, + r *resource, +) (*svcsdk.CreateOptionGroupInput, error) { + res := &svcsdk.CreateOptionGroupInput{} + + if r.ko.Spec.EngineName != nil { + res.EngineName = r.ko.Spec.EngineName + } + if r.ko.Spec.MajorEngineVersion != nil { + res.MajorEngineVersion = r.ko.Spec.MajorEngineVersion + } + if r.ko.Spec.OptionGroupDescription != nil { + res.OptionGroupDescription = r.ko.Spec.OptionGroupDescription + } + if r.ko.Spec.OptionGroupName != nil { + res.OptionGroupName = r.ko.Spec.OptionGroupName + } + if r.ko.Spec.Tags != nil { + f4 := []svcsdktypes.Tag{} + for _, f4iter := range r.ko.Spec.Tags { + f4elem := &svcsdktypes.Tag{} + if f4iter.Key != nil { + f4elem.Key = f4iter.Key + } + if f4iter.Value != nil { + f4elem.Value = f4iter.Value + } + f4 = append(f4, *f4elem) + } + res.Tags = f4 + } + + return res, nil +} + +// sdkUpdate patches the supplied resource in the backend AWS service API and +// returns a new resource with updated fields. +func (rm *resourceManager) sdkUpdate( + ctx context.Context, + desired *resource, + latest *resource, + delta *ackcompare.Delta, +) (updated *resource, err error) { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("rm.sdkUpdate") + defer func() { + exit(err) + }() + if delta.DifferentAt("Spec.Tags") { + if err = rm.syncTags(ctx, desired, latest); err != nil { + return nil, err + } + } + + if !delta.DifferentExcept("Spec.Tags") { + return desired, nil + } + + input, err := rm.newUpdateRequestPayload(ctx, desired, delta) + if err != nil { + return nil, err + } + + var resp *svcsdk.ModifyOptionGroupOutput + _ = resp + resp, err = rm.sdkapi.ModifyOptionGroup(ctx, input) + rm.metrics.RecordAPICall("UPDATE", "ModifyOptionGroup", err) + if err != nil { + return nil, err + } + // Merge in the information we read from the API call above to the copy of + // the original Kubernetes object we passed to the function + ko := desired.ko.DeepCopy() + + if resp.OptionGroup.AllowsVpcAndNonVpcInstanceMemberships != nil { + ko.Status.AllowsVPCAndNonVPCInstanceMemberships = resp.OptionGroup.AllowsVpcAndNonVpcInstanceMemberships + } else { + ko.Status.AllowsVPCAndNonVPCInstanceMemberships = nil + } + if resp.OptionGroup.CopyTimestamp != nil { + ko.Status.CopyTimestamp = &metav1.Time{*resp.OptionGroup.CopyTimestamp} + } else { + ko.Status.CopyTimestamp = nil + } + if resp.OptionGroup.EngineName != nil { + ko.Spec.EngineName = resp.OptionGroup.EngineName + } else { + ko.Spec.EngineName = nil + } + if resp.OptionGroup.MajorEngineVersion != nil { + ko.Spec.MajorEngineVersion = resp.OptionGroup.MajorEngineVersion + } else { + ko.Spec.MajorEngineVersion = nil + } + if ko.Status.ACKResourceMetadata == nil { + ko.Status.ACKResourceMetadata = &ackv1alpha1.ResourceMetadata{} + } + if resp.OptionGroup.OptionGroupArn != nil { + arn := ackv1alpha1.AWSResourceName(*resp.OptionGroup.OptionGroupArn) + ko.Status.ACKResourceMetadata.ARN = &arn + } + if resp.OptionGroup.OptionGroupDescription != nil { + ko.Spec.OptionGroupDescription = resp.OptionGroup.OptionGroupDescription + } else { + ko.Spec.OptionGroupDescription = nil + } + if resp.OptionGroup.OptionGroupName != nil { + ko.Spec.OptionGroupName = resp.OptionGroup.OptionGroupName + } else { + ko.Spec.OptionGroupName = nil + } + if resp.OptionGroup.Options != nil { + f7 := []*svcapitypes.Option{} + for _, f7iter := range resp.OptionGroup.Options { + f7elem := &svcapitypes.Option{} + if f7iter.DBSecurityGroupMemberships != nil { + f7elemf0 := []*svcapitypes.DBSecurityGroupMembership{} + for _, f7elemf0iter := range f7iter.DBSecurityGroupMemberships { + f7elemf0elem := &svcapitypes.DBSecurityGroupMembership{} + if f7elemf0iter.DBSecurityGroupName != nil { + f7elemf0elem.DBSecurityGroupName = f7elemf0iter.DBSecurityGroupName + } + if f7elemf0iter.Status != nil { + f7elemf0elem.Status = f7elemf0iter.Status + } + f7elemf0 = append(f7elemf0, f7elemf0elem) + } + f7elem.DBSecurityGroupMemberships = f7elemf0 + } + if f7iter.OptionDescription != nil { + f7elem.OptionDescription = f7iter.OptionDescription + } + if f7iter.OptionName != nil { + f7elem.OptionName = f7iter.OptionName + } + if f7iter.OptionSettings != nil { + f7elemf3 := []*svcapitypes.OptionSetting{} + for _, f7elemf3iter := range f7iter.OptionSettings { + f7elemf3elem := &svcapitypes.OptionSetting{} + if f7elemf3iter.AllowedValues != nil { + f7elemf3elem.AllowedValues = f7elemf3iter.AllowedValues + } + if f7elemf3iter.ApplyType != nil { + f7elemf3elem.ApplyType = f7elemf3iter.ApplyType + } + if f7elemf3iter.DataType != nil { + f7elemf3elem.DataType = f7elemf3iter.DataType + } + if f7elemf3iter.DefaultValue != nil { + f7elemf3elem.DefaultValue = f7elemf3iter.DefaultValue + } + if f7elemf3iter.Description != nil { + f7elemf3elem.Description = f7elemf3iter.Description + } + if f7elemf3iter.IsCollection != nil { + f7elemf3elem.IsCollection = f7elemf3iter.IsCollection + } + if f7elemf3iter.IsModifiable != nil { + f7elemf3elem.IsModifiable = f7elemf3iter.IsModifiable + } + if f7elemf3iter.Name != nil { + f7elemf3elem.Name = f7elemf3iter.Name + } + if f7elemf3iter.Value != nil { + f7elemf3elem.Value = f7elemf3iter.Value + } + f7elemf3 = append(f7elemf3, f7elemf3elem) + } + f7elem.OptionSettings = f7elemf3 + } + if f7iter.OptionVersion != nil { + f7elem.OptionVersion = f7iter.OptionVersion + } + if f7iter.Permanent != nil { + f7elem.Permanent = f7iter.Permanent + } + if f7iter.Persistent != nil { + f7elem.Persistent = f7iter.Persistent + } + if f7iter.Port != nil { + portCopy := int64(*f7iter.Port) + f7elem.Port = &portCopy + } + if f7iter.VpcSecurityGroupMemberships != nil { + f7elemf8 := []*svcapitypes.VPCSecurityGroupMembership{} + for _, f7elemf8iter := range f7iter.VpcSecurityGroupMemberships { + f7elemf8elem := &svcapitypes.VPCSecurityGroupMembership{} + if f7elemf8iter.Status != nil { + f7elemf8elem.Status = f7elemf8iter.Status + } + if f7elemf8iter.VpcSecurityGroupId != nil { + f7elemf8elem.VPCSecurityGroupID = f7elemf8iter.VpcSecurityGroupId + } + f7elemf8 = append(f7elemf8, f7elemf8elem) + } + f7elem.VPCSecurityGroupMemberships = f7elemf8 + } + f7 = append(f7, f7elem) + } + ko.Status.Options = f7 + } else { + ko.Status.Options = nil + } + if resp.OptionGroup.SourceAccountId != nil { + ko.Status.SourceAccountID = resp.OptionGroup.SourceAccountId + } else { + ko.Status.SourceAccountID = nil + } + if resp.OptionGroup.SourceOptionGroup != nil { + ko.Status.SourceOptionGroup = resp.OptionGroup.SourceOptionGroup + } else { + ko.Status.SourceOptionGroup = nil + } + if resp.OptionGroup.VpcId != nil { + ko.Status.VPCID = resp.OptionGroup.VpcId + } else { + ko.Status.VPCID = nil + } + + rm.setStatusDefaults(ko) + return &resource{ko}, nil +} + +// newUpdateRequestPayload returns an SDK-specific struct for the HTTP request +// payload of the Update API call for the resource +func (rm *resourceManager) newUpdateRequestPayload( + ctx context.Context, + r *resource, + delta *ackcompare.Delta, +) (*svcsdk.ModifyOptionGroupInput, error) { + res := &svcsdk.ModifyOptionGroupInput{} + + if r.ko.Spec.OptionGroupName != nil { + res.OptionGroupName = r.ko.Spec.OptionGroupName + } + + return res, nil +} + +// sdkDelete deletes the supplied resource in the backend AWS service API +func (rm *resourceManager) sdkDelete( + ctx context.Context, + r *resource, +) (latest *resource, err error) { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("rm.sdkDelete") + defer func() { + exit(err) + }() + input, err := rm.newDeleteRequestPayload(r) + if err != nil { + return nil, err + } + var resp *svcsdk.DeleteOptionGroupOutput + _ = resp + resp, err = rm.sdkapi.DeleteOptionGroup(ctx, input) + rm.metrics.RecordAPICall("DELETE", "DeleteOptionGroup", err) + return nil, err +} + +// newDeleteRequestPayload returns an SDK-specific struct for the HTTP request +// payload of the Delete API call for the resource +func (rm *resourceManager) newDeleteRequestPayload( + r *resource, +) (*svcsdk.DeleteOptionGroupInput, error) { + res := &svcsdk.DeleteOptionGroupInput{} + + if r.ko.Spec.OptionGroupName != nil { + res.OptionGroupName = r.ko.Spec.OptionGroupName + } + + return res, nil +} + +// setStatusDefaults sets default properties into supplied custom resource +func (rm *resourceManager) setStatusDefaults( + ko *svcapitypes.OptionGroup, +) { + if ko.Status.ACKResourceMetadata == nil { + ko.Status.ACKResourceMetadata = &ackv1alpha1.ResourceMetadata{} + } + if ko.Status.ACKResourceMetadata.Region == nil { + ko.Status.ACKResourceMetadata.Region = &rm.awsRegion + } + if ko.Status.ACKResourceMetadata.OwnerAccountID == nil { + ko.Status.ACKResourceMetadata.OwnerAccountID = &rm.awsAccountID + } + if ko.Status.Conditions == nil { + ko.Status.Conditions = []*ackv1alpha1.Condition{} + } +} + +// updateConditions returns updated resource, true; if conditions were updated +// else it returns nil, false +func (rm *resourceManager) updateConditions( + r *resource, + onSuccess bool, + err error, +) (*resource, bool) { + ko := r.ko.DeepCopy() + rm.setStatusDefaults(ko) + + // Terminal condition + var terminalCondition *ackv1alpha1.Condition = nil + var recoverableCondition *ackv1alpha1.Condition = nil + var syncCondition *ackv1alpha1.Condition = nil + for _, condition := range ko.Status.Conditions { + if condition.Type == ackv1alpha1.ConditionTypeTerminal { + terminalCondition = condition + } + if condition.Type == ackv1alpha1.ConditionTypeRecoverable { + recoverableCondition = condition + } + if condition.Type == ackv1alpha1.ConditionTypeResourceSynced { + syncCondition = condition + } + } + var termError *ackerr.TerminalError + if rm.terminalAWSError(err) || err == ackerr.SecretTypeNotSupported || err == ackerr.SecretNotFound || errors.As(err, &termError) { + if terminalCondition == nil { + terminalCondition = &ackv1alpha1.Condition{ + Type: ackv1alpha1.ConditionTypeTerminal, + } + ko.Status.Conditions = append(ko.Status.Conditions, terminalCondition) + } + var errorMessage = "" + if err == ackerr.SecretTypeNotSupported || err == ackerr.SecretNotFound || errors.As(err, &termError) { + errorMessage = err.Error() + } else { + awsErr, _ := ackerr.AWSError(err) + errorMessage = awsErr.Error() + } + terminalCondition.Status = corev1.ConditionTrue + terminalCondition.Message = &errorMessage + } else { + // Clear the terminal condition if no longer present + if terminalCondition != nil { + terminalCondition.Status = corev1.ConditionFalse + terminalCondition.Message = nil + } + // Handling Recoverable Conditions + if err != nil { + if recoverableCondition == nil { + // Add a new Condition containing a non-terminal error + recoverableCondition = &ackv1alpha1.Condition{ + Type: ackv1alpha1.ConditionTypeRecoverable, + } + ko.Status.Conditions = append(ko.Status.Conditions, recoverableCondition) + } + recoverableCondition.Status = corev1.ConditionTrue + awsErr, _ := ackerr.AWSError(err) + errorMessage := err.Error() + if awsErr != nil { + errorMessage = awsErr.Error() + } + recoverableCondition.Message = &errorMessage + } else if recoverableCondition != nil { + recoverableCondition.Status = corev1.ConditionFalse + recoverableCondition.Message = nil + } + } + // Required to avoid the "declared but not used" error in the default case + _ = syncCondition + if terminalCondition != nil || recoverableCondition != nil || syncCondition != nil { + return &resource{ko}, true // updated + } + return nil, false // not updated +} + +// terminalAWSError returns awserr, true; if the supplied error is an aws Error type +// and if the exception indicates that it is a Terminal exception +// 'Terminal' exception are specified in generator configuration +func (rm *resourceManager) terminalAWSError(err error) bool { + // No terminal_errors specified for this resource in generator config + return false +} diff --git a/pkg/resource/option_group/tags.go b/pkg/resource/option_group/tags.go new file mode 100644 index 0000000..451c9cb --- /dev/null +++ b/pkg/resource/option_group/tags.go @@ -0,0 +1,119 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file 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 option_group + +import ( + "slices" + "strings" + + acktags "github.com/aws-controllers-k8s/runtime/pkg/tags" + + svcapitypes "github.com/aws-controllers-k8s/rds-controller/apis/v1alpha1" +) + +var ( + _ = svcapitypes.OptionGroup{} + _ = acktags.NewTags() +) + +// convertToOrderedACKTags converts the tags parameter into 'acktags.Tags' shape. +// This method helps in creating the hub(acktags.Tags) for merging +// default controller tags with existing resource tags. It also returns a slice +// of keys maintaining the original key Order when the tags are a list +func convertToOrderedACKTags(tags []*svcapitypes.Tag) (acktags.Tags, []string) { + result := acktags.NewTags() + keyOrder := []string{} + + if len(tags) == 0 { + return result, keyOrder + } + for _, t := range tags { + if t.Key != nil { + keyOrder = append(keyOrder, *t.Key) + if t.Value != nil { + result[*t.Key] = *t.Value + } else { + result[*t.Key] = "" + } + } + } + + return result, keyOrder +} + +// fromACKTags converts the tags parameter into []*svcapitypes.Tag shape. +// This method helps in setting the tags back inside AWSResource after merging +// default controller tags with existing resource tags. When a list, +// it maintains the order from original +func fromACKTags(tags acktags.Tags, keyOrder []string) []*svcapitypes.Tag { + result := []*svcapitypes.Tag{} + + for _, k := range keyOrder { + v, ok := tags[k] + if ok { + tag := svcapitypes.Tag{Key: &k, Value: &v} + result = append(result, &tag) + delete(tags, k) + } + } + for k, v := range tags { + tag := svcapitypes.Tag{Key: &k, Value: &v} + result = append(result, &tag) + } + + return result +} + +// ignoreSystemTags ignores tags that have keys that start with "aws:" +// and systemTags defined on startup via the --resource-tags flag, +// to avoid patching them to the resourceSpec. +// Eg. resources created with cloudformation have tags that cannot be +// removed by an ACK controller +func ignoreSystemTags(tags acktags.Tags, systemTags []string) { + for k := range tags { + if strings.HasPrefix(k, "aws:") || + slices.Contains(systemTags, k) { + delete(tags, k) + } + } +} + +// syncAWSTags ensures AWS-managed tags (prefixed with "aws:") from the latest resource state +// are preserved in the desired state. This prevents the controller from attempting to +// modify AWS-managed tags, which would result in an error. +// +// AWS-managed tags are automatically added by AWS services (e.g., CloudFormation, Service Catalog) +// and cannot be modified or deleted through normal tag operations. Common examples include: +// - aws:cloudformation:stack-name +// - aws:servicecatalog:productArn +// +// Parameters: +// - a: The target Tags map to be updated (typically desired state) +// - b: The source Tags map containing AWS-managed tags (typically latest state) +// +// Example: +// +// latest := Tags{"aws:cloudformation:stack-name": "my-stack", "environment": "prod"} +// desired := Tags{"environment": "dev"} +// SyncAWSTags(desired, latest) +// desired now contains {"aws:cloudformation:stack-name": "my-stack", "environment": "dev"} +func syncAWSTags(a acktags.Tags, b acktags.Tags) { + for k := range b { + if strings.HasPrefix(k, "aws:") { + a[k] = b[k] + } + } +} diff --git a/templates/hooks/option_group/delta_pre_compare.go.tpl b/templates/hooks/option_group/delta_pre_compare.go.tpl new file mode 100644 index 0000000..33fb3fe --- /dev/null +++ b/templates/hooks/option_group/delta_pre_compare.go.tpl @@ -0,0 +1 @@ + compareTags(delta, a, b) diff --git a/templates/hooks/option_group/sdk_read_many_post_set_output.go.tpl b/templates/hooks/option_group/sdk_read_many_post_set_output.go.tpl new file mode 100644 index 0000000..cb3a549 --- /dev/null +++ b/templates/hooks/option_group/sdk_read_many_post_set_output.go.tpl @@ -0,0 +1,8 @@ + if ko.Status.ACKResourceMetadata != nil && ko.Status.ACKResourceMetadata.ARN != nil { + resourceARN := (*string)(ko.Status.ACKResourceMetadata.ARN) + tags, err := rm.getTags(ctx, *resourceARN) + if err != nil { + return nil, err + } + ko.Spec.Tags = tags + } diff --git a/templates/hooks/option_group/sdk_update_pre_build_request.go.tpl b/templates/hooks/option_group/sdk_update_pre_build_request.go.tpl new file mode 100644 index 0000000..75c09ac --- /dev/null +++ b/templates/hooks/option_group/sdk_update_pre_build_request.go.tpl @@ -0,0 +1,9 @@ + if delta.DifferentAt("Spec.Tags") { + if err = rm.syncTags(ctx, desired, latest); err != nil { + return nil, err + } + } + + if !delta.DifferentExcept("Spec.Tags") { + return desired, nil + } diff --git a/test/e2e/option_group.py b/test/e2e/option_group.py new file mode 100644 index 0000000..fefe056 --- /dev/null +++ b/test/e2e/option_group.py @@ -0,0 +1,57 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You may +# not use this file except in compliance with the License. A copy of the +# License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file 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. + +"""Utilities for working with OptionGroup resources""" + +import time +from typing import Dict, Optional, List + +import boto3 +import pytest + +def get_client(): + return boto3.client('rds') + +def get(option_group_name: str) -> Optional[Dict]: + c = get_client() + try: + resp = c.describe_option_groups( + OptionGroupName=option_group_name, + ) + if len(resp['OptionGroupsList']) == 0: + return None + return resp['OptionGroupsList'][0] + except c.exceptions.OptionGroupNotFoundFault: + return None + +def get_tags(option_group_arn: str) -> List[Dict]: + c = get_client() + try: + resp = c.list_tags_for_resource( + ResourceName=option_group_arn, + ) + return resp['TagList'] + except c.exceptions.OptionGroupNotFoundFault: + return [] + +def wait_until_deleted(option_group_name: str, max_attempts: int = 60) -> None: + attempt = 0 + while attempt < max_attempts: + if get(option_group_name) is None: + return + attempt += 1 + time.sleep(10) + pytest.fail( + "Timed out waiting for OptionGroup to be " + "deleted in RDS API" + ) diff --git a/test/e2e/resources/option_group.yaml b/test/e2e/resources/option_group.yaml new file mode 100644 index 0000000..a657834 --- /dev/null +++ b/test/e2e/resources/option_group.yaml @@ -0,0 +1,12 @@ +apiVersion: rds.services.k8s.aws/v1alpha1 +kind: OptionGroup +metadata: + name: $OPTION_GROUP_NAME +spec: + engineName: $ENGINE_NAME + majorEngineVersion: "$MAJOR_ENGINE_VERSION" + optionGroupDescription: $OPTION_GROUP_DESCRIPTION + optionGroupName: $OPTION_GROUP_NAME + tags: + - key: environment + value: dev diff --git a/test/e2e/tests/test_option_group.py b/test/e2e/tests/test_option_group.py new file mode 100644 index 0000000..e6c3937 --- /dev/null +++ b/test/e2e/tests/test_option_group.py @@ -0,0 +1,131 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You may +# not use this file except in compliance with the License. A copy of the +# License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file 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. + +"""Integration tests for the RDS API OptionGroup resource +""" + +import logging +import time + +import pytest + +from acktest.k8s import resource as k8s +from acktest.resources import random_suffix_name +from e2e import service_marker, CRD_GROUP, CRD_VERSION, load_rds_resource +from e2e.replacement_values import REPLACEMENT_VALUES +from e2e import option_group +from e2e import tag + +RESOURCE_PLURAL = 'optiongroups' + +CREATE_WAIT_AFTER_SECONDS = 10 +DELETE_WAIT_AFTER_SECONDS = 10 +MODIFY_WAIT_AFTER_SECONDS = 120 + +ENGINE_NAME = "mysql" +MAJOR_ENGINE_VERSION = "8.0" +OPTION_GROUP_DESCRIPTION = "Test option group for ACK integration tests" + + +@pytest.fixture +def option_group_resource(): + resource_name = random_suffix_name("ack-option-group", 24) + + replacements = REPLACEMENT_VALUES.copy() + replacements["OPTION_GROUP_NAME"] = resource_name + replacements["ENGINE_NAME"] = ENGINE_NAME + replacements["MAJOR_ENGINE_VERSION"] = MAJOR_ENGINE_VERSION + replacements["OPTION_GROUP_DESCRIPTION"] = OPTION_GROUP_DESCRIPTION + + resource_data = load_rds_resource( + "option_group", + additional_replacements=replacements, + ) + logging.debug(resource_data) + + # Create the k8s resource + ref = k8s.CustomResourceReference( + CRD_GROUP, CRD_VERSION, RESOURCE_PLURAL, + resource_name, namespace="default", + ) + k8s.create_custom_resource(ref, resource_data) + cr = k8s.wait_resource_consumed_by_controller(ref) + time.sleep(CREATE_WAIT_AFTER_SECONDS) + + assert cr is not None + assert k8s.get_resource_exists(ref) + + yield ref, cr, resource_name + + # Try to delete, if doesn't already exist + try: + _, deleted = k8s.delete_custom_resource(ref, 3, 10) + assert deleted + time.sleep(DELETE_WAIT_AFTER_SECONDS) + except: + pass + + option_group.wait_until_deleted(resource_name) + + +@service_marker +@pytest.mark.canary +class TestOptionGroup: + def test_crud(self, option_group_resource): + ref, cr, resource_name = option_group_resource + + # Let's check that the OptionGroup appears in RDS + latest = option_group.get(resource_name) + assert latest is not None + assert latest['OptionGroupName'] == resource_name + assert latest['EngineName'] == ENGINE_NAME + assert latest['MajorEngineVersion'] == MAJOR_ENGINE_VERSION + assert latest['OptionGroupDescription'] == OPTION_GROUP_DESCRIPTION + + # Test initial tags are created + initial_tags = [ + {"Key": "environment", "Value": "dev"} + ] + + option_group_arn = latest['OptionGroupArn'] + latest_tags = tag.clean(option_group.get_tags(option_group_arn)) + assert initial_tags == latest_tags + + # Update the tags + new_tags = [ + {"key": "environment", "value": "prod"}, + {"key": "application", "value": "testing"} + ] + updates = { + "spec": { + "tags": new_tags + } + } + k8s.patch_custom_resource(ref, updates) + time.sleep(MODIFY_WAIT_AFTER_SECONDS) + assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "True", wait_periods=5) + + # Verify tags were updated + expected_tags = [ + {"Key": "environment", "Value": "prod"}, + {"Key": "application", "Value": "testing"} + ] + latest_tags = tag.clean(option_group.get_tags(option_group_arn)) + assert expected_tags == latest_tags + + # Verify the option group still exists and properties are unchanged + latest = option_group.get(resource_name) + assert latest is not None + assert latest['OptionGroupName'] == resource_name + assert latest['EngineName'] == ENGINE_NAME + assert latest['MajorEngineVersion'] == MAJOR_ENGINE_VERSION