Skip to content

Commit 0eafdbb

Browse files
Merge pull request #8142 from r4f4/capi-aws-dns
CORS-2894,CORS-3051: Create DNS resources and PHZ for CAPI/aws
2 parents 5a1f5c2 + c423d63 commit 0eafdbb

File tree

2 files changed

+344
-23
lines changed

2 files changed

+344
-23
lines changed

pkg/asset/installconfig/aws/route53.go

Lines changed: 222 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,30 @@
11
package aws
22

33
import (
4+
"context"
45
"fmt"
6+
"strconv"
57
"strings"
8+
"time"
69

710
"github.com/aws/aws-sdk-go/aws"
811
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
912
"github.com/aws/aws-sdk-go/aws/endpoints"
1013
awss "github.com/aws/aws-sdk-go/aws/session"
1114
"github.com/aws/aws-sdk-go/service/route53"
15+
"github.com/sirupsen/logrus"
16+
"k8s.io/apimachinery/pkg/util/sets"
1217
"k8s.io/apimachinery/pkg/util/validation/field"
1318

1419
"github.com/openshift/installer/pkg/types"
1520
)
1621

1722
//go:generate mockgen -source=./route53.go -destination=mock/awsroute53_generated.go -package=mock
1823

24+
// regions for which ALIAS records are not available
25+
// https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-r53.html
26+
var cnameRegions = sets.New[string]("us-gov-west-1", "us-gov-east-1")
27+
1928
// API represents the calls made to the API.
2029
type API interface {
2130
GetHostedZone(hostedZone string, cfg *aws.Config) (*route53.GetHostedZoneOutput, error)
@@ -148,37 +157,227 @@ func GetR53ClientCfg(sess *awss.Session, roleARN string) *aws.Config {
148157
}
149158

150159
// CreateOrUpdateRecord Creates or Updates the Route53 Record for the cluster endpoint.
151-
func (c *Client) CreateOrUpdateRecord(ic *types.InstallConfig, target string, cfg *aws.Config) error {
152-
zone, err := c.GetBaseDomain(ic.BaseDomain)
153-
if err != nil {
154-
return err
160+
func (c *Client) CreateOrUpdateRecord(ctx context.Context, ic *types.InstallConfig, target string, intTarget string, phzID string) error {
161+
useCNAME := cnameRegions.Has(ic.AWS.Region)
162+
aliasZoneID := hostedZoneIDPerRegionNLBMap[ic.AWS.Region]
163+
164+
apiName := fmt.Sprintf("api.%s.", ic.ClusterDomain())
165+
apiIntName := fmt.Sprintf("api-int.%s.", ic.ClusterDomain())
166+
167+
// Create api record in public zone
168+
if ic.Publish == types.ExternalPublishingStrategy {
169+
zone, err := c.GetBaseDomain(ic.BaseDomain)
170+
if err != nil {
171+
return err
172+
}
173+
174+
svc := route53.New(c.ssn) // we dont want to assume role here
175+
if _, err := createRecord(ctx, svc, aws.StringValue(zone.Id), apiName, target, aliasZoneID, useCNAME); err != nil {
176+
return fmt.Errorf("failed to create records for api: %w", err)
177+
}
178+
logrus.Debugln("Created public API record in public zone")
179+
}
180+
181+
// Create service with assumed role for PHZ
182+
svc := route53.New(c.ssn, GetR53ClientCfg(c.ssn, ic.AWS.HostedZoneRole))
183+
184+
// Create api record in private zone
185+
if _, err := createRecord(ctx, svc, phzID, apiName, intTarget, aliasZoneID, useCNAME); err != nil {
186+
return fmt.Errorf("failed to create records for api: %w", err)
187+
}
188+
logrus.Debugln("Created public API record in private zone")
189+
190+
// Create api-int record in private zone
191+
if _, err := createRecord(ctx, svc, phzID, apiIntName, intTarget, aliasZoneID, useCNAME); err != nil {
192+
return fmt.Errorf("failed to create records for api-int: %w", err)
155193
}
194+
logrus.Debugln("Created private API record in private zone")
156195

157-
params := &route53.ChangeResourceRecordSetsInput{
196+
return nil
197+
}
198+
199+
func createRecord(ctx context.Context, client *route53.Route53, zoneID, name, dnsName, aliasZoneID string, useCNAME bool) (*route53.ChangeInfo, error) {
200+
recordSet := &route53.ResourceRecordSet{
201+
Name: aws.String(name),
202+
}
203+
if useCNAME {
204+
recordSet.SetType("CNAME")
205+
recordSet.SetTTL(10)
206+
recordSet.SetResourceRecords([]*route53.ResourceRecord{
207+
{Value: aws.String(dnsName)},
208+
})
209+
} else {
210+
recordSet.SetType("A")
211+
recordSet.SetAliasTarget(&route53.AliasTarget{
212+
DNSName: aws.String(dnsName),
213+
HostedZoneId: aws.String(aliasZoneID),
214+
EvaluateTargetHealth: aws.Bool(false),
215+
})
216+
}
217+
input := &route53.ChangeResourceRecordSetsInput{
218+
HostedZoneId: aws.String(zoneID),
158219
ChangeBatch: &route53.ChangeBatch{
159-
Comment: aws.String(fmt.Sprintf("Creating record for api and api-int in domain %s", ic.ClusterDomain())),
160-
},
161-
HostedZoneId: zone.Id,
162-
}
163-
for _, prefix := range []string{"api", "api-int"} {
164-
params.ChangeBatch.Changes = append(params.ChangeBatch.Changes, &route53.Change{
165-
Action: aws.String("UPSERT"),
166-
ResourceRecordSet: &route53.ResourceRecordSet{
167-
Name: aws.String(fmt.Sprintf("%s.%s.", prefix, ic.ClusterDomain())),
168-
Type: aws.String("A"),
169-
AliasTarget: &route53.AliasTarget{
170-
DNSName: aws.String(target),
171-
HostedZoneId: aws.String(hostedZoneIDPerRegionNLBMap[ic.AWS.Region]),
172-
EvaluateTargetHealth: aws.Bool(true),
220+
Comment: aws.String(fmt.Sprintf("Creating record %s", name)),
221+
Changes: []*route53.Change{
222+
{
223+
Action: aws.String("UPSERT"),
224+
ResourceRecordSet: recordSet,
173225
},
174226
},
175-
})
227+
},
176228
}
229+
res, err := client.ChangeResourceRecordSetsWithContext(ctx, input)
230+
if err != nil {
231+
return nil, err
232+
}
233+
234+
return res.ChangeInfo, nil
235+
}
236+
237+
// HostedZoneInput defines the input parameters for hosted zone creation.
238+
type HostedZoneInput struct {
239+
Name string
240+
InfraID string
241+
VpcID string
242+
Region string
243+
Role string
244+
UserTags map[string]string
245+
}
246+
247+
// CreateHostedZone creates a private hosted zone.
248+
func (c *Client) CreateHostedZone(ctx context.Context, input *HostedZoneInput) (*route53.HostedZone, error) {
249+
cfg := GetR53ClientCfg(c.ssn, input.Role)
177250
svc := route53.New(c.ssn, cfg)
178-
if _, err := svc.ChangeResourceRecordSets(params); err != nil {
179-
return fmt.Errorf("failed to create records for api/api-int: %w", err)
251+
252+
callRef := fmt.Sprintf("%d", time.Now().Unix())
253+
res, err := svc.CreateHostedZoneWithContext(ctx, &route53.CreateHostedZoneInput{
254+
CallerReference: aws.String(callRef),
255+
Name: aws.String(input.Name),
256+
HostedZoneConfig: &route53.HostedZoneConfig{
257+
PrivateZone: aws.Bool(true),
258+
Comment: aws.String("Created by Openshift Installer"),
259+
},
260+
VPC: &route53.VPC{
261+
VPCId: aws.String(input.VpcID),
262+
VPCRegion: aws.String(input.Region),
263+
},
264+
})
265+
if err != nil {
266+
return nil, fmt.Errorf("error creating private hosted zone: %w", err)
180267
}
181-
return nil
268+
269+
if res == nil {
270+
return nil, fmt.Errorf("error creating private hosted zone: %w", err)
271+
}
272+
273+
// Tag the hosted zone
274+
tags := mergeTags(input.UserTags, map[string]string{
275+
"Name": fmt.Sprintf("%s-int", input.InfraID),
276+
})
277+
_, err = svc.ChangeTagsForResourceWithContext(ctx, &route53.ChangeTagsForResourceInput{
278+
ResourceType: aws.String("hostedzone"),
279+
ResourceId: res.HostedZone.Id,
280+
AddTags: r53Tags(tags),
281+
})
282+
if err != nil {
283+
return nil, fmt.Errorf("failed to tag private hosted zone: %w", err)
284+
}
285+
286+
// Set SOA minimum TTL
287+
recordSet, err := existingRecordSet(ctx, svc, res.HostedZone.Id, input.Name, "SOA")
288+
if err != nil {
289+
return nil, fmt.Errorf("failed to find SOA record set for private zone: %w", err)
290+
}
291+
if len(recordSet.ResourceRecords) == 0 || recordSet.ResourceRecords[0] == nil || recordSet.ResourceRecords[0].Value == nil {
292+
return nil, fmt.Errorf("failed to find SOA record for private zone")
293+
}
294+
record := recordSet.ResourceRecords[0]
295+
fields := strings.Split(aws.StringValue(record.Value), " ")
296+
if len(fields) != 7 {
297+
return nil, fmt.Errorf("SOA record value has %d fields, expected 7", len(fields))
298+
}
299+
fields[0] = "60"
300+
record.Value = aws.String(strings.Join(fields, " "))
301+
req, err := svc.ChangeResourceRecordSetsWithContext(ctx, &route53.ChangeResourceRecordSetsInput{
302+
HostedZoneId: res.HostedZone.Id,
303+
ChangeBatch: &route53.ChangeBatch{
304+
Changes: []*route53.Change{
305+
{
306+
Action: aws.String("UPSERT"),
307+
ResourceRecordSet: recordSet,
308+
},
309+
},
310+
},
311+
})
312+
if err != nil {
313+
return nil, fmt.Errorf("failed to set SOA TTL to minimum: %w", err)
314+
}
315+
316+
if err = svc.WaitUntilResourceRecordSetsChangedWithContext(ctx, &route53.GetChangeInput{Id: req.ChangeInfo.Id}); err != nil {
317+
return nil, fmt.Errorf("failed to wait for SOA TTL change: %w", err)
318+
}
319+
320+
return res.HostedZone, nil
321+
}
322+
323+
func existingRecordSet(ctx context.Context, client *route53.Route53, zoneID *string, recordName string, recordType string) (*route53.ResourceRecordSet, error) {
324+
name := fqdn(strings.ToLower(recordName))
325+
res, err := client.ListResourceRecordSetsWithContext(ctx, &route53.ListResourceRecordSetsInput{
326+
HostedZoneId: zoneID,
327+
StartRecordName: aws.String(name),
328+
StartRecordType: aws.String(recordType),
329+
MaxItems: aws.String("1"),
330+
})
331+
if err != nil {
332+
return nil, fmt.Errorf("failed to list record sets: %w", err)
333+
}
334+
for _, rs := range res.ResourceRecordSets {
335+
resName := strings.ToLower(cleanRecordName(aws.StringValue(rs.Name)))
336+
resType := strings.ToUpper(aws.StringValue(rs.Type))
337+
if resName == name && resType == recordType {
338+
return rs, nil
339+
}
340+
}
341+
342+
return nil, fmt.Errorf("not found")
343+
}
344+
345+
func fqdn(name string) string {
346+
n := len(name)
347+
if n == 0 || name[n-1] == '.' {
348+
return name
349+
}
350+
return name + "."
351+
}
352+
353+
func cleanRecordName(name string) string {
354+
s, err := strconv.Unquote(`"` + name + `"`)
355+
if err != nil {
356+
return name
357+
}
358+
return s
359+
}
360+
361+
func mergeTags(lhsTags, rhsTags map[string]string) map[string]string {
362+
merged := make(map[string]string, len(lhsTags)+len(rhsTags))
363+
for k, v := range lhsTags {
364+
merged[k] = v
365+
}
366+
for k, v := range rhsTags {
367+
merged[k] = v
368+
}
369+
return merged
370+
}
371+
372+
func r53Tags(tags map[string]string) []*route53.Tag {
373+
rtags := make([]*route53.Tag, 0, len(tags))
374+
for k, v := range tags {
375+
rtags = append(rtags, &route53.Tag{
376+
Key: aws.String(k),
377+
Value: aws.String(v),
378+
})
379+
}
380+
return rtags
182381
}
183382

184383
// See https://docs.aws.amazon.com/general/latest/gr/elb.html#elb_region

0 commit comments

Comments
 (0)