diff --git a/apis/acm.aws/v1alpha1/certificate_types.go b/apis/acm.aws/v1alpha1/certificate_types.go index 714636c..15fa564 100644 --- a/apis/acm.aws/v1alpha1/certificate_types.go +++ b/apis/acm.aws/v1alpha1/certificate_types.go @@ -35,6 +35,10 @@ type CertificateSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file + // Region where to create the AWS resource. If not specified, the value of AWS_REGION is used. + // +kubebuilder:validation:Optional + Region string `json:"region"` + // DomainName is the fully qualified domain name (fqdn) used to create the AWS ACM Certificate. // +kubebuilder:validation:Required DomainName string `json:"domainName"` diff --git a/config/crd/bases/acm.aws.aeto.net_certificates.yaml b/config/crd/bases/acm.aws.aeto.net_certificates.yaml index e9fa368..af07063 100644 --- a/config/crd/bases/acm.aws.aeto.net_certificates.yaml +++ b/config/crd/bases/acm.aws.aeto.net_certificates.yaml @@ -59,6 +59,10 @@ spec: description: DomainName is the fully qualified domain name (fqdn) used to create the AWS ACM Certificate. type: string + region: + description: Region where to create the AWS resource. If not specified, + the value of AWS_REGION is used. + type: string tags: additionalProperties: type: string diff --git a/controllers/acm.aws/certificate_controller.go b/controllers/acm.aws/certificate_controller.go index 51cdcf8..b0f8119 100644 --- a/controllers/acm.aws/certificate_controller.go +++ b/controllers/acm.aws/certificate_controller.go @@ -111,9 +111,9 @@ func (r *CertificateReconciler) Reconcile(ctx context.Context, req ctrl.Request) func (r *CertificateReconciler) reconcileCertificate(ctx reconcile.Context, certificate acmawsv1alpha1.Certificate) (*acmtypes.CertificateDetail, reconcile.Result) { ca := "" - certs, err := r.AWS.FindAcmCertificatesByDomainName(ctx.Context, certificate.Spec.DomainName) + certs, err := r.AWS.FindAcmCertificatesByDomainName(ctx.Context, certificate.Spec.Region, certificate.Spec.DomainName) if err != nil { - ctx.Log.Error(err, "failed to find AWS ACM Certificate summaries matching domain name", "domain-name", certificate.Spec.DomainName) + ctx.Log.Error(err, "failed to find AWS ACM Certificate summaries matching domain name", "region", r.AWS.Region(certificate.Spec.Region), "domain-name", certificate.Spec.DomainName) return nil, ctx.Error(err) } @@ -124,14 +124,14 @@ func (r *CertificateReconciler) reconcileCertificate(ctx reconcile.Context, cert matchedArns := make([]string, 0) for _, cs := range certs { - tags, err := r.AWS.ListAcmCertificateTagsByArn(ctx.Context, *cs.CertificateArn) + tags, err := r.AWS.ListAcmCertificateTagsByArn(ctx.Context, certificate.Spec.Region, *cs.CertificateArn) if err != nil { var rnfe *acmtypes.ResourceNotFoundException if errors.As(err, &rnfe) { - ctx.Log.Info("AWS ACM Certificate tags not found", "arn", *cs.CertificateArn) + ctx.Log.Info("AWS ACM Certificate tags not found", "region", r.AWS.Region(certificate.Spec.Region), "arn", *cs.CertificateArn) return nil, ctx.RequeueIn(5, fmt.Sprintf("AWS ACM Certificate tags not found for arn %s", *cs.CertificateArn)) } else { - ctx.Log.Error(err, "failed to fetch AWS ACM Certificate tags", "arn", *cs.CertificateArn) + ctx.Log.Error(err, "failed to fetch AWS ACM Certificate tags", "region", r.AWS.Region(certificate.Spec.Region), "arn", *cs.CertificateArn) return nil, ctx.Error(err) } } @@ -169,10 +169,10 @@ func (r *CertificateReconciler) reconcileCertificate(ctx reconcile.Context, cert // No arn set, try and create it if ca == "" { - ctx.Log.Info("creating a new AWS ACM Certificate", "domain-name", certificate.Spec.DomainName) + ctx.Log.Info("creating a new AWS ACM Certificate", "region", r.AWS.Region(certificate.Spec.Region), "domain-name", certificate.Spec.DomainName) arn, err := r.newAcmCertificate(ctx, certificate, certificateTags) if err != nil { - ctx.Log.Error(err, "failed to create AWS ACM Certificate", "domain-name", certificate.Spec.DomainName) + ctx.Log.Error(err, "failed to create AWS ACM Certificate", "region", r.AWS.Region(certificate.Spec.Region), "domain-name", certificate.Spec.DomainName) return nil, ctx.Error(err) } ca = arn @@ -180,22 +180,22 @@ func (r *CertificateReconciler) reconcileCertificate(ctx reconcile.Context, cert // Arn set, get certificate and set tags if ca != "" { - cd, err := r.AWS.GetAcmCertificateDetailsByArn(ctx.Context, ca) + cd, err := r.AWS.GetAcmCertificateDetailsByArn(ctx.Context, certificate.Spec.Region, ca) if err != nil { var rnfe *acmtypes.ResourceNotFoundException if errors.As(err, &rnfe) { ctx.Log.Info("AWS ACM Certificate details not found", "arn", ca) return nil, ctx.RequeueIn(5, fmt.Sprintf("AWS ACM Certificate details not found for arn %s", ca)) } else { - ctx.Log.Error(err, "failed to fetch AWS ACM Certificate details", "arn", ca) + ctx.Log.Error(err, "failed to fetch AWS ACM Certificate details", "region", r.AWS.Region(certificate.Spec.Region), "arn", ca) return nil, ctx.Error(err) } } - ctx.Log.V(1).Info("reconciling tags for AWS ACM Certificate", "domain-name", cd.DomainName, "arn", cd.CertificateArn, "tags", certificate.Spec.Tags) - err = r.AWS.SetAcmCertificateTagsByArn(ctx.Context, ca, certificateTags) + ctx.Log.V(1).Info("reconciling tags for AWS ACM Certificate", "region", r.AWS.Region(certificate.Spec.Region), "domain-name", cd.DomainName, "arn", cd.CertificateArn, "tags", certificate.Spec.Tags) + err = r.AWS.SetAcmCertificateTagsByArn(ctx.Context, certificate.Spec.Region, ca, certificateTags) if err != nil { - ctx.Log.Error(err, "failed to reconcile tags for AWS ACM Certificate", "domain-name", certificate.Spec.DomainName, "arn", *cd.CertificateArn, "tags", certificate.Spec.Tags) + ctx.Log.Error(err, "failed to reconcile tags for AWS ACM Certificate", "region", r.AWS.Region(certificate.Spec.Region), "domain-name", certificate.Spec.DomainName, "arn", *cd.CertificateArn, "tags", certificate.Spec.Tags) return nil, ctx.Error(err) } @@ -216,7 +216,7 @@ func (r *CertificateReconciler) reconcileCertificateValidation(ctx reconcile.Con return r.reconcileCertificateDnsValidationRecord(ctx, certificate.Spec.Validation.Dns.HostedZonedId, *details) } - ctx.Log.Info("unhandled status for AWS ACM Certificate", "status", details.Status, "arn", *details.CertificateArn) + ctx.Log.Info("unhandled status for AWS ACM Certificate", "status", details.Status, "region", r.AWS.Region(certificate.Spec.Region), "arn", *details.CertificateArn) } return ctx.Done() @@ -271,14 +271,14 @@ func (r *CertificateReconciler) reconcileDelete(ctx reconcile.Context, certifica return ctx.Done() } - cd, err := r.AWS.GetAcmCertificateDetailsByArn(ctx.Context, ca) + cd, err := r.AWS.GetAcmCertificateDetailsByArn(ctx.Context, certificate.Spec.Region, ca) if err != nil { var rnfe *acmtypes.ResourceNotFoundException if errors.As(err, &rnfe) { - ctx.Log.Info("AWS ACM Certificate details not found", "arn", ca) + ctx.Log.Info("AWS ACM Certificate details not found", "region", r.AWS.Region(certificate.Spec.Region), "arn", ca) return ctx.Done() } else { - ctx.Log.Error(err, "failed to fetch AWS ACM Certificate", "arn", ca) + ctx.Log.Error(err, "failed to fetch AWS ACM Certificate", "region", r.AWS.Region(certificate.Spec.Region), "arn", ca) return ctx.Error(err) } } @@ -299,7 +299,7 @@ func (r *CertificateReconciler) reconcileDelete(ctx reconcile.Context, certifica TTL: aws.Int64(300), } - ctx.Log.Info("deleting Domain Validation record for AWS ACM Certificate", "arn", ca, "hosted-zone-id", certificate.Spec.Validation.Dns.HostedZonedId, "recordset", recordSet) + ctx.Log.Info("deleting Domain Validation record for AWS ACM Certificate", "region", r.AWS.Region(certificate.Spec.Region), "arn", ca, "hosted-zone-id", certificate.Spec.Validation.Dns.HostedZonedId, "recordset", recordSet) err := r.AWS.DeleteRoute53ResourceRecordSet(ctx.Context, certificate.Spec.Validation.Dns.HostedZonedId, recordSet, "deleting DNS validation record for AWS ACM Certificate") if err != nil { var oe *smithy.OperationError @@ -325,8 +325,8 @@ func (r *CertificateReconciler) reconcileDelete(ctx reconcile.Context, certifica } // Certificate - ctx.Log.Info("deleting AWS ACM Certificate", "arn", ca) - _, err = r.AWS.Acm.DeleteCertificate(ctx.Context, &acm.DeleteCertificateInput{ + ctx.Log.Info("deleting AWS ACM Certificate", "region", r.AWS.Region(certificate.Spec.Region), "arn", ca) + _, err = r.AWS.Acm(certificate.Spec.Region).DeleteCertificate(ctx.Context, &acm.DeleteCertificateInput{ CertificateArn: aws.String(ca), }) if err != nil { @@ -334,7 +334,7 @@ func (r *CertificateReconciler) reconcileDelete(ctx reconcile.Context, certifica if errors.As(err, &riue) { return ctx.RequeueIn(15, fmt.Sprintf("failed to delete AWS ACM Certificate %s as it is currently in use", ca)) } - ctx.Log.Error(err, "failed to delete AWS ACM Certificate", "arn", ca) + ctx.Log.Error(err, "failed to delete AWS ACM Certificate", "region", r.AWS.Region(certificate.Spec.Region), "arn", ca) return ctx.Error(err) } @@ -403,7 +403,7 @@ func (r *CertificateReconciler) newAcmCertificate(ctx reconcile.Context, certifi req.Tags = tags } - res, err := r.AWS.Acm.RequestCertificate(ctx.Context, req) + res, err := r.AWS.Acm(certificate.Spec.Region).RequestCertificate(ctx.Context, req) if err != nil { return "", err } diff --git a/internal/pkg/aws/clients.go b/internal/pkg/aws/clients.go index f0e55ed..af0f1fe 100644 --- a/internal/pkg/aws/clients.go +++ b/internal/pkg/aws/clients.go @@ -7,6 +7,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/acm" acmtypes "github.com/aws/aws-sdk-go-v2/service/acm/types" "github.com/aws/aws-sdk-go-v2/service/route53" @@ -17,10 +18,23 @@ import ( // Clients wrapper for AWS type Clients struct { Log logr.Logger - Acm *acm.Client + Config aws.Config Route53 *route53.Client } +func (c Clients) Region(region string) string { + if region != "" { + return region + } + return c.Config.Region +} + +func (c Clients) Acm(region string) *acm.Client { + region = c.Region(region) + cfg, _ := config.LoadDefaultConfig(context.TODO(), config.WithRegion(region)) + return acm.NewFromConfig(cfg) +} + var unescaper = strings.NewReplacer(`\057`, "/", `\052`, "*") type UniqueConstraintException struct { @@ -28,12 +42,12 @@ type UniqueConstraintException struct { } func (e *UniqueConstraintException) Error() string { - return "boom" + return e.message } // FindOneAcmCertificateByDomainName returns the first matching ACM Certificate by domain name -func (c Clients) FindOneAcmCertificateByDomainName(ctx context.Context, domainName string) (*acmtypes.CertificateSummary, error) { - items, err := c.ListAcmCertificates(ctx) +func (c Clients) FindOneAcmCertificateByDomainName(ctx context.Context, region string, domainName string) (*acmtypes.CertificateSummary, error) { + items, err := c.ListAcmCertificates(ctx, region) if err != nil { return nil, err } @@ -61,8 +75,8 @@ func (c Clients) FindOneAcmCertificateByDomainName(ctx context.Context, domainNa } // FindAcmCertificatesByDomainName returns the first matching ACM Certificate by domain name -func (c Clients) FindAcmCertificatesByDomainName(ctx context.Context, domainName string) ([]acmtypes.CertificateSummary, error) { - items, err := c.ListAcmCertificates(ctx) +func (c Clients) FindAcmCertificatesByDomainName(ctx context.Context, region string, domainName string) ([]acmtypes.CertificateSummary, error) { + items, err := c.ListAcmCertificates(ctx, region) if err != nil { return nil, err } @@ -80,8 +94,8 @@ func (c Clients) FindAcmCertificatesByDomainName(ctx context.Context, domainName } // GetAcmCertificateDetailsByArn returns ACM Certificate details by ARN -func (c Clients) GetAcmCertificateDetailsByArn(ctx context.Context, arn string) (acmtypes.CertificateDetail, error) { - res, err := c.Acm.DescribeCertificate(ctx, &acm.DescribeCertificateInput{ +func (c Clients) GetAcmCertificateDetailsByArn(ctx context.Context, region string, arn string) (acmtypes.CertificateDetail, error) { + res, err := c.Acm(region).DescribeCertificate(ctx, &acm.DescribeCertificateInput{ CertificateArn: aws.String(arn), }) if err != nil { @@ -92,10 +106,10 @@ func (c Clients) GetAcmCertificateDetailsByArn(ctx context.Context, arn string) } // ListAcmCertificates returns all ACM Certificates -func (c Clients) ListAcmCertificates(ctx context.Context) ([]acmtypes.CertificateSummary, error) { +func (c Clients) ListAcmCertificates(ctx context.Context, region string) ([]acmtypes.CertificateSummary, error) { items := make([]acmtypes.CertificateSummary, 0) - paginator := acm.NewListCertificatesPaginator(c.Acm, &acm.ListCertificatesInput{ + paginator := acm.NewListCertificatesPaginator(c.Acm(region), &acm.ListCertificatesInput{ MaxItems: aws.Int32(5), }) for paginator.HasMorePages() { @@ -110,8 +124,9 @@ func (c Clients) ListAcmCertificates(ctx context.Context) ([]acmtypes.Certificat } // SetAcmCertificateTagsByArn adds, removes and updates tags for the ACM Certificate by ARN -func (c Clients) SetAcmCertificateTagsByArn(ctx context.Context, arn string, tags map[string]string) error { - tagsRes, err := c.Acm.ListTagsForCertificate(ctx, &acm.ListTagsForCertificateInput{ +func (c Clients) SetAcmCertificateTagsByArn(ctx context.Context, region string, arn string, tags map[string]string) error { + rc := c.Acm(region) + tagsRes, err := rc.ListTagsForCertificate(ctx, &acm.ListTagsForCertificateInput{ CertificateArn: aws.String(arn), }) if err != nil { @@ -139,7 +154,7 @@ func (c Clients) SetAcmCertificateTagsByArn(ctx context.Context, arn string, tag } if len(remove) > 0 { - _, err := c.Acm.RemoveTagsFromCertificate(ctx, &acm.RemoveTagsFromCertificateInput{ + _, err := rc.RemoveTagsFromCertificate(ctx, &acm.RemoveTagsFromCertificateInput{ CertificateArn: aws.String(arn), Tags: remove, }) @@ -149,7 +164,7 @@ func (c Clients) SetAcmCertificateTagsByArn(ctx context.Context, arn string, tag } if len(add) > 0 { - _, err := c.Acm.AddTagsToCertificate(ctx, &acm.AddTagsToCertificateInput{ + _, err := rc.AddTagsToCertificate(ctx, &acm.AddTagsToCertificateInput{ CertificateArn: aws.String(arn), Tags: add, }) @@ -162,9 +177,9 @@ func (c Clients) SetAcmCertificateTagsByArn(ctx context.Context, arn string, tag } // ListAcmCertificateTagsByArn lists tags for the ACM Certificate by ARN -func (c Clients) ListAcmCertificateTagsByArn(ctx context.Context, arn string) (tags map[string]string, err error) { +func (c Clients) ListAcmCertificateTagsByArn(ctx context.Context, region string, arn string) (tags map[string]string, err error) { tags = make(map[string]string) - tagsRes, err := c.Acm.ListTagsForCertificate(ctx, &acm.ListTagsForCertificateInput{ + tagsRes, err := c.Acm(region).ListTagsForCertificate(ctx, &acm.ListTagsForCertificateInput{ CertificateArn: aws.String(arn), }) if err != nil { diff --git a/main.go b/main.go index ead402c..88fdb01 100644 --- a/main.go +++ b/main.go @@ -25,7 +25,6 @@ import ( "time" awsconfig "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/service/acm" "github.com/aws/aws-sdk-go-v2/service/route53" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) @@ -155,7 +154,7 @@ func main() { awsClients := aws.Clients{ Log: ctrl.Log.WithName("aws-client"), - Acm: acm.NewFromConfig(awsConfig), + Config: awsConfig, Route53: route53.NewFromConfig(awsConfig), }