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