Skip to content

Commit a880521

Browse files
committed
Add support for LetsEncrypt via domain annotation
* Expects root domain to already be created and validated on DigitalOcean (DO is not a registrar so we assume user has preconfigured domain) * Add domain annotation to specify either the root domain or a subdomain of your choosing to the LoadBalancer service * Automatically find or generate certificate, and attach to LoadBalancer * Automatically generate A-record for your subdomain to point to the LoadBalancer
1 parent 0807bd5 commit a880521

File tree

5 files changed

+831
-7
lines changed

5 files changed

+831
-7
lines changed

cloud-controller-manager/do/certificates.go

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,199 @@ limitations under the License.
1616

1717
package do
1818

19+
import (
20+
"context"
21+
"fmt"
22+
"net/http"
23+
"strings"
24+
25+
"github.com/digitalocean/godo"
26+
v1 "k8s.io/api/core/v1"
27+
"k8s.io/klog"
28+
)
29+
1930
const (
2031
// DO Certificate types
2132
certTypeLetsEncrypt = "lets_encrypt"
2233
certTypeCustom = "custom"
34+
35+
// Certificate constants
36+
certPrefix = "do-ccm-"
2337
)
38+
39+
// ensureDomain checks to see if the service contains the annDODomain annotation
40+
// and if it does it verifies the domain exists on the users account
41+
func (l *loadBalancers) ensureDomain(ctx context.Context, service *v1.Service) error {
42+
domain, err := getDomain(service)
43+
if err != nil {
44+
return err
45+
}
46+
47+
if domain != nil {
48+
klog.V(2).Infof("looking up root domain: %s", domain.root)
49+
_, resp, err := l.resources.gclient.Domains.Get(ctx, domain.root)
50+
if err != nil && resp.StatusCode == http.StatusNotFound {
51+
return fmt.Errorf("domain does not exist: %s", domain.root)
52+
} else if err != nil {
53+
return fmt.Errorf("failed to retrieve domain: %s", err)
54+
}
55+
}
56+
57+
return nil
58+
}
59+
60+
// ensureCertificate verifies the existing certificate on the service is still a valid
61+
// DO certificate, clearing if not. If the user also has the annDODomain annotation set,
62+
// it verifies the certificate is valid for the domain, creating a new one if it cannot
63+
// find a existing valid certificate in the account
64+
func (l *loadBalancers) ensureCertificate(ctx context.Context, service *v1.Service) error {
65+
var certificate *godo.Certificate
66+
serviceCertID := getCertificateID(service)
67+
68+
// verify the cert-id is still referencing a valid certificate
69+
if serviceCertID != "" {
70+
klog.V(2).Infof("looking up existing service certificate: %s", serviceCertID)
71+
certificate, _, err := l.resources.gclient.Certificates.Get(ctx, serviceCertID)
72+
73+
if err != nil {
74+
respErr, ok := err.(*godo.ErrorResponse)
75+
if !ok || respErr.Response.StatusCode != http.StatusNotFound {
76+
return fmt.Errorf("failed to get DO certificate for service: %s", err)
77+
}
78+
}
79+
80+
if certificate == nil {
81+
klog.V(2).Infof("service certificate is no longer valid: %s", serviceCertID)
82+
serviceCertID = ""
83+
updateServiceAnnotation(service, annDOCertificateID, serviceCertID)
84+
}
85+
}
86+
87+
domain, err := getDomain(service)
88+
if err != nil {
89+
return err
90+
}
91+
92+
// no domain annotation, no need to ensure certificate for domain
93+
if domain == nil {
94+
return nil
95+
}
96+
97+
// if the current cert represents the domain, save making an extra network request
98+
// this case will arise when certificates are automatically rotated
99+
if certificate != nil {
100+
for _, dnsName := range certificate.DNSNames {
101+
if dnsName == domain.full {
102+
// we found matching certificate, break out of ensureCertificate
103+
return nil
104+
}
105+
}
106+
107+
// the current certificate does not match the current domain. clear it so
108+
// we can either find the matching cert or create a new one below
109+
klog.V(2).Infof("service certificate is not valid for domain[%s]: %s", domain.full, serviceCertID)
110+
certificate = nil
111+
}
112+
113+
certificates, _, err := l.resources.gclient.Certificates.List(ctx, &godo.ListOptions{})
114+
if err != nil {
115+
return fmt.Errorf("failed to retrieve certificates: %s", err)
116+
}
117+
118+
findCert:
119+
for _, cert := range certificates {
120+
for _, dnsName := range cert.DNSNames {
121+
if dnsName == domain.full {
122+
klog.V(2).Infof("found existing certificate for domain[%s]: %s", domain.full, cert.ID)
123+
certificate = &cert
124+
break findCert
125+
}
126+
}
127+
}
128+
129+
if certificate == nil {
130+
certName := getCertificateName(domain.full)
131+
dnsNames := []string{domain.root}
132+
133+
if domain.sub != "" {
134+
dnsNames = append(dnsNames, domain.full)
135+
}
136+
137+
certificateReq := &godo.CertificateRequest{
138+
Name: certName,
139+
DNSNames: dnsNames,
140+
Type: certTypeLetsEncrypt,
141+
}
142+
143+
klog.V(2).Infof("generating new service certificate for domain: %s", domain.full)
144+
certificate, _, err = l.resources.gclient.Certificates.Create(ctx, certificateReq)
145+
if err != nil {
146+
return fmt.Errorf("failed to create certificate: %s", err)
147+
}
148+
}
149+
150+
updateServiceAnnotation(service, annDOCertificateID, certificate.ID)
151+
return nil
152+
}
153+
154+
// ensureDomainARecord ensures that if the service has a annDODomain annotation,
155+
// the domain has an A record for the full subdomain pointing to the loadbalancer
156+
func (l *loadBalancers) ensureDomainARecord(ctx context.Context, service *v1.Service, lb *godo.LoadBalancer) error {
157+
domain, err := getDomain(service)
158+
if err != nil {
159+
return err
160+
}
161+
162+
if domain == nil {
163+
return nil
164+
}
165+
166+
// the do loadbalancer service ensures the root domain associated with the
167+
// certificate has an A record pointing to the LB so we do not need to ensure
168+
if domain.sub == "" {
169+
klog.V(2).Infof("domain has no subdomain, no need to ensure A records: %s", domain.full)
170+
return nil
171+
}
172+
173+
records, _, err := l.resources.gclient.Domains.Records(ctx, domain.root, &godo.ListOptions{})
174+
if err != nil {
175+
return fmt.Errorf("failed to fetch records for domain: %s", err)
176+
}
177+
178+
var domainRecord *godo.DomainRecord
179+
for _, record := range records {
180+
if record.Type != "A" || record.Name != domain.sub {
181+
continue
182+
}
183+
184+
if record.Data != lb.IP {
185+
return fmt.Errorf("domain(%s) record already in use for another ip(%s)", domain.full, record.Data)
186+
}
187+
188+
klog.V(2).Infof("found A record to loadbalancer for domain: %s", domain.full)
189+
domainRecord = &record
190+
break
191+
}
192+
193+
if domainRecord == nil {
194+
domainRecordEditReq := &godo.DomainRecordEditRequest{
195+
Type: "A",
196+
Name: domain.sub,
197+
Data: lb.IP,
198+
TTL: defaultDomainRecordTTL,
199+
}
200+
klog.V(2).Infof("creating new A record to loadbalance for domain: %s", domain.full)
201+
domainRecord, _, err = l.resources.gclient.Domains.CreateRecord(ctx, domain.root, domainRecordEditReq)
202+
if err != nil {
203+
return fmt.Errorf("failed to create domain record: %s", err)
204+
}
205+
}
206+
207+
return nil
208+
}
209+
210+
// getCertificateName returns a prefixed certificate so we know to cleanup
211+
// certificate when a loadbalancer for the given domain is deleted
212+
func getCertificateName(fullDomain string) string {
213+
return fmt.Sprintf("%s%s", certPrefix, strings.ReplaceAll(fullDomain, ".", "-"))
214+
}

0 commit comments

Comments
 (0)