@@ -16,8 +16,199 @@ limitations under the License.
1616
1717package 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+
1930const (
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