Skip to content

Commit 4cfd14d

Browse files
authored
feat(endpoint): preserve CNAME during adoption (#3)
1 parent a470d74 commit 4cfd14d

6 files changed

Lines changed: 89 additions & 6 deletions

File tree

‎deploy/charts/dns-api/Chart.yaml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ apiVersion: v2
22
name: dns-api
33
description: Kubernetes DNS API controller for Route 53 and Cloudflare
44
type: application
5-
version: 0.2.6
6-
appVersion: "0.2.6"
5+
version: 0.2.7
6+
appVersion: "0.2.7"
77
kubeVersion: ">=1.22.0-0"

‎internal/go/apps/endpoint/controllers/endpointrecordset/controller.go‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const (
3838
generatedLabelEndpointRecordSetNamespace = "endpoint.dns.appthrust.io/endpointrecordset-namespace"
3939
generatedLabelEndpointRecordSetName = "endpoint.dns.appthrust.io/endpointrecordset-name"
4040
route53RecordSetAdoptionAnnotation = "endpoint.dns.appthrust.io/route53-recordset-adoption"
41+
route53RecordTypeAnnotation = "endpoint.dns.appthrust.io/route53-record-type"
4142
generatedRecordSetsFinalizer = "endpoint.dns.appthrust.io/generated-recordsets"
4243
)
4344

@@ -203,11 +204,16 @@ func (r *Reconciler) recordSetsForHostname(ctx context.Context, endpointRecordSe
203204
rejections = append(rejections, fmt.Sprintf("%s/%s: multiple EndpointProviderCapability resources for provider %s/%s", zone.Namespace, zone.Name, zone.Spec.Provider.Name, zone.Spec.Provider.Version))
204205
continue
205206
}
207+
preferredRecordType, err := preferredRecordTypeForEndpointRecordSet(endpointRecordSet)
208+
if err != nil {
209+
return status, nil, err
210+
}
206211
input := endpointv1alpha1.EndpointRecordSetConversionInput{
207-
Hostname: hostname,
208-
Name: relativeRecordName(hostname, zone.Spec.DomainName),
209-
Zone: endpointv1alpha1.EndpointRecordSetConversionZone{DomainName: zone.Spec.DomainName},
210-
Targets: endpointRecordSet.Spec.Targets,
212+
Hostname: hostname,
213+
Name: relativeRecordName(hostname, zone.Spec.DomainName),
214+
Zone: endpointv1alpha1.EndpointRecordSetConversionZone{DomainName: zone.Spec.DomainName},
215+
Targets: endpointRecordSet.Spec.Targets,
216+
PreferredRecordType: preferredRecordType,
211217
}
212218
fragments, message, err := r.convertEndpointRecordSet(ctx, capability, input)
213219
if err != nil {
@@ -269,6 +275,17 @@ func (r *Reconciler) recordSetsForHostname(ctx context.Context, endpointRecordSe
269275
return status, nil, nil
270276
}
271277

278+
func preferredRecordTypeForEndpointRecordSet(endpointRecordSet *endpointv1alpha1.EndpointRecordSet) (endpointv1alpha1.EndpointRecordSetType, error) {
279+
value := strings.ToUpper(strings.TrimSpace(endpointRecordSet.Annotations[route53RecordTypeAnnotation]))
280+
switch value {
281+
case "":
282+
return "", nil
283+
case string(endpointv1alpha1.EndpointRecordSetTypeCNAME):
284+
return endpointv1alpha1.EndpointRecordSetTypeCNAME, nil
285+
default:
286+
return "", fmt.Errorf("unsupported %s value %q", route53RecordTypeAnnotation, value)
287+
}
288+
}
272289
func (r *Reconciler) convertEndpointRecordSet(ctx context.Context, capability *endpointv1alpha1.EndpointProviderCapability, input endpointv1alpha1.EndpointRecordSetConversionInput) ([]endpointv1alpha1.RecordSetSpecFragment, string, error) {
273290
if r.conversionFunc != nil {
274291
return r.conversionFunc(ctx, capability, input)
@@ -286,6 +303,7 @@ func (r *Reconciler) convertEndpointRecordSet(ctx context.Context, capability *e
286303
Resource: capability.Spec.Conversion.Resource,
287304
}
288305
uid := string(uuid.NewUUID())
306+
289307
request := endpointconversionv1alpha1.EndpointRecordSetConversion{
290308
TypeMeta: metav1.TypeMeta{
291309
APIVersion: gvr.Group + "/" + gvr.Version,

‎internal/go/apps/endpoint/controllers/endpointrecordset/controller_test.go‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,27 @@ import (
1414
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1515
"k8s.io/apimachinery/pkg/runtime"
1616
ctrl "sigs.k8s.io/controller-runtime"
17+
1718
"sigs.k8s.io/controller-runtime/pkg/client"
1819
"sigs.k8s.io/controller-runtime/pkg/client/fake"
1920
)
2021

22+
func TestPreferredRecordTypeForEndpointRecordSet(t *testing.T) {
23+
recordSet := &endpointv1alpha1.EndpointRecordSet{
24+
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{
25+
route53RecordTypeAnnotation: "cname",
26+
}},
27+
}
28+
got, err := preferredRecordTypeForEndpointRecordSet(recordSet)
29+
if err != nil || got != endpointv1alpha1.EndpointRecordSetTypeCNAME {
30+
t.Fatalf("preferred record type = (%q, %v)", got, err)
31+
}
32+
recordSet.Annotations[route53RecordTypeAnnotation] = "A"
33+
if _, err := preferredRecordTypeForEndpointRecordSet(recordSet); err == nil {
34+
t.Fatal("unsupported preferred record type was accepted")
35+
}
36+
}
37+
2138
func TestEndpointRecordSetApexWildcardAliasLifecycleSurvivesControllerRestart(t *testing.T) {
2239
ctx := context.Background()
2340
scheme := runtime.NewScheme()

‎internal/go/providers/route53/conversion/handler.go‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ func convertEndpoint(input endpointv1alpha1.EndpointRecordSetConversionInput) ([
114114
return nil, failureResult("UnsupportedTarget", "multiple hostname targets require routing policy, which is not supported yet", false)
115115
}
116116
if len(hostnameTargets) == 1 {
117+
if input.PreferredRecordType == endpointv1alpha1.EndpointRecordSetTypeCNAME {
118+
return route53CNAMEFragments(input.Name, hostnameTargets[0])
119+
}
120+
if input.PreferredRecordType != "" {
121+
return nil, failureResult("UnsupportedTarget", "preferredRecordType must be CNAME for a hostname target", false)
122+
}
117123
return route53AliasFragments(input.Name, hostnameTargets[0])
118124
}
119125
fragments := make([]endpointv1alpha1.RecordSetSpecFragment, 0, 2)
@@ -140,6 +146,20 @@ func convertEndpoint(input endpointv1alpha1.EndpointRecordSetConversionInput) ([
140146
return fragments, successResult("Converted", "converted endpoint targets to Route 53 record sets")
141147
}
142148

149+
func route53CNAMEFragments(name, targetValue string) ([]endpointv1alpha1.RecordSetSpecFragment, endpointconversionv1alpha1.EndpointRecordSetConversionResult) {
150+
target := strings.TrimSuffix(strings.ToLower(strings.TrimSpace(targetValue)), ".")
151+
if target == "" {
152+
return nil, failureResult("InvalidInput", "CNAME target must not be empty", false)
153+
}
154+
ttl := int32(60)
155+
return []endpointv1alpha1.RecordSetSpecFragment{{
156+
Type: endpointv1alpha1.EndpointRecordSetTypeCNAME,
157+
Name: name,
158+
TTL: &ttl,
159+
CNAME: &dnsv1alpha1.CNAMERecordSet{Target: target},
160+
}}, successResult("Converted", "converted hostname endpoint target to Route 53 CNAME record set")
161+
}
162+
143163
func route53AliasFragments(name, targetValue string) ([]endpointv1alpha1.RecordSetSpecFragment, endpointconversionv1alpha1.EndpointRecordSetConversionResult) {
144164
target := normalizeRoute53AliasDNSName(targetValue)
145165
hostedZoneID := route53CanonicalHostedZone(target)

‎internal/go/providers/route53/conversion/handler_test.go‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,28 @@ func TestHandlerConvertsELBHostnameToAliasAAndAAAA(t *testing.T) {
7272
}
7373
}
7474

75+
func TestConvertELBHostnameToCNAMEWhenRequestedForAdoption(t *testing.T) {
76+
fragments, result := convertEndpoint(endpointv1alpha1.EndpointRecordSetConversionInput{
77+
Hostname: "api.example.com",
78+
Name: "api",
79+
Zone: endpointv1alpha1.EndpointRecordSetConversionZone{DomainName: "example.com"},
80+
PreferredRecordType: endpointv1alpha1.EndpointRecordSetTypeCNAME,
81+
Targets: []endpointv1alpha1.EndpointTarget{{
82+
Type: endpointv1alpha1.EndpointTargetTypeHostname,
83+
Value: "k8s-public-123456.ap-northeast-1.elb.amazonaws.com",
84+
}},
85+
})
86+
if result.Status != "Success" || len(fragments) != 1 {
87+
t.Fatalf("conversion = (%#v, %#v)", fragments, result)
88+
}
89+
if fragments[0].Type != endpointv1alpha1.EndpointRecordSetTypeCNAME ||
90+
fragments[0].CNAME == nil ||
91+
fragments[0].CNAME.Target != "k8s-public-123456.ap-northeast-1.elb.amazonaws.com" ||
92+
fragments[0].TTL == nil || *fragments[0].TTL != 60 {
93+
t.Fatalf("CNAME fragment = %#v", fragments[0])
94+
}
95+
}
96+
7597
func TestConvertOrganizationApexAndWildcardToAliasAAndAAAA(t *testing.T) {
7698
for _, recordName := range []string{"reo", "*.reo"} {
7799
t.Run(recordName, func(t *testing.T) {

‎pkg/go/api/endpoint/v1alpha1/endpointrecordset_types.go‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,12 @@ type EndpointRecordSetConversionInput struct {
182182
// +kubebuilder:validation:MinItems=1
183183
// +listType=atomic
184184
Targets []EndpointTarget `json:"targets"`
185+
186+
// PreferredRecordType asks a Provider conversion API to preserve an
187+
// existing provider-neutral record shape when supported. Omitted means
188+
// provider-default conversion.
189+
// +optional
190+
PreferredRecordType EndpointRecordSetType `json:"preferredRecordType,omitempty"`
185191
}
186192

187193
type EndpointRecordSetConversionZone struct {

0 commit comments

Comments
 (0)