-
Notifications
You must be signed in to change notification settings - Fork 4.6k
/
aws.go
450 lines (370 loc) · 17.7 KB
/
aws.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
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
/*
Copyright 2017 The Kubernetes 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 validation
import (
"fmt"
"net"
"strconv"
"strings"
"github.com/aws/aws-sdk-go/aws/arn"
"github.com/aws/aws-sdk-go/service/ec2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/kops/pkg/apis/kops"
"k8s.io/kops/upup/pkg/fi"
"k8s.io/kops/upup/pkg/fi/cloudup/awsup"
)
func awsValidateCluster(c *kops.Cluster) field.ErrorList {
allErrs := field.ErrorList{}
if c.Spec.API.LoadBalancer != nil {
lbPath := field.NewPath("spec", "api", "loadBalancer")
lbSpec := c.Spec.API.LoadBalancer
allErrs = append(allErrs, IsValidValue(lbPath.Child("class"), &lbSpec.Class, kops.SupportedLoadBalancerClasses)...)
allErrs = append(allErrs, awsValidateTopologyDNS(lbPath.Child("type"), c)...)
allErrs = append(allErrs, awsValidateSecurityGroupOverride(lbPath.Child("securityGroupOverride"), lbSpec)...)
allErrs = append(allErrs, awsValidateAdditionalSecurityGroups(lbPath.Child("additionalSecurityGroups"), lbSpec.AdditionalSecurityGroups)...)
if lbSpec.Class == kops.LoadBalancerClassNetwork && lbSpec.UseForInternalAPI && lbSpec.Type == kops.LoadBalancerTypeInternal {
allErrs = append(allErrs, field.Forbidden(lbPath.Child("useForInternalAPI"), "useForInternalAPI cannot be used with internal NLB due lack of hairpinning support"))
}
if lbSpec.SSLCertificate != "" && lbSpec.Class != kops.LoadBalancerClassNetwork {
allErrs = append(allErrs, field.Forbidden(lbPath.Child("sslCertificate"), "sslCertificate requires a network load balancer. See https://github.com/kubernetes/kops/blob/master/permalinks/acm_nlb.md"))
}
allErrs = append(allErrs, awsValidateSSLPolicy(lbPath.Child("sslPolicy"), lbSpec)...)
allErrs = append(allErrs, awsValidateLoadBalancerSubnets(lbPath.Child("subnets"), c.Spec)...)
}
allErrs = append(allErrs, awsValidateExternalCloudControllerManager(c)...)
if c.Spec.Authentication != nil && c.Spec.Authentication.AWS != nil {
allErrs = append(allErrs, awsValidateIAMAuthenticator(field.NewPath("spec", "authentication", "aws"), c.Spec.Authentication.AWS)...)
}
return allErrs
}
func awsValidateExternalCloudControllerManager(cluster *kops.Cluster) (allErrs field.ErrorList) {
c := cluster.Spec
if c.ExternalCloudControllerManager == nil {
return allErrs
}
fldPath := field.NewPath("spec", "externalCloudControllerManager")
if !hasAWSEBSCSIDriver(c) {
allErrs = append(allErrs, field.Forbidden(fldPath,
"AWS external CCM cannot be used without enabling spec.cloudProvider.aws.ebsCSIDriverSpec."))
}
return allErrs
}
func awsValidateInstanceGroup(ig *kops.InstanceGroup, cloud awsup.AWSCloud) field.ErrorList {
allErrs := field.ErrorList{}
allErrs = append(allErrs, awsValidateAdditionalSecurityGroups(field.NewPath("spec", "additionalSecurityGroups"), ig.Spec.AdditionalSecurityGroups)...)
allErrs = append(allErrs, awsValidateInstanceTypeAndImage(field.NewPath(ig.GetName(), "spec", "machineType"), field.NewPath(ig.GetName(), "spec", "image"), ig.Spec.MachineType, ig.Spec.Image, cloud)...)
allErrs = append(allErrs, awsValidateSpotDurationInMinute(field.NewPath(ig.GetName(), "spec", "spotDurationInMinutes"), ig)...)
allErrs = append(allErrs, awsValidateInstanceInterruptionBehavior(field.NewPath(ig.GetName(), "spec", "instanceInterruptionBehavior"), ig)...)
if ig.Spec.MixedInstancesPolicy != nil {
allErrs = append(allErrs, awsValidateMixedInstancesPolicy(field.NewPath("spec", "mixedInstancesPolicy"), ig.Spec.MixedInstancesPolicy, ig, cloud)...)
}
if ig.Spec.InstanceMetadata != nil {
allErrs = append(allErrs, awsValidateInstanceMetadata(field.NewPath("spec", "instanceMetadata"), ig.Spec.InstanceMetadata)...)
}
if ig.Spec.CPUCredits != nil {
allErrs = append(allErrs, awsValidateCPUCredits(field.NewPath("spec"), &ig.Spec, cloud)...)
}
if ig.Spec.MaxInstanceLifetime != nil {
allErrs = append(allErrs, awsValidateMaximumInstanceLifetime(field.NewPath(ig.GetName(), "spec"), ig.Spec.MaxInstanceLifetime)...)
}
return allErrs
}
func awsValidateMaximumInstanceLifetime(fieldPath *field.Path, maxInstanceLifetime *metav1.Duration) field.ErrorList {
allErrs := field.ErrorList{}
const minMaxInstanceLifetime = 86400
lifetimeSec := int64(maxInstanceLifetime.Seconds())
if lifetimeSec != 0 && lifetimeSec < minMaxInstanceLifetime {
allErrs = append(allErrs, field.Invalid(fieldPath.Child("maxInstanceLifetime"), maxInstanceLifetime, fmt.Sprintf("max instance lifetime must be greater than %d or equal to 0", int64(minMaxInstanceLifetime))))
}
return allErrs
}
func awsValidateInstanceMetadata(fieldPath *field.Path, instanceMetadata *kops.InstanceMetadataOptions) field.ErrorList {
allErrs := field.ErrorList{}
if instanceMetadata.HTTPTokens != nil {
allErrs = append(allErrs, IsValidValue(fieldPath.Child("httpTokens"), instanceMetadata.HTTPTokens, []string{"optional", "required"})...)
}
if instanceMetadata.HTTPPutResponseHopLimit != nil {
httpPutResponseHopLimit := fi.ValueOf(instanceMetadata.HTTPPutResponseHopLimit)
if httpPutResponseHopLimit < 1 || httpPutResponseHopLimit > 64 {
allErrs = append(allErrs, field.Invalid(fieldPath.Child("httpPutResponseHopLimit"), instanceMetadata.HTTPPutResponseHopLimit,
"HTTPPutResponseLimit must be a value between 1 and 64"))
}
}
return allErrs
}
func awsValidateAdditionalSecurityGroups(fieldPath *field.Path, groups []string) field.ErrorList {
allErrs := field.ErrorList{}
names := sets.NewString()
for i, s := range groups {
if names.Has(s) {
allErrs = append(allErrs, field.Duplicate(fieldPath.Index(i), s))
}
names.Insert(s)
if strings.TrimSpace(s) == "" {
allErrs = append(allErrs, field.Invalid(fieldPath.Index(i), s, "security group cannot be empty, if specified"))
continue
}
if !strings.HasPrefix(s, "sg-") {
allErrs = append(allErrs, field.Invalid(fieldPath.Index(i), s, "security group does not match the expected AWS format"))
}
}
return allErrs
}
func awsValidateSecurityGroupOverride(fieldPath *field.Path, lbSpec *kops.LoadBalancerAccessSpec) field.ErrorList {
if lbSpec.SecurityGroupOverride == nil {
return nil
}
allErrs := field.ErrorList{}
override := *lbSpec.SecurityGroupOverride
if strings.TrimSpace(override) == "" {
return append(allErrs, field.Invalid(fieldPath, override, "security group override cannot be empty, if specified"))
}
if !strings.HasPrefix(override, "sg-") {
allErrs = append(allErrs, field.Invalid(fieldPath, override, "security group override does not match the expected AWS format"))
}
if lbSpec.Class == kops.LoadBalancerClassNetwork {
allErrs = append(allErrs, field.Forbidden(fieldPath, "security group override cannot be specified for a Network Load Balancer"))
}
return allErrs
}
func awsValidateInstanceTypeAndImage(instanceTypeFieldPath *field.Path, imageFieldPath *field.Path, instanceTypes string, image string, cloud awsup.AWSCloud) field.ErrorList {
if cloud == nil || instanceTypes == "" {
return nil
}
allErrs := field.ErrorList{}
imageInfo, err := cloud.ResolveImage(image)
if err != nil {
return append(allErrs, field.Invalid(imageFieldPath, image,
fmt.Sprintf("specified image %q is invalid: %s", image, err)))
}
imageArch := fi.ValueOf(imageInfo.Architecture)
// Spotinst uses the instance type field to keep a "," separated list of instance types
for _, instanceType := range strings.Split(instanceTypes, ",") {
machineInfo, err := cloud.DescribeInstanceType(instanceType)
if err != nil {
allErrs = append(allErrs, field.Invalid(instanceTypeFieldPath, instanceTypes, fmt.Sprintf("machine type %q is invalid: %v", instanceType, err)))
continue
}
found := false
if machineInfo != nil && machineInfo.ProcessorInfo != nil {
for _, machineArch := range machineInfo.ProcessorInfo.SupportedArchitectures {
if imageArch == fi.ValueOf(machineArch) {
found = true
}
}
}
if !found {
var machineArch []string
if machineInfo != nil && machineInfo.ProcessorInfo != nil && machineInfo.ProcessorInfo.SupportedArchitectures != nil {
machineArch = fi.StringSliceValue(machineInfo.ProcessorInfo.SupportedArchitectures)
}
allErrs = append(allErrs, field.Invalid(instanceTypeFieldPath, instanceTypes,
fmt.Sprintf("machine type architecture %q does not match image architecture %q", strings.Join(machineArch, ","), imageArch)))
}
}
return allErrs
}
func awsValidateSpotDurationInMinute(fieldPath *field.Path, ig *kops.InstanceGroup) field.ErrorList {
allErrs := field.ErrorList{}
if ig.Spec.SpotDurationInMinutes != nil {
validSpotDurations := []string{"60", "120", "180", "240", "300", "360"}
spotDurationStr := strconv.FormatInt(*ig.Spec.SpotDurationInMinutes, 10)
allErrs = append(allErrs, IsValidValue(fieldPath, &spotDurationStr, validSpotDurations)...)
}
return allErrs
}
func awsValidateInstanceInterruptionBehavior(fieldPath *field.Path, ig *kops.InstanceGroup) field.ErrorList {
allErrs := field.ErrorList{}
if ig.Spec.InstanceInterruptionBehavior != nil {
instanceInterruptionBehavior := *ig.Spec.InstanceInterruptionBehavior
allErrs = append(allErrs, IsValidValue(fieldPath, &instanceInterruptionBehavior, ec2.InstanceInterruptionBehavior_Values())...)
}
return allErrs
}
// awsValidateMixedInstancesPolicy is responsible for validating the user input of a mixed instance policy
func awsValidateMixedInstancesPolicy(path *field.Path, spec *kops.MixedInstancesPolicySpec, ig *kops.InstanceGroup, cloud awsup.AWSCloud) field.ErrorList {
var errs field.ErrorList
mainMachineTypeInfo, err := awsup.GetMachineTypeInfo(cloud, ig.Spec.MachineType)
if err != nil {
errs = append(errs, field.Invalid(field.NewPath("spec", "machineType"), ig.Spec.MachineType, fmt.Sprintf("machine type specified is invalid: %q", ig.Spec.MachineType)))
return errs
}
hasGPU := mainMachineTypeInfo.GPU
// @step: check the instance types are valid
for i, instanceTypes := range spec.Instances {
fld := path.Child("instances").Index(i)
errs = append(errs, awsValidateInstanceTypeAndImage(path.Child("instances").Index(i), path.Child("image"), instanceTypes, ig.Spec.Image, cloud)...)
for _, instanceType := range strings.Split(instanceTypes, ",") {
machineTypeInfo, err := awsup.GetMachineTypeInfo(cloud, instanceType)
if err != nil {
errs = append(errs, field.Invalid(field.NewPath("spec", "machineType"), ig.Spec.MachineType, fmt.Sprintf("machine type specified is invalid: %q", ig.Spec.MachineType)))
return errs
}
if machineTypeInfo.GPU != hasGPU {
errs = append(errs, field.Forbidden(fld, "Cannot mix GPU and non-GPU machine types in the same Instance Group"))
}
}
}
if spec.OnDemandBase != nil {
if fi.ValueOf(spec.OnDemandBase) < 0 {
errs = append(errs, field.Invalid(path.Child("onDemandBase"), spec.OnDemandBase, "cannot be less than zero"))
}
if fi.ValueOf(spec.OnDemandBase) > int64(fi.ValueOf(ig.Spec.MaxSize)) {
errs = append(errs, field.Invalid(path.Child("onDemandBase"), spec.OnDemandBase, "cannot be greater than max size"))
}
}
if spec.OnDemandAboveBase != nil {
if fi.ValueOf(spec.OnDemandAboveBase) < 0 {
errs = append(errs, field.Invalid(path.Child("onDemandAboveBase"), spec.OnDemandAboveBase, "cannot be less than 0"))
}
if fi.ValueOf(spec.OnDemandAboveBase) > 100 {
errs = append(errs, field.Invalid(path.Child("onDemandAboveBase"), spec.OnDemandAboveBase, "cannot be greater than 100"))
}
}
errs = append(errs, IsValidValue(path.Child("spotAllocationStrategy"), spec.SpotAllocationStrategy, kops.SpotAllocationStrategies)...)
return errs
}
func awsValidateTopologyDNS(fieldPath *field.Path, c *kops.Cluster) field.ErrorList {
allErrs := field.ErrorList{}
if c.UsesNoneDNS() && c.Spec.API.LoadBalancer != nil && c.Spec.API.LoadBalancer.Class != kops.LoadBalancerClassNetwork {
allErrs = append(allErrs, field.Forbidden(fieldPath, "topology.dns.type=none requires Network Load Balancer"))
}
return allErrs
}
func awsValidateSSLPolicy(fieldPath *field.Path, spec *kops.LoadBalancerAccessSpec) field.ErrorList {
allErrs := field.ErrorList{}
if spec.SSLPolicy != nil {
if spec.Class != kops.LoadBalancerClassNetwork {
allErrs = append(allErrs, field.Forbidden(fieldPath, "sslPolicy should be specified with Network Load Balancer"))
}
if spec.SSLCertificate == "" {
allErrs = append(allErrs, field.Forbidden(fieldPath, "sslPolicy should not be specified without SSLCertificate"))
}
}
return allErrs
}
func awsValidateLoadBalancerSubnets(fieldPath *field.Path, spec kops.ClusterSpec) field.ErrorList {
allErrs := field.ErrorList{}
lbSpec := spec.API.LoadBalancer
for i, subnet := range lbSpec.Subnets {
var clusterSubnet *kops.ClusterSubnetSpec
if subnet.Name == "" {
allErrs = append(allErrs, field.Required(fieldPath.Index(i).Child("name"), "subnet name can't be empty"))
} else {
for _, cs := range spec.Networking.Subnets {
if subnet.Name == cs.Name {
clusterSubnet = &cs
break
}
}
if clusterSubnet == nil {
allErrs = append(allErrs, field.NotFound(fieldPath.Index(i).Child("name"), fmt.Sprintf("subnet %q not found in cluster subnets", subnet.Name)))
}
}
if subnet.PrivateIPv4Address != nil {
if *subnet.PrivateIPv4Address == "" {
allErrs = append(allErrs, field.Required(fieldPath.Index(i).Child("privateIPv4Address"), "privateIPv4Address can't be empty"))
}
ip := net.ParseIP(*subnet.PrivateIPv4Address)
if ip == nil || ip.To4() == nil {
allErrs = append(allErrs, field.Invalid(fieldPath.Index(i).Child("privateIPv4Address"), subnet, "privateIPv4Address is not a valid IPv4 address"))
} else if clusterSubnet != nil {
_, ipNet, err := net.ParseCIDR(clusterSubnet.CIDR)
if err == nil { // we assume that the cidr is actually valid
if !ipNet.Contains(ip) {
allErrs = append(allErrs, field.Invalid(fieldPath.Index(i).Child("privateIPv4Address"), subnet, "privateIPv4Address is not part of the subnet CIDR"))
}
}
}
if lbSpec.Class != kops.LoadBalancerClassNetwork || lbSpec.Type != kops.LoadBalancerTypeInternal {
allErrs = append(allErrs, field.Forbidden(fieldPath.Index(i).Child("privateIPv4Address"), "privateIPv4Address only allowed for internal NLBs"))
}
}
if subnet.AllocationID != nil {
if *subnet.AllocationID == "" {
allErrs = append(allErrs, field.Required(fieldPath.Index(i).Child("allocationID"), "allocationID can't be empty"))
}
if lbSpec.Class != kops.LoadBalancerClassNetwork || lbSpec.Type == kops.LoadBalancerTypeInternal {
allErrs = append(allErrs, field.Forbidden(fieldPath.Index(i).Child("allocationID"), "allocationID only allowed for Public NLBs"))
}
}
}
return allErrs
}
func awsValidateCPUCredits(fieldPath *field.Path, spec *kops.InstanceGroupSpec, cloud awsup.AWSCloud) field.ErrorList {
allErrs := field.ErrorList{}
allErrs = append(allErrs, IsValidValue(fieldPath.Child("cpuCredits"), spec.CPUCredits, []string{"standard", "unlimited"})...)
return allErrs
}
func awsValidateIAMAuthenticator(fieldPath *field.Path, spec *kops.AWSAuthenticationSpec) field.ErrorList {
allErrs := field.ErrorList{}
if !strings.Contains(spec.BackendMode, "CRD") && len(spec.IdentityMappings) > 0 {
allErrs = append(allErrs, field.Forbidden(fieldPath.Child("backendMode"), "backendMode must be CRD if identityMappings is set"))
}
for i, mapping := range spec.IdentityMappings {
parsedARN, err := arn.Parse(mapping.ARN)
if err != nil || (!strings.HasPrefix(parsedARN.Resource, "role/") && !strings.HasPrefix(parsedARN.Resource, "user/")) {
allErrs = append(allErrs, field.Invalid(fieldPath.Child("identityMappings").Index(i).Child("arn"), mapping.ARN,
"arn must be a valid IAM Role or User ARN such as arn:aws:iam::123456789012:role/KopsExampleRole"))
}
}
return allErrs
}
func hasAWSEBSCSIDriver(c kops.ClusterSpec) bool {
// EBSCSIDriverSpec will have a default value, so if this is all false, it will be populated on next pass
if c.CloudProvider.AWS.EBSCSIDriver == nil || c.CloudProvider.AWS.EBSCSIDriver.Enabled == nil {
return true
}
return *c.CloudProvider.AWS.EBSCSIDriver.Enabled
}
func awsValidateAdditionalRoutes(fieldPath *field.Path, routes []kops.RouteSpec, networkCIDRs []*net.IPNet) field.ErrorList {
allErrs := field.ErrorList{}
for i, r := range routes {
f := fieldPath.Index(i)
// Check if target is a known type
if !strings.HasPrefix(r.Target, "pcx-") &&
!strings.HasPrefix(r.Target, "i-") &&
!strings.HasPrefix(r.Target, "nat-") &&
!strings.HasPrefix(r.Target, "tgw-") &&
!strings.HasPrefix(r.Target, "igw-") &&
!strings.HasPrefix(r.Target, "eigw-") {
allErrs = append(allErrs, field.Invalid(f.Child("target"), r, "unknown target type for route"))
}
routeCIDR, errs := parseCIDR(f.Child("cidr"), r.CIDR)
allErrs = append(allErrs, errs...)
if routeCIDR != nil {
for _, clusterNet := range networkCIDRs {
if clusterNet.Contains(routeCIDR.IP) && strings.HasPrefix(r.Target, "pcx-") {
allErrs = append(allErrs, field.Forbidden(f.Child("target"), "target is more specific than a network CIDR block. This route can target only an interface or an instance."))
}
}
}
}
// Check for duplicated CIDR
{
cidrs := sets.NewString()
for _, cidr := range networkCIDRs {
cidrs.Insert(cidr.String())
}
for i := range routes {
rCidr := routes[i].CIDR
if cidrs.Has(rCidr) {
allErrs = append(allErrs, field.Duplicate(fieldPath.Index(i).Child("cidr"), rCidr))
}
cidrs.Insert(rCidr)
}
}
return allErrs
}