diff --git a/apis/v1alpha1/ack-generate-metadata.yaml b/apis/v1alpha1/ack-generate-metadata.yaml index e5dbc56..f84f315 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-05-02T16:46:04Z" - build_hash: f8dc5330705b3752ce07dce0ac831161fd4cb14f + build_date: "2025-05-03T16:27:40Z" + build_hash: 0909e7f0adb8ffe4120a8c20d5d58b991f2539e9 go_version: go1.24.2 - version: v0.45.0 -api_directory_checksum: 7e1c19231d3275a1147157f6943a7391953f7001 + version: v0.44.0-3-g0909e7f +api_directory_checksum: d7e5854d868fce7b5e9495117543d7d0b1201112 api_version: v1alpha1 aws_sdk_go_version: v1.32.6 generator_config_info: - file_checksum: 41d31ed2f36d33ae7877a9cd2c85d1e293f37cfe + file_checksum: a1e6585209ae3bff29e73f95edbd3ad234a510b1 original_file_name: generator.yaml last_modification: reason: API generation diff --git a/apis/v1alpha1/generator.yaml b/apis/v1alpha1/generator.yaml index dfcd061..f400dda 100644 --- a/apis/v1alpha1/generator.yaml +++ b/apis/v1alpha1/generator.yaml @@ -10,7 +10,7 @@ ignore: - PolicyVersion #- Role - SAMLProvider - - ServiceLinkedRole + #- ServiceLinkedRole - ServiceSpecificCredential #- User - VirtualMFADevice @@ -342,3 +342,16 @@ resources: Tags: compare: is_ignored: true + ServiceLinkedRole: + tags: + ignore: true + find_operation: + custom_method_name: customGetServiceLinkedRole + update_operation: + custom_method_name: customUpdateServiceLinkedRole + hooks: + sdk_create_post_set_output: + template_path: hooks/service_linked_role/sdk_create_post_set_output.go.tpl + exceptions: + terminal_codes: + - InvalidInput diff --git a/apis/v1alpha1/service_linked_role.go b/apis/v1alpha1/service_linked_role.go new file mode 100644 index 0000000..4f6f030 --- /dev/null +++ b/apis/v1alpha1/service_linked_role.go @@ -0,0 +1,133 @@ +// 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" +) + +// ServiceLinkedRoleSpec defines the desired state of ServiceLinkedRole. +type ServiceLinkedRoleSpec struct { + + // The service principal for the Amazon Web Services service to which this role + // is attached. You use a string similar to a URL but without the http:// in + // front. For example: elasticbeanstalk.amazonaws.com. + // + // Service principals are unique and case-sensitive. To find the exact service + // principal for your service-linked role, see Amazon Web Services services + // that work with IAM (https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_aws-services-that-work-with-iam.html) + // in the IAM User Guide. Look for the services that have Yes in the Service-Linked + // Role column. Choose the Yes link to view the service-linked role documentation + // for that service. + // +kubebuilder:validation:Required + AWSServiceName *string `json:"awsServiceName"` + // A string that you provide, which is combined with the service-provided prefix + // to form the complete role name. If you make multiple requests for the same + // service, then you must supply a different CustomSuffix for each request. + // Otherwise the request fails with a duplicate role name error. For example, + // you could add -1 or -debug to the suffix. + // + // Some services do not support the CustomSuffix parameter. If you provide an + // optional suffix and the operation fails, try the operation again without + // the suffix. + CustomSuffix *string `json:"customSuffix,omitempty"` + // The description of the role. + Description *string `json:"description,omitempty"` +} + +// ServiceLinkedRoleStatus defines the observed state of ServiceLinkedRole +type ServiceLinkedRoleStatus 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"` + // The policy that grants an entity permission to assume the role. + // +kubebuilder:validation:Optional + AssumeRolePolicyDocument *string `json:"assumeRolePolicyDocument,omitempty"` + // The date and time, in ISO 8601 date-time format (http://www.iso.org/iso/iso8601), + // when the role was created. + // +kubebuilder:validation:Optional + CreateDate *metav1.Time `json:"createDate,omitempty"` + // The maximum session duration (in seconds) for the specified role. Anyone + // who uses the CLI, or API to assume the role can specify the duration using + // the optional DurationSeconds API parameter or duration-seconds CLI parameter. + // +kubebuilder:validation:Optional + MaxSessionDuration *int64 `json:"maxSessionDuration,omitempty"` + // The path to the role. For more information about paths, see IAM identifiers + // (https://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html) + // in the IAM User Guide. + // +kubebuilder:validation:Optional + Path *string `json:"path,omitempty"` + // The ARN of the policy used to set the permissions boundary for the role. + // + // For more information about permissions boundaries, see Permissions boundaries + // for IAM identities (https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) + // in the IAM User Guide. + // +kubebuilder:validation:Optional + PermissionsBoundary *AttachedPermissionsBoundary `json:"permissionsBoundary,omitempty"` + // The stable and unique string identifying the role. For more information about + // IDs, see IAM identifiers (https://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html) + // in the IAM User Guide. + // +kubebuilder:validation:Optional + RoleID *string `json:"roleID,omitempty"` + // Contains information about the last time that an IAM role was used. This + // includes the date and time and the Region in which the role was last used. + // Activity is only reported for the trailing 400 days. This period can be shorter + // if your Region began supporting these features within the last year. The + // role might have been used more than 400 days ago. For more information, see + // Regions where data is tracked (https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_access-advisor.html#access-advisor_tracking-period) + // in the IAM user Guide. + // +kubebuilder:validation:Optional + RoleLastUsed *RoleLastUsed `json:"roleLastUsed,omitempty"` + // The friendly name that identifies the role. + // +kubebuilder:validation:Optional + RoleName *string `json:"roleName,omitempty"` + // A list of tags that are attached to the role. For more information about + // tagging, see Tagging IAM resources (https://docs.aws.amazon.com/IAM/latest/UserGuide/id_tags.html) + // in the IAM User Guide. + // +kubebuilder:validation:Optional + Tags []*Tag `json:"tags,omitempty"` +} + +// ServiceLinkedRole is the Schema for the ServiceLinkedRoles API +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type ServiceLinkedRole struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec ServiceLinkedRoleSpec `json:"spec,omitempty"` + Status ServiceLinkedRoleStatus `json:"status,omitempty"` +} + +// ServiceLinkedRoleList contains a list of ServiceLinkedRole +// +kubebuilder:object:root=true +type ServiceLinkedRoleList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ServiceLinkedRole `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ServiceLinkedRole{}, &ServiceLinkedRoleList{}) +} diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index cc49e68..1c68e4b 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -2012,6 +2012,176 @@ func (in *ServiceLastAccessed) DeepCopy() *ServiceLastAccessed { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceLinkedRole) DeepCopyInto(out *ServiceLinkedRole) { + *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 ServiceLinkedRole. +func (in *ServiceLinkedRole) DeepCopy() *ServiceLinkedRole { + if in == nil { + return nil + } + out := new(ServiceLinkedRole) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServiceLinkedRole) 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 *ServiceLinkedRoleList) DeepCopyInto(out *ServiceLinkedRoleList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ServiceLinkedRole, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceLinkedRoleList. +func (in *ServiceLinkedRoleList) DeepCopy() *ServiceLinkedRoleList { + if in == nil { + return nil + } + out := new(ServiceLinkedRoleList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServiceLinkedRoleList) 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 *ServiceLinkedRoleSpec) DeepCopyInto(out *ServiceLinkedRoleSpec) { + *out = *in + if in.AWSServiceName != nil { + in, out := &in.AWSServiceName, &out.AWSServiceName + *out = new(string) + **out = **in + } + if in.CustomSuffix != nil { + in, out := &in.CustomSuffix, &out.CustomSuffix + *out = new(string) + **out = **in + } + if in.Description != nil { + in, out := &in.Description, &out.Description + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceLinkedRoleSpec. +func (in *ServiceLinkedRoleSpec) DeepCopy() *ServiceLinkedRoleSpec { + if in == nil { + return nil + } + out := new(ServiceLinkedRoleSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceLinkedRoleStatus) DeepCopyInto(out *ServiceLinkedRoleStatus) { + *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.AssumeRolePolicyDocument != nil { + in, out := &in.AssumeRolePolicyDocument, &out.AssumeRolePolicyDocument + *out = new(string) + **out = **in + } + if in.CreateDate != nil { + in, out := &in.CreateDate, &out.CreateDate + *out = (*in).DeepCopy() + } + if in.MaxSessionDuration != nil { + in, out := &in.MaxSessionDuration, &out.MaxSessionDuration + *out = new(int64) + **out = **in + } + if in.Path != nil { + in, out := &in.Path, &out.Path + *out = new(string) + **out = **in + } + if in.PermissionsBoundary != nil { + in, out := &in.PermissionsBoundary, &out.PermissionsBoundary + *out = new(AttachedPermissionsBoundary) + (*in).DeepCopyInto(*out) + } + if in.RoleID != nil { + in, out := &in.RoleID, &out.RoleID + *out = new(string) + **out = **in + } + if in.RoleLastUsed != nil { + in, out := &in.RoleLastUsed, &out.RoleLastUsed + *out = new(RoleLastUsed) + (*in).DeepCopyInto(*out) + } + if in.RoleName != nil { + in, out := &in.RoleName, &out.RoleName + *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 ServiceLinkedRoleStatus. +func (in *ServiceLinkedRoleStatus) DeepCopy() *ServiceLinkedRoleStatus { + if in == nil { + return nil + } + out := new(ServiceLinkedRoleStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceSpecificCredential) DeepCopyInto(out *ServiceSpecificCredential) { *out = *in diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 1da02e2..90b7f12 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -44,6 +44,7 @@ import ( _ "github.com/aws-controllers-k8s/iam-controller/pkg/resource/open_id_connect_provider" _ "github.com/aws-controllers-k8s/iam-controller/pkg/resource/policy" _ "github.com/aws-controllers-k8s/iam-controller/pkg/resource/role" + _ "github.com/aws-controllers-k8s/iam-controller/pkg/resource/service_linked_role" _ "github.com/aws-controllers-k8s/iam-controller/pkg/resource/user" "github.com/aws-controllers-k8s/iam-controller/pkg/version" diff --git a/config/controller/kustomization.yaml b/config/controller/kustomization.yaml index ce4bbb0..0143bbb 100644 --- a/config/controller/kustomization.yaml +++ b/config/controller/kustomization.yaml @@ -6,4 +6,4 @@ kind: Kustomization images: - name: controller newName: public.ecr.aws/aws-controllers-k8s/iam-controller - newTag: 1.3.20 + newTag: 0.0.0-non-release-version diff --git a/config/crd/bases/iam.services.k8s.aws_servicelinkedroles.yaml b/config/crd/bases/iam.services.k8s.aws_servicelinkedroles.yaml new file mode 100644 index 0000000..ddca814 --- /dev/null +++ b/config/crd/bases/iam.services.k8s.aws_servicelinkedroles.yaml @@ -0,0 +1,230 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.2 + name: servicelinkedroles.iam.services.k8s.aws +spec: + group: iam.services.k8s.aws + names: + kind: ServiceLinkedRole + listKind: ServiceLinkedRoleList + plural: servicelinkedroles + singular: servicelinkedrole + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ServiceLinkedRole is the Schema for the ServiceLinkedRoles API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ServiceLinkedRoleSpec defines the desired state of ServiceLinkedRole. + properties: + awsServiceName: + description: |- + The service principal for the Amazon Web Services service to which this role + is attached. You use a string similar to a URL but without the http:// in + front. For example: elasticbeanstalk.amazonaws.com. + + Service principals are unique and case-sensitive. To find the exact service + principal for your service-linked role, see Amazon Web Services services + that work with IAM (https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_aws-services-that-work-with-iam.html) + in the IAM User Guide. Look for the services that have Yes in the Service-Linked + Role column. Choose the Yes link to view the service-linked role documentation + for that service. + type: string + customSuffix: + description: |- + A string that you provide, which is combined with the service-provided prefix + to form the complete role name. If you make multiple requests for the same + service, then you must supply a different CustomSuffix for each request. + Otherwise the request fails with a duplicate role name error. For example, + you could add -1 or -debug to the suffix. + + Some services do not support the CustomSuffix parameter. If you provide an + optional suffix and the operation fails, try the operation again without + the suffix. + type: string + description: + description: The description of the role. + type: string + required: + - awsServiceName + type: object + status: + description: ServiceLinkedRoleStatus defines the observed state of ServiceLinkedRole + 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 + assumeRolePolicyDocument: + description: The policy that grants an entity permission to assume + the role. + type: string + 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 + createDate: + description: |- + The date and time, in ISO 8601 date-time format (http://www.iso.org/iso/iso8601), + when the role was created. + format: date-time + type: string + maxSessionDuration: + description: |- + The maximum session duration (in seconds) for the specified role. Anyone + who uses the CLI, or API to assume the role can specify the duration using + the optional DurationSeconds API parameter or duration-seconds CLI parameter. + format: int64 + type: integer + path: + description: |- + The path to the role. For more information about paths, see IAM identifiers + (https://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html) + in the IAM User Guide. + type: string + permissionsBoundary: + description: |- + The ARN of the policy used to set the permissions boundary for the role. + + For more information about permissions boundaries, see Permissions boundaries + for IAM identities (https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) + in the IAM User Guide. + properties: + permissionsBoundaryARN: + description: |- + The Amazon Resource Name (ARN). ARNs are unique identifiers for Amazon Web + Services resources. + + For more information about ARNs, go to Amazon Resource Names (ARNs) (https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) + in the Amazon Web Services General Reference. + type: string + permissionsBoundaryType: + type: string + type: object + roleID: + description: |- + The stable and unique string identifying the role. For more information about + IDs, see IAM identifiers (https://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html) + in the IAM User Guide. + type: string + roleLastUsed: + description: |- + Contains information about the last time that an IAM role was used. This + includes the date and time and the Region in which the role was last used. + Activity is only reported for the trailing 400 days. This period can be shorter + if your Region began supporting these features within the last year. The + role might have been used more than 400 days ago. For more information, see + Regions where data is tracked (https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_access-advisor.html#access-advisor_tracking-period) + in the IAM user Guide. + properties: + lastUsedDate: + format: date-time + type: string + region: + type: string + type: object + roleName: + description: The friendly name that identifies the role. + type: string + tags: + description: |- + A list of tags that are attached to the role. For more information about + tagging, see Tagging IAM resources (https://docs.aws.amazon.com/IAM/latest/UserGuide/id_tags.html) + in the IAM User Guide. + items: + description: |- + A structure that represents user-provided metadata that can be associated + with an IAM resource. For more information about tagging, see Tagging IAM + resources (https://docs.aws.amazon.com/IAM/latest/UserGuide/id_tags.html) + in the IAM User Guide. + properties: + key: + type: string + value: + type: string + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 45da769..8804161 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -7,4 +7,5 @@ resources: - bases/iam.services.k8s.aws_openidconnectproviders.yaml - bases/iam.services.k8s.aws_policies.yaml - bases/iam.services.k8s.aws_roles.yaml + - bases/iam.services.k8s.aws_servicelinkedroles.yaml - bases/iam.services.k8s.aws_users.yaml diff --git a/config/rbac/cluster-role-controller.yaml b/config/rbac/cluster-role-controller.yaml index c5c7c3a..8344b9b 100644 --- a/config/rbac/cluster-role-controller.yaml +++ b/config/rbac/cluster-role-controller.yaml @@ -30,6 +30,7 @@ rules: - openidconnectproviders - policies - roles + - servicelinkedroles - users verbs: - create @@ -47,6 +48,7 @@ rules: - openidconnectproviders/status - policies/status - roles/status + - servicelinkedroles/status - users/status verbs: - get diff --git a/config/rbac/role-reader.yaml b/config/rbac/role-reader.yaml index 2c1f73b..d68acf3 100644 --- a/config/rbac/role-reader.yaml +++ b/config/rbac/role-reader.yaml @@ -14,6 +14,7 @@ rules: - openidconnectproviders - policies - roles + - servicelinkedroles - users verbs: - get diff --git a/config/rbac/role-writer.yaml b/config/rbac/role-writer.yaml index 6afd19e..463fae0 100644 --- a/config/rbac/role-writer.yaml +++ b/config/rbac/role-writer.yaml @@ -14,6 +14,7 @@ rules: - openidconnectproviders - policies - roles + - servicelinkedroles - users verbs: - create @@ -31,6 +32,7 @@ rules: - openidconnectproviders - policies - roles + - servicelinkedroles - users verbs: - get diff --git a/generator.yaml b/generator.yaml index dfcd061..f400dda 100644 --- a/generator.yaml +++ b/generator.yaml @@ -10,7 +10,7 @@ ignore: - PolicyVersion #- Role - SAMLProvider - - ServiceLinkedRole + #- ServiceLinkedRole - ServiceSpecificCredential #- User - VirtualMFADevice @@ -342,3 +342,16 @@ resources: Tags: compare: is_ignored: true + ServiceLinkedRole: + tags: + ignore: true + find_operation: + custom_method_name: customGetServiceLinkedRole + update_operation: + custom_method_name: customUpdateServiceLinkedRole + hooks: + sdk_create_post_set_output: + template_path: hooks/service_linked_role/sdk_create_post_set_output.go.tpl + exceptions: + terminal_codes: + - InvalidInput diff --git a/helm/Chart.yaml b/helm/Chart.yaml index 9c8e6a5..ddba43e 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v1 name: iam-chart description: A Helm chart for the ACK service controller for AWS Identity & Access Management (IAM) -version: 1.3.20 -appVersion: 1.3.20 +version: 0.0.0-non-release-version +appVersion: 0.0.0-non-release-version home: https://github.com/aws-controllers-k8s/iam-controller icon: https://raw.githubusercontent.com/aws/eks-charts/master/docs/logo/aws.png sources: diff --git a/helm/crds/iam.services.k8s.aws_servicelinkedroles.yaml b/helm/crds/iam.services.k8s.aws_servicelinkedroles.yaml new file mode 100644 index 0000000..ddca814 --- /dev/null +++ b/helm/crds/iam.services.k8s.aws_servicelinkedroles.yaml @@ -0,0 +1,230 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.2 + name: servicelinkedroles.iam.services.k8s.aws +spec: + group: iam.services.k8s.aws + names: + kind: ServiceLinkedRole + listKind: ServiceLinkedRoleList + plural: servicelinkedroles + singular: servicelinkedrole + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ServiceLinkedRole is the Schema for the ServiceLinkedRoles API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ServiceLinkedRoleSpec defines the desired state of ServiceLinkedRole. + properties: + awsServiceName: + description: |- + The service principal for the Amazon Web Services service to which this role + is attached. You use a string similar to a URL but without the http:// in + front. For example: elasticbeanstalk.amazonaws.com. + + Service principals are unique and case-sensitive. To find the exact service + principal for your service-linked role, see Amazon Web Services services + that work with IAM (https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_aws-services-that-work-with-iam.html) + in the IAM User Guide. Look for the services that have Yes in the Service-Linked + Role column. Choose the Yes link to view the service-linked role documentation + for that service. + type: string + customSuffix: + description: |- + A string that you provide, which is combined with the service-provided prefix + to form the complete role name. If you make multiple requests for the same + service, then you must supply a different CustomSuffix for each request. + Otherwise the request fails with a duplicate role name error. For example, + you could add -1 or -debug to the suffix. + + Some services do not support the CustomSuffix parameter. If you provide an + optional suffix and the operation fails, try the operation again without + the suffix. + type: string + description: + description: The description of the role. + type: string + required: + - awsServiceName + type: object + status: + description: ServiceLinkedRoleStatus defines the observed state of ServiceLinkedRole + 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 + assumeRolePolicyDocument: + description: The policy that grants an entity permission to assume + the role. + type: string + 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 + createDate: + description: |- + The date and time, in ISO 8601 date-time format (http://www.iso.org/iso/iso8601), + when the role was created. + format: date-time + type: string + maxSessionDuration: + description: |- + The maximum session duration (in seconds) for the specified role. Anyone + who uses the CLI, or API to assume the role can specify the duration using + the optional DurationSeconds API parameter or duration-seconds CLI parameter. + format: int64 + type: integer + path: + description: |- + The path to the role. For more information about paths, see IAM identifiers + (https://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html) + in the IAM User Guide. + type: string + permissionsBoundary: + description: |- + The ARN of the policy used to set the permissions boundary for the role. + + For more information about permissions boundaries, see Permissions boundaries + for IAM identities (https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) + in the IAM User Guide. + properties: + permissionsBoundaryARN: + description: |- + The Amazon Resource Name (ARN). ARNs are unique identifiers for Amazon Web + Services resources. + + For more information about ARNs, go to Amazon Resource Names (ARNs) (https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) + in the Amazon Web Services General Reference. + type: string + permissionsBoundaryType: + type: string + type: object + roleID: + description: |- + The stable and unique string identifying the role. For more information about + IDs, see IAM identifiers (https://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html) + in the IAM User Guide. + type: string + roleLastUsed: + description: |- + Contains information about the last time that an IAM role was used. This + includes the date and time and the Region in which the role was last used. + Activity is only reported for the trailing 400 days. This period can be shorter + if your Region began supporting these features within the last year. The + role might have been used more than 400 days ago. For more information, see + Regions where data is tracked (https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_access-advisor.html#access-advisor_tracking-period) + in the IAM user Guide. + properties: + lastUsedDate: + format: date-time + type: string + region: + type: string + type: object + roleName: + description: The friendly name that identifies the role. + type: string + tags: + description: |- + A list of tags that are attached to the role. For more information about + tagging, see Tagging IAM resources (https://docs.aws.amazon.com/IAM/latest/UserGuide/id_tags.html) + in the IAM User Guide. + items: + description: |- + A structure that represents user-provided metadata that can be associated + with an IAM resource. For more information about tagging, see Tagging IAM + resources (https://docs.aws.amazon.com/IAM/latest/UserGuide/id_tags.html) + in the IAM User Guide. + properties: + key: + type: string + value: + type: string + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/helm/templates/NOTES.txt b/helm/templates/NOTES.txt index ffc020d..0a9df91 100644 --- a/helm/templates/NOTES.txt +++ b/helm/templates/NOTES.txt @@ -1,5 +1,5 @@ {{ .Chart.Name }} has been installed. -This chart deploys "public.ecr.aws/aws-controllers-k8s/iam-controller:1.3.20". +This chart deploys "public.ecr.aws/aws-controllers-k8s/iam-controller:0.0.0-non-release-version". Check its status by running: kubectl --namespace {{ .Release.Namespace }} get pods -l "app.kubernetes.io/instance={{ .Release.Name }}" diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl index ac5c848..a9f7d06 100644 --- a/helm/templates/_helpers.tpl +++ b/helm/templates/_helpers.tpl @@ -77,6 +77,7 @@ rules: - openidconnectproviders - policies - roles + - servicelinkedroles - users verbs: - create @@ -94,6 +95,7 @@ rules: - openidconnectproviders/status - policies/status - roles/status + - servicelinkedroles/status - users/status verbs: - get diff --git a/helm/templates/role-reader.yaml b/helm/templates/role-reader.yaml index 788762a..019b971 100644 --- a/helm/templates/role-reader.yaml +++ b/helm/templates/role-reader.yaml @@ -14,6 +14,7 @@ rules: - openidconnectproviders - policies - roles + - servicelinkedroles - users verbs: - get diff --git a/helm/templates/role-writer.yaml b/helm/templates/role-writer.yaml index 6a72a9a..7f5e2c0 100644 --- a/helm/templates/role-writer.yaml +++ b/helm/templates/role-writer.yaml @@ -14,6 +14,7 @@ rules: - openidconnectproviders - policies - roles + - servicelinkedroles - users verbs: - create @@ -31,6 +32,7 @@ rules: - openidconnectproviders - policies - roles + - servicelinkedroles - users verbs: - get diff --git a/helm/values.yaml b/helm/values.yaml index d78f524..eb172d3 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -4,7 +4,7 @@ image: repository: public.ecr.aws/aws-controllers-k8s/iam-controller - tag: 1.3.20 + tag: 0.0.0-non-release-version pullPolicy: IfNotPresent pullSecrets: [] diff --git a/pkg/resource/group/resource.go b/pkg/resource/group/resource.go index e1b899b..927bdeb 100644 --- a/pkg/resource/group/resource.go +++ b/pkg/resource/group/resource.go @@ -16,8 +16,6 @@ package 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" @@ -99,7 +97,7 @@ func (r *resource) SetIdentifiers(identifier *ackv1alpha1.AWSIdentifiers) error func (r *resource) PopulateResourceFromAnnotation(fields map[string]string) error { tmp, ok := fields["name"] if !ok { - return ackerrors.NewTerminalError(fmt.Errorf("required field missing: name")) + return ackerrors.MissingNameIdentifier } r.ko.Spec.Name = &tmp diff --git a/pkg/resource/instance_profile/resource.go b/pkg/resource/instance_profile/resource.go index 3b868b0..f53a9e4 100644 --- a/pkg/resource/instance_profile/resource.go +++ b/pkg/resource/instance_profile/resource.go @@ -16,8 +16,6 @@ package instance_profile 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" @@ -99,7 +97,7 @@ func (r *resource) SetIdentifiers(identifier *ackv1alpha1.AWSIdentifiers) error func (r *resource) PopulateResourceFromAnnotation(fields map[string]string) error { tmp, ok := fields["name"] if !ok { - return ackerrors.NewTerminalError(fmt.Errorf("required field missing: name")) + return ackerrors.MissingNameIdentifier } r.ko.Spec.Name = &tmp diff --git a/pkg/resource/open_id_connect_provider/resource.go b/pkg/resource/open_id_connect_provider/resource.go index cdd3966..8d9e1c5 100644 --- a/pkg/resource/open_id_connect_provider/resource.go +++ b/pkg/resource/open_id_connect_provider/resource.go @@ -16,8 +16,6 @@ package open_id_connect_provider 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" @@ -99,7 +97,7 @@ func (r *resource) SetIdentifiers(identifier *ackv1alpha1.AWSIdentifiers) error func (r *resource) PopulateResourceFromAnnotation(fields map[string]string) error { tmp, ok := fields["arn"] if !ok { - return ackerrors.NewTerminalError(fmt.Errorf("required field missing: arn")) + return ackerrors.MissingNameIdentifier } if r.ko.Status.ACKResourceMetadata == nil { diff --git a/pkg/resource/policy/resource.go b/pkg/resource/policy/resource.go index 287c0d2..ea92706 100644 --- a/pkg/resource/policy/resource.go +++ b/pkg/resource/policy/resource.go @@ -16,8 +16,6 @@ package policy 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" @@ -99,7 +97,7 @@ func (r *resource) SetIdentifiers(identifier *ackv1alpha1.AWSIdentifiers) error func (r *resource) PopulateResourceFromAnnotation(fields map[string]string) error { tmp, ok := fields["arn"] if !ok { - return ackerrors.NewTerminalError(fmt.Errorf("required field missing: arn")) + return ackerrors.MissingNameIdentifier } if r.ko.Status.ACKResourceMetadata == nil { diff --git a/pkg/resource/role/resource.go b/pkg/resource/role/resource.go index 5c11630..2c02e95 100644 --- a/pkg/resource/role/resource.go +++ b/pkg/resource/role/resource.go @@ -16,8 +16,6 @@ package role 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" @@ -99,7 +97,7 @@ func (r *resource) SetIdentifiers(identifier *ackv1alpha1.AWSIdentifiers) error func (r *resource) PopulateResourceFromAnnotation(fields map[string]string) error { tmp, ok := fields["name"] if !ok { - return ackerrors.NewTerminalError(fmt.Errorf("required field missing: name")) + return ackerrors.MissingNameIdentifier } r.ko.Spec.Name = &tmp diff --git a/pkg/resource/service_linked_role/delta.go b/pkg/resource/service_linked_role/delta.go new file mode 100644 index 0000000..19875e3 --- /dev/null +++ b/pkg/resource/service_linked_role/delta.go @@ -0,0 +1,69 @@ +// 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 service_linked_role + +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 + } + + if ackcompare.HasNilDifference(a.ko.Spec.AWSServiceName, b.ko.Spec.AWSServiceName) { + delta.Add("Spec.AWSServiceName", a.ko.Spec.AWSServiceName, b.ko.Spec.AWSServiceName) + } else if a.ko.Spec.AWSServiceName != nil && b.ko.Spec.AWSServiceName != nil { + if *a.ko.Spec.AWSServiceName != *b.ko.Spec.AWSServiceName { + delta.Add("Spec.AWSServiceName", a.ko.Spec.AWSServiceName, b.ko.Spec.AWSServiceName) + } + } + if ackcompare.HasNilDifference(a.ko.Spec.CustomSuffix, b.ko.Spec.CustomSuffix) { + delta.Add("Spec.CustomSuffix", a.ko.Spec.CustomSuffix, b.ko.Spec.CustomSuffix) + } else if a.ko.Spec.CustomSuffix != nil && b.ko.Spec.CustomSuffix != nil { + if *a.ko.Spec.CustomSuffix != *b.ko.Spec.CustomSuffix { + delta.Add("Spec.CustomSuffix", a.ko.Spec.CustomSuffix, b.ko.Spec.CustomSuffix) + } + } + if ackcompare.HasNilDifference(a.ko.Spec.Description, b.ko.Spec.Description) { + delta.Add("Spec.Description", a.ko.Spec.Description, b.ko.Spec.Description) + } else if a.ko.Spec.Description != nil && b.ko.Spec.Description != nil { + if *a.ko.Spec.Description != *b.ko.Spec.Description { + delta.Add("Spec.Description", a.ko.Spec.Description, b.ko.Spec.Description) + } + } + + return delta +} diff --git a/pkg/resource/service_linked_role/descriptor.go b/pkg/resource/service_linked_role/descriptor.go new file mode 100644 index 0000000..0d39594 --- /dev/null +++ b/pkg/resource/service_linked_role/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 service_linked_role + +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/iam-controller/apis/v1alpha1" +) + +const ( + FinalizerString = "finalizers.iam.services.k8s.aws/ServiceLinkedRole" +) + +var ( + GroupVersionResource = svcapitypes.GroupVersion.WithResource("servicelinkedroles") + GroupKind = metav1.GroupKind{ + Group: "iam.services.k8s.aws", + Kind: "ServiceLinkedRole", + } +) + +// 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.ServiceLinkedRole{} +} + +// 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.ServiceLinkedRole), + } +} + +// 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/service_linked_role/hooks.go b/pkg/resource/service_linked_role/hooks.go new file mode 100644 index 0000000..fd2b7f8 --- /dev/null +++ b/pkg/resource/service_linked_role/hooks.go @@ -0,0 +1,180 @@ +package service_linked_role + +import ( + "context" + "errors" + + svcapitypes "github.com/aws-controllers-k8s/iam-controller/apis/v1alpha1" + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" + ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare" + ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors" + ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log" + svcsdk "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/aws/smithy-go" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (rm *resourceManager) customGetServiceLinkedRole( + ctx context.Context, + r *resource, +) (latest *resource, err error) { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("rm.customGetServiceLinkedRole") + 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.requiredFieldsMissingFromReadOneInput(r) { + return nil, ackerr.NotFound + } + + input, err := rm.newDescribeRequestPayload(r) + if err != nil { + return nil, err + } + + var resp *svcsdk.GetRoleOutput + resp, err = rm.sdkapi.GetRole(ctx, input) + rm.metrics.RecordAPICall("READ_ONE", "GetRole", err) + if err != nil { + var awsErr smithy.APIError + if errors.As(err, &awsErr) && awsErr.ErrorCode() == "NoSuchEntity" { + 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() + + if ko.Status.ACKResourceMetadata == nil { + ko.Status.ACKResourceMetadata = &ackv1alpha1.ResourceMetadata{} + } + if resp.Role.Arn != nil { + arn := ackv1alpha1.AWSResourceName(*resp.Role.Arn) + ko.Status.ACKResourceMetadata.ARN = &arn + } + if resp.Role.CreateDate != nil { + ko.Status.CreateDate = &metav1.Time{*resp.Role.CreateDate} + } else { + ko.Status.CreateDate = nil + } + if resp.Role.Description != nil { + ko.Spec.Description = resp.Role.Description + } else { + ko.Spec.Description = nil + } + if resp.Role.MaxSessionDuration != nil { + maxSessionDurationCopy := int64(*resp.Role.MaxSessionDuration) + ko.Status.MaxSessionDuration = &maxSessionDurationCopy + } else { + ko.Status.MaxSessionDuration = nil + } + if resp.Role.RoleId != nil { + ko.Status.RoleID = resp.Role.RoleId + } else { + ko.Status.RoleID = nil + } + if resp.Role.RoleLastUsed != nil { + f8 := &svcapitypes.RoleLastUsed{} + if resp.Role.RoleLastUsed.LastUsedDate != nil { + f8.LastUsedDate = &metav1.Time{*resp.Role.RoleLastUsed.LastUsedDate} + } + if resp.Role.RoleLastUsed.Region != nil { + f8.Region = resp.Role.RoleLastUsed.Region + } + ko.Status.RoleLastUsed = f8 + } else { + ko.Status.RoleLastUsed = nil + } + if resp.Role.RoleName != nil { + ko.Status.RoleName = resp.Role.RoleName + } else { + ko.Status.RoleName = nil + } + + rm.setStatusDefaults(ko) + + return &resource{ko}, nil +} + +// newDescribeRequestPayload returns SDK-specific struct for the HTTP request +// payload of the Describe API call for the resource +func (rm *resourceManager) newDescribeRequestPayload( + r *resource, +) (*svcsdk.GetRoleInput, error) { + res := &svcsdk.GetRoleInput{} + + if r.ko.Spec.AWSServiceName != nil { + res.RoleName = r.ko.Status.RoleName + } + + return res, nil +} + +// requiredFieldsMissingFromReadOneInput returns true if there are any fields +// for the ReadOne Input shape that are required but not present in the +// resource's Spec or Status +func (rm *resourceManager) requiredFieldsMissingFromReadOneInput( + r *resource, +) bool { + // for service linked roles we get the roleName from the status + return r.ko.Status.RoleName == nil + +} + +// sdkUpdate patches the supplied resource in the backend AWS service API and +// returns a new resource with updated fields. +func (rm *resourceManager) customUpdateServiceLinkedRole( + ctx context.Context, + desired *resource, + latest *resource, + delta *ackcompare.Delta, +) (updated *resource, err error) { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("rm.customUpdateServiceLinkedRole") + defer func() { + exit(err) + }() + + input, err := rm.newUpdateRequestPayload(ctx, desired, delta) + if err != nil { + return nil, err + } + + var resp *svcsdk.UpdateRoleOutput + _ = resp + resp, err = rm.sdkapi.UpdateRole(ctx, input) + rm.metrics.RecordAPICall("UPDATE", "UpdateRole", 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() + + 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.UpdateRoleInput, error) { + res := &svcsdk.UpdateRoleInput{} + + res.RoleName = r.ko.Status.RoleName + + if r.ko.Spec.Description != nil { + res.Description = r.ko.Spec.Description + } + + return res, nil +} diff --git a/pkg/resource/service_linked_role/identifiers.go b/pkg/resource/service_linked_role/identifiers.go new file mode 100644 index 0000000..0fe47c5 --- /dev/null +++ b/pkg/resource/service_linked_role/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 service_linked_role + +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/service_linked_role/manager.go b/pkg/resource/service_linked_role/manager.go new file mode 100644 index 0000000..ec8cff7 --- /dev/null +++ b/pkg/resource/service_linked_role/manager.go @@ -0,0 +1,376 @@ +// 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 service_linked_role + +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/iam" + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + + svcapitypes "github.com/aws-controllers-k8s/iam-controller/apis/v1alpha1" +) + +var ( + _ = ackutil.InStrings + _ = acktags.NewTags() + _ = ackrt.MissingImageTagValue + _ = svcapitypes.ServiceLinkedRole{} +) + +// +kubebuilder:rbac:groups=iam.services.k8s.aws,resources=servicelinkedroles,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=iam.services.k8s.aws,resources=servicelinkedroles/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:iam:%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 { + + return nil +} + +// FilterAWSTags ignores tags that have keys that start with "aws:" +// is needed to ensure the controller does not attempt to remove +// tags set by AWS. This function needs to be called after each Read +// operation. +// Eg. resources created with cloudformation have tags that cannot be +// removed by an ACK controller +func (rm *resourceManager) FilterSystemTags(res acktypes.AWSResource) { + +} + +// 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) { + +} + +// 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/service_linked_role/manager_factory.go b/pkg/resource/service_linked_role/manager_factory.go new file mode 100644 index 0000000..0296e75 --- /dev/null +++ b/pkg/resource/service_linked_role/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 service_linked_role + +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/iam-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/service_linked_role/references.go b/pkg/resource/service_linked_role/references.go new file mode 100644 index 0000000..6503b09 --- /dev/null +++ b/pkg/resource/service_linked_role/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 service_linked_role + +import ( + "context" + + "sigs.k8s.io/controller-runtime/pkg/client" + + acktypes "github.com/aws-controllers-k8s/runtime/pkg/types" + + svcapitypes "github.com/aws-controllers-k8s/iam-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.ServiceLinkedRole) error { + return nil +} diff --git a/pkg/resource/service_linked_role/resource.go b/pkg/resource/service_linked_role/resource.go new file mode 100644 index 0000000..839c944 --- /dev/null +++ b/pkg/resource/service_linked_role/resource.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 service_linked_role + +import ( + 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" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + rtclient "sigs.k8s.io/controller-runtime/pkg/client" + + svcapitypes "github.com/aws-controllers-k8s/iam-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.ServiceLinkedRole +} + +// 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 { + return nil +} + +// PopulateResourceFromAnnotation populates the fields passed from adoption annotation +func (r *resource) PopulateResourceFromAnnotation(fields map[string]string) error { + 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/service_linked_role/sdk.go b/pkg/resource/service_linked_role/sdk.go new file mode 100644 index 0000000..c1eb7f2 --- /dev/null +++ b/pkg/resource/service_linked_role/sdk.go @@ -0,0 +1,361 @@ +// 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 service_linked_role + +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/iam" + 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/iam-controller/apis/v1alpha1" +) + +// Hack to avoid import errors during build... +var ( + _ = &metav1.Time{} + _ = strings.ToLower("") + _ = &svcsdk.Client{} + _ = &svcapitypes.ServiceLinkedRole{} + _ = 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, +) (*resource, error) { + return rm.customGetServiceLinkedRole(ctx, r) +} + +// 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.CreateServiceLinkedRoleOutput + _ = resp + resp, err = rm.sdkapi.CreateServiceLinkedRole(ctx, input) + rm.metrics.RecordAPICall("CREATE", "CreateServiceLinkedRole", 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 ko.Status.ACKResourceMetadata == nil { + ko.Status.ACKResourceMetadata = &ackv1alpha1.ResourceMetadata{} + } + if resp.Role.Arn != nil { + arn := ackv1alpha1.AWSResourceName(*resp.Role.Arn) + ko.Status.ACKResourceMetadata.ARN = &arn + } + if resp.Role.AssumeRolePolicyDocument != nil { + ko.Status.AssumeRolePolicyDocument = resp.Role.AssumeRolePolicyDocument + } else { + ko.Status.AssumeRolePolicyDocument = nil + } + if resp.Role.CreateDate != nil { + ko.Status.CreateDate = &metav1.Time{*resp.Role.CreateDate} + } else { + ko.Status.CreateDate = nil + } + if resp.Role.Description != nil { + ko.Spec.Description = resp.Role.Description + } else { + ko.Spec.Description = nil + } + if resp.Role.MaxSessionDuration != nil { + maxSessionDurationCopy := int64(*resp.Role.MaxSessionDuration) + ko.Status.MaxSessionDuration = &maxSessionDurationCopy + } else { + ko.Status.MaxSessionDuration = nil + } + if resp.Role.Path != nil { + ko.Status.Path = resp.Role.Path + } else { + ko.Status.Path = nil + } + if resp.Role.PermissionsBoundary != nil { + f6 := &svcapitypes.AttachedPermissionsBoundary{} + if resp.Role.PermissionsBoundary.PermissionsBoundaryArn != nil { + f6.PermissionsBoundaryARN = resp.Role.PermissionsBoundary.PermissionsBoundaryArn + } + if resp.Role.PermissionsBoundary.PermissionsBoundaryType != "" { + f6.PermissionsBoundaryType = aws.String(string(resp.Role.PermissionsBoundary.PermissionsBoundaryType)) + } + ko.Status.PermissionsBoundary = f6 + } else { + ko.Status.PermissionsBoundary = nil + } + if resp.Role.RoleId != nil { + ko.Status.RoleID = resp.Role.RoleId + } else { + ko.Status.RoleID = nil + } + if resp.Role.RoleLastUsed != nil { + f8 := &svcapitypes.RoleLastUsed{} + if resp.Role.RoleLastUsed.LastUsedDate != nil { + f8.LastUsedDate = &metav1.Time{*resp.Role.RoleLastUsed.LastUsedDate} + } + if resp.Role.RoleLastUsed.Region != nil { + f8.Region = resp.Role.RoleLastUsed.Region + } + ko.Status.RoleLastUsed = f8 + } else { + ko.Status.RoleLastUsed = nil + } + if resp.Role.RoleName != nil { + ko.Status.RoleName = resp.Role.RoleName + } else { + ko.Status.RoleName = nil + } + if resp.Role.Tags != nil { + f10 := []*svcapitypes.Tag{} + for _, f10iter := range resp.Role.Tags { + f10elem := &svcapitypes.Tag{} + if f10iter.Key != nil { + f10elem.Key = f10iter.Key + } + if f10iter.Value != nil { + f10elem.Value = f10iter.Value + } + f10 = append(f10, f10elem) + } + ko.Status.Tags = f10 + } else { + ko.Status.Tags = nil + } + + rm.setStatusDefaults(ko) + ackcondition.SetSynced(&resource{ko}, corev1.ConditionFalse, nil, nil) + + 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.CreateServiceLinkedRoleInput, error) { + res := &svcsdk.CreateServiceLinkedRoleInput{} + + if r.ko.Spec.AWSServiceName != nil { + res.AWSServiceName = r.ko.Spec.AWSServiceName + } + if r.ko.Spec.CustomSuffix != nil { + res.CustomSuffix = r.ko.Spec.CustomSuffix + } + if r.ko.Spec.Description != nil { + res.Description = r.ko.Spec.Description + } + + 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, +) (*resource, error) { + return rm.customUpdateServiceLinkedRole(ctx, desired, latest, delta) +} + +// 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.DeleteServiceLinkedRoleOutput + _ = resp + resp, err = rm.sdkapi.DeleteServiceLinkedRole(ctx, input) + rm.metrics.RecordAPICall("DELETE", "DeleteServiceLinkedRole", 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.DeleteServiceLinkedRoleInput, error) { + res := &svcsdk.DeleteServiceLinkedRoleInput{} + + if r.ko.Status.RoleName != nil { + res.RoleName = r.ko.Status.RoleName + } + + return res, nil +} + +// setStatusDefaults sets default properties into supplied custom resource +func (rm *resourceManager) setStatusDefaults( + ko *svcapitypes.ServiceLinkedRole, +) { + 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 { + if err == nil { + return false + } + + var terminalErr smithy.APIError + if !errors.As(err, &terminalErr) { + return false + } + switch terminalErr.ErrorCode() { + case "InvalidInput": + return true + default: + return false + } +} diff --git a/pkg/resource/user/resource.go b/pkg/resource/user/resource.go index dc87122..9874d54 100644 --- a/pkg/resource/user/resource.go +++ b/pkg/resource/user/resource.go @@ -16,8 +16,6 @@ package user 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" @@ -99,7 +97,7 @@ func (r *resource) SetIdentifiers(identifier *ackv1alpha1.AWSIdentifiers) error func (r *resource) PopulateResourceFromAnnotation(fields map[string]string) error { tmp, ok := fields["name"] if !ok { - return ackerrors.NewTerminalError(fmt.Errorf("required field missing: name")) + return ackerrors.MissingNameIdentifier } r.ko.Spec.Name = &tmp diff --git a/templates/hooks/service_linked_role/sdk_create_post_set_output.go.tpl b/templates/hooks/service_linked_role/sdk_create_post_set_output.go.tpl new file mode 100644 index 0000000..c7d9b17 --- /dev/null +++ b/templates/hooks/service_linked_role/sdk_create_post_set_output.go.tpl @@ -0,0 +1 @@ + ackcondition.SetSynced(&resource{ko}, corev1.ConditionFalse, nil, nil) diff --git a/test/e2e/common/types.py b/test/e2e/common/types.py index 2c7d2e6..db7144f 100644 --- a/test/e2e/common/types.py +++ b/test/e2e/common/types.py @@ -2,3 +2,4 @@ ROLE_RESOURCE_PLURAL = 'roles' POLICY_RESOURCE_PLURAL = 'policies' USER_RESOURCE_PLURAL = 'users' +SERVICE_LINKED_ROLE_RESOURCE_PLURAL = 'servicelinkedroles' diff --git a/test/e2e/resources/service_linked_role.yaml b/test/e2e/resources/service_linked_role.yaml new file mode 100644 index 0000000..2de3ad7 --- /dev/null +++ b/test/e2e/resources/service_linked_role.yaml @@ -0,0 +1,7 @@ +apiVersion: iam.services.k8s.aws/v1alpha1 +kind: ServiceLinkedRole +metadata: + name: $ROLE_NAME +spec: + description: $ROLE_DESCRIPTION + awsServiceName: $AWS_SERVICE_NAME diff --git a/test/e2e/tests/test_service_linked_role.py b/test/e2e/tests/test_service_linked_role.py new file mode 100644 index 0000000..0c086e7 --- /dev/null +++ b/test/e2e/tests/test_service_linked_role.py @@ -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. + +"""Integration tests for the IAM Service-Linked Role resource""" + +import time +import pytest +from acktest.k8s import condition +from acktest.k8s import resource as k8s +from acktest.resources import random_suffix_name +from e2e import service_marker, CRD_GROUP, CRD_VERSION, load_resource +from e2e.common.types import SERVICE_LINKED_ROLE_RESOURCE_PLURAL +from e2e.replacement_values import REPLACEMENT_VALUES +from e2e import role + +DELETE_WAIT_AFTER_SECONDS = 10 +CHECK_STATUS_WAIT_SECONDS = 10 +ELASTICBEANSTALK_SERVICE_LINKED_ROLE_NAME="AWSServiceRoleForElasticBeanstalk" + +@pytest.fixture(scope="module") +def service_linked_role(): + role_name = random_suffix_name("my-simple-role", 24) + + replacements = REPLACEMENT_VALUES.copy() + replacements["ROLE_NAME"] = role_name + replacements['ROLE_DESCRIPTION'] = "a service-linked role" + replacements['AWS_SERVICE_NAME'] = "elasticbeanstalk.amazonaws.com" + + resource_data = load_resource( + "service_linked_role", + additional_replacements=replacements, + ) + + service_linked_role=ELASTICBEANSTALK_SERVICE_LINKED_ROLE_NAME + + ref = k8s.CustomResourceReference( + CRD_GROUP, CRD_VERSION, SERVICE_LINKED_ROLE_RESOURCE_PLURAL, + role_name, namespace="default", + ) + k8s.create_custom_resource(ref, resource_data) + cr = k8s.wait_resource_consumed_by_controller(ref) + + role.wait_until_exists(service_linked_role) + + assert cr is not None + assert k8s.get_resource_exists(ref) + + yield (ref, cr) + + _, deleted = k8s.delete_custom_resource( + ref, + period_length=DELETE_WAIT_AFTER_SECONDS, + ) + assert deleted + + role.wait_until_deleted(service_linked_role) + +@service_marker +@pytest.mark.canary +class TestServiceLinkedRole: + def test_crud(self, service_linked_role): + ref, res = service_linked_role + role_name = ELASTICBEANSTALK_SERVICE_LINKED_ROLE_NAME + + time.sleep(CHECK_STATUS_WAIT_SECONDS) + + condition.assert_synced(ref) + + # Validate the service-linked role exists + latest = role.get(role_name) + + assert latest is not None + print(latest) + assert latest['Description'] == "a service-linked role" + assert latest['Path'] == "/aws-service-role/elasticbeanstalk.amazonaws.com/" + + # Update the service-linked role (if applicable) + updates = { + "spec": { + "description": "an updated service-linked role", + }, + } + k8s.patch_custom_resource(ref, updates) + time.sleep(CHECK_STATUS_WAIT_SECONDS) + + condition.assert_synced(ref) + + latest = role.get(role_name) + assert latest is not None + assert latest["Description"] == "an updated service-linked role" \ No newline at end of file