diff --git a/data/data/aws/cluster/main.tf b/data/data/aws/cluster/main.tf index 89cc41cc141..26257025649 100644 --- a/data/data/aws/cluster/main.tf +++ b/data/data/aws/cluster/main.tf @@ -70,9 +70,11 @@ module "dns" { cluster_id = var.cluster_id tags = local.tags internal_zone = var.aws_internal_zone + internal_zone_role = var.aws_internal_zone_role vpc_id = module.vpc.vpc_id region = var.aws_region publish_strategy = var.aws_publish_strategy + custom_endpoints = var.custom_endpoints } module "vpc" { diff --git a/data/data/aws/cluster/route53/base.tf b/data/data/aws/cluster/route53/base.tf index 967b863cced..f7760ea69e5 100644 --- a/data/data/aws/cluster/route53/base.tf +++ b/data/data/aws/cluster/route53/base.tf @@ -9,6 +9,27 @@ locals { use_alias = ! local.use_cname } +provider "aws" { + alias = "private_hosted_zone" + + assume_role { + role_arn = var.internal_zone_role + } + + region = var.region + + skip_region_validation = true + + endpoints { + ec2 = lookup(var.custom_endpoints, "ec2", null) + elb = lookup(var.custom_endpoints, "elasticloadbalancing", null) + iam = lookup(var.custom_endpoints, "iam", null) + route53 = lookup(var.custom_endpoints, "route53", null) + s3 = lookup(var.custom_endpoints, "s3", null) + sts = lookup(var.custom_endpoints, "sts", null) + } +} + data "aws_route53_zone" "public" { count = local.public_endpoints ? 1 : 0 @@ -18,6 +39,8 @@ data "aws_route53_zone" "public" { } data "aws_route53_zone" "int" { + provider = aws.private_hosted_zone + zone_id = var.internal_zone == null ? aws_route53_zone.new_int[0].id : var.internal_zone } @@ -54,7 +77,8 @@ resource "aws_route53_record" "api_external_alias" { } resource "aws_route53_record" "api_internal_alias" { - count = local.use_alias ? 1 : 0 + provider = aws.private_hosted_zone + count = local.use_alias ? 1 : 0 zone_id = data.aws_route53_zone.int.zone_id name = "api-int.${var.cluster_domain}" @@ -68,7 +92,8 @@ resource "aws_route53_record" "api_internal_alias" { } resource "aws_route53_record" "api_external_internal_zone_alias" { - count = local.use_alias ? 1 : 0 + provider = aws.private_hosted_zone + count = local.use_alias ? 1 : 0 zone_id = data.aws_route53_zone.int.zone_id name = "api.${var.cluster_domain}" @@ -93,7 +118,8 @@ resource "aws_route53_record" "api_external_cname" { } resource "aws_route53_record" "api_internal_cname" { - count = local.use_cname ? 1 : 0 + provider = aws.private_hosted_zone + count = local.use_cname ? 1 : 0 zone_id = data.aws_route53_zone.int.zone_id name = "api-int.${var.cluster_domain}" @@ -104,7 +130,8 @@ resource "aws_route53_record" "api_internal_cname" { } resource "aws_route53_record" "api_external_internal_zone_cname" { - count = local.use_cname ? 1 : 0 + provider = aws.private_hosted_zone + count = local.use_cname ? 1 : 0 zone_id = data.aws_route53_zone.int.zone_id name = "api.${var.cluster_domain}" diff --git a/data/data/aws/cluster/route53/variables.tf b/data/data/aws/cluster/route53/variables.tf index 67f938ed2c9..47c983f8729 100644 --- a/data/data/aws/cluster/route53/variables.tf +++ b/data/data/aws/cluster/route53/variables.tf @@ -28,6 +28,12 @@ variable "internal_zone" { description = "An existing hosted zone (zone ID) to use for the internal API." } +variable "internal_zone_role" { + type = string + default = null + description = "(optional) A role to assume when using an existing hosted zone from another account." +} + variable "api_external_lb_dns_name" { description = "External API's LB DNS name" type = string @@ -63,3 +69,16 @@ variable "region" { type = string description = "The target AWS region for the cluster." } + +variable "custom_endpoints" { + type = map(string) + + description = < 0 { - allErrs = append(allErrs, errors...) + if errs := validateHostedZone(zoneOutput, zonePath, zoneName, metadata); len(errs) > 0 { + allErrs = append(allErrs, errs...) } zone = zoneOutput.HostedZone @@ -416,8 +417,8 @@ func ValidateForProvisioning(client API, ic *types.InstallConfig, metadata *Meta zone = baseDomainOutput } - if errors = client.ValidateZoneRecords(zone, zoneName, zonePath, ic); len(errors) > 0 { - allErrs = append(allErrs, errors...) + if errs := client.ValidateZoneRecords(zone, zoneName, zonePath, ic, r53cfg); len(errs) > 0 { + allErrs = append(allErrs, errs...) } return allErrs.ToAggregate() diff --git a/pkg/asset/installconfig/aws/validation_test.go b/pkg/asset/installconfig/aws/validation_test.go index f26a4645538..28c91ed452c 100644 --- a/pkg/asset/installconfig/aws/validation_test.go +++ b/pkg/asset/installconfig/aws/validation_test.go @@ -840,13 +840,13 @@ func TestValidateForProvisioning(t *testing.T) { }, { name: "internal publish strategy invalid hosted zone", edits: editFunctions{publishInternal, invalidateHostedZone}, - expectedErr: "aws.hostedZone: Invalid value: \"invalid-hosted-zone\": cannot find hosted zone", + expectedErr: "aws.hostedZone: Invalid value: \"invalid-hosted-zone\": unable to retrieve hosted zone", }, { name: "external publish strategy valid hosted zone", }, { name: "external publish strategy invalid hosted zone", edits: editFunctions{invalidateHostedZone}, - expectedErr: "aws.hostedZone: Invalid value: \"invalid-hosted-zone\": cannot find hosted zone", + expectedErr: "aws.hostedZone: Invalid value: \"invalid-hosted-zone\": unable to retrieve hosted zone", }} mockCtrl := gomock.NewController(t) @@ -861,12 +861,12 @@ func TestValidateForProvisioning(t *testing.T) { route53Client.EXPECT().GetBaseDomain("").Return(nil, fmt.Errorf("invalid value: \"\": cannot find base domain")).AnyTimes() route53Client.EXPECT().GetBaseDomain(invalidBaseDomain).Return(nil, fmt.Errorf("invalid value: \"%s\": cannot find base domain", invalidBaseDomain)).AnyTimes() - route53Client.EXPECT().ValidateZoneRecords(&validDomainOutput, gomock.Any(), gomock.Any(), gomock.Any()).Return(field.ErrorList{}).AnyTimes() - route53Client.EXPECT().ValidateZoneRecords(gomock.Any(), validHostedZoneName, gomock.Any(), gomock.Any()).Return(field.ErrorList{}).AnyTimes() + route53Client.EXPECT().ValidateZoneRecords(&validDomainOutput, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(field.ErrorList{}).AnyTimes() + route53Client.EXPECT().ValidateZoneRecords(gomock.Any(), validHostedZoneName, gomock.Any(), gomock.Any(), gomock.Any()).Return(field.ErrorList{}).AnyTimes() // An invalid hosted zone should provide an error - route53Client.EXPECT().GetHostedZone(validHostedZoneName).Return(&validHostedZoneOutput, nil).AnyTimes() - route53Client.EXPECT().GetHostedZone(gomock.Not(validHostedZoneName)).Return(nil, fmt.Errorf("invalid value: \"invalid-hosted-zone\": cannot find hosted zone")).AnyTimes() + route53Client.EXPECT().GetHostedZone(validHostedZoneName, gomock.Any()).Return(&validHostedZoneOutput, nil).AnyTimes() + route53Client.EXPECT().GetHostedZone(gomock.Not(validHostedZoneName), gomock.Any()).Return(nil, fmt.Errorf("invalid value: \"invalid-hosted-zone\": cannot find hosted zone")).AnyTimes() for _, test := range cases { t.Run(test.name, func(t *testing.T) { @@ -939,7 +939,7 @@ func TestGetSubDomainDNSRecords(t *testing.T) { if test.expectedErr != "" { if test.problematicRecords == nil { - route53Client.EXPECT().GetSubDomainDNSRecords(&validDomainOutput, ic).Return(nil, errors.Errorf(test.expectedErr)).AnyTimes() + route53Client.EXPECT().GetSubDomainDNSRecords(&validDomainOutput, ic, gomock.Any()).Return(nil, errors.Errorf(test.expectedErr)).AnyTimes() } else { // mimic the results of what should happen in the internal function passed to // ListResourceRecordSetsPages by GetSubDomainDNSRecords. Skip certain problematicRecords @@ -950,13 +950,13 @@ func TestGetSubDomainDNSRecords(t *testing.T) { returnedProblems = append(returnedProblems, pr) } } - route53Client.EXPECT().GetSubDomainDNSRecords(&validDomainOutput, ic).Return(returnedProblems, errors.Errorf(test.expectedErr)).AnyTimes() + route53Client.EXPECT().GetSubDomainDNSRecords(&validDomainOutput, ic, gomock.Any()).Return(returnedProblems, errors.Errorf(test.expectedErr)).AnyTimes() } } else { - route53Client.EXPECT().GetSubDomainDNSRecords(&validDomainOutput, ic).Return(nil, nil).AnyTimes() + route53Client.EXPECT().GetSubDomainDNSRecords(&validDomainOutput, ic, gomock.Any()).Return(nil, nil).AnyTimes() } - _, err := route53Client.GetSubDomainDNSRecords(&validDomainOutput, ic) + _, err := route53Client.GetSubDomainDNSRecords(&validDomainOutput, ic, nil) if test.expectedErr == "" { assert.NoError(t, err) } else { diff --git a/pkg/asset/installconfig/vsphere/mock/tagmanager_generated.go b/pkg/asset/installconfig/vsphere/mock/tagmanager_generated.go new file mode 100644 index 00000000000..c324b56789b --- /dev/null +++ b/pkg/asset/installconfig/vsphere/mock/tagmanager_generated.go @@ -0,0 +1,127 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./validation.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + tags "github.com/vmware/govmomi/vapi/tags" + mo "github.com/vmware/govmomi/vim25/mo" +) + +// MockTagManager is a mock of TagManager interface. +type MockTagManager struct { + ctrl *gomock.Controller + recorder *MockTagManagerMockRecorder +} + +// MockTagManagerMockRecorder is the mock recorder for MockTagManager. +type MockTagManagerMockRecorder struct { + mock *MockTagManager +} + +// NewMockTagManager creates a new mock instance. +func NewMockTagManager(ctrl *gomock.Controller) *MockTagManager { + mock := &MockTagManager{ctrl: ctrl} + mock.recorder = &MockTagManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTagManager) EXPECT() *MockTagManagerMockRecorder { + return m.recorder +} + +// GetAttachedTags mocks base method. +func (m *MockTagManager) GetAttachedTags(ctx context.Context, ref mo.Reference) ([]tags.Tag, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAttachedTags", ctx, ref) + ret0, _ := ret[0].([]tags.Tag) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAttachedTags indicates an expected call of GetAttachedTags. +func (mr *MockTagManagerMockRecorder) GetAttachedTags(ctx, ref interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAttachedTags", reflect.TypeOf((*MockTagManager)(nil).GetAttachedTags), ctx, ref) +} + +// GetAttachedTagsOnObjects mocks base method. +func (m *MockTagManager) GetAttachedTagsOnObjects(ctx context.Context, objectID []mo.Reference) ([]tags.AttachedTags, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAttachedTagsOnObjects", ctx, objectID) + ret0, _ := ret[0].([]tags.AttachedTags) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAttachedTagsOnObjects indicates an expected call of GetAttachedTagsOnObjects. +func (mr *MockTagManagerMockRecorder) GetAttachedTagsOnObjects(ctx, objectID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAttachedTagsOnObjects", reflect.TypeOf((*MockTagManager)(nil).GetAttachedTagsOnObjects), ctx, objectID) +} + +// GetCategories mocks base method. +func (m *MockTagManager) GetCategories(ctx context.Context) ([]tags.Category, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCategories", ctx) + ret0, _ := ret[0].([]tags.Category) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCategories indicates an expected call of GetCategories. +func (mr *MockTagManagerMockRecorder) GetCategories(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCategories", reflect.TypeOf((*MockTagManager)(nil).GetCategories), ctx) +} + +// GetCategory mocks base method. +func (m *MockTagManager) GetCategory(ctx context.Context, id string) (*tags.Category, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCategory", ctx, id) + ret0, _ := ret[0].(*tags.Category) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCategory indicates an expected call of GetCategory. +func (mr *MockTagManagerMockRecorder) GetCategory(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCategory", reflect.TypeOf((*MockTagManager)(nil).GetCategory), ctx, id) +} + +// GetTagsForCategory mocks base method. +func (m *MockTagManager) GetTagsForCategory(ctx context.Context, id string) ([]tags.Tag, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTagsForCategory", ctx, id) + ret0, _ := ret[0].([]tags.Tag) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTagsForCategory indicates an expected call of GetTagsForCategory. +func (mr *MockTagManagerMockRecorder) GetTagsForCategory(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTagsForCategory", reflect.TypeOf((*MockTagManager)(nil).GetTagsForCategory), ctx, id) +} + +// ListCategories mocks base method. +func (m *MockTagManager) ListCategories(ctx context.Context) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListCategories", ctx) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListCategories indicates an expected call of ListCategories. +func (mr *MockTagManagerMockRecorder) ListCategories(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCategories", reflect.TypeOf((*MockTagManager)(nil).ListCategories), ctx) +} diff --git a/pkg/asset/manifests/dns.go b/pkg/asset/manifests/dns.go index 49664fcfbbf..4c71216ece9 100644 --- a/pkg/asset/manifests/dns.go +++ b/pkg/asset/manifests/dns.go @@ -118,6 +118,15 @@ func (d *DNS) Generate(dependencies asset.Parents) error { }} } else { config.Spec.PrivateZone = &configv1.DNSZone{ID: hostedZone} + + if r := installConfig.Config.AWS.HostedZoneRole; r != "" { + config.Spec.Platform = configv1.DNSPlatformSpec{ + Type: configv1.AWSPlatformType, + AWS: &configv1.AWSDNSSpec{ + PrivateZoneIAMRole: r, + }, + } + } } case azuretypes.Name: dnsConfig, err := installConfig.Azure.DNSConfig() diff --git a/pkg/destroy/aws/aws.go b/pkg/destroy/aws/aws.go index a5bc3f559ed..9f0f34e8651 100644 --- a/pkg/destroy/aws/aws.go +++ b/pkg/destroy/aws/aws.go @@ -9,6 +9,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/credentials/stscreds" "github.com/aws/aws-sdk-go/aws/endpoints" "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/aws/session" @@ -56,11 +57,12 @@ type ClusterUninstaller struct { // } // // will match resources with (a:b and c:d) or d:e. - Filters []Filter // filter(s) we will be searching for - Logger logrus.FieldLogger - Region string - ClusterID string - ClusterDomain string + Filters []Filter // filter(s) we will be searching for + Logger logrus.FieldLogger + Region string + ClusterID string + ClusterDomain string + HostedZoneRole string // Session is the AWS session to be used for deletion. If nil, a // new session will be created based on the usual credential @@ -84,12 +86,13 @@ func New(logger logrus.FieldLogger, metadata *types.ClusterMetadata) (providers. } return &ClusterUninstaller{ - Filters: filters, - Region: region, - Logger: logger, - ClusterID: metadata.InfraID, - ClusterDomain: metadata.AWS.ClusterDomain, - Session: session, + Filters: filters, + Region: region, + Logger: logger, + ClusterID: metadata.InfraID, + ClusterDomain: metadata.AWS.ClusterDomain, + Session: session, + HostedZoneRole: metadata.AWS.HostedZoneRole, }, nil } @@ -134,6 +137,17 @@ func (o *ClusterUninstaller) RunWithContext(ctx context.Context) ([]string, erro resourcegroupstaggingapi.New(awsSession), } + if o.HostedZoneRole != "" { + creds := stscreds.NewCredentials(awsSession, o.HostedZoneRole) + // This client is specifically for finding route53 zones, + // so it needs to use the "global" us-east-1 region. + cfg := &aws.Config{ + Credentials: creds, + Region: aws.String(endpoints.UsEast1RegionID), + } + tagClients = append(tagClients, resourcegroupstaggingapi.New(awsSession, cfg)) + } + switch o.Region { case endpoints.CnNorth1RegionID, endpoints.CnNorthwest1RegionID: break diff --git a/pkg/destroy/aws/shared.go b/pkg/destroy/aws/shared.go index df5299ded42..b9da24dd48f 100644 --- a/pkg/destroy/aws/shared.go +++ b/pkg/destroy/aws/shared.go @@ -9,6 +9,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/credentials/stscreds" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi" @@ -189,8 +190,6 @@ func (o *ClusterUninstaller) cleanSharedARN(ctx context.Context, session *sessio } func (o *ClusterUninstaller) cleanSharedRoute53(ctx context.Context, session *session.Session, arn arn.ARN, logger logrus.FieldLogger) error { - client := route53.New(session) - resourceType, id, err := splitSlash("resource", arn.Resource) if err != nil { return err @@ -199,27 +198,38 @@ func (o *ClusterUninstaller) cleanSharedRoute53(ctx context.Context, session *se switch resourceType { case "hostedzone": - return o.cleanSharedHostedZone(ctx, client, id, logger) + return o.cleanSharedHostedZone(ctx, session, id, logger) default: logger.Debugf("Nothing to clean for shared %s resource", resourceType) return nil } } -func (o *ClusterUninstaller) cleanSharedHostedZone(ctx context.Context, client *route53.Route53, id string, logger logrus.FieldLogger) error { +func (o *ClusterUninstaller) cleanSharedHostedZone(ctx context.Context, session *session.Session, id string, logger logrus.FieldLogger) error { + publicClient := route53.New(session) + + // The private hosted zone (phz) may belong to a different account, + // in which case we need a separate client. + phzClient := publicClient + if o.HostedZoneRole != "" { + creds := stscreds.NewCredentials(session, o.HostedZoneRole) + phzClient = route53.New(session, &aws.Config{Credentials: creds}) + logger.Infof("Assuming role %s to destroy records in private hosted zone", o.HostedZoneRole) + } + if o.ClusterDomain == "" { logger.Debug("No cluster domain specified in metadata; cannot clean the shared hosted zone") return nil } dottedClusterDomain := o.ClusterDomain + "." - publicZoneID, err := findAncestorPublicRoute53(ctx, client, dottedClusterDomain, logger) + publicZoneID, err := findAncestorPublicRoute53(ctx, publicClient, dottedClusterDomain, logger) if err != nil { return err } var lastError error - err = client.ListResourceRecordSetsPagesWithContext( + err = phzClient.ListResourceRecordSetsPagesWithContext( ctx, &route53.ListResourceRecordSetsInput{HostedZoneId: aws.String(id)}, func(results *route53.ListResourceRecordSetsOutput, lastPage bool) bool { @@ -236,7 +246,7 @@ func (o *ClusterUninstaller) cleanSharedHostedZone(ctx context.Context, client * // delete any matching record sets in the public hosted zone if publicZoneID != "" { publicZoneLogger := logger.WithField("id", publicZoneID) - if err := deleteMatchingRecordSetInPublicZone(ctx, client, publicZoneID, recordSet, publicZoneLogger); err != nil { + if err := deleteMatchingRecordSetInPublicZone(ctx, publicClient, publicZoneID, recordSet, publicZoneLogger); err != nil { if lastError != nil { publicZoneLogger.Debug(lastError) } @@ -248,7 +258,7 @@ func (o *ClusterUninstaller) cleanSharedHostedZone(ctx context.Context, client * publicZoneLogger.WithFields(recordsetFields).Debug("Deleted from public zone") } // delete the record set - if err := deleteRoute53RecordSet(ctx, client, id, recordSet, logger); err != nil { + if err := deleteRoute53RecordSet(ctx, phzClient, id, recordSet, logger); err != nil { if lastError != nil { logger.Debug(lastError) } diff --git a/pkg/explain/printer_test.go b/pkg/explain/printer_test.go index bd9b06b55b1..35df45867cc 100644 --- a/pkg/explain/printer_test.go +++ b/pkg/explain/printer_test.go @@ -156,6 +156,9 @@ func Test_PrintFields(t *testing.T) { hostedZone HostedZone is the ID of an existing hosted zone into which to add DNS records for the cluster's internal API. An existing hosted zone can only be used when also using existing subnets. The hosted zone must be associated with the VPC containing the subnets. Leave the hosted zone unset to have the installer create the hosted zone on your behalf. + hostedZoneRole + HostedZoneRole is the ARN of a role to be assumed when performing operations on the provided HostedZone. HostedZoneRole can be used in a shared VPC scenario when the private hosted zone belongs to a different account than the rest of the cluster resources. If HostedZoneRole is set, HostedZone must also be set. + lbType LBType is an optional field to specify a load balancer type. When this field is specified, the default ingresscontroller will be created using the specified load-balancer type. diff --git a/pkg/tfvars/aws/aws.go b/pkg/tfvars/aws/aws.go index 59859c677ac..d4af26ad412 100644 --- a/pkg/tfvars/aws/aws.go +++ b/pkg/tfvars/aws/aws.go @@ -33,6 +33,7 @@ type config struct { PrivateSubnets []string `json:"aws_private_subnets,omitempty"` PublicSubnets *[]string `json:"aws_public_subnets,omitempty"` InternalZone string `json:"aws_internal_zone,omitempty"` + InternalZoneRole string `json:"aws_internal_zone_role,omitempty"` PublishStrategy string `json:"aws_publish_strategy,omitempty"` IgnitionBucket string `json:"aws_ignition_bucket"` BootstrapIgnitionStub string `json:"aws_bootstrap_stub_ignition"` @@ -44,10 +45,10 @@ type config struct { // TFVarsSources contains the parameters to be converted into Terraform variables type TFVarsSources struct { - VPC string - PrivateSubnets, PublicSubnets []string - InternalZone string - Services []typesaws.ServiceEndpoint + VPC string + PrivateSubnets, PublicSubnets []string + InternalZone, InternalZoneRole string + Services []typesaws.ServiceEndpoint Publish types.PublishingStrategy @@ -132,6 +133,7 @@ func TFVars(sources TFVarsSources) ([]byte, error) { VPC: sources.VPC, PrivateSubnets: sources.PrivateSubnets, InternalZone: sources.InternalZone, + InternalZoneRole: sources.InternalZoneRole, PublishStrategy: string(sources.Publish), IgnitionBucket: sources.IgnitionBucket, MasterIAMRoleName: sources.MasterIAMRoleName, diff --git a/pkg/types/aws/metadata.go b/pkg/types/aws/metadata.go index 609c48baaee..343f736489d 100644 --- a/pkg/types/aws/metadata.go +++ b/pkg/types/aws/metadata.go @@ -18,4 +18,8 @@ type Metadata struct { // ClusterDomain is the domain for the cluster. ClusterDomain string `json:"clusterDomain"` + + // HostedZoneRole is the role to assume when performing operations + // on a hosted zone owned by another account. + HostedZoneRole string `json:"hostedZoneRole,omitempty"` } diff --git a/pkg/types/aws/platform.go b/pkg/types/aws/platform.go index 44f932cfcc6..dfdc28319eb 100644 --- a/pkg/types/aws/platform.go +++ b/pkg/types/aws/platform.go @@ -41,6 +41,15 @@ type Platform struct { // +optional HostedZone string `json:"hostedZone,omitempty"` + // HostedZoneRole is the ARN of a role to be assumed when performing + // operations on the provided HostedZone. HostedZoneRole can be used + // in a shared VPC scenario when the private hosted zone belongs to a + // different account than the rest of the cluster resources. + // If HostedZoneRole is set, HostedZone must also be set. + // + // +optional + HostedZoneRole string `json:"hostedZoneRole,omitempty"` + // UserTags additional keys and values that the installer will add // as tags to all resources that it creates. Resources created by the // cluster itself may not include these tags. diff --git a/pkg/types/aws/validation/platform.go b/pkg/types/aws/validation/platform.go index 07f729e89b3..72d518dcd7c 100644 --- a/pkg/types/aws/validation/platform.go +++ b/pkg/types/aws/validation/platform.go @@ -39,6 +39,10 @@ func ValidatePlatform(p *aws.Platform, fldPath *field.Path) field.ErrorList { } } + if p.HostedZoneRole != "" && p.HostedZone == "" { + allErrs = append(allErrs, field.Invalid(fldPath.Child("hostedZoneRole"), p.HostedZoneRole, "may not specify a role to assume for hosted zone operations without also specifying a hosted zone")) + } + allErrs = append(allErrs, validateServiceEndpoints(p.ServiceEndpoints, fldPath.Child("serviceEndpoints"))...) allErrs = append(allErrs, validateUserTags(p.UserTags, p.PropagateUserTag, fldPath.Child("userTags"))...) diff --git a/pkg/types/aws/validation/platform_test.go b/pkg/types/aws/validation/platform_test.go index a48f9283b8f..6d2f9cf899c 100644 --- a/pkg/types/aws/validation/platform_test.go +++ b/pkg/types/aws/validation/platform_test.go @@ -175,6 +175,23 @@ func TestValidatePlatform(t *testing.T) { }, expected: fmt.Sprintf(`^\Qtest-path.userTags: Too many: %d: must have at most %d items`, userTagLimit+1, userTagLimit), }, + { + name: "hosted zone role without hosted zone", + platform: &aws.Platform{ + Region: "us-east-1", + HostedZoneRole: "test-hosted-zone-role", + }, + expected: `^test-path\.hostedZoneRole: Invalid value: "test-hosted-zone-role": may not specify a role to assume for hosted zone operations without also specifying a hosted zone$`, + }, + { + name: "hosted zone & role", + platform: &aws.Platform{ + Region: "us-east-1", + Subnets: []string{"test-subnet"}, + HostedZone: "test-hosted-zone", + HostedZoneRole: "test-hosted-zone-role", + }, + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { diff --git a/pkg/types/validation/installconfig.go b/pkg/types/validation/installconfig.go index d3b3089ab67..7a24c392be4 100644 --- a/pkg/types/validation/installconfig.go +++ b/pkg/types/validation/installconfig.go @@ -1050,6 +1050,12 @@ func validateFeatureSet(c *types.InstallConfig) field.ErrorList { allErrs = append(allErrs, field.Forbidden(field.NewPath("platform", "azure", "userTags"), errMsg)) } + if c.AWS != nil { + if len(c.AWS.HostedZoneRole) > 0 { + allErrs = append(allErrs, field.Forbidden(field.NewPath("platform", "aws", "hostedZoneRole"), errMsg)) + } + } + if c.OpenStack != nil { for _, f := range openstackvalidation.FilledInTechPreviewFields(c) { allErrs = append(allErrs, field.Forbidden(f, errMsg)) diff --git a/pkg/types/validation/installconfig_test.go b/pkg/types/validation/installconfig_test.go index b95e670306c..3d2bae6e6e1 100644 --- a/pkg/types/validation/installconfig_test.go +++ b/pkg/types/validation/installconfig_test.go @@ -2135,6 +2135,18 @@ func TestValidateInstallConfig(t *testing.T) { }(), expectedError: "platform.vsphere.apiVIPs: Required value: must specify VIP for API, when VIP for ingress is set", }, + { + name: "platform.aws.hostedZoneRole should return error without TechPreviewNoUpgrade", + installConfig: func() *types.InstallConfig { + c := validInstallConfig() + c.Platform = types.Platform{ + AWS: validAWSPlatform(), + } + c.Platform.AWS.HostedZoneRole = "test-zone" + return c + }(), + expectedError: `platform.aws.hostedZoneRole: Forbidden: the TechPreviewNoUpgrade feature set must be enabled to use this field`, + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) {