From 01ef21eeb42e3ccd69048f4afddec0920577e244 Mon Sep 17 00:00:00 2001 From: "Kirill Sushkov (teeverr)" Date: Thu, 28 Sep 2023 15:58:05 +0200 Subject: [PATCH] move everything to setup.go, delete redundant clinet methods, add cr.Status.AtProvider.LastProvisioningParameters --- apis/servicecatalog/generator-config.yaml | 4 + .../v1alpha1/zz_generated.deepcopy.go | 11 + .../v1alpha1/zz_provisioned_product.go | 2 + ...aws.crossplane.io_provisionedproducts.yaml | 9 + pkg/clients/servicecatalog/servicecatalog.go | 14 - .../provisionedproduct/lifecycle.go | 179 ---------- .../provisionedproduct/setup.go | 311 +++++++++++++++--- .../provisionedproduct/utils.go | 87 ----- 8 files changed, 285 insertions(+), 332 deletions(-) delete mode 100644 pkg/controller/servicecatalog/provisionedproduct/lifecycle.go delete mode 100644 pkg/controller/servicecatalog/provisionedproduct/utils.go diff --git a/apis/servicecatalog/generator-config.yaml b/apis/servicecatalog/generator-config.yaml index f3660c9814..c889753ca3 100644 --- a/apis/servicecatalog/generator-config.yaml +++ b/apis/servicecatalog/generator-config.yaml @@ -77,6 +77,10 @@ resources: from: operation: DescribeProvisionedProduct path: ProvisionedProductDetail.ProvisioningArtifactId + LastProvisioningParameters: + is_read_only: true + custom_field: + list_of: ProvisioningParameter LaunchRoleArn: is_read_only: true from: diff --git a/apis/servicecatalog/v1alpha1/zz_generated.deepcopy.go b/apis/servicecatalog/v1alpha1/zz_generated.deepcopy.go index 23d673f727..546ae1f472 100644 --- a/apis/servicecatalog/v1alpha1/zz_generated.deepcopy.go +++ b/apis/servicecatalog/v1alpha1/zz_generated.deepcopy.go @@ -560,6 +560,17 @@ func (in *ProvisionedProductObservation) DeepCopyInto(out *ProvisionedProductObs *out = new(string) **out = **in } + if in.LastProvisioningParameters != nil { + in, out := &in.LastProvisioningParameters, &out.LastProvisioningParameters + *out = make([]*ProvisioningParameter, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(ProvisioningParameter) + (*in).DeepCopyInto(*out) + } + } + } if in.LastProvisioningRecordID != nil { in, out := &in.LastProvisioningRecordID, &out.LastProvisioningRecordID *out = new(string) diff --git a/apis/servicecatalog/v1alpha1/zz_provisioned_product.go b/apis/servicecatalog/v1alpha1/zz_provisioned_product.go index 4af2272098..0fed0ad1e7 100644 --- a/apis/servicecatalog/v1alpha1/zz_provisioned_product.go +++ b/apis/servicecatalog/v1alpha1/zz_provisioned_product.go @@ -91,6 +91,8 @@ type ProvisionedProductObservation struct { LastProductID *string `json:"lastProductID,omitempty"` // The identifier of the provisioning artifact. For example, pa-4abcdjnxjj6ne. LastProvisioningArtifactID *string `json:"lastProvisioningArtifactID,omitempty"` + + LastProvisioningParameters []*ProvisioningParameter `json:"lastProvisioningParameters,omitempty"` // The record identifier of the last request performed on this provisioned product // of the following types: // diff --git a/package/crds/servicecatalog.aws.crossplane.io_provisionedproducts.yaml b/package/crds/servicecatalog.aws.crossplane.io_provisionedproducts.yaml index 22d0f624bc..24b6d5ec71 100644 --- a/package/crds/servicecatalog.aws.crossplane.io_provisionedproducts.yaml +++ b/package/crds/servicecatalog.aws.crossplane.io_provisionedproducts.yaml @@ -386,6 +386,15 @@ spec: description: The identifier of the provisioning artifact. For example, pa-4abcdjnxjj6ne. type: string + lastProvisioningParameters: + items: + properties: + key: + type: string + value: + type: string + type: object + type: array lastProvisioningRecordID: description: "The record identifier of the last request performed on this provisioned product of the following types: \n * ProvisionedProduct diff --git a/pkg/clients/servicecatalog/servicecatalog.go b/pkg/clients/servicecatalog/servicecatalog.go index e62802b351..ff4913b3be 100644 --- a/pkg/clients/servicecatalog/servicecatalog.go +++ b/pkg/clients/servicecatalog/servicecatalog.go @@ -22,7 +22,6 @@ import ( cfsdkv2 "github.com/aws/aws-sdk-go-v2/service/cloudformation" cfsdkv2types "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" - "github.com/aws/aws-sdk-go/aws/request" svcsdk "github.com/aws/aws-sdk-go/service/servicecatalog" svcsdkapi "github.com/aws/aws-sdk-go/service/servicecatalog/servicecatalogiface" ) @@ -38,8 +37,6 @@ type Client interface { GetProvisionedProductOutputs(*svcsdk.GetProvisionedProductOutputsInput) (*svcsdk.GetProvisionedProductOutputsOutput, error) DescribeRecord(*svcsdk.DescribeRecordInput) (*svcsdk.DescribeRecordOutput, error) DescribeProduct(*svcsdk.DescribeProductInput) (*svcsdk.DescribeProductOutput, error) - DescribeProvisioningArtifact(*svcsdk.DescribeProvisioningArtifactInput) (*svcsdk.DescribeProvisioningArtifactOutput, error) - UpdateProvisionedProductWithContext(context.Context, *svcsdk.UpdateProvisionedProductInput, ...request.Option) (*svcsdk.UpdateProvisionedProductOutput, error) } // CustomServiceCatalogClient is the implementation of a Client @@ -79,17 +76,6 @@ func (c *CustomServiceCatalogClient) DescribeRecord(describeRecordInput *svcsdk. return describeRecordOutput, err } -// DescribeProvisioningArtifact is wrapped (*ServiceCatalog) DescribeProvisioningArtifact from github.com/aws/aws-sdk-go/service/servicecatalog -func (c *CustomServiceCatalogClient) DescribeProvisioningArtifact(input *svcsdk.DescribeProvisioningArtifactInput) (*svcsdk.DescribeProvisioningArtifactOutput, error) { - output, err := c.Client.DescribeProvisioningArtifact(input) - return output, err -} - -// UpdateProvisionedProductWithContext is wrapped (*ServiceCatalog) UpdateProvisionedProductWithContext from github.com/aws/aws-sdk-go/service/servicecatalog -func (c *CustomServiceCatalogClient) UpdateProvisionedProductWithContext(ctx context.Context, in *svcsdk.UpdateProvisionedProductInput, opts ...request.Option) (*svcsdk.UpdateProvisionedProductOutput, error) { - return c.Client.UpdateProvisionedProductWithContext(ctx, in, opts...) -} - // DescribeProduct is wrapped (*ServiceCatalog) DescribeProduct from github.com/aws/aws-sdk-go/service/servicecatalog func (c *CustomServiceCatalogClient) DescribeProduct(input *svcsdk.DescribeProductInput) (*svcsdk.DescribeProductOutput, error) { return c.Client.DescribeProduct(input) diff --git a/pkg/controller/servicecatalog/provisionedproduct/lifecycle.go b/pkg/controller/servicecatalog/provisionedproduct/lifecycle.go deleted file mode 100644 index f0639f8f75..0000000000 --- a/pkg/controller/servicecatalog/provisionedproduct/lifecycle.go +++ /dev/null @@ -1,179 +0,0 @@ -package provisionedproduct - -import ( - "context" - - "github.com/aws/aws-sdk-go/aws/awserr" - svcsdk "github.com/aws/aws-sdk-go/service/servicecatalog" - xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" - "github.com/crossplane/crossplane-runtime/pkg/meta" - "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" - "github.com/pkg/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/pointer" - - svcapitypes "github.com/crossplane-contrib/provider-aws/apis/servicecatalog/v1alpha1" - aws "github.com/crossplane-contrib/provider-aws/pkg/clients" - awsclient "github.com/crossplane-contrib/provider-aws/pkg/clients" -) - -const ( - msgProvisionedProductStatusSdkTainted = "provisioned product has status TAINTED" - msgProvisionedProductStatusSdkUnderChange = "provisioned product is updating, availability depends on product" - msgProvisionedProductStatusSdkPlanInProgress = "provisioned product is awaiting plan approval" - msgProvisionedProductStatusSdkError = "provisioned product has status ERROR" - - errUpdatePending = "provisioned product is already under change, not updating" - errCouldNotGetProvisionedProductOutputs = "could not get provisioned product outputs" - errCouldNotGetCFParameters = "could not get cloudformation stack parameters" - errCouldNotDescribeRecord = "could not describe record" -) - -func (c *custom) lateInitialize(spec *svcapitypes.ProvisionedProductParameters, _ *svcsdk.DescribeProvisionedProductOutput) error { - acceptLanguageEnglish := acceptLanguageEnglish - spec.AcceptLanguage = awsclient.LateInitializeStringPtr(spec.AcceptLanguage, &acceptLanguageEnglish) - return nil -} - -func (c *custom) isUpToDate(_ context.Context, ds *svcapitypes.ProvisionedProduct, resp *svcsdk.DescribeProvisionedProductOutput) (bool, string, error) { - // If the product is undergoing change, we want to assume that it is not up-to-date. This will force this resource - // to be queued for an update (which will be skipped due to UNDER_CHANGE), and once that update fails, we will - // recheck the status again. This will allow us to quickly transition from UNDER_CHANGE to AVAILABLE without having - // to wait for the entire polling interval to pass before re-checking the status. - if pointer.StringDeref(ds.Status.AtProvider.Status, "") == string(svcapitypes.ProvisionedProductStatus_SDK_UNDER_CHANGE) { - return true, "", nil - } - - getPPOutputInput := &svcsdk.GetProvisionedProductOutputsInput{ProvisionedProductId: resp.ProvisionedProductDetail.Id} - getPPOutput, err := c.client.GetProvisionedProductOutputs(getPPOutputInput) - if err != nil { - // We want to specifically handle this exception, since it will occur when something - // is wrong with the provisioned product (error on creation, tainted, etc) - // We will be able to handle those specific cases in postObserve - var aerr awserr.Error - if ok := errors.As(err, &aerr); ok { - if aerr.Code() == svcsdk.ErrCodeInvalidParametersException && aerr.Message() == "Last Successful Provisioning Record doesn't exist." { - return false, "", nil - } - } - return false, "", errors.Wrap(err, errCouldNotGetProvisionedProductOutputs) - } - c.cache.getProvisionedProductOutputs = getPPOutput.Outputs - cfStackParameters, err := c.client.GetCloudformationStackParameters(getPPOutput.Outputs) - if err != nil { - return false, "", errors.Wrap(err, errCouldNotGetCFParameters) - } - - productOrArtifactAreChanged, err := c.productOrArtifactAreChanged(&ds.Spec.ForProvider, resp.ProvisionedProductDetail) - if err != nil { - return false, "", errors.Wrap(err, "could not discover if product or artifact ids have changed") - } - - if productOrArtifactAreChanged || provisioningParamsAreChanged(cfStackParameters, ds.Spec.ForProvider.ProvisioningParameters) { - return false, "", nil - } - return true, "", nil -} - -func (c *custom) postObserve(_ context.Context, cr *svcapitypes.ProvisionedProduct, resp *svcsdk.DescribeProvisionedProductOutput, obs managed.ExternalObservation, err error) (managed.ExternalObservation, error) { - if err != nil { - return managed.ExternalObservation{}, err - } - meta.SetExternalName(cr, *cr.Spec.ForProvider.Name) - - describeRecordInput := svcsdk.DescribeRecordInput{Id: resp.ProvisionedProductDetail.LastRecordId} - describeRecordOutput, err := c.client.DescribeRecord(&describeRecordInput) - if err != nil { - return managed.ExternalObservation{}, errors.Wrap(err, errCouldNotDescribeRecord) - } - - setConditions(describeRecordOutput, resp, cr) - - var outputs = make(map[string]*svcapitypes.RecordOutput) - for _, v := range c.cache.getProvisionedProductOutputs { - outputs[*v.OutputKey] = &svcapitypes.RecordOutput{ - Description: v.Description, - OutputValue: v.OutputValue} - } - - cr.Status.AtProvider.Outputs = outputs - cr.Status.AtProvider.ARN = resp.ProvisionedProductDetail.Arn - cr.Status.AtProvider.CreatedTime = &metav1.Time{Time: *resp.ProvisionedProductDetail.CreatedTime} - cr.Status.AtProvider.LastProvisioningRecordID = resp.ProvisionedProductDetail.LastProvisioningRecordId - cr.Status.AtProvider.LaunchRoleARN = resp.ProvisionedProductDetail.LaunchRoleArn - cr.Status.AtProvider.Status = resp.ProvisionedProductDetail.Status - cr.Status.AtProvider.StatusMessage = resp.ProvisionedProductDetail.StatusMessage - cr.Status.AtProvider.ProvisionedProductType = resp.ProvisionedProductDetail.Type - cr.Status.AtProvider.RecordType = describeRecordOutput.RecordDetail.RecordType - cr.Status.AtProvider.LastPathID = describeRecordOutput.RecordDetail.PathId - cr.Status.AtProvider.LastProductID = describeRecordOutput.RecordDetail.ProductId - cr.Status.AtProvider.LastProvisioningArtifactID = describeRecordOutput.RecordDetail.ProvisioningArtifactId - - return obs, nil -} - -func (c *custom) preCreate(_ context.Context, cr *svcapitypes.ProvisionedProduct, obj *svcsdk.ProvisionProductInput) error { - obj.ProvisionToken = aws.String(genIdempotencyToken()) - - // We want to specifically set this to match the forProvider name, as that - // is what we use to track the provisioned product - if cr.Spec.ForProvider.Name != nil { - meta.SetExternalName(cr, *cr.Spec.ForProvider.Name) - } - - return nil -} - -func (c *custom) postCreate(_ context.Context, cr *svcapitypes.ProvisionedProduct, obj *svcsdk.ProvisionProductOutput, cre managed.ExternalCreation, err error) (managed.ExternalCreation, error) { - if err != nil { - return cre, err - } - - // We are expected to set the external-name annotation upon creation since - // it can differ from the metadata.name for the ProvisionedProduct - if obj.RecordDetail != nil && obj.RecordDetail.ProvisionedProductName != nil { - meta.SetExternalName(cr, *obj.RecordDetail.ProvisionedProductName) - } - - return cre, nil -} - -func (c *custom) preUpdate(_ context.Context, cr *svcapitypes.ProvisionedProduct, input *svcsdk.UpdateProvisionedProductInput) error { - if pointer.StringDeref(cr.Status.AtProvider.Status, "") == string(svcapitypes.ProvisionedProductStatus_SDK_UNDER_CHANGE) { - return errors.New(errUpdatePending) - } - input.UpdateToken = aws.String(genIdempotencyToken()) - input.ProvisionedProductName = aws.String(meta.GetExternalName(cr)) - return nil -} - -func (c *custom) preDelete(_ context.Context, cr *svcapitypes.ProvisionedProduct, obj *svcsdk.TerminateProvisionedProductInput) (bool, error) { - if pointer.StringDeref(cr.Status.AtProvider.Status, "") == string(svcapitypes.ProvisionedProductStatus_SDK_UNDER_CHANGE) { - return true, nil - } - obj.TerminateToken = aws.String(genIdempotencyToken()) - obj.ProvisionedProductName = aws.String(meta.GetExternalName(cr)) - return false, nil -} - -func setConditions(describeRecordOutput *svcsdk.DescribeRecordOutput, resp *svcsdk.DescribeProvisionedProductOutput, cr *svcapitypes.ProvisionedProduct) { - var recordType string = pointer.StringDeref(describeRecordOutput.RecordDetail.RecordType, "UPDATE_PROVISIONED_PRODUCT") - - ppStatus := aws.StringValue(resp.ProvisionedProductDetail.Status) - switch { - case ppStatus == string(svcapitypes.ProvisionedProductStatus_SDK_AVAILABLE): - cr.SetConditions(xpv1.Available()) - case ppStatus == string(svcapitypes.ProvisionedProductStatus_SDK_UNDER_CHANGE) && recordType == "PROVISION_PRODUCT": - cr.SetConditions(xpv1.Creating()) - case ppStatus == string(svcapitypes.ProvisionedProductStatus_SDK_UNDER_CHANGE) && recordType == "UPDATE_PROVISIONED_PRODUCT": - cr.SetConditions(xpv1.Available().WithMessage(msgProvisionedProductStatusSdkUnderChange)) - case ppStatus == string(svcapitypes.ProvisionedProductStatus_SDK_UNDER_CHANGE) && recordType == "TERMINATE_PROVISIONED_PRODUCT": - cr.SetConditions(xpv1.Deleting()) - case ppStatus == string(svcapitypes.ProvisionedProductStatus_SDK_PLAN_IN_PROGRESS): - cr.SetConditions(xpv1.Unavailable().WithMessage(msgProvisionedProductStatusSdkPlanInProgress)) - case ppStatus == string(svcapitypes.ProvisionedProductStatus_SDK_ERROR): - cr.SetConditions(xpv1.Unavailable().WithMessage(msgProvisionedProductStatusSdkError)) - case ppStatus == string(svcapitypes.ProvisionedProductStatus_SDK_TAINTED): - cr.SetConditions(xpv1.Unavailable().WithMessage(msgProvisionedProductStatusSdkTainted)) - } -} diff --git a/pkg/controller/servicecatalog/provisionedproduct/setup.go b/pkg/controller/servicecatalog/provisionedproduct/setup.go index 965863cd0c..8a502cf913 100644 --- a/pkg/controller/servicecatalog/provisionedproduct/setup.go +++ b/pkg/controller/servicecatalog/provisionedproduct/setup.go @@ -18,7 +18,21 @@ package provisionedproduct import ( "context" + "fmt" + "strings" + cfsdkv2types "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" + "github.com/google/uuid" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + awsclient "github.com/crossplane-contrib/provider-aws/pkg/clients" + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/pointer" + + awsconfig "github.com/aws/aws-sdk-go-v2/config" cfsdkv2 "github.com/aws/aws-sdk-go-v2/service/cloudformation" svcsdk "github.com/aws/aws-sdk-go/service/servicecatalog" "github.com/crossplane/crossplane-runtime/pkg/connection" @@ -28,45 +42,45 @@ import ( "github.com/crossplane/crossplane-runtime/pkg/resource" "github.com/pkg/errors" ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" svcapitypes "github.com/crossplane-contrib/provider-aws/apis/servicecatalog/v1alpha1" "github.com/crossplane-contrib/provider-aws/apis/v1alpha1" - awsclient "github.com/crossplane-contrib/provider-aws/pkg/clients" clientset "github.com/crossplane-contrib/provider-aws/pkg/clients/servicecatalog" "github.com/crossplane-contrib/provider-aws/pkg/features" ) const ( acceptLanguageEnglish = "en" -) -type customConnector struct { - kube client.Client -} - -type custom struct { - *external - - client clientset.Client - cache cache -} + msgProvisionedProductStatusSdkTainted = "provisioned product has status TAINTED" + msgProvisionedProductStatusSdkUnderChange = "provisioned product is updating, availability depends on product" + msgProvisionedProductStatusSdkPlanInProgress = "provisioned product is awaiting plan approval" + msgProvisionedProductStatusSdkError = "provisioned product has status ERROR" -type cache struct { - getProvisionedProductOutputs []*svcsdk.RecordOutput -} + errUpdatePending = "provisioned product is already under change, not updating" + errCouldNotGetProvisionedProductOutputs = "could not get provisioned product outputs" + errCouldNotGetCFParameters = "could not get cloudformation stack parameters" + errCouldNotDescribeRecord = "could not describe record" + errCouldNotLookupProduct = "could not lookup product" + errAwsAPICodeInvalidParametersException = "Last Successful Provisioning Record doesn't exist." +) // SetupProvisionedProduct adds a controller that reconciles a ProvisionedProduct func SetupProvisionedProduct(mgr ctrl.Manager, o controller.Options) error { name := managed.ControllerName(svcapitypes.ProvisionedProductKind) - + awsCfg, err := awsconfig.LoadDefaultConfig(context.TODO()) + if err != nil { + return err + } + cfClient := cfsdkv2.NewFromConfig(awsCfg) + opts := []option{prepareSetupExternal(cfClient)} cps := []managed.ConnectionPublisher{managed.NewAPISecretPublisher(mgr.GetClient(), mgr.GetScheme())} if o.Features.Enabled(features.EnableAlphaExternalSecretStores) { cps = append(cps, connection.NewDetailsManager(mgr.GetClient(), v1alpha1.StoreConfigGroupVersionKind)) } reconcilerOpts := []managed.ReconcilerOption{ - managed.WithExternalConnecter(&customConnector{kube: mgr.GetClient()}), + managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), opts: opts}), managed.WithPollInterval(o.PollInterval), managed.WithLogger(o.Logger.WithValues("controller", name)), managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))), @@ -87,52 +101,245 @@ func SetupProvisionedProduct(mgr ctrl.Manager, o controller.Options) error { reconcilerOpts...)) } -func (c *customConnector) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) { - cr, ok := mg.(*svcapitypes.ProvisionedProduct) - if !ok { - return nil, errors.New(errUnexpectedObject) +func prepareSetupExternal(cfClient *cfsdkv2.Client) func(*external) { + return func(e *external) { + c := &custom{client: &clientset.CustomServiceCatalogClient{CfClient: cfClient, Client: e.client}} + e.isUpToDate = c.isUpToDate + e.lateInitialize = c.lateInitialize + e.postObserve = c.postObserve + e.preUpdate = c.preUpdate + e.preCreate = preCreate + e.postCreate = c.postCreate + e.preDelete = preDelete } - sess, err := awsclient.GetConfigV1(ctx, c.kube, mg, cr.Spec.ForProvider.Region) +} + +type custom struct { + client clientset.Client + cache cache +} + +type cache struct { + getProvisionedProductOutputs []*svcsdk.RecordOutput +} + +func (c *custom) lateInitialize(spec *svcapitypes.ProvisionedProductParameters, _ *svcsdk.DescribeProvisionedProductOutput) error { + acceptLanguageEnglish := acceptLanguageEnglish + spec.AcceptLanguage = awsclient.LateInitializeStringPtr(spec.AcceptLanguage, &acceptLanguageEnglish) + return nil +} + +func (c *custom) isUpToDate(_ context.Context, ds *svcapitypes.ProvisionedProduct, resp *svcsdk.DescribeProvisionedProductOutput) (bool, string, error) { + // If the product is undergoing change, we want to assume that it is not up-to-date. This will force this resource + // to be queued for an update (which will be skipped due to UNDER_CHANGE), and once that update fails, we will + // recheck the status again. This will allow us to quickly transition from UNDER_CHANGE to AVAILABLE without having + // to wait for the entire polling interval to pass before re-checking the status. + if pointer.StringDeref(ds.Status.AtProvider.Status, "") == string(svcapitypes.ProvisionedProductStatus_SDK_UNDER_CHANGE) { + return true, "", nil + } + + getPPOutputInput := &svcsdk.GetProvisionedProductOutputsInput{ProvisionedProductId: resp.ProvisionedProductDetail.Id} + getPPOutput, err := c.client.GetProvisionedProductOutputs(getPPOutputInput) + if err != nil { + // We want to specifically handle this exception, since it will occur when something + // is wrong with the provisioned product (error on creation, tainted, etc) + // We will be able to handle those specific cases in postObserve + var aerr awserr.Error + if ok := errors.As(err, &aerr); ok { + if aerr.Code() == svcsdk.ErrCodeInvalidParametersException && aerr.Message() == errAwsAPICodeInvalidParametersException { + return false, "", nil + } + } + return false, "", errors.Wrap(err, errCouldNotGetProvisionedProductOutputs) + } + c.cache.getProvisionedProductOutputs = getPPOutput.Outputs + cfStackParameters, err := c.client.GetCloudformationStackParameters(getPPOutput.Outputs) if err != nil { - return nil, errors.Wrap(err, errCreateSession) + return false, "", errors.Wrap(err, errCouldNotGetCFParameters) } - awsCfg, err := awsclient.GetConfig(ctx, c.kube, mg, cr.Spec.ForProvider.Region) + productOrArtifactAreChanged, err := c.productOrArtifactAreChanged(&ds.Spec.ForProvider, resp.ProvisionedProductDetail) if err != nil { - return nil, errors.Wrap(err, errCreateSession) + return false, "", errors.Wrap(err, "could not discover if product or artifact ids have changed") } - cfClient := cfsdkv2.NewFromConfig(*awsCfg) - svcclient := svcsdk.New(sess) + if productOrArtifactAreChanged || c.provisioningParamsAreChanged(cfStackParameters, ds.Spec.ForProvider.ProvisioningParameters) { + return false, "", nil + } + return true, "", nil +} - cust := &custom{ - client: &clientset.CustomServiceCatalogClient{ - CfClient: cfClient, - Client: svcclient, - }, +func (c *custom) postObserve(_ context.Context, cr *svcapitypes.ProvisionedProduct, resp *svcsdk.DescribeProvisionedProductOutput, obs managed.ExternalObservation, err error) (managed.ExternalObservation, error) { + if err != nil { + return managed.ExternalObservation{}, err } + meta.SetExternalName(cr, *cr.Spec.ForProvider.Name) - // We do not re-implement all of the ExternalClient interface, so we want - // to reuse the generated one as much as we can (mostly for the Observe, - // Create, Update, Delete methods which call all of our custom hooks) - cust.external = &external{ - kube: c.kube, - client: svcclient, + describeRecordInput := svcsdk.DescribeRecordInput{Id: resp.ProvisionedProductDetail.LastRecordId} + describeRecordOutput, err := c.client.DescribeRecord(&describeRecordInput) + if err != nil { + return managed.ExternalObservation{}, errors.Wrap(err, errCouldNotDescribeRecord) + } - // All of our overrides must go here - postObserve: cust.postObserve, - lateInitialize: cust.lateInitialize, - isUpToDate: cust.isUpToDate, - preCreate: cust.preCreate, - postCreate: cust.postCreate, - preDelete: cust.preDelete, - preUpdate: cust.preUpdate, + setConditions(describeRecordOutput, resp, cr) - // If we do not implement a method, we must specify the no-op function - preObserve: nopPreObserve, - postDelete: nopPostDelete, - postUpdate: nopPostUpdate, + var outputs = make(map[string]*svcapitypes.RecordOutput) + for _, v := range c.cache.getProvisionedProductOutputs { + outputs[*v.OutputKey] = &svcapitypes.RecordOutput{ + Description: v.Description, + OutputValue: v.OutputValue} } - return cust, nil + cr.Status.AtProvider.Outputs = outputs + cr.Status.AtProvider.ARN = resp.ProvisionedProductDetail.Arn + cr.Status.AtProvider.CreatedTime = &metav1.Time{Time: *resp.ProvisionedProductDetail.CreatedTime} + cr.Status.AtProvider.LastProvisioningRecordID = resp.ProvisionedProductDetail.LastProvisioningRecordId + cr.Status.AtProvider.LaunchRoleARN = resp.ProvisionedProductDetail.LaunchRoleArn + cr.Status.AtProvider.Status = resp.ProvisionedProductDetail.Status + cr.Status.AtProvider.StatusMessage = resp.ProvisionedProductDetail.StatusMessage + cr.Status.AtProvider.ProvisionedProductType = resp.ProvisionedProductDetail.Type + cr.Status.AtProvider.RecordType = describeRecordOutput.RecordDetail.RecordType + cr.Status.AtProvider.LastPathID = describeRecordOutput.RecordDetail.PathId + cr.Status.AtProvider.LastProductID = describeRecordOutput.RecordDetail.ProductId + cr.Status.AtProvider.LastProvisioningArtifactID = describeRecordOutput.RecordDetail.ProvisioningArtifactId + cr.Status.AtProvider.LastProvisioningParameters = cr.Spec.ForProvider.ProvisioningParameters + + return obs, nil +} + +func preCreate(_ context.Context, cr *svcapitypes.ProvisionedProduct, obj *svcsdk.ProvisionProductInput) error { + obj.ProvisionToken = aws.String(genIdempotencyToken()) + + // We want to specifically set this to match the forProvider name, as that + // is what we use to track the provisioned product + if cr.Spec.ForProvider.Name != nil { + meta.SetExternalName(cr, *cr.Spec.ForProvider.Name) + } + + return nil +} + +func (c *custom) postCreate(_ context.Context, cr *svcapitypes.ProvisionedProduct, obj *svcsdk.ProvisionProductOutput, cre managed.ExternalCreation, err error) (managed.ExternalCreation, error) { + if err != nil { + return cre, err + } + + // We are expected to set the external-name annotation upon creation since + // it can differ from the metadata.name for the ProvisionedProduct + if obj.RecordDetail != nil && obj.RecordDetail.ProvisionedProductName != nil { + meta.SetExternalName(cr, *obj.RecordDetail.ProvisionedProductName) + } + + return cre, nil +} + +func (c *custom) preUpdate(_ context.Context, cr *svcapitypes.ProvisionedProduct, input *svcsdk.UpdateProvisionedProductInput) error { + if pointer.StringDeref(cr.Status.AtProvider.Status, "") == string(svcapitypes.ProvisionedProductStatus_SDK_UNDER_CHANGE) { + return errors.New(errUpdatePending) + } + input.UpdateToken = aws.String(genIdempotencyToken()) + input.ProvisionedProductName = aws.String(meta.GetExternalName(cr)) + return nil +} + +func preDelete(_ context.Context, cr *svcapitypes.ProvisionedProduct, obj *svcsdk.TerminateProvisionedProductInput) (bool, error) { + if pointer.StringDeref(cr.Status.AtProvider.Status, "") == string(svcapitypes.ProvisionedProductStatus_SDK_UNDER_CHANGE) { + return true, nil + } + obj.TerminateToken = aws.String(genIdempotencyToken()) + obj.ProvisionedProductName = aws.String(meta.GetExternalName(cr)) + return false, nil +} + +func setConditions(describeRecordOutput *svcsdk.DescribeRecordOutput, resp *svcsdk.DescribeProvisionedProductOutput, cr *svcapitypes.ProvisionedProduct) { + var recordType string = pointer.StringDeref(describeRecordOutput.RecordDetail.RecordType, "UPDATE_PROVISIONED_PRODUCT") + + ppStatus := aws.StringValue(resp.ProvisionedProductDetail.Status) + switch { + case ppStatus == string(svcapitypes.ProvisionedProductStatus_SDK_AVAILABLE): + cr.SetConditions(xpv1.Available()) + case ppStatus == string(svcapitypes.ProvisionedProductStatus_SDK_UNDER_CHANGE) && recordType == "PROVISION_PRODUCT": + cr.SetConditions(xpv1.Creating()) + case ppStatus == string(svcapitypes.ProvisionedProductStatus_SDK_UNDER_CHANGE) && recordType == "UPDATE_PROVISIONED_PRODUCT": + cr.SetConditions(xpv1.Available().WithMessage(msgProvisionedProductStatusSdkUnderChange)) + case ppStatus == string(svcapitypes.ProvisionedProductStatus_SDK_UNDER_CHANGE) && recordType == "TERMINATE_PROVISIONED_PRODUCT": + cr.SetConditions(xpv1.Deleting()) + case ppStatus == string(svcapitypes.ProvisionedProductStatus_SDK_PLAN_IN_PROGRESS): + cr.SetConditions(xpv1.Unavailable().WithMessage(msgProvisionedProductStatusSdkPlanInProgress)) + case ppStatus == string(svcapitypes.ProvisionedProductStatus_SDK_ERROR): + cr.SetConditions(xpv1.Unavailable().WithMessage(msgProvisionedProductStatusSdkError)) + case ppStatus == string(svcapitypes.ProvisionedProductStatus_SDK_TAINTED): + cr.SetConditions(xpv1.Unavailable().WithMessage(msgProvisionedProductStatusSdkTainted)) + } +} + +func (c *custom) provisioningParamsAreChanged(cfStackParams []cfsdkv2types.Parameter, currentParams []*svcapitypes.ProvisioningParameter) bool { + + cfStackKeyValue := make(map[string]string) + for _, v := range cfStackParams { + cfStackKeyValue[*v.ParameterKey] = pointer.StringDeref(v.ParameterValue, "") + } + + for _, v := range currentParams { + // In this statement/comparison, the provider ignores spaces from the left and right of the parameter value from + // the desired state. Because on cloudformation side spaces are also trimmed + if cfv, ok := cfStackKeyValue[*v.Key]; ok && strings.TrimSpace(pointer.StringDeref(v.Value, "")) == cfv { + continue + } else { + return true + } + } + + return false +} + +func (c *custom) productOrArtifactAreChanged(ds *svcapitypes.ProvisionedProductParameters, resp *svcsdk.ProvisionedProductDetail) (bool, error) { + // ProvisioningArtifactID and ProvisioningArtifactName are mutual exclusive params, the same about ProductID and ProductName + // But if describe a provisioned product aws api will return only IDs, so it's impossible to compare names with ids + // Conditional statement below works only if desired state includes ProvisioningArtifactID and ProductID + if ds.ProvisioningArtifactID != nil && ds.ProductID != nil && + (*ds.ProvisioningArtifactID != *resp.ProvisioningArtifactId || + *ds.ProductID != *resp.ProductId) { + return true, nil + // In case if desired state includes not only IDs provider runs func `getArtifactID`, which produces + // additional request to aws api and retrieves an artifact id(even if it is already defined in the desired state) + // based on ProductId/ProductName for further comparison with artifact id in the current state + } else if ds.ProvisioningArtifactName != nil || ds.ProductName != nil { + desiredArtifactID, err := c.getArtifactID(ds) + if err != nil { + return false, err + } + if desiredArtifactID != *resp.ProvisioningArtifactId { + return true, nil + } + } + return false, nil +} + +func (c *custom) getArtifactID(ds *svcapitypes.ProvisionedProductParameters) (string, error) { + if ds.ProvisioningArtifactName != nil && ds.ProvisioningArtifactID != nil { + return "", errors.Wrap(errors.New("artifact id and name are mutually exclusive"), errCouldNotLookupProduct) + } + + input := svcsdk.DescribeProductInput{ + Id: ds.ProductID, + Name: ds.ProductName, + } + // DescribeProvisioningArtifact methods fits much better, but it has a bug + output, err := c.client.DescribeProduct(&input) + if err != nil { + return "", errors.Wrap(err, errCouldNotLookupProduct) + } + + for _, artifact := range output.ProvisioningArtifacts { + if pointer.StringDeref(ds.ProvisioningArtifactName, "") == *artifact.Name || + pointer.StringDeref(ds.ProvisioningArtifactID, "") == *artifact.Id { + return pointer.StringDeref(artifact.Id, ""), nil + } + } + return "", errors.Wrap(errors.New("artifact not found"), errCouldNotLookupProduct) +} + +func genIdempotencyToken() string { + return fmt.Sprintf("provider-aws-%s", uuid.New()) } diff --git a/pkg/controller/servicecatalog/provisionedproduct/utils.go b/pkg/controller/servicecatalog/provisionedproduct/utils.go deleted file mode 100644 index 548402d8f4..0000000000 --- a/pkg/controller/servicecatalog/provisionedproduct/utils.go +++ /dev/null @@ -1,87 +0,0 @@ -package provisionedproduct - -import ( - "fmt" - "strings" - - cfsdkv2types "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" - svcsdk "github.com/aws/aws-sdk-go/service/servicecatalog" - "github.com/google/uuid" - "github.com/pkg/errors" - "k8s.io/utils/pointer" - - svcapitypes "github.com/crossplane-contrib/provider-aws/apis/servicecatalog/v1alpha1" -) - -const ( - errCouldNotLookupProduct = "could not lookup product" -) - -func provisioningParamsAreChanged(cfStackParams []cfsdkv2types.Parameter, currentParams []*svcapitypes.ProvisioningParameter) bool { - cfStackKeyValue := make(map[string]string) - for _, v := range cfStackParams { - cfStackKeyValue[*v.ParameterKey] = pointer.StringDeref(v.ParameterValue, "") - } - - for _, v := range currentParams { - // In this statement/comparison, the provider ignores spaces from the left and right of the parameter value from - // the desired state. Because on cloudformation side spaces are also trimmed - if cfv, ok := cfStackKeyValue[*v.Key]; ok && strings.TrimSpace(pointer.StringDeref(v.Value, "")) == cfv { - continue - } else { - return true - } - } - - return false -} - -func (c *custom) productOrArtifactAreChanged(ds *svcapitypes.ProvisionedProductParameters, resp *svcsdk.ProvisionedProductDetail) (bool, error) { - // ProvisioningArtifactID and ProvisioningArtifactName are mutual exclusive params, the same about ProductID and ProductName - // But if describe a provisioned product aws api will return only IDs, so it's impossible to compare names with ids - // Conditional statement below works only if desired state includes ProvisioningArtifactID and ProductID - if ds.ProvisioningArtifactID != nil && ds.ProductID != nil && - (*ds.ProvisioningArtifactID != *resp.ProvisioningArtifactId || - *ds.ProductID != *resp.ProductId) { - return true, nil - // In case if desired state includes not only IDs provider runs func `getArtifactID`, which produces - // additional request to aws api and retrieves an artifact id(even if it is already defined in the desired state) - // based on ProductId/ProductName for further comparison with artifact id in the current state - } else if ds.ProvisioningArtifactName != nil || ds.ProductName != nil { - desiredArtifactID, err := c.getArtifactID(ds) - if err != nil { - return false, err - } - if desiredArtifactID != *resp.ProvisioningArtifactId { - return true, nil - } - } - return false, nil -} - -func (c *custom) getArtifactID(ds *svcapitypes.ProvisionedProductParameters) (string, error) { - input := svcsdk.DescribeProductInput{ - Id: ds.ProductID, - Name: ds.ProductName, - } - // DescribeProvisioningArtifact methods fits much better, but it has a bug - output, err := c.client.DescribeProduct(&input) - if err != nil { - return "", errors.Wrap(err, errCouldNotLookupProduct) - } - if ds.ProvisioningArtifactName != nil && ds.ProvisioningArtifactID != nil { - return "", errors.Wrap(errors.New("artifact id and name are mutually exclusive"), errCouldNotLookupProduct) - } - - for _, artifact := range output.ProvisioningArtifacts { - if pointer.StringDeref(ds.ProvisioningArtifactName, "") == *artifact.Name || - pointer.StringDeref(ds.ProvisioningArtifactID, "") == *artifact.Id { - return pointer.StringDeref(artifact.Id, ""), nil - } - } - return "", errors.Wrap(errors.New("artifact not found"), errCouldNotLookupProduct) -} - -func genIdempotencyToken() string { - return fmt.Sprintf("provider-aws-%s", uuid.New()) -}