Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NE-1323: Add AWS RoleARN for Shared VPC support #195

Merged
merged 1 commit into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions api/v1beta1/externaldns_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,9 +257,14 @@ type ExternalDNSAWSProviderOptions struct {
// +kubebuilder:validation:Required
// +required
Credentials SecretReference `json:"credentials"`
// 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)

// assumeRole is a reference to the IAM role that
// ExternalDNS will be assuming in order to perform
// any DNS updates.
//
// +kubebuilder:validation:Optional
gcs278 marked this conversation as resolved.
Show resolved Hide resolved
// +optional
AssumeRole *ExternalDNSAWSAssumeRoleOptions `json:"assumeRole,omitempty"`
}

type ExternalDNSGCPProviderOptions struct {
Expand Down Expand Up @@ -485,6 +490,15 @@ const (
HostnameAnnotationPolicyAllow HostnameAnnotationPolicy = "Allow"
)

type ExternalDNSAWSAssumeRoleOptions struct {
// arn is an AWS role ARN that the ExternalDNS
// operator will assume when making DNS updates.
//
// +kubebuilder:validation:Required
// +required
ARN string `json:"arn,omitempty"`
}

// ExternalDNSServiceSourceOptions describes options
// specific to the ExternalDNS service source.
type ExternalDNSServiceSourceOptions struct {
Expand Down
12 changes: 12 additions & 0 deletions api/v1beta1/externaldns_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"
"regexp"

"github.com/aws/aws-sdk-go/aws/arn"
"k8s.io/apimachinery/pkg/runtime"

utilErrors "k8s.io/apimachinery/pkg/util/errors"
Expand Down Expand Up @@ -71,6 +72,7 @@ func (r *ExternalDNS) validate(old runtime.Object) error {
r.validateSources(old),
r.validateHostnameAnnotationPolicy(),
r.validateProviderCredentials(),
r.validateAWSRoleARN(),
})
}

Expand Down Expand Up @@ -156,3 +158,13 @@ func (r *ExternalDNS) validateProviderCredentials() error {
}
return nil
}

