diff --git a/api/v1beta1/externaldns_types.go b/api/v1beta1/externaldns_types.go index bcc67cd0..21e9e0f7 100644 --- a/api/v1beta1/externaldns_types.go +++ b/api/v1beta1/externaldns_types.go @@ -257,6 +257,14 @@ type ExternalDNSAWSProviderOptions struct { // +kubebuilder:validation:Required // +required Credentials SecretReference `json:"credentials"` + + // RoleARN contains the ARN of a IAM role that will be assumed when using the AWS API. + // It provides the ability to use a hosted zone in another AWS account. + // + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Pattern:=`^arn:(aws|aws-cn|aws-us-gov):iam::[0-9]{12}:role\/.*$` + // +optional + RoleARN *string `json:"roleARN,omitempty"` // TODO: Additionally support access for: // - kiam/kube2iam enabled clusters ("iam.amazonaws.com/role" POD's annotation to assume IAM role) // - EKS clusters ("eks.amazonaws.com/role-arn" ServiceAccount's annotation to assume IAM role) diff --git a/api/v1beta1/webhook_test.go b/api/v1beta1/webhook_test.go index c5b7097f..9187b4e0 100644 --- a/api/v1beta1/webhook_test.go +++ b/api/v1beta1/webhook_test.go @@ -175,6 +175,31 @@ var _ = Describe("ExternalDNS admission webhook", func() { Expect(err).ShouldNot(Succeed()) Expect(err.Error()).Should(ContainSubstring("credentials secret must be specified when provider type is AWS")) }) + It("valid RoleARN", func() { + resource := makeExternalDNS("test-valid-rolearn", nil) + resource.Spec.Provider = ExternalDNSProvider{ + Type: ProviderTypeAWS, + AWS: &ExternalDNSAWSProviderOptions{ + RoleARN: pointer.String("arn:aws:iam::123456789012:role/foo"), + Credentials: SecretReference{Name: "credentials"}, + }, + } + err := k8sClient.Create(context.Background(), resource) + Expect(err).Should(Succeed()) + }) + It("invalid RoleARN rejected", func() { + resource := makeExternalDNS("test-invalid-rolearn", nil) + resource.Spec.Provider = ExternalDNSProvider{ + Type: ProviderTypeAWS, + AWS: &ExternalDNSAWSProviderOptions{ + RoleARN: pointer.String("arn:aws:iam:bad:123456789012:role/foo"), + Credentials: SecretReference{Name: "credentials"}, + }, + } + err := k8sClient.Create(context.Background(), resource) + Expect(err).ShouldNot(Succeed()) + Expect(err.Error()).Should(ContainSubstring(`ExternalDNS.externaldns.olm.openshift.io "test-invalid-rolearn" is invalid: spec.provider.aws.roleARN: Invalid value: "arn:aws:iam:bad:123456789012:role/foo": spec.provider.aws.roleARN in body should match '^arn:(aws|aws-cn|aws-us-gov):iam::[0-9]{12}:role\/.*$'`)) + }) }) Context("resource with Azure provider", func() { diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 22383946..075b580c 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -58,6 +58,11 @@ func (in *ExternalDNS) DeepCopyObject() runtime.Object { func (in *ExternalDNSAWSProviderOptions) DeepCopyInto(out *ExternalDNSAWSProviderOptions) { *out = *in out.Credentials = in.Credentials + if in.RoleARN != nil { + in, out := &in.RoleARN, &out.RoleARN + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalDNSAWSProviderOptions. @@ -253,7 +258,7 @@ func (in *ExternalDNSProvider) DeepCopyInto(out *ExternalDNSProvider) { if in.AWS != nil { in, out := &in.AWS, &out.AWS *out = new(ExternalDNSAWSProviderOptions) - **out = **in + (*in).DeepCopyInto(*out) } if in.GCP != nil { in, out := &in.GCP, &out.GCP diff --git a/config/crd/bases/externaldns.olm.openshift.io_externaldnses.yaml b/config/crd/bases/externaldns.olm.openshift.io_externaldnses.yaml index 4c46596d..13da05cd 100644 --- a/config/crd/bases/externaldns.olm.openshift.io_externaldnses.yaml +++ b/config/crd/bases/externaldns.olm.openshift.io_externaldnses.yaml @@ -584,6 +584,12 @@ spec: required: - name type: object + roleARN: + description: RoleARN contains the ARN of a IAM role that will + be assumed when using the AWS API. It provides the ability + to use a hosted zone in another AWS account. + pattern: ^arn:(aws|aws-cn|aws-us-gov):iam::[0-9]{12}:role\/.*$ + type: string required: - credentials type: object diff --git a/pkg/operator/controller/externaldns/deployment_test.go b/pkg/operator/controller/externaldns/deployment_test.go index 79ec8d59..75abf22d 100644 --- a/pkg/operator/controller/externaldns/deployment_test.go +++ b/pkg/operator/controller/externaldns/deployment_test.go @@ -1783,6 +1783,55 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { }, }, }, + { + name: "RoleARN set AWS Route", + inputExternalDNS: testAWSExternalDNSRoleARN(operatorv1beta1.SourceTypeRoute, "arn:aws:iam:123456789012:role/foo"), + expectedTemplatePodSpec: corev1.PodSpec{ + ServiceAccountName: test.OperandName, + NodeSelector: map[string]string{ + osLabel: linuxOS, + masterNodeRoleLabel: "", + }, + Tolerations: []corev1.Toleration{ + { + Key: masterNodeRoleLabel, + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoSchedule, + }, + }, + Containers: []corev1.Container{ + { + Name: ExternalDNSContainerName, + Image: test.OperandImage, + Args: []string{ + "--aws-assume-role=arn:aws:iam:123456789012:role/foo", + "--metrics-address=127.0.0.1:7979", + "--txt-owner-id=external-dns-test", + "--zone-id-filter=my-dns-public-zone", + "--provider=aws", + "--source=openshift-route", + "--policy=sync", + "--registry=txt", + "--log-level=debug", + "--ignore-hostname-annotation", + `--fqdn-template={{""}}`, + "--txt-prefix=external-dns-", + }, + SecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{allCapabilities}, + }, + Privileged: pointer.Bool(false), + RunAsNonRoot: pointer.Bool(true), + AllowPrivilegeEscalation: pointer.Bool(false), + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + }, + }, + }, + }, + }, + }, { name: "Nominal Azure Route", inputSecretName: azureSecret, @@ -5317,6 +5366,14 @@ func testAWSExternalDNSDomainFilter(zones []string, source operatorv1beta1.Exter return extdns } +func testAWSExternalDNSRoleARN(source operatorv1beta1.ExternalDNSSourceType, roleARN string) *operatorv1beta1.ExternalDNS { + extdns := testCreateDNSFromSourceWRTCloudProvider(source, operatorv1beta1.ProviderTypeAWS, nil, "") + extdns.Spec.Provider.AWS = &operatorv1beta1.ExternalDNSAWSProviderOptions{ + RoleARN: pointer.String(roleARN), + } + return extdns +} + func testPlatformStatusGCP(projectID string) *configv1.PlatformStatus { return &configv1.PlatformStatus{ Type: configv1.GCPPlatformType, diff --git a/pkg/operator/controller/externaldns/pod.go b/pkg/operator/controller/externaldns/pod.go index 237eeec5..e64096c4 100644 --- a/pkg/operator/controller/externaldns/pod.go +++ b/pkg/operator/controller/externaldns/pod.go @@ -194,6 +194,10 @@ func (b *externalDNSContainerBuilder) fillProviderAgnosticFields(seq int, zone s args = append(args, "--ignore-hostname-annotation") } + if b.externalDNS.Spec.Provider.AWS != nil && b.externalDNS.Spec.Provider.AWS.RoleARN != nil { + args = append(args, fmt.Sprintf("--aws-assume-role=%s", *b.externalDNS.Spec.Provider.AWS.RoleARN)) + } + if len(b.externalDNS.Spec.Source.FQDNTemplate) > 0 { args = append(args, fmt.Sprintf("--fqdn-template=%s", strings.Join(b.externalDNS.Spec.Source.FQDNTemplate, ","))) } else {