From 285dd912ac9b6ee74003b706942b6ea8f50697b5 Mon Sep 17 00:00:00 2001 From: Jay Pipes Date: Fri, 20 Jan 2023 13:51:07 -0500 Subject: [PATCH] Adds basic e2e test for certificate (#3) Also adds a bunch of read-only fields to the Certificate's Status struct that needed to be manually added to the generator.yaml file because the Create operation only returns the CertificateArn... Issue aws-controllers-k8s/community#482 Signed-off-by: Jay Pipes By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. --- apis/v1alpha1/ack-generate-metadata.yaml | 6 +- apis/v1alpha1/certificate.go | 95 +++++++- apis/v1alpha1/generator.yaml | 118 +++++++++- apis/v1alpha1/zz_generated.deepcopy.go | 112 +++++++++- .../acm.services.k8s.aws_certificates.yaml | 167 +++++++++++++- config/iam/recommended-inline-policy | 18 ++ generator.yaml | 118 +++++++++- .../acm.services.k8s.aws_certificates.yaml | 167 +++++++++++++- pkg/resource/certificate/delta.go | 7 - pkg/resource/certificate/manager.go | 20 +- pkg/resource/certificate/manager_factory.go | 2 +- pkg/resource/certificate/sdk.go | 206 +++++++++++++++++- .../sdk_create_post_set_output.go.tpl | 4 - test/e2e/__init__.py | 2 +- test/e2e/certificate.py | 134 ++++++++++++ test/e2e/condition.py | 145 ++++++++++++ test/e2e/resources/certificate_public.yaml | 11 + test/e2e/tests/test_certificate.py | 126 +++++++++++ 18 files changed, 1403 insertions(+), 55 deletions(-) create mode 100644 config/iam/recommended-inline-policy delete mode 100644 templates/hooks/certificate/sdk_create_post_set_output.go.tpl create mode 100644 test/e2e/certificate.py create mode 100644 test/e2e/condition.py create mode 100644 test/e2e/resources/certificate_public.yaml create mode 100644 test/e2e/tests/test_certificate.py diff --git a/apis/v1alpha1/ack-generate-metadata.yaml b/apis/v1alpha1/ack-generate-metadata.yaml index af33063..2a73617 100755 --- a/apis/v1alpha1/ack-generate-metadata.yaml +++ b/apis/v1alpha1/ack-generate-metadata.yaml @@ -1,13 +1,13 @@ ack_generate_info: - build_date: "2023-01-11T19:39:12Z" + build_date: "2023-01-14T16:13:40Z" build_hash: cfce82dfeed4e658da394699720394b1f7d23ff6 go_version: go1.19.4 version: v0.22.0-1-gcfce82d -api_directory_checksum: 090c67b92b4d0ddb4b58db94aa4b07b8c69dd530 +api_directory_checksum: 202e02932e71256f27a9cd0f6454e508c5b7e9b6 api_version: v1alpha1 aws_sdk_go_version: v1.44.177 generator_config_info: - file_checksum: aa72a600b2490b566fcd54554ee64e386a001799 + file_checksum: cf8ed525d9422f011b706c0edf1984d5f70853e5 original_file_name: generator.yaml last_modification: reason: API generation diff --git a/apis/v1alpha1/certificate.go b/apis/v1alpha1/certificate.go index 94184ad..22d99ac 100644 --- a/apis/v1alpha1/certificate.go +++ b/apis/v1alpha1/certificate.go @@ -91,11 +91,6 @@ type CertificateSpec struct { SubjectAlternativeNames []*string `json:"subjectAlternativeNames,omitempty"` // One or more resource tags to associate with the certificate. Tags []*Tag `json:"tags,omitempty"` - // The method you want to use if you are requesting a public certificate to - // validate that you own or control domain. You can validate with DNS (https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-dns.html) - // or validate with email (https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-email.html). - // We recommend that you use DNS validation. - ValidationMethod *string `json:"validationMethod,omitempty"` } // CertificateStatus defines the observed state of Certificate @@ -111,6 +106,96 @@ type CertificateStatus struct { // resource // +kubebuilder:validation:Optional Conditions []*ackv1alpha1.Condition `json:"conditions"` + // The time at which the certificate was requested. + // +kubebuilder:validation:Optional + CreatedAt *metav1.Time `json:"createdAt,omitempty"` + // Contains a list of Extended Key Usage X.509 v3 extension objects. Each object + // specifies a purpose for which the certificate public key can be used and + // consists of a name and an object identifier (OID). + // +kubebuilder:validation:Optional + ExtendedKeyUsages []*ExtendedKeyUsage `json:"extendedKeyUsages,omitempty"` + // The reason the certificate request failed. This value exists only when the + // certificate status is FAILED. For more information, see Certificate Request + // Failed (https://docs.aws.amazon.com/acm/latest/userguide/troubleshooting.html#troubleshooting-failed) + // in the Certificate Manager User Guide. + // +kubebuilder:validation:Optional + FailureReason *string `json:"failureReason,omitempty"` + // The date and time when the certificate was imported. This value exists only + // when the certificate type is IMPORTED. + // +kubebuilder:validation:Optional + ImportedAt *metav1.Time `json:"importedAt,omitempty"` + // A list of ARNs for the Amazon Web Services resources that are using the certificate. + // A certificate can be used by multiple Amazon Web Services resources. + // +kubebuilder:validation:Optional + InUseBy []*string `json:"inUseBy,omitempty"` + // The time at which the certificate was issued. This value exists only when + // the certificate type is AMAZON_ISSUED. + // +kubebuilder:validation:Optional + IssuedAt *metav1.Time `json:"issuedAt,omitempty"` + // The name of the certificate authority that issued and signed the certificate. + // +kubebuilder:validation:Optional + Issuer *string `json:"issuer,omitempty"` + // A list of Key Usage X.509 v3 extension objects. Each object is a string value + // that identifies the purpose of the public key contained in the certificate. + // Possible extension values include DIGITAL_SIGNATURE, KEY_ENCHIPHERMENT, NON_REPUDIATION, + // and more. + // +kubebuilder:validation:Optional + KeyUsages []*KeyUsage `json:"keyUsages,omitempty"` + // The time after which the certificate is not valid. + // +kubebuilder:validation:Optional + NotAfter *metav1.Time `json:"notAfter,omitempty"` + // The time before which the certificate is not valid. + // +kubebuilder:validation:Optional + NotBefore *metav1.Time `json:"notBefore,omitempty"` + // Specifies whether the certificate is eligible for renewal. At this time, + // only exported private certificates can be renewed with the RenewCertificate + // command. + // +kubebuilder:validation:Optional + RenewalEligibility *string `json:"renewalEligibility,omitempty"` + // Contains information about the status of ACM's managed renewal (https://docs.aws.amazon.com/acm/latest/userguide/acm-renewal.html) + // for the certificate. This field exists only when the certificate type is + // AMAZON_ISSUED. + // +kubebuilder:validation:Optional + RenewalSummary *RenewalSummary `json:"renewalSummary,omitempty"` + // The reason the certificate was revoked. This value exists only when the certificate + // status is REVOKED. + // +kubebuilder:validation:Optional + RevocationReason *string `json:"revocationReason,omitempty"` + // The time at which the certificate was revoked. This value exists only when + // the certificate status is REVOKED. + // +kubebuilder:validation:Optional + RevokedAt *metav1.Time `json:"revokedAt,omitempty"` + // The serial number of the certificate. + // +kubebuilder:validation:Optional + Serial *string `json:"serial,omitempty"` + // The algorithm that was used to sign the certificate. + // +kubebuilder:validation:Optional + SignatureAlgorithm *string `json:"signatureAlgorithm,omitempty"` + // The status of the certificate. + // + // A certificate enters status PENDING_VALIDATION upon being requested, unless + // it fails for any of the reasons given in the troubleshooting topic Certificate + // request fails (https://docs.aws.amazon.com/acm/latest/userguide/troubleshooting-failed.html). + // ACM makes repeated attempts to validate a certificate for 72 hours and then + // times out. If a certificate shows status FAILED or VALIDATION_TIMED_OUT, + // delete the request, correct the issue with DNS validation (https://docs.aws.amazon.com/acm/latest/userguide/dns-validation.html) + // or Email validation (https://docs.aws.amazon.com/acm/latest/userguide/email-validation.html), + // and try again. If validation succeeds, the certificate enters status ISSUED. + // +kubebuilder:validation:Optional + Status *string `json:"status,omitempty"` + // The name of the entity that is associated with the public key contained in + // the certificate. + // +kubebuilder:validation:Optional + Subject *string `json:"subject,omitempty"` + // The source of the certificate. For certificates provided by ACM, this value + // is AMAZON_ISSUED. For certificates that you imported with ImportCertificate, + // this value is IMPORTED. ACM does not provide managed renewal (https://docs.aws.amazon.com/acm/latest/userguide/acm-renewal.html) + // for imported certificates. For more information about the differences between + // certificates that you import and those that ACM provides, see Importing Certificates + // (https://docs.aws.amazon.com/acm/latest/userguide/import-certificate.html) + // in the Certificate Manager User Guide. + // +kubebuilder:validation:Optional + Type *string `json:"type_,omitempty"` } // Certificate is the Schema for the Certificates API diff --git a/apis/v1alpha1/generator.yaml b/apis/v1alpha1/generator.yaml index 0a60386..97b5c83 100644 --- a/apis/v1alpha1/generator.yaml +++ b/apis/v1alpha1/generator.yaml @@ -1,6 +1,7 @@ ignore: field_paths: - "RequestCertificateInput.IdempotencyToken" + - "RequestCertificateInput.ValidationMethod" operations: RequestCertificate: resource_name: Certificate @@ -26,8 +27,121 @@ resources: hooks: sdk_create_pre_build_request: template_path: hooks/certificate/sdk_create_pre_build_request.go.tpl - sdk_create_post_set_output: - template_path: hooks/certificate/sdk_create_post_set_output.go.tpl + exceptions: + terminal_codes: + - InvalidParameter + - InvalidDomainValidationOptionsException + - InvalidTagException + - TagPolicyException + - TooManyTagsException + reconcile: + requeue_on_success_seconds: 60 fields: + DomainValidationOptions: + late_initialize: {} + Options: + late_initialize: {} + SubjectAlternativeNames: + late_initialize: {} KeyAlgorithm: late_initialize: {} + # NOTE(jaypipes): The Create operation (RequestCertificate) has a + # response with only a single field (certificateArn). All of the status + # fields for the certificate are in the ReadOne operation + # (DescribeCertificate) response, so we need to tell the code-generator + # about all of those fields manually here... + CreatedAt: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.CreatedAt + ExtendedKeyUsages: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.ExtendedKeyUsages + FailureReason: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.FailureReason + ImportedAt: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.ImportedAt + InUseBy: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.InUseBy + IssuedAt: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.IssuedAt + Issuer: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.Issuer + KeyUsages: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.KeyUsages + NotAfter: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.NotAfter + NotBefore: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.NotBefore + RenewalEligibility: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.RenewalEligibility + RenewalSummary: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.RenewalSummary + RevocationReason: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.RevocationReason + RevokedAt: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.RevokedAt + Serial: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.Serial + SignatureAlgorithm: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.SignatureAlgorithm + Status: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.Status + Subject: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.Subject + Type: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.Type diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index 81f917f..639313c 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -329,11 +329,6 @@ func (in *CertificateSpec) DeepCopyInto(out *CertificateSpec) { } } } - if in.ValidationMethod != nil { - in, out := &in.ValidationMethod, &out.ValidationMethod - *out = new(string) - **out = **in - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateSpec. @@ -365,6 +360,113 @@ func (in *CertificateStatus) DeepCopyInto(out *CertificateStatus) { } } } + if in.CreatedAt != nil { + in, out := &in.CreatedAt, &out.CreatedAt + *out = (*in).DeepCopy() + } + if in.ExtendedKeyUsages != nil { + in, out := &in.ExtendedKeyUsages, &out.ExtendedKeyUsages + *out = make([]*ExtendedKeyUsage, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(ExtendedKeyUsage) + (*in).DeepCopyInto(*out) + } + } + } + if in.FailureReason != nil { + in, out := &in.FailureReason, &out.FailureReason + *out = new(string) + **out = **in + } + if in.ImportedAt != nil { + in, out := &in.ImportedAt, &out.ImportedAt + *out = (*in).DeepCopy() + } + if in.InUseBy != nil { + in, out := &in.InUseBy, &out.InUseBy + *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.IssuedAt != nil { + in, out := &in.IssuedAt, &out.IssuedAt + *out = (*in).DeepCopy() + } + if in.Issuer != nil { + in, out := &in.Issuer, &out.Issuer + *out = new(string) + **out = **in + } + if in.KeyUsages != nil { + in, out := &in.KeyUsages, &out.KeyUsages + *out = make([]*KeyUsage, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(KeyUsage) + (*in).DeepCopyInto(*out) + } + } + } + if in.NotAfter != nil { + in, out := &in.NotAfter, &out.NotAfter + *out = (*in).DeepCopy() + } + if in.NotBefore != nil { + in, out := &in.NotBefore, &out.NotBefore + *out = (*in).DeepCopy() + } + if in.RenewalEligibility != nil { + in, out := &in.RenewalEligibility, &out.RenewalEligibility + *out = new(string) + **out = **in + } + if in.RenewalSummary != nil { + in, out := &in.RenewalSummary, &out.RenewalSummary + *out = new(RenewalSummary) + (*in).DeepCopyInto(*out) + } + if in.RevocationReason != nil { + in, out := &in.RevocationReason, &out.RevocationReason + *out = new(string) + **out = **in + } + if in.RevokedAt != nil { + in, out := &in.RevokedAt, &out.RevokedAt + *out = (*in).DeepCopy() + } + if in.Serial != nil { + in, out := &in.Serial, &out.Serial + *out = new(string) + **out = **in + } + if in.SignatureAlgorithm != nil { + in, out := &in.SignatureAlgorithm, &out.SignatureAlgorithm + *out = new(string) + **out = **in + } + if in.Status != nil { + in, out := &in.Status, &out.Status + *out = new(string) + **out = **in + } + if in.Subject != nil { + in, out := &in.Subject, &out.Subject + *out = new(string) + **out = **in + } + if in.Type != nil { + in, out := &in.Type, &out.Type + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateStatus. diff --git a/config/crd/bases/acm.services.k8s.aws_certificates.yaml b/config/crd/bases/acm.services.k8s.aws_certificates.yaml index d820794..daee6c2 100644 --- a/config/crd/bases/acm.services.k8s.aws_certificates.yaml +++ b/config/crd/bases/acm.services.k8s.aws_certificates.yaml @@ -129,13 +129,6 @@ spec: type: string type: object type: array - validationMethod: - description: The method you want to use if you are requesting a public - certificate to validate that you own or control domain. You can - validate with DNS (https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-dns.html) - or validate with email (https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-email.html). - We recommend that you use DNS validation. - type: string required: - domainName type: object @@ -203,6 +196,166 @@ spec: - type type: object type: array + createdAt: + description: The time at which the certificate was requested. + format: date-time + type: string + extendedKeyUsages: + description: Contains a list of Extended Key Usage X.509 v3 extension + objects. Each object specifies a purpose for which the certificate + public key can be used and consists of a name and an object identifier + (OID). + items: + description: The Extended Key Usage X.509 v3 extension defines one + or more purposes for which the public key can be used. This is + in addition to or in place of the basic purposes specified by + the Key Usage extension. + properties: + name: + type: string + oid: + type: string + type: object + type: array + failureReason: + description: The reason the certificate request failed. This value + exists only when the certificate status is FAILED. For more information, + see Certificate Request Failed (https://docs.aws.amazon.com/acm/latest/userguide/troubleshooting.html#troubleshooting-failed) + in the Certificate Manager User Guide. + type: string + importedAt: + description: The date and time when the certificate was imported. + This value exists only when the certificate type is IMPORTED. + format: date-time + type: string + inUseBy: + description: A list of ARNs for the Amazon Web Services resources + that are using the certificate. A certificate can be used by multiple + Amazon Web Services resources. + items: + type: string + type: array + issuedAt: + description: The time at which the certificate was issued. This value + exists only when the certificate type is AMAZON_ISSUED. + format: date-time + type: string + issuer: + description: The name of the certificate authority that issued and + signed the certificate. + type: string + keyUsages: + description: A list of Key Usage X.509 v3 extension objects. Each + object is a string value that identifies the purpose of the public + key contained in the certificate. Possible extension values include + DIGITAL_SIGNATURE, KEY_ENCHIPHERMENT, NON_REPUDIATION, and more. + items: + description: The Key Usage X.509 v3 extension defines the purpose + of the public key contained in the certificate. + properties: + name: + type: string + type: object + type: array + notAfter: + description: The time after which the certificate is not valid. + format: date-time + type: string + notBefore: + description: The time before which the certificate is not valid. + format: date-time + type: string + renewalEligibility: + description: Specifies whether the certificate is eligible for renewal. + At this time, only exported private certificates can be renewed + with the RenewCertificate command. + type: string + renewalSummary: + description: Contains information about the status of ACM's managed + renewal (https://docs.aws.amazon.com/acm/latest/userguide/acm-renewal.html) + for the certificate. This field exists only when the certificate + type is AMAZON_ISSUED. + properties: + domainValidationOptions: + items: + description: Contains information about the validation of each + domain name in the certificate. + properties: + domainName: + type: string + resourceRecord: + description: Contains a DNS record value that you can use + to validate ownership or control of a domain. This is + used by the DescribeCertificate action. + properties: + name: + type: string + type_: + type: string + value: + type: string + type: object + validationDomain: + type: string + validationEmails: + items: + type: string + type: array + validationMethod: + type: string + validationStatus: + type: string + type: object + type: array + renewalStatus: + type: string + renewalStatusReason: + type: string + updatedAt: + format: date-time + type: string + type: object + revocationReason: + description: The reason the certificate was revoked. This value exists + only when the certificate status is REVOKED. + type: string + revokedAt: + description: The time at which the certificate was revoked. This value + exists only when the certificate status is REVOKED. + format: date-time + type: string + serial: + description: The serial number of the certificate. + type: string + signatureAlgorithm: + description: The algorithm that was used to sign the certificate. + type: string + status: + description: "The status of the certificate. \n A certificate enters + status PENDING_VALIDATION upon being requested, unless it fails + for any of the reasons given in the troubleshooting topic Certificate + request fails (https://docs.aws.amazon.com/acm/latest/userguide/troubleshooting-failed.html). + ACM makes repeated attempts to validate a certificate for 72 hours + and then times out. If a certificate shows status FAILED or VALIDATION_TIMED_OUT, + delete the request, correct the issue with DNS validation (https://docs.aws.amazon.com/acm/latest/userguide/dns-validation.html) + or Email validation (https://docs.aws.amazon.com/acm/latest/userguide/email-validation.html), + and try again. If validation succeeds, the certificate enters status + ISSUED." + type: string + subject: + description: The name of the entity that is associated with the public + key contained in the certificate. + type: string + type_: + description: The source of the certificate. For certificates provided + by ACM, this value is AMAZON_ISSUED. For certificates that you imported + with ImportCertificate, this value is IMPORTED. ACM does not provide + managed renewal (https://docs.aws.amazon.com/acm/latest/userguide/acm-renewal.html) + for imported certificates. For more information about the differences + between certificates that you import and those that ACM provides, + see Importing Certificates (https://docs.aws.amazon.com/acm/latest/userguide/import-certificate.html) + in the Certificate Manager User Guide. + type: string type: object type: object served: true diff --git a/config/iam/recommended-inline-policy b/config/iam/recommended-inline-policy new file mode 100644 index 0000000..a6eeca5 --- /dev/null +++ b/config/iam/recommended-inline-policy @@ -0,0 +1,18 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "acm:DescribeCertificate", + "acm:RequestCertificate", + "acm:UpdateCertificateOptions", + "acm:DeleteCertificate", + "acm:AddTagsToCertificate", + "acm:RemoveTagsFromCertificate", + "acm:ListTagsForCertificate" + ], + "Resource": "*" + } + ] +} diff --git a/generator.yaml b/generator.yaml index 0a60386..97b5c83 100644 --- a/generator.yaml +++ b/generator.yaml @@ -1,6 +1,7 @@ ignore: field_paths: - "RequestCertificateInput.IdempotencyToken" + - "RequestCertificateInput.ValidationMethod" operations: RequestCertificate: resource_name: Certificate @@ -26,8 +27,121 @@ resources: hooks: sdk_create_pre_build_request: template_path: hooks/certificate/sdk_create_pre_build_request.go.tpl - sdk_create_post_set_output: - template_path: hooks/certificate/sdk_create_post_set_output.go.tpl + exceptions: + terminal_codes: + - InvalidParameter + - InvalidDomainValidationOptionsException + - InvalidTagException + - TagPolicyException + - TooManyTagsException + reconcile: + requeue_on_success_seconds: 60 fields: + DomainValidationOptions: + late_initialize: {} + Options: + late_initialize: {} + SubjectAlternativeNames: + late_initialize: {} KeyAlgorithm: late_initialize: {} + # NOTE(jaypipes): The Create operation (RequestCertificate) has a + # response with only a single field (certificateArn). All of the status + # fields for the certificate are in the ReadOne operation + # (DescribeCertificate) response, so we need to tell the code-generator + # about all of those fields manually here... + CreatedAt: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.CreatedAt + ExtendedKeyUsages: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.ExtendedKeyUsages + FailureReason: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.FailureReason + ImportedAt: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.ImportedAt + InUseBy: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.InUseBy + IssuedAt: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.IssuedAt + Issuer: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.Issuer + KeyUsages: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.KeyUsages + NotAfter: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.NotAfter + NotBefore: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.NotBefore + RenewalEligibility: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.RenewalEligibility + RenewalSummary: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.RenewalSummary + RevocationReason: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.RevocationReason + RevokedAt: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.RevokedAt + Serial: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.Serial + SignatureAlgorithm: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.SignatureAlgorithm + Status: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.Status + Subject: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.Subject + Type: + is_read_only: true + from: + operation: DescribeCertificate + path: Certificate.Type diff --git a/helm/crds/acm.services.k8s.aws_certificates.yaml b/helm/crds/acm.services.k8s.aws_certificates.yaml index b151533..a76520a 100644 --- a/helm/crds/acm.services.k8s.aws_certificates.yaml +++ b/helm/crds/acm.services.k8s.aws_certificates.yaml @@ -129,13 +129,6 @@ spec: type: string type: object type: array - validationMethod: - description: The method you want to use if you are requesting a public - certificate to validate that you own or control domain. You can - validate with DNS (https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-dns.html) - or validate with email (https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-email.html). - We recommend that you use DNS validation. - type: string required: - domainName type: object @@ -203,6 +196,166 @@ spec: - type type: object type: array + createdAt: + description: The time at which the certificate was requested. + format: date-time + type: string + extendedKeyUsages: + description: Contains a list of Extended Key Usage X.509 v3 extension + objects. Each object specifies a purpose for which the certificate + public key can be used and consists of a name and an object identifier + (OID). + items: + description: The Extended Key Usage X.509 v3 extension defines one + or more purposes for which the public key can be used. This is + in addition to or in place of the basic purposes specified by + the Key Usage extension. + properties: + name: + type: string + oid: + type: string + type: object + type: array + failureReason: + description: The reason the certificate request failed. This value + exists only when the certificate status is FAILED. For more information, + see Certificate Request Failed (https://docs.aws.amazon.com/acm/latest/userguide/troubleshooting.html#troubleshooting-failed) + in the Certificate Manager User Guide. + type: string + importedAt: + description: The date and time when the certificate was imported. + This value exists only when the certificate type is IMPORTED. + format: date-time + type: string + inUseBy: + description: A list of ARNs for the Amazon Web Services resources + that are using the certificate. A certificate can be used by multiple + Amazon Web Services resources. + items: + type: string + type: array + issuedAt: + description: The time at which the certificate was issued. This value + exists only when the certificate type is AMAZON_ISSUED. + format: date-time + type: string + issuer: + description: The name of the certificate authority that issued and + signed the certificate. + type: string + keyUsages: + description: A list of Key Usage X.509 v3 extension objects. Each + object is a string value that identifies the purpose of the public + key contained in the certificate. Possible extension values include + DIGITAL_SIGNATURE, KEY_ENCHIPHERMENT, NON_REPUDIATION, and more. + items: + description: The Key Usage X.509 v3 extension defines the purpose + of the public key contained in the certificate. + properties: + name: + type: string + type: object + type: array + notAfter: + description: The time after which the certificate is not valid. + format: date-time + type: string + notBefore: + description: The time before which the certificate is not valid. + format: date-time + type: string + renewalEligibility: + description: Specifies whether the certificate is eligible for renewal. + At this time, only exported private certificates can be renewed + with the RenewCertificate command. + type: string + renewalSummary: + description: Contains information about the status of ACM's managed + renewal (https://docs.aws.amazon.com/acm/latest/userguide/acm-renewal.html) + for the certificate. This field exists only when the certificate + type is AMAZON_ISSUED. + properties: + domainValidationOptions: + items: + description: Contains information about the validation of each + domain name in the certificate. + properties: + domainName: + type: string + resourceRecord: + description: Contains a DNS record value that you can use + to validate ownership or control of a domain. This is + used by the DescribeCertificate action. + properties: + name: + type: string + type_: + type: string + value: + type: string + type: object + validationDomain: + type: string + validationEmails: + items: + type: string + type: array + validationMethod: + type: string + validationStatus: + type: string + type: object + type: array + renewalStatus: + type: string + renewalStatusReason: + type: string + updatedAt: + format: date-time + type: string + type: object + revocationReason: + description: The reason the certificate was revoked. This value exists + only when the certificate status is REVOKED. + type: string + revokedAt: + description: The time at which the certificate was revoked. This value + exists only when the certificate status is REVOKED. + format: date-time + type: string + serial: + description: The serial number of the certificate. + type: string + signatureAlgorithm: + description: The algorithm that was used to sign the certificate. + type: string + status: + description: "The status of the certificate. \n A certificate enters + status PENDING_VALIDATION upon being requested, unless it fails + for any of the reasons given in the troubleshooting topic Certificate + request fails (https://docs.aws.amazon.com/acm/latest/userguide/troubleshooting-failed.html). + ACM makes repeated attempts to validate a certificate for 72 hours + and then times out. If a certificate shows status FAILED or VALIDATION_TIMED_OUT, + delete the request, correct the issue with DNS validation (https://docs.aws.amazon.com/acm/latest/userguide/dns-validation.html) + or Email validation (https://docs.aws.amazon.com/acm/latest/userguide/email-validation.html), + and try again. If validation succeeds, the certificate enters status + ISSUED." + type: string + subject: + description: The name of the entity that is associated with the public + key contained in the certificate. + type: string + type_: + description: The source of the certificate. For certificates provided + by ACM, this value is AMAZON_ISSUED. For certificates that you imported + with ImportCertificate, this value is IMPORTED. ACM does not provide + managed renewal (https://docs.aws.amazon.com/acm/latest/userguide/acm-renewal.html) + for imported certificates. For more information about the differences + between certificates that you import and those that ACM provides, + see Importing Certificates (https://docs.aws.amazon.com/acm/latest/userguide/import-certificate.html) + in the Certificate Manager User Guide. + type: string type: object type: object served: true diff --git a/pkg/resource/certificate/delta.go b/pkg/resource/certificate/delta.go index 06896da..c9b8630 100644 --- a/pkg/resource/certificate/delta.go +++ b/pkg/resource/certificate/delta.go @@ -82,13 +82,6 @@ func newResourceDelta( if !reflect.DeepEqual(a.ko.Spec.Tags, b.ko.Spec.Tags) { delta.Add("Spec.Tags", a.ko.Spec.Tags, b.ko.Spec.Tags) } - if ackcompare.HasNilDifference(a.ko.Spec.ValidationMethod, b.ko.Spec.ValidationMethod) { - delta.Add("Spec.ValidationMethod", a.ko.Spec.ValidationMethod, b.ko.Spec.ValidationMethod) - } else if a.ko.Spec.ValidationMethod != nil && b.ko.Spec.ValidationMethod != nil { - if *a.ko.Spec.ValidationMethod != *b.ko.Spec.ValidationMethod { - delta.Add("Spec.ValidationMethod", a.ko.Spec.ValidationMethod, b.ko.Spec.ValidationMethod) - } - } return delta } diff --git a/pkg/resource/certificate/manager.go b/pkg/resource/certificate/manager.go index 93143fa..a58b1d0 100644 --- a/pkg/resource/certificate/manager.go +++ b/pkg/resource/certificate/manager.go @@ -51,7 +51,7 @@ var ( // +kubebuilder:rbac:groups=acm.services.k8s.aws,resources=certificates,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=acm.services.k8s.aws,resources=certificates/status,verbs=get;update;patch -var lateInitializeFieldNames = []string{"KeyAlgorithm"} +var lateInitializeFieldNames = []string{"DomainValidationOptions", "KeyAlgorithm", "Options", "SubjectAlternativeNames"} // resourceManager is responsible for providing a consistent way to perform // CRUD operations in a backend AWS service API for Book custom resources. @@ -249,9 +249,18 @@ func (rm *resourceManager) incompleteLateInitialization( res acktypes.AWSResource, ) bool { ko := rm.concreteResource(res).ko.DeepCopy() + if ko.Spec.DomainValidationOptions == nil { + return true + } if ko.Spec.KeyAlgorithm == nil { return true } + if ko.Spec.Options == nil { + return true + } + if ko.Spec.SubjectAlternativeNames == nil { + return true + } return false } @@ -263,9 +272,18 @@ func (rm *resourceManager) lateInitializeFromReadOneOutput( ) acktypes.AWSResource { observedKo := rm.concreteResource(observed).ko.DeepCopy() latestKo := rm.concreteResource(latest).ko.DeepCopy() + if observedKo.Spec.DomainValidationOptions != nil && latestKo.Spec.DomainValidationOptions == nil { + latestKo.Spec.DomainValidationOptions = observedKo.Spec.DomainValidationOptions + } if observedKo.Spec.KeyAlgorithm != nil && latestKo.Spec.KeyAlgorithm == nil { latestKo.Spec.KeyAlgorithm = observedKo.Spec.KeyAlgorithm } + if observedKo.Spec.Options != nil && latestKo.Spec.Options == nil { + latestKo.Spec.Options = observedKo.Spec.Options + } + if observedKo.Spec.SubjectAlternativeNames != nil && latestKo.Spec.SubjectAlternativeNames == nil { + latestKo.Spec.SubjectAlternativeNames = observedKo.Spec.SubjectAlternativeNames + } return &resource{latestKo} } diff --git a/pkg/resource/certificate/manager_factory.go b/pkg/resource/certificate/manager_factory.go index 7c1baad..edb1f7e 100644 --- a/pkg/resource/certificate/manager_factory.go +++ b/pkg/resource/certificate/manager_factory.go @@ -82,7 +82,7 @@ func (f *resourceManagerFactory) IsAdoptable() bool { // 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 + return 60 } func newResourceManagerFactory() *resourceManagerFactory { diff --git a/pkg/resource/certificate/sdk.go b/pkg/resource/certificate/sdk.go index 3ee9aef..76d96a6 100644 --- a/pkg/resource/certificate/sdk.go +++ b/pkg/resource/certificate/sdk.go @@ -99,6 +99,11 @@ func (rm *resourceManager) sdkFind( } else { ko.Spec.CertificateAuthorityARN = nil } + if resp.Certificate.CreatedAt != nil { + ko.Status.CreatedAt = &metav1.Time{*resp.Certificate.CreatedAt} + } else { + ko.Status.CreatedAt = nil + } if resp.Certificate.DomainName != nil { ko.Spec.DomainName = resp.Certificate.DomainName } else { @@ -120,11 +125,81 @@ func (rm *resourceManager) sdkFind( } else { ko.Spec.DomainValidationOptions = nil } + if resp.Certificate.ExtendedKeyUsages != nil { + f5 := []*svcapitypes.ExtendedKeyUsage{} + for _, f5iter := range resp.Certificate.ExtendedKeyUsages { + f5elem := &svcapitypes.ExtendedKeyUsage{} + if f5iter.Name != nil { + f5elem.Name = f5iter.Name + } + if f5iter.OID != nil { + f5elem.OID = f5iter.OID + } + f5 = append(f5, f5elem) + } + ko.Status.ExtendedKeyUsages = f5 + } else { + ko.Status.ExtendedKeyUsages = nil + } + if resp.Certificate.FailureReason != nil { + ko.Status.FailureReason = resp.Certificate.FailureReason + } else { + ko.Status.FailureReason = nil + } + if resp.Certificate.ImportedAt != nil { + ko.Status.ImportedAt = &metav1.Time{*resp.Certificate.ImportedAt} + } else { + ko.Status.ImportedAt = nil + } + if resp.Certificate.InUseBy != nil { + f8 := []*string{} + for _, f8iter := range resp.Certificate.InUseBy { + var f8elem string + f8elem = *f8iter + f8 = append(f8, &f8elem) + } + ko.Status.InUseBy = f8 + } else { + ko.Status.InUseBy = nil + } + if resp.Certificate.IssuedAt != nil { + ko.Status.IssuedAt = &metav1.Time{*resp.Certificate.IssuedAt} + } else { + ko.Status.IssuedAt = nil + } + if resp.Certificate.Issuer != nil { + ko.Status.Issuer = resp.Certificate.Issuer + } else { + ko.Status.Issuer = nil + } if resp.Certificate.KeyAlgorithm != nil { ko.Spec.KeyAlgorithm = resp.Certificate.KeyAlgorithm } else { ko.Spec.KeyAlgorithm = nil } + if resp.Certificate.KeyUsages != nil { + f12 := []*svcapitypes.KeyUsage{} + for _, f12iter := range resp.Certificate.KeyUsages { + f12elem := &svcapitypes.KeyUsage{} + if f12iter.Name != nil { + f12elem.Name = f12iter.Name + } + f12 = append(f12, f12elem) + } + ko.Status.KeyUsages = f12 + } else { + ko.Status.KeyUsages = nil + } + if resp.Certificate.NotAfter != nil { + ko.Status.NotAfter = &metav1.Time{*resp.Certificate.NotAfter} + } else { + ko.Status.NotAfter = nil + } + if resp.Certificate.NotBefore != nil { + ko.Status.NotBefore = &metav1.Time{*resp.Certificate.NotBefore} + } else { + ko.Status.NotBefore = nil + } if resp.Certificate.Options != nil { f15 := &svcapitypes.CertificateOptions{} if resp.Certificate.Options.CertificateTransparencyLoggingPreference != nil { @@ -134,6 +209,98 @@ func (rm *resourceManager) sdkFind( } else { ko.Spec.Options = nil } + if resp.Certificate.RenewalEligibility != nil { + ko.Status.RenewalEligibility = resp.Certificate.RenewalEligibility + } else { + ko.Status.RenewalEligibility = nil + } + if resp.Certificate.RenewalSummary != nil { + f17 := &svcapitypes.RenewalSummary{} + if resp.Certificate.RenewalSummary.DomainValidationOptions != nil { + f17f0 := []*svcapitypes.DomainValidation{} + for _, f17f0iter := range resp.Certificate.RenewalSummary.DomainValidationOptions { + f17f0elem := &svcapitypes.DomainValidation{} + if f17f0iter.DomainName != nil { + f17f0elem.DomainName = f17f0iter.DomainName + } + if f17f0iter.ResourceRecord != nil { + f17f0elemf1 := &svcapitypes.ResourceRecord{} + if f17f0iter.ResourceRecord.Name != nil { + f17f0elemf1.Name = f17f0iter.ResourceRecord.Name + } + if f17f0iter.ResourceRecord.Type != nil { + f17f0elemf1.Type = f17f0iter.ResourceRecord.Type + } + if f17f0iter.ResourceRecord.Value != nil { + f17f0elemf1.Value = f17f0iter.ResourceRecord.Value + } + f17f0elem.ResourceRecord = f17f0elemf1 + } + if f17f0iter.ValidationDomain != nil { + f17f0elem.ValidationDomain = f17f0iter.ValidationDomain + } + if f17f0iter.ValidationEmails != nil { + f17f0elemf3 := []*string{} + for _, f17f0elemf3iter := range f17f0iter.ValidationEmails { + var f17f0elemf3elem string + f17f0elemf3elem = *f17f0elemf3iter + f17f0elemf3 = append(f17f0elemf3, &f17f0elemf3elem) + } + f17f0elem.ValidationEmails = f17f0elemf3 + } + if f17f0iter.ValidationMethod != nil { + f17f0elem.ValidationMethod = f17f0iter.ValidationMethod + } + if f17f0iter.ValidationStatus != nil { + f17f0elem.ValidationStatus = f17f0iter.ValidationStatus + } + f17f0 = append(f17f0, f17f0elem) + } + f17.DomainValidationOptions = f17f0 + } + if resp.Certificate.RenewalSummary.RenewalStatus != nil { + f17.RenewalStatus = resp.Certificate.RenewalSummary.RenewalStatus + } + if resp.Certificate.RenewalSummary.RenewalStatusReason != nil { + f17.RenewalStatusReason = resp.Certificate.RenewalSummary.RenewalStatusReason + } + if resp.Certificate.RenewalSummary.UpdatedAt != nil { + f17.UpdatedAt = &metav1.Time{*resp.Certificate.RenewalSummary.UpdatedAt} + } + ko.Status.RenewalSummary = f17 + } else { + ko.Status.RenewalSummary = nil + } + if resp.Certificate.RevocationReason != nil { + ko.Status.RevocationReason = resp.Certificate.RevocationReason + } else { + ko.Status.RevocationReason = nil + } + if resp.Certificate.RevokedAt != nil { + ko.Status.RevokedAt = &metav1.Time{*resp.Certificate.RevokedAt} + } else { + ko.Status.RevokedAt = nil + } + if resp.Certificate.Serial != nil { + ko.Status.Serial = resp.Certificate.Serial + } else { + ko.Status.Serial = nil + } + if resp.Certificate.SignatureAlgorithm != nil { + ko.Status.SignatureAlgorithm = resp.Certificate.SignatureAlgorithm + } else { + ko.Status.SignatureAlgorithm = nil + } + if resp.Certificate.Status != nil { + ko.Status.Status = resp.Certificate.Status + } else { + ko.Status.Status = nil + } + if resp.Certificate.Subject != nil { + ko.Status.Subject = resp.Certificate.Subject + } else { + ko.Status.Subject = nil + } if resp.Certificate.SubjectAlternativeNames != nil { f24 := []*string{} for _, f24iter := range resp.Certificate.SubjectAlternativeNames { @@ -145,6 +312,11 @@ func (rm *resourceManager) sdkFind( } else { ko.Spec.SubjectAlternativeNames = nil } + if resp.Certificate.Type != nil { + ko.Status.Type = resp.Certificate.Type + } else { + ko.Status.Type = nil + } rm.setStatusDefaults(ko) return &resource{ko}, nil @@ -221,11 +393,6 @@ func (rm *resourceManager) sdkCreate( } rm.setStatusDefaults(ko) - // See note on https://docs.aws.amazon.com/acm/latest/APIReference/API_RequestCertificate.html - // about DescribeCertificate not being ready to call for several seconds - // after a successful RequestCertificate API call... - waitAfterSuccessfulCreate() - return &resource{ko}, nil } @@ -290,7 +457,6 @@ func (rm *resourceManager) newCreateRequestPayload( } res.SetTags(f6) } - res.SetValidationMethod("DNS") return res, nil } @@ -472,8 +638,13 @@ func (rm *resourceManager) updateConditions( recoverableCondition.Message = nil } } - // Required to avoid the "declared but not used" error in the default case - _ = syncCondition + if syncCondition == nil && onSuccess { + syncCondition = &ackv1alpha1.Condition{ + Type: ackv1alpha1.ConditionTypeResourceSynced, + Status: corev1.ConditionTrue, + } + ko.Status.Conditions = append(ko.Status.Conditions, syncCondition) + } if terminalCondition != nil || recoverableCondition != nil || syncCondition != nil { return &resource{ko}, true // updated } @@ -484,6 +655,21 @@ func (rm *resourceManager) updateConditions( // and if the exception indicates that it is a Terminal exception // 'Terminal' exception are specified in generator configuration func (rm *resourceManager) terminalAWSError(err error) bool { - // No terminal_errors specified for this resource in generator config - return false + if err == nil { + return false + } + awsErr, ok := ackerr.AWSError(err) + if !ok { + return false + } + switch awsErr.Code() { + case "InvalidParameter", + "InvalidDomainValidationOptionsException", + "InvalidTagException", + "TagPolicyException", + "TooManyTagsException": + return true + default: + return false + } } diff --git a/templates/hooks/certificate/sdk_create_post_set_output.go.tpl b/templates/hooks/certificate/sdk_create_post_set_output.go.tpl deleted file mode 100644 index 07a1602..0000000 --- a/templates/hooks/certificate/sdk_create_post_set_output.go.tpl +++ /dev/null @@ -1,4 +0,0 @@ - // See note on https://docs.aws.amazon.com/acm/latest/APIReference/API_RequestCertificate.html - // about DescribeCertificate not being ready to call for several seconds - // after a successful RequestCertificate API call... - waitAfterSuccessfulCreate() diff --git a/test/e2e/__init__.py b/test/e2e/__init__.py index cf157d7..90e775f 100644 --- a/test/e2e/__init__.py +++ b/test/e2e/__init__.py @@ -27,7 +27,7 @@ bootstrap_directory = Path(__file__).parent resource_directory = Path(__file__).parent / "resources" -def load_acm_resource(resource_name: str, additional_replacements: Dict[str, Any] = {}): +def load_resource(resource_name: str, additional_replacements: Dict[str, Any] = {}): """ Overrides the default `load_resource_file` to access the specific resources directory for the current service. """ diff --git a/test/e2e/certificate.py b/test/e2e/certificate.py new file mode 100644 index 0000000..6b6d9c6 --- /dev/null +++ b/test/e2e/certificate.py @@ -0,0 +1,134 @@ +# 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 Certificate resources""" + +import datetime +import time +import typing + +import boto3 +import pytest + +DEFAULT_WAIT_UNTIL_TIMEOUT_SECONDS = 60*30 +DEFAULT_WAIT_UNTIL_INTERVAL_SECONDS = 15 +DEFAULT_WAIT_UNTIL_DELETED_TIMEOUT_SECONDS = 60*10 +DEFAULT_WAIT_UNTIL_DELETED_INTERVAL_SECONDS = 15 + +CertificateMatchFunc = typing.NewType( + 'CertificateMatchFunc', + typing.Callable[[dict], bool], +) + +class StatusMatcher: + def __init__(self, status): + self.match_on = status + + def __call__(self, record: dict) -> bool: + return ('Status' in record + and record['Status'] == self.match_on) + + +def status_matches(status: str) -> CertificateMatchFunc: + return StatusMatcher(status) + + +def wait_until( + certificate_arn: str, + match_fn: CertificateMatchFunc, + timeout_seconds: int = DEFAULT_WAIT_UNTIL_TIMEOUT_SECONDS, + interval_seconds: int = DEFAULT_WAIT_UNTIL_INTERVAL_SECONDS, + ) -> None: + """Waits until a Certificate with a supplied ARN is returned from the ACM + API and the matching functor returns True. + + Usage: + from e2e.certificate import wait_until, status_matches + + wait_until( + certificate_arn, + status_matches("ISSUED"), + ) + + Raises: + pytest.fail upon timeout + """ + now = datetime.datetime.now() + timeout = now + datetime.timedelta(seconds=timeout_seconds) + + while not match_fn(get(certificate_arn)): + if datetime.datetime.now() >= timeout: + pytest.fail("failed to match Certificate before timeout") + time.sleep(interval_seconds) + + +def wait_until_deleted( + certificate_arn: str, + timeout_seconds: int = DEFAULT_WAIT_UNTIL_DELETED_TIMEOUT_SECONDS, + interval_seconds: int = DEFAULT_WAIT_UNTIL_DELETED_INTERVAL_SECONDS, + ) -> None: + """Waits until a Certificate with a supplied ID is no longer returned from + the ACM API. + + Usage: + from e2e.db_instance import wait_until_deleted + + wait_until_deleted(instance_id) + + Raises: + pytest.fail upon timeout or if the Certificate goes to any other status + other than 'deleting' + """ + now = datetime.datetime.now() + timeout = now + datetime.timedelta(seconds=timeout_seconds) + + while True: + if datetime.datetime.now() >= timeout: + pytest.fail( + "Timed out waiting for Certificate to be " + "deleted in ACM API" + ) + time.sleep(interval_seconds) + + latest = get(certificate_arn) + if latest is None: + break + + +def get(certificate_arn): + """Returns a dict containing the Certificate record from the ACM API. + + If no such Certificate exists, returns None. + """ + c = boto3.client('acm') + try: + resp = c.describe_certificate(CertificateArn=certificate_arn) + return resp['Certificate'] + except c.exceptions.ResourceNotFoundException: + return None + + +def get_tags(db_instance_arn): + """Returns a dict containing the Certificate's tag records from the ACM + API. + + If no such Certificate exists, returns None. + """ + c = boto3.client('rds') + try: + resp = c.list_tags_for_resource( + ResourceName=db_instance_arn, + ) + return resp['TagList'] + except c.exceptions.DBCertificateNotFoundFault: + return None diff --git a/test/e2e/condition.py b/test/e2e/condition.py new file mode 100644 index 0000000..3d92b5f --- /dev/null +++ b/test/e2e/condition.py @@ -0,0 +1,145 @@ +# 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. + +"""Utility functions to help processing Kubernetes resource conditions""" + +# TODO(jaypipes): Move these functions to acktest library. The reason these are +# here is because the existing k8s.assert_condition_state_message doesn't +# actually assert anything. It returns true or false and logs messages. + +import pytest + +from acktest.k8s import resource as k8s + +CONDITION_TYPE_ADOPTED = "ACK.Adopted" +CONDITION_TYPE_RESOURCE_SYNCED = "ACK.ResourceSynced" +CONDITION_TYPE_TERMINAL = "ACK.Terminal" +CONDITION_TYPE_RECOVERABLE = "ACK.Recoverable" +CONDITION_TYPE_ADVISORY = "ACK.Advisory" +CONDITION_TYPE_LATE_INITIALIZED = "ACK.LateInitialized" + + +def assert_type_status( + ref: k8s.CustomResourceReference, + cond_type_match: str = CONDITION_TYPE_RESOURCE_SYNCED, + cond_status_match: bool = True, +): + """Asserts that the supplied resource has a condition of type + ACK.ResourceSynced and that the Status of this condition is True. + + Usage: + from acktest.k8s import resource as k8s + + from e2e import condition + + ref = k8s.CustomResourceReference( + CRD_GROUP, CRD_VERSION, RESOURCE_PLURAL, + db_cluster_id, namespace="default", + ) + k8s.create_custom_resource(ref, resource_data) + k8s.wait_resource_consumed_by_controller(ref) + condition.assert_type_status( + ref, + condition.CONDITION_TYPE_RESOURCE_SYNCED, + False) + + Raises: + pytest.fail when condition of the specified type is not found or is not + in the supplied status. + """ + cond = k8s.get_resource_condition(ref, cond_type_match) + if cond is None: + msg = (f"Failed to find {cond_type_match} condition in " + f"resource {ref}") + pytest.fail(msg) + + cond_status = cond.get('status', None) + if str(cond_status) != str(cond_status_match): + msg = (f"Expected {cond_type_match} condition to " + f"have status {cond_status_match} but found {cond_status}") + pytest.fail(msg) + + +def assert_synced_status( + ref: k8s.CustomResourceReference, + cond_status_match: bool, +): + """Asserts that the supplied resource has a condition of type + ACK.ResourceSynced and that the Status of this condition is True. + + Usage: + from acktest.k8s import resource as k8s + + from e2e import condition + + ref = k8s.CustomResourceReference( + CRD_GROUP, CRD_VERSION, RESOURCE_PLURAL, + db_cluster_id, namespace="default", + ) + k8s.create_custom_resource(ref, resource_data) + k8s.wait_resource_consumed_by_controller(ref) + condition.assert_synced_status(ref, False) + + Raises: + pytest.fail when ACK.ResourceSynced condition is not found or is not in + a True status. + """ + assert_type_status(ref, CONDITION_TYPE_RESOURCE_SYNCED, cond_status_match) + + +def assert_synced(ref: k8s.CustomResourceReference): + """Asserts that the supplied resource has a condition of type + ACK.ResourceSynced and that the Status of this condition is True. + + Usage: + from acktest.k8s import resource as k8s + + from e2e import condition + + ref = k8s.CustomResourceReference( + CRD_GROUP, CRD_VERSION, RESOURCE_PLURAL, + db_cluster_id, namespace="default", + ) + k8s.create_custom_resource(ref, resource_data) + k8s.wait_resource_consumed_by_controller(ref) + condition.assert_synced(ref) + + Raises: + pytest.fail when ACK.ResourceSynced condition is not found or is not in + a True status. + """ + return assert_synced_status(ref, True) + + +def assert_not_synced(ref: k8s.CustomResourceReference): + """Asserts that the supplied resource has a condition of type + ACK.ResourceSynced and that the Status of this condition is False. + + Usage: + from acktest.k8s import resource as k8s + + from e2e import condition + + ref = k8s.CustomResourceReference( + CRD_GROUP, CRD_VERSION, RESOURCE_PLURAL, + db_cluster_id, namespace="default", + ) + k8s.create_custom_resource(ref, resource_data) + k8s.wait_resource_consumed_by_controller(ref) + condition.assert_not_synced(ref) + + Raises: + pytest.fail when ACK.ResourceSynced condition is not found or is not in + a False status. + """ + return assert_synced_status(ref, False) diff --git a/test/e2e/resources/certificate_public.yaml b/test/e2e/resources/certificate_public.yaml new file mode 100644 index 0000000..333a138 --- /dev/null +++ b/test/e2e/resources/certificate_public.yaml @@ -0,0 +1,11 @@ +apiVersion: acm.services.k8s.aws/v1alpha1 +kind: Certificate +metadata: + name: $CERTIFICATE_NAME +spec: + domainName: $DOMAIN_NAME + # NOTE(jaypipes): Having an empty certificateAuthorityARN field indicates + # that this is a public certificate request... + tags: + - key: environment + value: dev diff --git a/test/e2e/tests/test_certificate.py b/test/e2e/tests/test_certificate.py new file mode 100644 index 0000000..98bc117 --- /dev/null +++ b/test/e2e/tests/test_certificate.py @@ -0,0 +1,126 @@ +# 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 ACM API Certificate resource +""" + +import time + +import pytest + +from acktest.k8s import resource as k8s +from acktest.resources import random_suffix_name +from e2e import service_marker, CRD_GROUP, CRD_VERSION, load_resource +from e2e.replacement_values import REPLACEMENT_VALUES +from e2e import certificate + +RESOURCE_PLURAL = 'certificates' + +# NOTE(jaypipes): requeue_on_success_seconds = 60 for certificates, and in the +# tests we check for Status.Status, which will only appear after a successful +# Describe +CREATE_WAIT_AFTER_SECONDS = 65 +FAILED_WAIT_AFTER_SECONDS = 60 +DELETE_WAIT_AFTER_SECONDS = 30 + +# Time we wait for the certificate to get to ACK.ResourceSynced=True +MAX_WAIT_FOR_SYNCED_MINUTES = 1 + + +@pytest.fixture +def certificate_public(): + certificate_name = random_suffix_name("certificate", 20) + domain_name = "example.com" + + replacements = REPLACEMENT_VALUES.copy() + replacements['CERTIFICATE_NAME'] = certificate_name + replacements['DOMAIN_NAME'] = domain_name + + resource_data = load_resource( + "certificate_public", + additional_replacements=replacements, + ) + + # Create the k8s resource + ref = k8s.CustomResourceReference( + CRD_GROUP, CRD_VERSION, RESOURCE_PLURAL, + certificate_name, namespace="default", + ) + k8s.create_custom_resource(ref, resource_data) + cr = k8s.wait_resource_consumed_by_controller(ref) + + assert cr is not None + assert k8s.get_resource_exists(ref) + + time.sleep(CREATE_WAIT_AFTER_SECONDS) + + yield (ref, cr) + + # Try to delete, if doesn't already exist + try: + _, deleted = k8s.delete_custom_resource(ref, 3, 10) + assert deleted + certificate.wait_until_deleted(certificate_arn) + except: + pass + + +@service_marker +@pytest.mark.canary +class TestCertificate: + def test_crud_public( + self, + certificate_public, + ): + (ref, cr) = certificate_public + assert "status" in cr + assert "ackResourceMetadata" in cr["status"] + assert "arn" in cr["status"]["ackResourceMetadata"] + certificate_arn = cr["status"]["ackResourceMetadata"]["arn"] + + assert 'status' in cr['status'] + # NOTE(jaypipes): The certificate request will quickly transition from + # PENDING_VALIDATION to FAILED, so this just checks to make sure we're + # in one of those states... + assert cr['status']['status'] in ['PENDING_VALIDATION', 'FAILED'] + + # Wait for the resource to get synced + assert k8s.wait_on_condition( + ref, + "ACK.ResourceSynced", + "True", + wait_periods=MAX_WAIT_FOR_SYNCED_MINUTES, + ) + + # NOTE(jaypipes): The domain name is example.com, which will cause the + # certificate to transition to a FAILED status due to additional + # verification being needed. + certificate.wait_until( + certificate_arn, + certificate.status_matches("FAILED"), + ) + + time.sleep(FAILED_WAIT_AFTER_SECONDS) + + # The corresponding CR should be updated to a FAILED status as well + # because we have requeue_on_success_seconds = 60... + cr = k8s.get_resource(ref) + assert "status" in cr + assert 'status' in cr['status'] + assert cr['status']['status'] == 'FAILED' + + k8s.delete_custom_resource(ref) + + time.sleep(DELETE_WAIT_AFTER_SECONDS) + + certificate.wait_until_deleted(certificate_arn)