func (r *ExternalDNS) validateAWSRoleARN() error {
// Ensure we have a valid arn if it is specified.
provider := r.Spec.Provider
if provider.AWS != nil && provider.AWS.AssumeRole != nil && !arn.IsARN(provider.AWS.AssumeRole.ARN) {
return fmt.Errorf("arn %q is not a valid AWS ARN", provider.AWS.AssumeRole.ARN)
}

return nil
}
56 changes: 56 additions & 0 deletions api/v1beta1/webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,33 @@ var _ = Describe("ExternalDNS admission webhook when platform is OCP", func() {
err := k8sClient.Create(context.Background(), resource)
Expect(err).Should(Succeed())
})
It("valid RoleARN", func() {
resource := makeExternalDNS("test-valid-rolearn-openshift", nil)
resource.Spec.Provider = ExternalDNSProvider{
Type: ProviderTypeAWS,
AWS: &ExternalDNSAWSProviderOptions{
AssumeRole: &ExternalDNSAWSAssumeRoleOptions{
ARN: "arn:aws:iam::123456789012:role/foo",
},
},
}
err := k8sClient.Create(context.Background(), resource)
Expect(err).Should(Succeed())
})
It("invalid RoleARN rejected", func() {
resource := makeExternalDNS("test-invalid-rolearn-openshift", nil)
resource.Spec.Provider = ExternalDNSProvider{
Type: ProviderTypeAWS,
AWS: &ExternalDNSAWSProviderOptions{
AssumeRole: &ExternalDNSAWSAssumeRoleOptions{
ARN: "arn:aws:iam:bad123456789012:role/foo",
},
},
}
err := k8sClient.Create(context.Background(), resource)
Expect(err).ShouldNot(Succeed())
Expect(err.Error()).Should(ContainSubstring(`arn "arn:aws:iam:bad123456789012:role/foo" is not a valid AWS ARN`))
})
})
Context("resource with Azure provider", func() {
It("ignores when provider Azure credentials are not specified", func() {
Expand Down Expand Up @@ -175,6 +202,35 @@ 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() {
alebedev87 marked this conversation as resolved.
Show resolved Hide resolved
resource := makeExternalDNS("test-valid-rolearn", nil)
resource.Spec.Provider = ExternalDNSProvider{
Type: ProviderTypeAWS,
AWS: &ExternalDNSAWSProviderOptions{
AssumeRole: &ExternalDNSAWSAssumeRoleOptions{
ARN: "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{
AssumeRole: &ExternalDNSAWSAssumeRoleOptions{
ARN: "arn:aws:iam:bad123456789012:role/foo",
},
Credentials: SecretReference{Name: "credentials"},
},
}
err := k8sClient.Create(context.Background(), resource)
Expect(err).ShouldNot(Succeed())
Expect(err.Error()).Should(ContainSubstring(`arn "arn:aws:iam:bad123456789012:role/foo" is not a valid AWS ARN`))
})
})

Context("resource with Azure provider", func() {
Expand Down
22 changes: 21 additions & 1 deletion api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions bundle/manifests/externaldns.olm.openshift.io_externaldnses.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,16 @@ spec:
description: AWS describes provider configuration options specific
to AWS (Route 53).
properties:
assumeRole:
description: assumeRole is a reference to the IAM role that
ExternalDNS will be assuming in order to perform any DNS
updates.
properties:
arn:
description: arn is an AWS role ARN that the ExternalDNS
operator will assume when making DNS updates.
type: string
type: object
credentials:
description: "Credentials is a reference to a secret containing
the following keys (with corresponding values): \n * aws_access_key_id
Expand Down
10 changes: 10 additions & 0 deletions config/crd/bases/externaldns.olm.openshift.io_externaldnses.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,16 @@ spec:
description: AWS describes provider configuration options specific
to AWS (Route 53).
properties:
assumeRole:
description: assumeRole is a reference to the IAM role that
ExternalDNS will be assuming in order to perform any DNS
updates.
properties:
arn:
description: arn is an AWS role ARN that the ExternalDNS
operator will assume when making DNS updates.
type: string
type: object
credentials:
description: "Credentials is a reference to a secret containing
the following keys (with corresponding values): \n * aws_access_key_id
Expand Down
54 changes: 50 additions & 4 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ provider configuration in the `ExternalDNS` resource. However, it does not provi
it expects the credentials to be in the same namespace as the operator itself. It then copies over the credentials into
the namespace where the _external-dns_ deployments are created so that they can be mounted by the pods.

## AWS
# AWS

Create a secret with the access key id and secret:

Expand Down Expand Up @@ -55,11 +55,57 @@ spec:
Once this is created the _external-dns-operator_ will create a deployment of _external-dns_ which is configured to
manage DNS records in AWS Route53.

## AWS GovCloud
## Assume Role

The _external-dns-operator_ supports managing records in another AWS account's hosted zone. To achieve this, you will
require an IAM Role ARN with the necessary permissions properly set up. This Role ARN should then be specified in the
`ExternalDNS` resource in the following manner:

```yaml
apiVersion: externaldns.olm.openshift.io/v1beta1
kind: ExternalDNS
metadata:
name: aws-example
spec:
provider:
type: AWS
aws:
credentials:
name: aws-access-key
assumeRole:
arn: arn:aws:iam::123456789012:role/role-name # Replace with the desire Role ARN
zones: # Replace with the desired hosted zone IDs
- "Z3URY6TWQ91KXX"
source:
type: Service
fqdnTemplate:
- '{{.Name}}.mydomain.net'
```

**Note**: Due to a limitation of the `v1beta1` API requiring the `credentials` field, OpenShift users will be required
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW I am planning to add a section on using a Shared VPC to OpenShift's docs: https://docs.openshift.com/container-platform/4.13/networking/external_dns_operator/nw-creating-dns-records-on-aws.html

If you think we don't need both (this repo docs and OpenShift docs), I can drop this one. But I figure better safe than sorry.

I know the docs in this repo are not just for OpenShift users, but it does feel a little strange there isn't a mention of the Cloud Credential Operator providing the credentials for OpenShift anywhere. I wonder if we should introduce that concept somewhere around here, as a new user, that wasn't clear to me I didn't need credentials from reading these docs.

I feel like we should also have a aws-openshift.md doc just like the azure and gcp openshift docs in https://github.com/openshift/external-dns-operator/blob/main/docs. That's seems odd to me.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you think we don't need both (this repo docs and OpenShift docs), I can drop this one. But I figure better safe than sorry.

OCP docs are always the priority over this repo.

I know the docs in this repo are not just for OpenShift users, but it does feel a little strange there isn't a mention of the Cloud Credential Operator providing the credentials for OpenShift anywhere. I wonder if we should introduce that concept somewhere around here, as a new user, that wasn't clear to me I didn't need credentials from reading these docs.

I feel like we should also have a aws-openshift.md doc just like the azure and gcp openshift docs in https://github.com/openshift/external-dns-operator/blob/main/docs. That's seems odd to me.

I believe that the initial purpose of this doc was to show how to configure the DNS providers: format of the secrets, configuration and tuning options. Basically anything that lays in spec.provider which differs from provider to provider, not only credentials. It all started from more exotic providers like BlueCat and Inflobox which could not be used in CCO context. That's why Credentials for DNS providers chapter talks about ExternalDNS Operator not provisioning the credentials.

I think I agree that we need to mention CredentialsRequest for the big 3 providers and keep the mention about Infoblox and BlueCat as not managed by CCO. That we can do in this PR I guess or feel free to submit another PR with a dedicated .md for aws.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay thanks. I'll leave what I have now, and possibly open another PR later with details on CredentialsRequest to keep this PR cleaner.

to provide an empty (`""`) credentials field. The empty credentials will be ignored and the secret provided by
OpenShift's Cloud Credentials Operator will be used:

```yaml
apiVersion: externaldns.olm.openshift.io/v1beta1
kind: ExternalDNS
metadata:
name: aws-example
spec:
provider:
type: AWS
aws:
credentials:
name: "" # Empty Credentials
assumeRole:
arn: arn:aws:iam::123456789012:role/role-name # Replace with the desire Role ARN
```

## GovCloud
The operator makes the assumption that `ExternalDNS` instances which target GovCloud DNS also run on the GovCloud. This is needed to detect the AWS region.
As for the rest: the usage is exactly the same as for `AWS`.

## Infoblox
# Infoblox

Before creating an `ExternalDNS` resource for the [Infoblox](https://www.infoblox.com/wp-content/uploads/infoblox-deployment-infoblox-rest-api.pdf)
the following information is required:
Expand Down Expand Up @@ -110,7 +156,7 @@ spec:
Once this is created the _external-dns-operator_ will create a deployment of _external-dns_ which is configured to
manage DNS records in Infoblox.

## BlueCat
# BlueCat

The BlueCat provider requires
the [BlueCat Gateway](https://docs.bluecatnetworks.com/r/Gateway-Installation-Guide/Installing-BlueCat-Gateway/20.3.1)
Expand Down
1 change: 1 addition & 0 deletions pkg/operator/controller/externaldns/credentials_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ func createProviderConfig(externalDNS *operatorv1beta1.ExternalDNS, platformStat
"route53:ListHostedZones",
"route53:ListResourceRecordSets",
"tag:GetResources",
"sts:AssumeRole",
gcs278 marked this conversation as resolved.
Show resolved Hide resolved
},
Resource: "*",
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ func desiredAWSProviderSpec() runtime.Object {
"route53:ListHostedZones",
"route53:ListResourceRecordSets",
"tag:GetResources",
"sts:AssumeRole",
},
Resource: "*",
},
Expand All @@ -311,6 +312,7 @@ func desiredAWSProviderSpecGovARN() runtime.Object {
"route53:ListHostedZones",
"route53:ListResourceRecordSets",
"tag:GetResources",
"sts:AssumeRole",
},
Resource: "*",
},
Expand Down
59 changes: 59 additions & 0 deletions pkg/operator/controller/externaldns/deployment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -5317,6 +5366,16 @@ 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{
AssumeRole: &operatorv1beta1.ExternalDNSAWSAssumeRoleOptions{
ARN: roleARN,
},
}
return extdns
}

func testPlatformStatusGCP(projectID string) *configv1.PlatformStatus {
return &configv1.PlatformStatus{
Type: configv1.GCPPlatformType,
Expand Down
4 changes: 4 additions & 0 deletions pkg/operator/controller/externaldns/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,10 @@ func (b *externalDNSContainerBuilder) fillAWSFields(container *corev1.Container)
container.Args = append(container.Args, "--aws-prefer-cname")
}

if b.externalDNS.Spec.Provider.AWS != nil && b.externalDNS.Spec.Provider.AWS.AssumeRole != nil {
container.Args = append(container.Args, fmt.Sprintf("--aws-assume-role=%s", b.externalDNS.Spec.Provider.AWS.AssumeRole.ARN))
}

// don't add empty credentials environment variables if no secret was given
if len(b.secretName) == 0 {
return
Expand Down
Loading