diff --git a/apis/v1alpha1/resource_share_invitation.go b/apis/v1alpha1/resource_share_invitation.go new file mode 100644 index 0000000..df32d0e --- /dev/null +++ b/apis/v1alpha1/resource_share_invitation.go @@ -0,0 +1,98 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package v1alpha1 + +import ( + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ResourceShareAccepterSpec defines the desired state of ResourceShareAccepter. +// +// This resource accepts a RAM resource share invitation. When you create this +// resource, the controller finds the pending invitation for the specified ShareARN +// and accepts it. When you delete this resource, the controller disassociates +// from the resource share (leaves the share). +type ResourceShareAccepterSpec struct { + // The Amazon Resource Name (ARN) of the resource share to accept. + // This is immutable - you cannot change which share you're accepting after creation. + // +kubebuilder:validation:Required + ShareARN *string `json:"shareARN"` +} + +// ResourceShareAccepterStatus defines the observed state of ResourceShareAccepter +type ResourceShareAccepterStatus 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 ARN of the invitation that was accepted. + // +kubebuilder:validation:Optional + InvitationARN *string `json:"invitationARN,omitempty"` + // The current status of the invitation. + // Possible values: PENDING, ACCEPTED, REJECTED, EXPIRED + // +kubebuilder:validation:Optional + InvitationStatus *string `json:"invitationStatus,omitempty"` + // The ID of the AWS account that sent the invitation. + // +kubebuilder:validation:Optional + SenderAccountID *string `json:"senderAccountID,omitempty"` + // The ID of the AWS account that received the invitation. + // +kubebuilder:validation:Optional + ReceiverAccountID *string `json:"receiverAccountID,omitempty"` + // The name of the resource share. + // +kubebuilder:validation:Optional + ShareName *string `json:"shareName,omitempty"` + // The date and time when the invitation was sent. + // +kubebuilder:validation:Optional + InvitationTime *metav1.Time `json:"invitationTime,omitempty"` + // The resources included in the resource share. + // +kubebuilder:validation:Optional + Resources []*string `json:"resources,omitempty"` + // The current status of the resource share. + // +kubebuilder:validation:Optional + ShareStatus *string `json:"shareStatus,omitempty"` +} + +// ResourceShareAccepter is the Schema for the ResourceShareAccepters API +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="SHARE-ARN",type=string,priority=0,JSONPath=`.spec.shareARN` +// +kubebuilder:printcolumn:name="STATUS",type=string,priority=0,JSONPath=`.status.invitationStatus` +// +kubebuilder:printcolumn:name="SENDER",type=string,priority=1,JSONPath=`.status.senderAccountID` +type ResourceShareAccepter struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec ResourceShareAccepterSpec `json:"spec,omitempty"` + Status ResourceShareAccepterStatus `json:"status,omitempty"` +} + +// ResourceShareAccepterList contains a list of ResourceShareAccepter +// +kubebuilder:object:root=true +type ResourceShareAccepterList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ResourceShareAccepter `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ResourceShareAccepter{}, &ResourceShareAccepterList{}) +} + diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index e452406..141876c 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -413,6 +413,161 @@ func (in *ResourceShare) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceShareAccepter) DeepCopyInto(out *ResourceShareAccepter) { + *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 ResourceShareAccepter. +func (in *ResourceShareAccepter) DeepCopy() *ResourceShareAccepter { + if in == nil { + return nil + } + out := new(ResourceShareAccepter) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ResourceShareAccepter) 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 *ResourceShareAccepterList) DeepCopyInto(out *ResourceShareAccepterList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ResourceShareAccepter, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceShareAccepterList. +func (in *ResourceShareAccepterList) DeepCopy() *ResourceShareAccepterList { + if in == nil { + return nil + } + out := new(ResourceShareAccepterList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ResourceShareAccepterList) 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 *ResourceShareAccepterSpec) DeepCopyInto(out *ResourceShareAccepterSpec) { + *out = *in + if in.ShareARN != nil { + in, out := &in.ShareARN, &out.ShareARN + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceShareAccepterSpec. +func (in *ResourceShareAccepterSpec) DeepCopy() *ResourceShareAccepterSpec { + if in == nil { + return nil + } + out := new(ResourceShareAccepterSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceShareAccepterStatus) DeepCopyInto(out *ResourceShareAccepterStatus) { + *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.InvitationARN != nil { + in, out := &in.InvitationARN, &out.InvitationARN + *out = new(string) + **out = **in + } + if in.InvitationStatus != nil { + in, out := &in.InvitationStatus, &out.InvitationStatus + *out = new(string) + **out = **in + } + if in.SenderAccountID != nil { + in, out := &in.SenderAccountID, &out.SenderAccountID + *out = new(string) + **out = **in + } + if in.ReceiverAccountID != nil { + in, out := &in.ReceiverAccountID, &out.ReceiverAccountID + *out = new(string) + **out = **in + } + if in.ShareName != nil { + in, out := &in.ShareName, &out.ShareName + *out = new(string) + **out = **in + } + if in.InvitationTime != nil { + in, out := &in.InvitationTime, &out.InvitationTime + *out = (*in).DeepCopy() + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = make([]*string, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(string) + **out = **in + } + } + } + if in.ShareStatus != nil { + in, out := &in.ShareStatus, &out.ShareStatus + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceShareAccepterStatus. +func (in *ResourceShareAccepterStatus) DeepCopy() *ResourceShareAccepterStatus { + if in == nil { + return nil + } + out := new(ResourceShareAccepterStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ResourceShareAssociation) DeepCopyInto(out *ResourceShareAssociation) { *out = *in diff --git a/cmd/controller/main.go b/cmd/controller/main.go index a5374bb..d9c4c42 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -41,6 +41,7 @@ import ( _ "github.com/aws-controllers-k8s/ram-controller/pkg/resource/permission" _ "github.com/aws-controllers-k8s/ram-controller/pkg/resource/resource_share" + _ "github.com/aws-controllers-k8s/ram-controller/pkg/resource/resource_share_invitation" "github.com/aws-controllers-k8s/ram-controller/pkg/version" ) diff --git a/config/crd/bases/ram.services.k8s.aws_resourceshareaccepters.yaml b/config/crd/bases/ram.services.k8s.aws_resourceshareaccepters.yaml new file mode 100644 index 0000000..ab804e0 --- /dev/null +++ b/config/crd/bases/ram.services.k8s.aws_resourceshareaccepters.yaml @@ -0,0 +1,170 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: resourceshareaccepters.ram.services.k8s.aws +spec: + group: ram.services.k8s.aws + names: + kind: ResourceShareAccepter + listKind: ResourceShareAccepterList + plural: resourceshareaccepters + singular: resourceshareaccepter + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.shareARN + name: SHARE-ARN + type: string + - jsonPath: .status.invitationStatus + name: STATUS + type: string + - jsonPath: .status.senderAccountID + name: SENDER + priority: 1 + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: ResourceShareAccepter is the Schema for the ResourceShareAccepters + 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: |- + ResourceShareAccepterSpec defines the desired state of ResourceShareAccepter. + + This resource accepts a RAM resource share invitation. When you create this + resource, the controller finds the pending invitation for the specified ShareARN + and accepts it. When you delete this resource, the controller disassociates + from the resource share (leaves the share). + properties: + shareARN: + description: |- + The Amazon Resource Name (ARN) of the resource share to accept. + This is immutable - you cannot change which share you're accepting after creation. + type: string + required: + - shareARN + type: object + status: + description: ResourceShareAccepterStatus defines the observed state of + ResourceShareAccepter + 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 + 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 + invitationARN: + description: The ARN of the invitation that was accepted. + type: string + invitationStatus: + description: |- + The current status of the invitation. + Possible values: PENDING, ACCEPTED, REJECTED, EXPIRED + type: string + invitationTime: + description: The date and time when the invitation was sent. + format: date-time + type: string + receiverAccountID: + description: The ID of the AWS account that received the invitation. + type: string + resources: + description: The resources included in the resource share. + items: + type: string + type: array + senderAccountID: + description: The ID of the AWS account that sent the invitation. + type: string + shareName: + description: The name of the resource share. + type: string + shareStatus: + description: The current status of the resource share. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index e363eaa..5318665 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -3,4 +3,5 @@ kind: Kustomization resources: - common - bases/ram.services.k8s.aws_permissions.yaml + - bases/ram.services.k8s.aws_resourceshareaccepters.yaml - bases/ram.services.k8s.aws_resourceshares.yaml diff --git a/helm/crds/ram.services.k8s.aws_resourceshareaccepters.yaml b/helm/crds/ram.services.k8s.aws_resourceshareaccepters.yaml new file mode 100644 index 0000000..ab804e0 --- /dev/null +++ b/helm/crds/ram.services.k8s.aws_resourceshareaccepters.yaml @@ -0,0 +1,170 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: resourceshareaccepters.ram.services.k8s.aws +spec: + group: ram.services.k8s.aws + names: + kind: ResourceShareAccepter + listKind: ResourceShareAccepterList + plural: resourceshareaccepters + singular: resourceshareaccepter + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.shareARN + name: SHARE-ARN + type: string + - jsonPath: .status.invitationStatus + name: STATUS + type: string + - jsonPath: .status.senderAccountID + name: SENDER + priority: 1 + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: ResourceShareAccepter is the Schema for the ResourceShareAccepters + 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: |- + ResourceShareAccepterSpec defines the desired state of ResourceShareAccepter. + + This resource accepts a RAM resource share invitation. When you create this + resource, the controller finds the pending invitation for the specified ShareARN + and accepts it. When you delete this resource, the controller disassociates + from the resource share (leaves the share). + properties: + shareARN: + description: |- + The Amazon Resource Name (ARN) of the resource share to accept. + This is immutable - you cannot change which share you're accepting after creation. + type: string + required: + - shareARN + type: object + status: + description: ResourceShareAccepterStatus defines the observed state of + ResourceShareAccepter + 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 + 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 + invitationARN: + description: The ARN of the invitation that was accepted. + type: string + invitationStatus: + description: |- + The current status of the invitation. + Possible values: PENDING, ACCEPTED, REJECTED, EXPIRED + type: string + invitationTime: + description: The date and time when the invitation was sent. + format: date-time + type: string + receiverAccountID: + description: The ID of the AWS account that received the invitation. + type: string + resources: + description: The resources included in the resource share. + items: + type: string + type: array + senderAccountID: + description: The ID of the AWS account that sent the invitation. + type: string + shareName: + description: The name of the resource share. + type: string + shareStatus: + description: The current status of the resource share. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/helm/values.yaml b/helm/values.yaml index 7f6173c..0258c0f 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -152,6 +152,7 @@ reconcile: resources: - Permission - ResourceShare + - ResourceShareAccepter serviceAccount: # Specifies whether a service account should be created diff --git a/pkg/resource/resource_share_invitation/descriptor.go b/pkg/resource/resource_share_invitation/descriptor.go new file mode 100644 index 0000000..c430782 --- /dev/null +++ b/pkg/resource/resource_share_invitation/descriptor.go @@ -0,0 +1,151 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package resource_share_invitation + +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/ram-controller/apis/v1alpha1" +) + +const ( + FinalizerString = "finalizers.ram.services.k8s.aws/ResourceShareAccepter" +) + +var ( + // GroupKind is the metav1.GroupKind for the ResourceShareAccepter resource + GroupKind = metav1.GroupKind{ + Group: "ram.services.k8s.aws", + Kind: "ResourceShareAccepter", + } +) + +// resourceDescriptor implements the acktypes.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.ResourceShareAccepter{} +} + +// 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.ResourceShareAccepter), + } +} + +// Delta returns an `ackcompare.Delta` object containing the difference between +// one `AWSResource` and another. +func (d *resourceDescriptor) Delta( + a acktypes.AWSResource, + b acktypes.AWSResource, +) *ackcompare.Delta { + return ackcompare.NewDelta() +} + +// 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") + } + 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/resource_share_invitation/hooks.go b/pkg/resource/resource_share_invitation/hooks.go new file mode 100644 index 0000000..d3df7fd --- /dev/null +++ b/pkg/resource/resource_share_invitation/hooks.go @@ -0,0 +1,23 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package resource_share_invitation + +import ( + svcresource "github.com/aws-controllers-k8s/ram-controller/pkg/resource" +) + +func init() { + svcresource.RegisterManagerFactory(newResourceManagerFactory()) +} + diff --git a/pkg/resource/resource_share_invitation/identifiers.go b/pkg/resource/resource_share_invitation/identifiers.go new file mode 100644 index 0000000..5dce175 --- /dev/null +++ b/pkg/resource/resource_share_invitation/identifiers.go @@ -0,0 +1,59 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package resource_share_invitation + +import ( + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" +) + +// resourceIdentifiers implements the AWSResourceIdentifiers interface +type resourceIdentifiers struct { + r *resource +} + +// ARN returns the AWS Resource Name for the resource +// For ResourceShareInvitation, we use the ShareARN as the primary identifier +func (ri *resourceIdentifiers) ARN() *ackv1alpha1.AWSResourceName { + if ri.r.ko.Spec.ShareARN == nil { + return nil + } + arn := ackv1alpha1.AWSResourceName(*ri.r.ko.Spec.ShareARN) + return &arn +} + +// OwnerAccountID returns the AWS account identifier for the owner of the resource +func (ri *resourceIdentifiers) OwnerAccountID() *ackv1alpha1.AWSAccountID { + if ri.r.ko.Status.ACKResourceMetadata == nil { + return nil + } + return ri.r.ko.Status.ACKResourceMetadata.OwnerAccountID +} + +// Region returns the AWS region the resource exists in +func (ri *resourceIdentifiers) Region() *ackv1alpha1.AWSRegion { + if ri.r.ko.Status.ACKResourceMetadata == nil { + return nil + } + return ri.r.ko.Status.ACKResourceMetadata.Region +} + +// NameOrID returns a string that can be used to uniquely identify the resource +// For ResourceShareInvitation, we use the ShareARN +func (ri *resourceIdentifiers) NameOrID() string { + if ri.r.ko.Spec.ShareARN == nil { + return "" + } + return *ri.r.ko.Spec.ShareARN +} + diff --git a/pkg/resource/resource_share_invitation/manager.go b/pkg/resource/resource_share_invitation/manager.go new file mode 100644 index 0000000..922ca1a --- /dev/null +++ b/pkg/resource/resource_share_invitation/manager.go @@ -0,0 +1,263 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package resource_share_invitation + +import ( + "context" + + 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" + acktypes "github.com/aws-controllers-k8s/runtime/pkg/types" + "github.com/aws/aws-sdk-go-v2/aws" + svcsdk "github.com/aws/aws-sdk-go-v2/service/ram" + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// +kubebuilder:rbac:groups=ram.services.k8s.aws,resources=resourceshareaccepters,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=ram.services.k8s.aws,resources=resourceshareaccepters/status,verbs=get;update;patch + +// resourceManager is responsible for providing a consistent way to perform +// CRUD operations in a backend AWS service API for ResourceShareInvitation 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/ram 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 { + 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") + } + latest, err := rm.sdkFind(ctx, r) + if err != nil { + return nil, err + } + return latest, nil +} + +// 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. +func (rm *resourceManager) Update( + ctx context.Context, + desired acktypes.AWSResource, + latest acktypes.AWSResource, + delta *ackcompare.Delta, +) (acktypes.AWSResource, error) { + dres := rm.concreteResource(desired) + lres := rm.concreteResource(latest) + if dres.ko == nil || lres.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, dres, lres, delta) + if err != nil { + return rm.onError(lres, 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 Delete() method received resource with nil CR object") + } + if err := rm.sdkDelete(ctx, r); err != nil { + return rm.onError(r, err) + } + return r, nil +} + + + + +// ARNFromName returns an AWS ARN for a given resource name +func (rm *resourceManager) ARNFromName(name string) string { + return "" +} + +// LateInitialize is a no-op for ResourceShareInvitation +func (rm *resourceManager) LateInitialize( + ctx context.Context, + latest acktypes.AWSResource, +) (acktypes.AWSResource, error) { + return latest, nil +} + +// EnsureTags is a no-op for ResourceShareAccepter (invitations don't have tags) +func (rm *resourceManager) EnsureTags( + ctx context.Context, + res acktypes.AWSResource, + md acktypes.ServiceControllerMetadata, +) error { + return nil +} + +// IsSynced returns true if the resource is synced +func (rm *resourceManager) IsSynced( + ctx context.Context, + res acktypes.AWSResource, +) (bool, error) { + return true, 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, +) (*resource, 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 sets the Synced condition to True and returns the resource +func (rm *resourceManager) onSuccess( + r *resource, +) (*resource, error) { + ackcondition.SetSynced(r, corev1.ConditionTrue, nil, nil) + return r, nil +} + +// newResourceManager returns a new resourceManager instance +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 +} + +// GetRegion returns the AWS Region this resource manager is configured for +func (rm *resourceManager) GetRegion() ackv1alpha1.AWSRegion { + return rm.awsRegion +} + +// ResolveReferences is a no-op for ResourceShareAccepter +func (rm *resourceManager) ResolveReferences( + ctx context.Context, + apiReader client.Reader, + res acktypes.AWSResource, +) (acktypes.AWSResource, bool, error) { + return res, false, nil +} + +// ClearResolvedReferences is a no-op for ResourceShareAccepter +func (rm *resourceManager) ClearResolvedReferences( + res acktypes.AWSResource, +) acktypes.AWSResource { + return res +} + +// FilterSystemTags is a no-op for ResourceShareAccepter (invitations don't have tags) +func (rm *resourceManager) FilterSystemTags(res acktypes.AWSResource, systemTags []string) { + // No-op: ResourceShareAccepter doesn't support tags +} diff --git a/pkg/resource/resource_share_invitation/manager_factory.go b/pkg/resource/resource_share_invitation/manager_factory.go new file mode 100644 index 0000000..a55a907 --- /dev/null +++ b/pkg/resource/resource_share_invitation/manager_factory.go @@ -0,0 +1,92 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package resource_share_invitation + +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" +) + +// resourceManagerFactory produces resourceManager objects. It implements the +// `acktypes.AWSResourceManagerFactory` interface. +type resourceManagerFactory struct { + sync.RWMutex + // rmCache contains resource managers for a particular AWS account ID and region + rmCache map[string]*resourceManager +} + +// ResourceDescriptor returns an AWSResourceDescriptor object that can be used +// by the upstream controller-runtime to introspect the CRs that the resource +// manager will manage as well as produce Kubernetes runtime object prototypes +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) { + 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{}, + } +} + diff --git a/pkg/resource/resource_share_invitation/resource.go b/pkg/resource/resource_share_invitation/resource.go new file mode 100644 index 0000000..c7a5b1f --- /dev/null +++ b/pkg/resource/resource_share_invitation/resource.go @@ -0,0 +1,109 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package resource_share_invitation + +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" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + rtclient "sigs.k8s.io/controller-runtime/pkg/client" + + svcapitypes "github.com/aws-controllers-k8s/ram-controller/apis/v1alpha1" +) + +// resource implements the `acktypes.AWSResource` interface +type resource struct { + // ko represents the Kubernetes object for the ResourceShareAccepter + ko *svcapitypes.ResourceShareAccepter +} + +// 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} +} + +// 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 +} + +// Conditions returns the ACK Conditions collection for the AWSResource +func (r *resource) Conditions() []*ackv1alpha1.Condition { + return r.ko.Status.Conditions +} + +// SetConditions sets the ACK Conditions collection for the AWSResource +func (r *resource) SetConditions(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 +} + +// DeepCopy will return a copy of the resource +func (r *resource) DeepCopy() acktypes.AWSResource { + koCopy := r.ko.DeepCopy() + return &resource{koCopy} +} + +// SetIdentifiers sets the Spec or Status field that is referenced as the unique +// resource identifier +func (r *resource) SetIdentifiers(identifier *ackv1alpha1.AWSIdentifiers) error { + if identifier.NameOrID == "" { + return ackerrors.MissingNameIdentifier + } + r.ko.Spec.ShareARN = &identifier.NameOrID + return nil +} + +// PopulateResourceFromAnnotation populates the fields passed from adoption annotation +func (r *resource) PopulateResourceFromAnnotation(fields map[string]string) error { + shareARN, ok := fields["shareARN"] + if !ok { + return ackerrors.NewTerminalError(fmt.Errorf("required field missing: shareARN")) + } + r.ko.Spec.ShareARN = &shareARN + return nil +} + +// ReplaceConditions replaces the resource's conditions collection entirely +func (r *resource) ReplaceConditions(conditions []*ackv1alpha1.Condition) { + r.ko.Status.Conditions = conditions +} + diff --git a/pkg/resource/resource_share_invitation/sdk.go b/pkg/resource/resource_share_invitation/sdk.go new file mode 100644 index 0000000..10e81fd --- /dev/null +++ b/pkg/resource/resource_share_invitation/sdk.go @@ -0,0 +1,366 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package resource_share_invitation + +import ( + "context" + "errors" + "fmt" + + 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" + "github.com/aws/aws-sdk-go-v2/aws" + svcsdk "github.com/aws/aws-sdk-go-v2/service/ram" + svcsdktypes "github.com/aws/aws-sdk-go-v2/service/ram/types" + smithy "github.com/aws/smithy-go" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + svcapitypes "github.com/aws-controllers-k8s/ram-controller/apis/v1alpha1" +) + +// sdkFind returns SDK-specific information about a supplied resource +// For ResourceShareInvitation, this finds the invitation by ShareARN +func (rm *resourceManager) sdkFind( + ctx context.Context, + r *resource, +) (latest *resource, err error) { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("rm.sdkFind") + defer func() { + exit(err) + }() + + if r.ko.Spec.ShareARN == nil { + return nil, ackerr.NotFound + } + + // Get the invitation for this share ARN + input := &svcsdk.GetResourceShareInvitationsInput{ + ResourceShareArns: []string{*r.ko.Spec.ShareARN}, + } + + var resp *svcsdk.GetResourceShareInvitationsOutput + resp, err = rm.sdkapi.GetResourceShareInvitations(ctx, input) + rm.metrics.RecordAPICall("READ_ONE", "GetResourceShareInvitations", err) + if err != nil { + var awsErr smithy.APIError + if errors.As(err, &awsErr) { + if awsErr.ErrorCode() == "ResourceShareInvitationArnNotFoundException" || + awsErr.ErrorCode() == "UnknownResourceException" { + return nil, ackerr.NotFound + } + } + return nil, err + } + + // Find the accepted invitation for this share + ko := r.ko.DeepCopy() + rm.setStatusDefaults(ko) + found := false + + for _, invitation := range resp.ResourceShareInvitations { + // We're looking for an ACCEPTED invitation for this share + if invitation.Status == svcsdktypes.ResourceShareInvitationStatusAccepted { + found = true + rm.setResourceFromInvitation(ko, &invitation) + break + } + } + + if !found { + return nil, ackerr.NotFound + } + + return &resource{ko}, nil +} + +// sdkCreate creates the supplied resource in the backend AWS service API +// For ResourceShareInvitation, this means finding a pending invitation and accepting it +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) + }() + + if desired.ko.Spec.ShareARN == nil { + return nil, fmt.Errorf("ShareARN is required") + } + + // First, find the pending invitation for this share ARN + getInput := &svcsdk.GetResourceShareInvitationsInput{ + ResourceShareArns: []string{*desired.ko.Spec.ShareARN}, + } + + var getResp *svcsdk.GetResourceShareInvitationsOutput + getResp, err = rm.sdkapi.GetResourceShareInvitations(ctx, getInput) + rm.metrics.RecordAPICall("READ_ONE", "GetResourceShareInvitations", err) + if err != nil { + return nil, err + } + + // Find a pending invitation + var pendingInvitationARN *string + for _, invitation := range getResp.ResourceShareInvitations { + if invitation.Status == svcsdktypes.ResourceShareInvitationStatusPending { + pendingInvitationARN = invitation.ResourceShareInvitationArn + break + } + } + + if pendingInvitationARN == nil { + return nil, ackerr.NewTerminalError(fmt.Errorf("no pending invitation found for share ARN %s. "+ + "NOTE: If both AWS accounts are in the same AWS Organization and RAM Sharing with AWS Organizations is enabled, "+ + "this resource is not necessary", *desired.ko.Spec.ShareARN)) + } + + // Accept the invitation + acceptInput := &svcsdk.AcceptResourceShareInvitationInput{ + ResourceShareInvitationArn: pendingInvitationARN, + } + + var acceptResp *svcsdk.AcceptResourceShareInvitationOutput + acceptResp, err = rm.sdkapi.AcceptResourceShareInvitation(ctx, acceptInput) + rm.metrics.RecordAPICall("CREATE", "AcceptResourceShareInvitation", err) + if err != nil { + return nil, err + } + + ko := desired.ko.DeepCopy() + rm.setStatusDefaults(ko) + rm.setResourceFromInvitation(ko, acceptResp.ResourceShareInvitation) + + return &resource{ko}, nil +} + + + +// sdkUpdate is not supported for ResourceShareAccepter +// Invitations are immutable once accepted +func (rm *resourceManager) sdkUpdate( + ctx context.Context, + desired *resource, + latest *resource, + delta *ackcompare.Delta, +) (updated *resource, err error) { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("rm.sdkUpdate") + defer func() { + exit(err) + }() + + // ResourceShareInvitations cannot be updated + // Return the latest resource as-is + return latest, nil +} + +// sdkDelete deletes the supplied resource in the backend AWS service API +// For ResourceShareInvitation, this means disassociating from the resource share (leaving it) +func (rm *resourceManager) sdkDelete( + ctx context.Context, + r *resource, +) (err error) { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("rm.sdkDelete") + defer func() { + exit(err) + }() + + if r.ko.Spec.ShareARN == nil { + return fmt.Errorf("ShareARN is required for deletion") + } + + if r.ko.Status.ReceiverAccountID == nil { + return fmt.Errorf("ReceiverAccountID is required for deletion") + } + + // Disassociate from the resource share (leave it) + input := &svcsdk.DisassociateResourceShareInput{ + ResourceShareArn: r.ko.Spec.ShareARN, + Principals: []string{*r.ko.Status.ReceiverAccountID}, + } + + _, err = rm.sdkapi.DisassociateResourceShare(ctx, input) + rm.metrics.RecordAPICall("DELETE", "DisassociateResourceShare", err) + if err != nil { + var awsErr smithy.APIError + if errors.As(err, &awsErr) { + // If the resource is already gone, that's fine + if awsErr.ErrorCode() == "UnknownResourceException" { + return nil + } + // If we're not permitted to disassociate, log but don't fail + if awsErr.ErrorCode() == "OperationNotPermittedException" { + rlog.Info("Resource share could not be disassociated, but continuing", "error", err) + return nil + } + } + return err + } + + return nil +} + +// setResourceFromInvitation sets the resource fields from an invitation object +func (rm *resourceManager) setResourceFromInvitation( + ko *svcapitypes.ResourceShareAccepter, + invitation *svcsdktypes.ResourceShareInvitation, +) { + if invitation.ResourceShareInvitationArn != nil { + ko.Status.InvitationARN = invitation.ResourceShareInvitationArn + } + if invitation.Status != "" { + ko.Status.InvitationStatus = aws.String(string(invitation.Status)) + } + if invitation.SenderAccountId != nil { + ko.Status.SenderAccountID = invitation.SenderAccountId + } + if invitation.ReceiverAccountId != nil { + ko.Status.ReceiverAccountID = invitation.ReceiverAccountId + } + if invitation.ResourceShareName != nil { + ko.Status.ShareName = invitation.ResourceShareName + } + if invitation.InvitationTimestamp != nil { + ko.Status.InvitationTime = &metav1.Time{*invitation.InvitationTimestamp} + } + if invitation.ResourceShareArn != nil { + if ko.Status.ACKResourceMetadata == nil { + ko.Status.ACKResourceMetadata = &ackv1alpha1.ResourceMetadata{} + } + tmpARN := ackv1alpha1.AWSResourceName(*invitation.ResourceShareArn) + ko.Status.ACKResourceMetadata.ARN = &tmpARN + } +} + +// setStatusDefaults sets default properties into supplied custom resource +func (rm *resourceManager) setStatusDefaults( + ko *svcapitypes.ResourceShareAccepter, +) { + 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 "MalformedArnException": + return true + default: + return false + } +} diff --git a/test/e2e/README.md b/test/e2e/README.md new file mode 100644 index 0000000..d9ac0ac --- /dev/null +++ b/test/e2e/README.md @@ -0,0 +1,124 @@ +# RAM Controller E2E Tests + +Integration tests for the AWS RAM (Resource Access Manager) ACK controller. + +## Prerequisites + +1. **Kubernetes cluster** with the RAM controller deployed +2. **AWS credentials** configured +3. **Python 3.8+** installed +4. **kubectl** configured to access your cluster + +## Setup + +Install test dependencies: + +```bash +cd test/e2e +pip install -r requirements.txt +``` + +## Running Tests + +### Option 1: With Controller Running in Cluster + +If the RAM controller is already deployed to your cluster: + +```bash +export AWS_PROFILE="your-aws-profile" +pytest tests/ -v +``` + +### Option 2: With Controller Running Locally + +Terminal 1 - Start the controller: +```bash +export AWS_PROFILE="your-aws-profile" +cd /path/to/ram-controller +./bin/controller --aws-region us-east-2 --enable-development-logging +``` + +Terminal 2 - Run tests: +```bash +export AWS_PROFILE="your-aws-profile" +cd test/e2e +pytest tests/ -v +``` + +### Run Specific Tests + +```bash +# Run only ResourceShare tests +pytest tests/test_resource_share.py -v + +# Run only ResourceShareAccepter tests +pytest tests/test_resource_share_accepter.py -v + +# Run only Permission tests +pytest tests/test_permission.py -v + +# Run with specific markers +pytest -m canary -v +``` + +## Test Structure + +- `tests/` - Test files + - `test_resource_share.py` - ResourceShare CRUD tests + - `test_resource_share_accepter.py` - ResourceShareAccepter tests + - `test_permission.py` - Permission tests + +- `resources/` - YAML templates for test resources + - `ram_resource_share.yaml` + - `ram_resource_share_accepter.yaml` + - `ram_permission.yaml` + +- Helper modules: + - `ram_resource_share.py` - ResourceShare utilities + - `ram_resource_share_accepter.py` - ResourceShareAccepter utilities + - `ram_permission.py` - Permission utilities + +## ResourceShareAccepter Tests + +The `test_resource_share_accepter.py` tests validate the ResourceShareAccepter CRD. + +### Current Tests + +- **test_no_pending_invitation**: Tests error handling when no pending invitation exists + +### Future Tests (Require Cross-Account Setup) + +To test actual invitation acceptance, you need: +1. Two AWS accounts (sender and receiver) +2. Sender account creates a ResourceShare +3. Receiver account uses ResourceShareAccepter to accept + +See comments in `test_resource_share_accepter.py` for implementation guidance. + +## Troubleshooting + +### "Controller did not consume the resource" + +**Cause**: The RAM controller is not running or not watching the namespace. + +**Solution**: +- Verify controller is running: `kubectl get pods -n ack-system` +- Check controller logs: `kubectl logs -n ack-system deployment/ack-ram-controller` +- Or run controller locally (see Option 2 above) + +### "Status not populated" + +**Cause**: Controller hasn't reconciled the resource yet. + +**Solution**: Wait longer or check controller logs for errors. + +### AWS Credential Errors + +**Cause**: AWS credentials not configured or expired. + +**Solution**: +```bash +export AWS_PROFILE="your-profile" +aws sts get-caller-identity # Verify credentials work +``` + diff --git a/test/e2e/ram_resource_share_accepter.py b/test/e2e/ram_resource_share_accepter.py new file mode 100644 index 0000000..478f89f --- /dev/null +++ b/test/e2e/ram_resource_share_accepter.py @@ -0,0 +1,74 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You may +# not use this file except in compliance with the License. A copy of the +# License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. + +"""Utilities for working with RAM ResourceShareAccepter resources""" + +import datetime +import time + +import boto3 +import pytest + +DEFAULT_WAIT_TIMEOUT_SECONDS = 120 +DEFAULT_WAIT_INTERVAL_SECONDS = 15 + + +def get_resource_share_invitation(share_arn: str): + """Returns the ResourceShareInvitation for a supplied share ARN. + + If no pending invitation exists, returns None. + """ + c = boto3.client('ram') + try: + resp = c.get_resource_share_invitations( + resourceShareArns=[share_arn] + ) + invitations = resp.get('resourceShareInvitations', []) + # Return the first pending invitation for this share + for invitation in invitations: + if invitation.get('status') == 'PENDING': + return invitation + return None + except Exception as e: + return None + + +def wait_until_invitation_accepted( + share_arn: str, + timeout_seconds: int = DEFAULT_WAIT_TIMEOUT_SECONDS, + interval_seconds: int = DEFAULT_WAIT_INTERVAL_SECONDS, + ) -> None: + """Waits until a ResourceShareInvitation for the supplied share ARN is ACCEPTED. + + Usage: + from e2e.ram_resource_share_accepter import wait_until_invitation_accepted + + wait_until_invitation_accepted(share_arn) + + Raises: + pytest.fail upon timeout + """ + now = datetime.datetime.now() + timeout = now + datetime.timedelta(seconds=timeout_seconds) + + while True: + if datetime.datetime.now() >= timeout: + pytest.fail( + "Timed out waiting for ResourceShareInvitation to be ACCEPTED" + ) + time.sleep(interval_seconds) + + invitation = get_resource_share_invitation(share_arn) + if invitation and invitation.get('status') == 'ACCEPTED': + break + diff --git a/test/e2e/resources/ram_resource_share_accepter.yaml b/test/e2e/resources/ram_resource_share_accepter.yaml new file mode 100644 index 0000000..ba760eb --- /dev/null +++ b/test/e2e/resources/ram_resource_share_accepter.yaml @@ -0,0 +1,7 @@ +apiVersion: ram.services.k8s.aws/v1alpha1 +kind: ResourceShareAccepter +metadata: + name: $RESOURCE_SHARE_ACCEPTER_NAME +spec: + shareARN: $SHARE_ARN + diff --git a/test/e2e/tests/test_resource_share_accepter.py b/test/e2e/tests/test_resource_share_accepter.py new file mode 100644 index 0000000..7606086 --- /dev/null +++ b/test/e2e/tests/test_resource_share_accepter.py @@ -0,0 +1,159 @@ +# 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 ResourceShareAccepter API. + +NOTE: These tests require the RAM controller to be running in the cluster. + The controller must be deployed before running these tests. +""" + +import pytest +import time +import logging + +from acktest.resources import random_suffix_name +from acktest.k8s import resource as k8s +from e2e import service_marker, CRD_GROUP, CRD_VERSION, load_ram_resource +from e2e.replacement_values import REPLACEMENT_VALUES + +RESOURCE_KIND = "ResourceShareAccepter" +RESOURCE_PLURAL = "resourceshareaccepters" + +CREATE_WAIT_AFTER_SECONDS = 15 +DELETE_WAIT_AFTER_SECONDS = 10 + + +@pytest.fixture(scope="module") +def resource_share_accepter_no_invitation(): + """Creates a ResourceShareAccepter for a non-existent share. + + This tests the error handling path when no pending invitation exists. + """ + resource_name = random_suffix_name("share-accepter", 24) + + # Use a properly formatted but non-existent share ARN + # This ARN format is valid but the UUID doesn't exist, so there will be no invitation + fake_share_arn = "arn:aws:ram:us-east-2:065002218531:resource-share/ffffffff-ffff-ffff-ffff-ffffffffffff" + + replacements = REPLACEMENT_VALUES.copy() + replacements["RESOURCE_SHARE_ACCEPTER_NAME"] = resource_name + replacements["SHARE_ARN"] = fake_share_arn + + # Load ResourceShareAccepter CR + resource_data = load_ram_resource( + "ram_resource_share_accepter", + additional_replacements=replacements, + ) + logging.debug(resource_data) + + # Create k8s resource + ref = k8s.CustomResourceReference( + CRD_GROUP, CRD_VERSION, RESOURCE_PLURAL, + resource_name, namespace="default", + ) + k8s.create_custom_resource(ref, resource_data) + cr = k8s.wait_resource_consumed_by_controller(ref) + + yield cr, ref + + # Cleanup + try: + k8s.delete_custom_resource(ref, period_length=DELETE_WAIT_AFTER_SECONDS) + except: + pass + + +@service_marker +@pytest.mark.canary +class TestResourceShareAccepter: + def test_no_pending_invitation(self, resource_share_accepter_no_invitation): + """Test that ResourceShareAccepter handles 'no pending invitation' gracefully. + + This test validates: + - Resource is created successfully + - Controller processes the resource + - Terminal condition is set when no invitation exists + - Spec fields are preserved + + REQUIRES: RAM controller must be running in the cluster + """ + res, ref = resource_share_accepter_no_invitation + + # Verify the controller consumed the resource + assert res is not None, "Controller did not consume the resource. Is the RAM controller running?" + + time.sleep(CREATE_WAIT_AFTER_SECONDS) + + # Get the resource + cr = k8s.get_resource(ref) + assert cr is not None + assert 'spec' in cr + assert 'shareARN' in cr['spec'] + + # Verify spec is preserved + share_arn = cr['spec']['shareARN'] + assert share_arn.startswith("arn:aws:ram:") + + # Verify status exists + assert 'status' in cr, "Status not populated. Controller may not be running or reconciliation failed." + + # Verify conditions exist and contain terminal state + # The controller should set a Terminal condition when no invitation is found + assert 'conditions' in cr['status'] + conditions = cr['status']['conditions'] + assert conditions is not None + assert len(conditions) > 0 + + # Check for Terminal condition + terminal_condition = None + for condition in conditions: + if condition['type'] == 'ACK.Terminal': + terminal_condition = condition + break + + assert terminal_condition is not None, f"No Terminal condition found. Conditions: {conditions}" + assert terminal_condition['status'] == 'True' + # The message should indicate no invitation was found + assert 'invitation' in terminal_condition['message'].lower() or 'not found' in terminal_condition['message'].lower() + + logging.info(f"✓ Terminal condition message: {terminal_condition['message']}") + + # Delete k8s resource + _, deleted = k8s.delete_custom_resource( + ref, + period_length=DELETE_WAIT_AFTER_SECONDS, + ) + assert deleted + + +# NOTE: To test actual invitation acceptance, you need: +# 1. Two AWS accounts (sender and receiver) +# 2. Sender account creates a ResourceShare and shares with receiver account +# 3. Receiver account creates ResourceShareAccepter with the share ARN +# 4. Verify invitation is accepted and status fields are populated +# +# Example test structure (requires cross-account setup): +# +# @pytest.fixture(scope="module") +# def resource_share_accepter_with_invitation(): +# # This would require: +# # - Creating a share in sender account +# # - Getting the share ARN +# # - Creating ResourceShareAccepter in receiver account +# pass +# +# def test_accept_invitation(self, resource_share_accepter_with_invitation): +# # Verify invitation is accepted +# # Verify status fields: invitationARN, invitationStatus, senderAccountID, etc. +# pass +