Skip to content

Commit

Permalink
contrib: update route53 zone cleaning utility
Browse files Browse the repository at this point in the history
  • Loading branch information
sjenning committed Dec 10, 2024
1 parent a7794ad commit 30d5274
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 67 deletions.
5 changes: 5 additions & 0 deletions contrib/cleanzones/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM docker.io/golang:latest
WORKDIR /cleanzones
COPY . .
RUN go build .
ENTRYPOINT /cleanzones/cleanzones
7 changes: 7 additions & 0 deletions contrib/cleanzones/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module github.com/openshift/hypershift/contrib/cleanzones

go 1.22.7

require github.com/aws/aws-sdk-go v1.55.5

require github.com/jmespath/go-jmespath v0.4.0 // indirect
14 changes: 14 additions & 0 deletions contrib/cleanzones/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
153 changes: 86 additions & 67 deletions contrib/cleanzones/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,130 +39,149 @@ func main() {
if err != nil {
log.Fatal(err)
}
var vpcs []string
for _, vpc := range output.Vpcs {
vpcs = append(vpcs, *vpc.VpcId)
}

// Find all zones in use by VPCs
zonesIdsInUse := make(map[string]string)
for _, vpc := range vpcs {
output, err := route53client.ListHostedZonesByVPCWithContext(ctx, &route53.ListHostedZonesByVPCInput{VPCId: aws.String(vpc), VPCRegion: aws.String("us-east-1")})
if err != nil {
log.Fatal(err)
}
for _, zoneSummary := range output.HostedZoneSummaries {
zonesIdsInUse[*zoneSummary.Name] = *zoneSummary.HostedZoneId
// Get the list of infraIDs in use
var infraIDsInUse []string
for _, vpc := range output.Vpcs {
// Get the VPC name out of the tags
for _, tag := range vpc.Tags {
if *tag.Key == "Name" && len(strings.TrimSpace(*tag.Value)) > 0 {
infraID := strings.TrimSuffix(*tag.Value, "-vpc")
if len(infraID) > 0 {
infraIDsInUse = append(infraIDsInUse, infraID)
}
}
}
}

for name, id := range zonesIdsInUse {
log.Printf("%s %s in use", name, id)
}
log.Println("infraIDs in use:", infraIDsInUse)

examplePrivateZoneName := regexp.MustCompile("example-([a-z0-9]{5}).hypershift.local.")

publicZones := map[string]string{
"ci.hypershift.devcluster.openshift.com.": "",
"hive.hypershift.devcluster.openshift.com.": "",
"ci.hypershift.devcluster.openshift.com.": "",
"service.ci.hypershift.devcluster.openshift.com.": "",
}

// Delete unused private zones
err = route53client.ListHostedZonesPagesWithContext(ctx, &route53.ListHostedZonesInput{}, func(lhzo *route53.ListHostedZonesOutput, lastPage bool) bool {
for _, zone := range lhzo.HostedZones {
// Sanity check
if zone == nil || zone.Name == nil || zone.Id == nil {
continue
}
zoneName := *zone.Name
zoneId := strings.TrimPrefix(*zone.Id, "/hostedzone/")

// Check if the zone is in the list of public zones
// If it is, store the zoneId in publicZones value for the next step
for zoneName := range publicZones {
if *zone.Name == zoneName {
publicZones[zoneName] = zoneId
}
}

// Exclude public zones
if zone.Config == nil || zone.Config.PrivateZone == nil || !*zone.Config.PrivateZone {
continue
}

// Exclude the CI public zones (reduneant with the public zones check above for saftey)
// and private zones that are not example-<infraID>.hypershift.local.
if !strings.HasSuffix(*zone.Name, ".ci.hypershift.devcluster.openshift.com.") &&
!strings.HasSuffix(*zone.Name, ".hive.hypershift.devcluster.openshift.com.") &&
!strings.HasSuffix(*zone.Name, ".service.ci.hypershift.devcluster.openshift.com.") &&
!examplePrivateZoneName.MatchString(*zone.Name) {
continue
}

// Check if the zone is in use
inUse := false
for _, zoneIdInUse := range zonesIdsInUse {
if zoneId == zoneIdInUse {
for _, infraID := range infraIDsInUse {
if strings.Contains(zoneName, infraID) {
inUse = true
break
}
}

// If the zone is in use, skip it
if inUse {
continue
}
log.Printf("deleting hosted zone %s with id %s", *zone.Name, *zone.Id)
deleteZone(ctx, strings.TrimSuffix(*zone.Name, "."), strings.TrimPrefix(*zone.Id, "/hostedzone/"), route53client)

// The zone is not in use, delete it
log.Printf("deleting hosted zone %s with id %s", zoneName, zoneId)
deleteZone(ctx, zoneId, route53client)
}
return !lastPage
})
if err != nil {
log.Fatal(err)
}

// Delete unused api.* records from public zone
// Delete unused records from public zones
for _, publicZoneId := range publicZones {
// Sanity check
if publicZoneId == "" {
continue
}
lrrsi := &route53.ListResourceRecordSetsInput{
HostedZoneId: aws.String(publicZoneId),
MaxItems: aws.String("200"),
}
output, err := route53client.ListResourceRecordSetsWithContext(ctx, lrrsi)
if err != nil {
log.Fatal(err)
}
if len(output.ResourceRecordSets) == 0 {
continue
MaxItems: aws.String("100"),
}
var changeBatch route53.ChangeBatch
var deleteRequired bool
for _, rrs := range output.ResourceRecordSets {
if *rrs.Type != "A" {
continue
}
inUseRecord := false
for zoneNameInUse := range zonesIdsInUse {
if strings.HasSuffix(*rrs.Name, zoneNameInUse) {
inUseRecord = true
break
err := route53client.ListResourceRecordSetsPagesWithContext(ctx, lrrsi, func(output *route53.ListResourceRecordSetsOutput, lastPage bool) bool {
var changeBatch route53.ChangeBatch
var deleteRequired bool
for _, rrs := range output.ResourceRecordSets {
// Sanity exlusion checks
if *rrs.Type != "A" && *rrs.Type != "CNAME" && *rrs.Type != "TXT" {
continue
}

// Check if the record is in use
inUseRecord := false
for _, infraID := range infraIDsInUse {
if strings.Contains(*rrs.Name, infraID) {
inUseRecord = true
break
}
}

// If the record is in use, skip it
if inUseRecord {
continue
}

// The record is not in use, delete it
deleteRequired = true
log.Printf("deleting record %s", *rrs.Name)

// Enqueue the record for deletion
changeBatch.Changes = append(changeBatch.Changes, &route53.Change{
Action: aws.String("DELETE"),
ResourceRecordSet: rrs,
})
}
if inUseRecord {
continue
}
deleteRequired = true
log.Printf("deleting record %s", *rrs.Name)
changeBatch.Changes = append(changeBatch.Changes, &route53.Change{
Action: aws.String("DELETE"),
ResourceRecordSet: rrs,
})
}
if !deleteRequired {
continue
}
crrsi := &route53.ChangeResourceRecordSetsInput{
HostedZoneId: aws.String(publicZoneId),
ChangeBatch: &changeBatch,
}
if deleteRequired {
// At least one record from the current page needs to be deleted
crrsi := &route53.ChangeResourceRecordSetsInput{
HostedZoneId: aws.String(publicZoneId),
ChangeBatch: &changeBatch,
}

if !dryRun {
_, err = route53client.ChangeResourceRecordSetsWithContext(ctx, crrsi)
if err != nil {
log.Fatal(err)
if !dryRun {
_, err = route53client.ChangeResourceRecordSetsWithContext(ctx, crrsi)
if err != nil {
log.Fatal(err)
}
}
}
return !lastPage
})
if err != nil {
log.Fatal(err)
}
}
}

func deleteZone(ctx context.Context, zoneName string, zoneId string, client route53iface.Route53API) error {
func deleteZone(ctx context.Context, zoneId string, client route53iface.Route53API) error {
err := deleteRecords(ctx, client, zoneId)
if err != nil {
return fmt.Errorf("failed to delete zone records")
Expand Down
5 changes: 5 additions & 0 deletions contrib/cleanzones/manifests/01-serviceaccount.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: cleanzones
namespace: hypershift
34 changes: 34 additions & 0 deletions contrib/cleanzones/manifests/02-cronjob.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: cleanzones
namespace: hypershift
spec:
schedule: "0 0 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: cleanzones
image: quay.io/hypershift/cleanzones:latest
command: ["/cleanzones/cleanzones"]
env:
- name: AWS_ROLE_ARN
value: "arn:aws:iam::820196288204:role/hypershift-ci-2-cleanzones"
- name: AWS_WEB_IDENTITY_TOKEN_FILE
value: "/var/run/secrets/openshift/token"
imagePullPolicy: Always
volumeMounts:
- name: token
mountPath: /var/run/secrets/openshift
readOnly: true
restartPolicy: Never
serviceAccountName: cleanzones
volumes:
- name: token
projected:
sources:
- serviceAccountToken:
path: token
audience: openshift
16 changes: 16 additions & 0 deletions contrib/cleanzones/role-policy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"route53:ListHostedZones",
"route53:ListResourceRecordSets",
"route53:ChangeResourceRecordSets",
"route53:DeleteHostedZone",
"ec2:DescribeVpcs"
],
"Resource": "*"
}
]
}

0 comments on commit 30d5274

Please sign in to comment.