@@ -16,8 +16,212 @@ 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 ) (* domain , error ) {
42+ domain , err := getDomain (service )
43+ if err != nil {
44+ return domain , err
45+ }
46+
47+ if domain == nil {
48+ return nil , nil
49+ }
50+
51+ klog .V (2 ).Infof ("Looking up root domain specified in service: %s" , domain .root )
52+ _ , _ , err = l .resources .gclient .Domains .Get (ctx , domain .root )
53+ if err != nil {
54+ return nil , fmt .Errorf ("failed to retrieve root domain %s: %s" , domain .root , err )
55+ }
56+
57+ return domain , nil
58+ }
59+
60+ func (l * loadBalancers ) validateCertificateExistence (ctx context.Context , certificateID string ) (* godo.Certificate , error ) {
61+ if certificateID == "" {
62+ return nil , nil
63+ }
64+
65+ certificate , resp , err := l .resources .gclient .Certificates .Get (ctx , certificateID )
66+ if err != nil && resp .StatusCode != http .StatusNotFound {
67+ return nil , fmt .Errorf ("failed to fetch certificate: %s" , err )
68+ }
69+
70+ return certificate , nil
71+ }
72+
73+ // validateServiceCertificate ensures the certificate specified in the service annotation
74+ // still exists. If it does not, then the annotation is cleared from the service.
75+ func (l * loadBalancers ) validateServiceCertificate (ctx context.Context , service * v1.Service ) (* godo.Certificate , error ) {
76+ certificateID := getCertificateID (service )
77+ klog .V (2 ).Infof ("Looking up certificate for service %s/%s by ID %s" , service .Namespace , service .Name , certificateID )
78+ certificate , err := l .validateCertificateExistence (ctx , certificateID )
79+ if err != nil {
80+ return nil , err
81+ }
82+
83+ if certificate == nil {
84+ updateServiceAnnotation (service , annDOCertificateID , "" )
85+ }
86+
87+ return certificate , nil
88+ }
89+
90+ func (l * loadBalancers ) ensureCertificateForDomain (ctx context.Context , serviceCertificate * godo.Certificate , domain * domain ) (* godo.Certificate , error ) {
91+ if serviceCertificate != nil && isValidCertificateForDomain (serviceCertificate , domain ) {
92+ return serviceCertificate , nil
93+ }
94+
95+ serviceCertificate , err := l .findCertificateForDomain (ctx , domain )
96+ if err != nil {
97+ return nil , err
98+ }
99+
100+ if serviceCertificate == nil {
101+ serviceCertificate , err = l .generateCertificateForDomain (ctx , domain )
102+ if err != nil {
103+ return nil , err
104+ }
105+ }
106+
107+ return serviceCertificate , nil
108+ }
109+
110+ func isValidCertificateForDomain (certificate * godo.Certificate , domain * domain ) bool {
111+ for _ , dnsName := range certificate .DNSNames {
112+ if dnsName == domain .full {
113+ // we found matching certificate, break out of ensureCertificate
114+ return true
115+ }
116+ }
117+
118+ return false
119+ }
120+
121+ func (l * loadBalancers ) findCertificateForDomain (ctx context.Context , domain * domain ) (* godo.Certificate , error ) {
122+ certificates , _ , err := l .resources .gclient .Certificates .List (ctx , & godo.ListOptions {})
123+ if err != nil {
124+ return nil , fmt .Errorf ("Failed to list certificates: %s" , err )
125+ }
126+
127+ var certificate * godo.Certificate
128+
129+ for _ , c := range certificates {
130+ if isValidCertificateForDomain (& c , domain ) {
131+ certificate = & c
132+ break
133+ }
134+ }
135+
136+ return certificate , nil
137+ }
138+
139+ func (l * loadBalancers ) generateCertificateForDomain (ctx context.Context , domain * domain ) (* godo.Certificate , error ) {
140+ certName := getCertificateName (domain .full )
141+ dnsNames := []string {domain .root }
142+
143+ if domain .sub != "" {
144+ dnsNames = append (dnsNames , domain .full )
145+ }
146+
147+ certificateReq := & godo.CertificateRequest {
148+ Name : certName ,
149+ DNSNames : dnsNames ,
150+ Type : certTypeLetsEncrypt ,
151+ }
152+
153+ klog .V (2 ).Infof ("Generating new certificate for domain: %s" , domain .full )
154+ certificate , _ , err := l .resources .gclient .Certificates .Create (ctx , certificateReq )
155+ if err != nil {
156+ return nil , fmt .Errorf ("failed to create certificate: %s" , err )
157+ }
158+
159+ return certificate , nil
160+ }
161+
162+ func findARecordForNameAndIP (records []godo.DomainRecord , name string , ip string ) (* godo.DomainRecord , error ) {
163+ var record * godo.DomainRecord
164+
165+ for _ , r := range records {
166+ if r .Type != "A" || r .Name != name {
167+ continue
168+ }
169+
170+ if r .Data != ip {
171+ return nil , fmt .Errorf ("the A record(%s) is already in use with another IP(%s)" , name , r .Data )
172+ }
173+
174+ record = & r
175+ break
176+ }
177+
178+ return record , nil
179+ }
180+
181+ // ensureDomainARecords ensures that if the service has a domain annotation,
182+ // the domain has an A record for the full subdomain pointing to the loadbalancer
183+ func (l * loadBalancers ) ensureDomainARecords (ctx context.Context , domain * domain , lb * godo.LoadBalancer ) error {
184+ records , _ , err := l .resources .gclient .Domains .Records (ctx , domain .root , & godo.ListOptions {})
185+ if err != nil {
186+ return fmt .Errorf ("failed to fetch records for domain(%s): %s" , domain .root , err )
187+ }
188+
189+ err = l .ensureDomainARecord (ctx , records , domain .root , "@" , lb .IP )
190+ if err != nil {
191+ return err
192+ }
193+
194+ err = l .ensureDomainARecord (ctx , records , domain .root , domain .sub , lb .IP )
195+ if err != nil {
196+ return err
197+ }
198+
199+ return nil
200+ }
201+
202+ func (l * loadBalancers ) ensureDomainARecord (ctx context.Context , records []godo.DomainRecord , domain string , name string , ip string ) error {
203+ record , err := findARecordForNameAndIP (records , name , ip )
204+ if err != nil {
205+ return err
206+ }
207+
208+ if record == nil {
209+ _ , _ , err = l .resources .gclient .Domains .CreateRecord (ctx , domain , & godo.DomainRecordEditRequest {
210+ Type : "A" ,
211+ Name : name ,
212+ Data : ip ,
213+ TTL : defaultDomainRecordTTL ,
214+ })
215+ if err != nil {
216+ return err
217+ }
218+ }
219+
220+ return nil
221+ }
222+
223+ // getCertificateName returns a prefixed certificate so we know to cleanup
224+ // certificate when a loadbalancer for the given domain is deleted
225+ func getCertificateName (fullDomain string ) string {
226+ return fmt .Sprintf ("%s%s" , certPrefix , strings .ReplaceAll (fullDomain , "." , "-" ))
227+ }
0 commit comments