-
Notifications
You must be signed in to change notification settings - Fork 2.1k
/
util.go
365 lines (324 loc) · 12.7 KB
/
util.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
/*
Copyright 2020 The cert-manager Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package acmeorders
import (
"context"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/cert-manager/cert-manager/pkg/acme"
acmecl "github.com/cert-manager/cert-manager/pkg/acme/client"
"github.com/cert-manager/cert-manager/pkg/api/util"
cmacme "github.com/cert-manager/cert-manager/pkg/apis/acme/v1"
cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
"github.com/cert-manager/cert-manager/pkg/controller/acmeorders/selectors"
logf "github.com/cert-manager/cert-manager/pkg/logs"
)
var (
orderGvk = cmacme.SchemeGroupVersion.WithKind("Order")
)
// buildPartialRequiredChallenges builds partial required ACME challenges by
// looking at authorization on order spec and related issuer. It does not call
// ACME. ensureKeysForChallenge must be called before creating the Challenge.
func buildPartialRequiredChallenges(ctx context.Context, issuer cmapi.GenericIssuer, o *cmacme.Order) ([]*cmacme.Challenge, error) {
chs := make([]*cmacme.Challenge, 0)
for _, a := range o.Status.Authorizations {
if a.InitialState == cmacme.Valid {
wc := false
if a.Wildcard != nil {
wc = *a.Wildcard
}
logf.FromContext(ctx).V(logf.DebugLevel).Info("Authorization already valid, not creating Challenge resource", "identifier", a.Identifier, "is_wildcard", wc)
continue
}
ch, err := buildPartialChallenge(ctx, issuer, o, a)
if err != nil {
return nil, err
}
chs = append(chs, ch)
}
return chs, nil
}
// buildPartialChallenge builds a challenge for the required ACME Authorization.
// The spec will be populated with fields that can be determined by looking at
// the ACME Authorization object returned in Order.
func buildPartialChallenge(ctx context.Context, issuer cmapi.GenericIssuer, o *cmacme.Order, authz cmacme.ACMEAuthorization) (*cmacme.Challenge, error) {
chSpec, err := partialChallengeSpecForAuthorization(ctx, issuer, o, authz)
if err != nil {
// TODO: in this case, we should probably not return the error as it's
// unlikely we can make it succeed by retrying.
return nil, err
}
chName, err := util.ComputeName(o.Name, chSpec)
if err != nil {
return nil, err
}
return &cmacme.Challenge{
ObjectMeta: metav1.ObjectMeta{
Name: chName,
Namespace: o.Namespace,
OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(o, orderGvk)},
},
Spec: *chSpec,
}, nil
}
// partialChallengeSpecForAuthorization builds a partial challenge spec by
// looking at the ACME authorization object and issuer. It does not make any
// ACME calls.
func partialChallengeSpecForAuthorization(ctx context.Context, issuer cmapi.GenericIssuer, o *cmacme.Order, authz cmacme.ACMEAuthorization) (*cmacme.ChallengeSpec, error) {
log := logf.FromContext(ctx, "challengeSpecForAuthorization")
dbg := log.V(logf.DebugLevel)
// 1. fetch solvers from issuer
solvers := issuer.GetSpec().ACME.Solvers
wc := false
if authz.Wildcard != nil {
wc = *authz.Wildcard
}
domainToFind := authz.Identifier
if wc {
domainToFind = "*." + domainToFind
}
var selectedSolver *cmacme.ACMEChallengeSolver
var selectedChallenge *cmacme.ACMEChallenge
selectedNumLabelsMatch := 0
selectedNumDNSNamesMatch := 0
selectedNumDNSZonesMatch := 0
challengeForSolver := func(solver *cmacme.ACMEChallengeSolver) *cmacme.ACMEChallenge {
for _, ch := range authz.Challenges {
switch {
case ch.Type == "http-01" && solver.HTTP01 != nil:
return &ch
case ch.Type == "dns-01" && solver.DNS01 != nil:
return &ch
}
}
return nil
}
// 2. filter solvers to only those that matchLabels
for _, cfg := range solvers {
acmech := challengeForSolver(&cfg) // #nosec G601 -- False positive. See https://github.com/golang/go/discussions/56010
if acmech == nil {
dbg.Info("cannot use solver as the ACME authorization does not allow solvers of this type")
continue
}
if cfg.Selector == nil {
if selectedSolver != nil {
dbg.Info("not selecting solver as previously selected solver has a just as or more specific selector")
continue
}
dbg.Info("selecting solver due to match all selector and no previously selected solver")
selectedSolver = cfg.DeepCopy()
selectedChallenge = acmech
continue
}
labelsMatch, numLabelsMatch := selectors.Labels(*cfg.Selector).Matches(o.ObjectMeta, domainToFind)
dnsNamesMatch, numDNSNamesMatch := selectors.DNSNames(*cfg.Selector).Matches(o.ObjectMeta, domainToFind)
dnsZonesMatch, numDNSZonesMatch := selectors.DNSZones(*cfg.Selector).Matches(o.ObjectMeta, domainToFind)
if !labelsMatch || !dnsNamesMatch || !dnsZonesMatch {
dbg.Info("not selecting solver", "labels_match", labelsMatch, "dnsnames_match", dnsNamesMatch, "dnszones_match", dnsZonesMatch)
continue
}
dbg.Info("selector matches")
selectSolver := func() {
selectedSolver = cfg.DeepCopy()
selectedChallenge = acmech
selectedNumLabelsMatch = numLabelsMatch
selectedNumDNSNamesMatch = numDNSNamesMatch
selectedNumDNSZonesMatch = numDNSZonesMatch
}
if selectedSolver == nil {
dbg.Info("selecting solver as there is no previously selected solver")
selectSolver()
continue
}
dbg.Info("determining whether this match is more significant than last")
// because we don't count multiple dnsName matches as extra 'weight'
// in the selection process, we normalize the numDNSNamesMatch vars
// to be either 1 or 0 (i.e. true or false)
selectedHasMatchingDNSNames := selectedNumDNSNamesMatch > 0
hasMatchingDNSNames := numDNSNamesMatch > 0
// dnsName selectors have the highest precedence, so check them first
switch {
case !selectedHasMatchingDNSNames && hasMatchingDNSNames:
dbg.Info("selecting solver as this solver has matching DNS names and the previous one does not")
selectSolver()
continue
case selectedHasMatchingDNSNames && !hasMatchingDNSNames:
dbg.Info("not selecting solver as the previous one has matching DNS names and this one does not")
continue
case !selectedHasMatchingDNSNames && !hasMatchingDNSNames:
dbg.Info("solver does not have any matching DNS names, checking dnsZones")
// check zones
case selectedHasMatchingDNSNames && hasMatchingDNSNames:
dbg.Info("both this solver and the previously selected one matches dnsNames, comparing zones")
if numDNSZonesMatch > selectedNumDNSZonesMatch {
dbg.Info("selecting solver as this one has a more specific dnsZone match than the previously selected one")
selectSolver()
continue
}
if selectedNumDNSZonesMatch > numDNSZonesMatch {
dbg.Info("not selecting this solver as the previously selected one has a more specific dnsZone match")
continue
}
dbg.Info("both this solver and the previously selected one match dnsZones, comparing labels")
// choose the one with the most labels
if numLabelsMatch > selectedNumLabelsMatch {
dbg.Info("selecting solver as this one has more labels than the previously selected one")
selectSolver()
continue
}
dbg.Info("not selecting this solver as previous one has either the same number of or more labels")
continue
}
selectedHasMatchingDNSZones := selectedNumDNSZonesMatch > 0
hasMatchingDNSZones := numDNSZonesMatch > 0
switch {
case !selectedHasMatchingDNSZones && hasMatchingDNSZones:
dbg.Info("selecting solver as this solver has matching DNS zones and the previous one does not")
selectSolver()
continue
case selectedHasMatchingDNSZones && !hasMatchingDNSZones:
dbg.Info("not selecting solver as the previous one has matching DNS zones and this one does not")
continue
case !selectedHasMatchingDNSZones && !hasMatchingDNSZones:
dbg.Info("solver does not have any matching DNS zones, checking labels")
// check labels
case selectedHasMatchingDNSZones && hasMatchingDNSZones:
dbg.Info("both this solver and the previously selected one matches dnsZones")
dbg.Info("comparing number of matching domain segments")
// choose the one with the most matching DNS zone segments
if numDNSZonesMatch > selectedNumDNSZonesMatch {
dbg.Info("selecting solver because this one has more matching DNS zone segments")
selectSolver()
continue
}
if selectedNumDNSZonesMatch > numDNSZonesMatch {
dbg.Info("not selecting solver because previous one has more matching DNS zone segments")
continue
}
// choose the one with the most labels
if numLabelsMatch > selectedNumLabelsMatch {
dbg.Info("selecting solver because this one has more labels than the previous one")
selectSolver()
continue
}
dbg.Info("not selecting solver as this one's number of matching labels is equal to or less than the last one")
continue
}
if numLabelsMatch > selectedNumLabelsMatch {
dbg.Info("selecting solver as this one has more labels than the last one")
selectSolver()
continue
}
dbg.Info("not selecting solver as this one's number of matching labels is equal to or less than the last one (reached end of loop)")
// if we get here, the number of matches is less than or equal so we
// fallback to choosing the first in the list
}
if selectedSolver == nil || selectedChallenge == nil {
return nil, fmt.Errorf("no configured challenge solvers can be used for this challenge")
}
// It should never be possible for this case to be hit as earlier in this
// method we already assert that the challenge type is one of 'http-01'
// or 'dns-01'.
chType, err := challengeType(selectedChallenge.Type)
if err != nil {
return nil, err
}
// 4. handle overriding the HTTP01 ingress class and name fields using the
// ACMECertificateHTTP01IngressNameOverride & Class annotations
if err := applyIngressParameterAnnotationOverrides(o, selectedSolver); err != nil {
return nil, err
}
// 5. construct Challenge resource with spec.solver field set
return &cmacme.ChallengeSpec{
AuthorizationURL: authz.URL,
Type: chType,
URL: selectedChallenge.URL,
DNSName: authz.Identifier,
Token: selectedChallenge.Token,
// selectedSolver cannot be nil due to the check above.
Solver: *selectedSolver,
Wildcard: wc,
IssuerRef: o.Spec.IssuerRef,
}, nil
}
func challengeType(t string) (cmacme.ACMEChallengeType, error) {
switch t {
case "http-01":
return cmacme.ACMEChallengeTypeHTTP01, nil
case "dns-01":
return cmacme.ACMEChallengeTypeDNS01, nil
default:
return "", fmt.Errorf("unsupported challenge type: %v", t)
}
}
func applyIngressParameterAnnotationOverrides(o *cmacme.Order, s *cmacme.ACMEChallengeSolver) error {
if s.HTTP01 == nil || s.HTTP01.Ingress == nil || o.Annotations == nil {
return nil
}
manualIngressName, hasManualIngressName := o.Annotations[cmacme.ACMECertificateHTTP01IngressNameOverride]
manualIngressClass, hasManualIngressClass := o.Annotations[cmacme.ACMECertificateHTTP01IngressClassOverride]
// don't allow both override annotations to be specified at once
if hasManualIngressName && hasManualIngressClass {
return fmt.Errorf("both ingress name and ingress class overrides specified - only one may be specified at a time")
}
// if an override annotation is specified, clear out the existing solver
// config
if hasManualIngressClass || hasManualIngressName {
s.HTTP01.Ingress.Class = nil
s.HTTP01.Ingress.Name = ""
}
if hasManualIngressName {
s.HTTP01.Ingress.Name = manualIngressName
}
if hasManualIngressClass {
s.HTTP01.Ingress.Class = &manualIngressClass
}
return nil
}
func ensureKeysForChallenges(cl acmecl.Interface, challenges []*cmacme.Challenge) ([]*cmacme.Challenge, error) {
for _, ch := range challenges {
var (
key string
err error
)
switch ch.Spec.Type {
case cmacme.ACMEChallengeTypeHTTP01:
key, err = cl.HTTP01ChallengeResponse(ch.Spec.Token)
case cmacme.ACMEChallengeTypeDNS01:
key, err = cl.DNS01ChallengeRecord(ch.Spec.Token)
default:
return nil, fmt.Errorf("challenge %s has unsupported challenge type: %s", ch.Name, ch.Spec.Type)
}
if err != nil {
return nil, err
}
ch.Spec.Key = key
}
return challenges, nil
}
func anyChallengesFailed(chs []*cmacme.Challenge) bool {
for _, ch := range chs {
if acme.IsFailureState(ch.Status.State) {
return true
}
}
return false
}
func allChallengesFinal(chs []*cmacme.Challenge) bool {
for _, ch := range chs {
if !acme.IsFinalState(ch.Status.State) {
return false
}
}
return true
}