-
Notifications
You must be signed in to change notification settings - Fork 277
/
validate.go
374 lines (326 loc) Β· 13.9 KB
/
validate.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
package tinkerbell
import (
"errors"
"fmt"
"strings"
tinkv1alpha1 "github.com/tinkerbell/tink/pkg/apis/core/v1alpha1"
"github.com/aws/eks-anywhere/pkg/api/v1alpha1"
"github.com/aws/eks-anywhere/pkg/networkutils"
"github.com/aws/eks-anywhere/pkg/providers/tinkerbell/hardware"
"github.com/aws/eks-anywhere/pkg/semver"
)
func validateOsFamily(spec *ClusterSpec) error {
controlPlaneRef := spec.Cluster.Spec.ControlPlaneConfiguration.MachineGroupRef
controlPlaneOsFamily := spec.MachineConfigs[controlPlaneRef.Name].OSFamily()
if spec.Cluster.Spec.ExternalEtcdConfiguration != nil {
etcdMachineRef := spec.Cluster.Spec.ExternalEtcdConfiguration.MachineGroupRef
if spec.MachineConfigs[etcdMachineRef.Name].OSFamily() != controlPlaneOsFamily {
return errors.New("etcd osFamily cannot be different from control plane osFamily")
}
}
if controlPlaneOsFamily == v1alpha1.Bottlerocket {
if err := validateK8sVersionForBottleRocketOS(string(spec.Cluster.Spec.KubernetesVersion)); err != nil {
return fmt.Errorf("machineGroupRef %s: %v", controlPlaneRef.Name, err)
}
}
for _, group := range spec.Cluster.Spec.WorkerNodeGroupConfigurations {
groupRef := group.MachineGroupRef
if spec.MachineConfigs[groupRef.Name].OSFamily() != controlPlaneOsFamily {
return errors.New("worker node group osFamily cannot be different from control plane osFamily")
}
if group.KubernetesVersion != nil && *group.KubernetesVersion != "" && spec.MachineConfigs[groupRef.Name].OSFamily() == v1alpha1.Bottlerocket {
if err := validateK8sVersionForBottleRocketOS(string(*group.KubernetesVersion)); err != nil {
return fmt.Errorf("machineGroupRef %s: %v", groupRef.Name, err)
}
}
}
if controlPlaneOsFamily != v1alpha1.Bottlerocket && spec.DatacenterConfig.Spec.OSImageURL == "" && spec.ControlPlaneMachineConfig().Spec.OSImageURL == "" {
return errors.New("please use bottlerocket as osFamily for auto-importing or provide a valid osImageURL")
}
return nil
}
func validateK8sVersionForBottleRocketOS(kubernetesVersion string) error {
kubeVersionSemver, err := semver.New(kubernetesVersion + ".0")
if err != nil {
return fmt.Errorf("converting kubeVersion %v to semver %v", kubernetesVersion, err)
}
kube128Semver, _ := semver.New(string(v1alpha1.Kube128) + ".0")
if kubeVersionSemver.GreaterThan(kube128Semver) {
return errors.New("tinkerbell provider does not support K8s version 1.29+ for BottleRocket OS")
}
return nil
}
func validateUpgradeRolloutStrategy(spec *ClusterSpec) error {
cpUpgradeRolloutStrategyType := v1alpha1.RollingUpdateStrategyType
if spec.ControlPlaneConfiguration().UpgradeRolloutStrategy != nil {
cpUpgradeRolloutStrategyType = spec.ControlPlaneConfiguration().UpgradeRolloutStrategy.Type
controlPlaneRef := spec.ControlPlaneConfiguration().MachineGroupRef
controlPlaneOsFamily := spec.MachineConfigs[controlPlaneRef.Name].OSFamily()
if controlPlaneOsFamily != v1alpha1.Ubuntu && cpUpgradeRolloutStrategyType == v1alpha1.InPlaceStrategyType {
return errors.New("InPlace upgrades are only supported on the Ubuntu OS family")
}
}
for _, group := range spec.Cluster.Spec.WorkerNodeGroupConfigurations {
wnUpgradeRolloutStrategyType := v1alpha1.RollingUpdateStrategyType
groupRef := group.MachineGroupRef
if group.UpgradeRolloutStrategy != nil {
wnUpgradeRolloutStrategyType = group.UpgradeRolloutStrategy.Type
if spec.MachineConfigs[groupRef.Name].OSFamily() != v1alpha1.Ubuntu && wnUpgradeRolloutStrategyType == v1alpha1.InPlaceStrategyType {
return errors.New("InPlace upgrades are only supported on the Ubuntu OS family")
}
}
if wnUpgradeRolloutStrategyType != cpUpgradeRolloutStrategyType {
return errors.New("cannot specify different upgrade rollout strategy types for control plane and worker node group configurations")
}
}
return nil
}
func validateAutoScalerDisabledForInPlace(spec *ClusterSpec) error {
cpUpgradeRolloutStrategyType := spec.ControlPlaneConfiguration().UpgradeRolloutStrategy
// We do not support different strategy types for Inplace between CP and worker nodes so it is okay to check only CP
if cpUpgradeRolloutStrategyType == nil || cpUpgradeRolloutStrategyType.Type != v1alpha1.InPlaceStrategyType {
return nil
}
for _, wng := range spec.Cluster.Spec.WorkerNodeGroupConfigurations {
if wng.AutoScalingConfiguration != nil {
return errors.New("austoscaler configuration not supported with InPlace upgrades")
}
}
return nil
}
func validateOSImageURL(spec *ClusterSpec) error {
dcOSImageURL := spec.DatacenterConfig.Spec.OSImageURL
for _, mc := range spec.MachineConfigs {
if mc.Spec.OSImageURL != "" && dcOSImageURL != "" {
return errors.New("cannot specify OSImageURL on both TinkerbellMachineConfig's and TinkerbellDatacenterConfig")
}
if mc.Spec.OSImageURL == "" && dcOSImageURL == "" && mc.Spec.OSFamily != v1alpha1.Bottlerocket {
return fmt.Errorf("missing OSImageURL on TinkerbellMachineConfig '%s'", mc.ObjectMeta.Name)
}
}
return validateK8sVersionInOSImageURLs(spec)
}
func validateK8sVersionInOSImageURLs(spec *ClusterSpec) error {
// If the user specifies the OSImageURL via the datacenter config then ensure all kube versions specified
// on the cluster config are specified in the OSImageURL as the user could technically use a single image.
//
// When the user specifies OSImageURLs on each individual machine config (typical for modular upgrades) ensure
// each machine config OSImageURL specifies the Kubernetes version. We don't explicitly take into consideration
// the fact control plane, etcd and worker node groups can all reference the same machine config. If 2 components
// specify different kube versions this will ensure both are present in the image URL (as above).
if spec.DatacenterConfig.Spec.OSImageURL != "" {
kvs := spec.Cluster.KubernetesVersions()
for _, v := range kvs {
if !containsK8sVersion(spec.DatacenterConfig.Spec.OSImageURL, string(v)) {
return fmt.Errorf("missing kube version from OSImageURL: url=%v, version=%v",
spec.DatacenterConfig.Spec.OSImageURL, v)
}
}
} else {
// For Bottlerocket we vend images but we still allow the user to specify them if they wish. We only want
// to default machine config OSImageURLs if the datacenter config doesn't specify one and we default
// to whatever is in the bundle.
//
// TODO: Investigate how we could refactor our logic to make this unnecessary.
//
// We validate elsewhere that all machine configs specify the same OSFamily so we can rely on the
// control plane machine config only for the need to default OSImageURLs.
if spec.ControlPlaneMachineConfig().OSFamily() == v1alpha1.Bottlerocket {
defaultBottlerocketOSImageURLs(spec)
}
if !containsK8sVersion(spec.ControlPlaneMachineConfig().Spec.OSImageURL, string(spec.Cluster.Spec.KubernetesVersion)) {
return fmt.Errorf("missing kube version from control plane machine config OSImageURL: url=%v, version=%v",
spec.ControlPlaneMachineConfig().Spec.OSImageURL, spec.Cluster.Spec.KubernetesVersion)
}
for _, wng := range spec.WorkerNodeGroupConfigurations() {
url := spec.MachineConfigs[wng.MachineGroupRef.Name].Spec.OSImageURL
version := spec.Cluster.Spec.KubernetesVersion
if wng.KubernetesVersion != nil && *wng.KubernetesVersion != "" {
version = *wng.KubernetesVersion
}
if !containsK8sVersion(url, string(version)) {
return fmt.Errorf("missing kube version from worker node group machine config OSImageURL: url=%v, version=%v",
url, version)
}
}
}
return nil
}
func defaultBottlerocketOSImageURLs(spec *ClusterSpec) {
if spec.ControlPlaneMachineConfig().Spec.OSImageURL == "" {
spec.ControlPlaneMachineConfig().Spec.OSImageURL = spec.RootVersionsBundle().EksD.Raw.Bottlerocket.URI
}
for _, wng := range spec.WorkerNodeGroupConfigurations() {
mc := spec.MachineConfigs[wng.MachineGroupRef.Name]
version := spec.Cluster.Spec.KubernetesVersion
if wng.KubernetesVersion != nil {
version = *wng.KubernetesVersion
}
if mc.Spec.OSImageURL == "" {
mc.Spec.OSImageURL = spec.VersionsBundle(version).EksD.Raw.Bottlerocket.URI
}
}
}
func containsK8sVersion(imageURL, k8sVersion string) bool {
versionExtractor := strings.NewReplacer("-", "", ".", "", "_", "")
osImageURL := versionExtractor.Replace(imageURL)
kubeVersion := versionExtractor.Replace(k8sVersion)
// we set the containsK8sVersion to false if the OS image URL does not contain the specified kubernetes version.
// For ex if the kubernetes version is 1.23,
// the image url should include 1.23 or 1-23, 1_23 or 123 i.e. ubuntu-1-23.gz or similar in the string.
return strings.Contains(osImageURL, kubeVersion)
}
func validateMachineRefExists(
ref *v1alpha1.Ref,
machineConfigs map[string]*v1alpha1.TinkerbellMachineConfig,
) error {
if _, ok := machineConfigs[ref.Name]; !ok {
return fmt.Errorf("missing machine config ref: kind=%v; name=%v", ref.Kind, ref.Name)
}
return nil
}
func validateMachineConfigNamespacesMatchDatacenterConfig(
datacenterConfig *v1alpha1.TinkerbellDatacenterConfig,
machineConfigs map[string]*v1alpha1.TinkerbellMachineConfig,
) error {
for _, machineConfig := range machineConfigs {
if machineConfig.Namespace != datacenterConfig.Namespace {
return fmt.Errorf(
"TinkerbellMachineConfig's namespace must match TinkerbellDatacenterConfig's namespace: %v",
machineConfig.Name,
)
}
}
return nil
}
func validateIPUnused(client networkutils.NetClient, ip string) error {
if networkutils.IsIPInUse(client, ip) {
return fmt.Errorf("ip in use: %v", ip)
}
return nil
}
func validatePortsAvailable(client networkutils.NetClient, host string) error {
unavailablePorts := getPortsUnavailable(client, host)
if len(unavailablePorts) != 0 {
return fmt.Errorf("localhost ports [%v] are already in use, please ensure these ports are available", strings.Join(unavailablePorts, ", "))
}
return nil
}
func getPortsUnavailable(client networkutils.NetClient, host string) []string {
ports := []string{"80", "42113", "50061"}
var unavailablePorts []string
for _, port := range ports {
if networkutils.IsPortInUse(client, host, port) {
unavailablePorts = append(unavailablePorts, port)
}
}
return unavailablePorts
}
// minimumHardwareRequirement defines the minimum requirement for a hardware selector.
type minimumHardwareRequirement struct {
// MinCount is the minimum number of hardware required to satisfy the requirement
MinCount int
// Selector defines what labels should be present on Hardware to consider it eligable for
// this requirement.
Selector v1alpha1.HardwareSelector
// count is used internally by validation to sum the actual available hardware.
count int
}
// minimumHardwareRequirements is a collection of minimumHardwareRequirement instances.
// it stores requirements in a map where the key is derived from selectors. This ensures selectors
// specifying the same key-value pairs are combined.
type minimumHardwareRequirements map[string]*minimumHardwareRequirement
// Add a minimumHardwareRequirement to r.
func (r *minimumHardwareRequirements) Add(selector v1alpha1.HardwareSelector, min int) error {
name, err := selector.ToString()
if err != nil {
return err
}
(*r)[name] = &minimumHardwareRequirement{
MinCount: min,
Selector: selector,
}
return nil
}
// validateMinimumHardwareRequirements validates all requirements can be satisfied using hardware
// registered with catalogue.
func validateMinimumHardwareRequirements(requirements minimumHardwareRequirements, catalogue *hardware.Catalogue) error {
// Count all hardware that meets the selector requirements for each requirement.
// This does not consider whether or not a piece of hardware is selectable by multiple
// selectors. That requires a different validation ideally run before this one.
for _, h := range catalogue.AllHardware() {
for _, r := range requirements {
if hardware.LabelsMatchSelector(r.Selector, h.Labels) {
r.count++
}
}
}
// Validate counts of hardware meet the minimum required count.
for name, r := range requirements {
if r.count < r.MinCount {
return fmt.Errorf(
"minimum hardware count not met for selector '%v': have %v, require %v",
name,
r.count,
r.MinCount,
)
}
}
return nil
}
// validateHardwareSatisfiesOnlyOneSelector ensures hardware in allHardware meets one and only one
// selector in selectors. selectors uses the selectorSet construct to ensure we don't
// operate on duplicate selectors given a selector can be re-used among groups as they may reference
// the same TinkerbellMachineConfig.
func validateHardwareSatisfiesOnlyOneSelector(allHardware []*tinkv1alpha1.Hardware, selectors selectorSet) error {
for _, h := range allHardware {
if matches := getMatchingHardwareSelectors(h, selectors); len(matches) > 1 {
slctrStrs, err := getHardwareSelectorsAsStrings(matches)
if err != nil {
return err
}
return fmt.Errorf(
"hardware must only satisfy 1 selector: hardware name '%v'; selectors '%v'",
h.Name,
strings.Join(slctrStrs, ", "),
)
}
}
return nil
}
// selectorSet defines a set of selectors. Selectors should be added using the Add method to ensure
// deterministic key generation. The construct is useful to avoid treating selectors that are the
// same as different.
type selectorSet map[string]v1alpha1.HardwareSelector
// Add adds selector to ss.
func (ss *selectorSet) Add(selector v1alpha1.HardwareSelector) error {
slctrStr, err := selector.ToString()
if err != nil {
return err
}
(*ss)[slctrStr] = selector
return nil
}
func getMatchingHardwareSelectors(
hw *tinkv1alpha1.Hardware,
selectors selectorSet,
) []v1alpha1.HardwareSelector {
var satisfies []v1alpha1.HardwareSelector
for _, selector := range selectors {
if hardware.LabelsMatchSelector(selector, hw.Labels) {
satisfies = append(satisfies, selector)
}
}
return satisfies
}
func getHardwareSelectorsAsStrings(selectors []v1alpha1.HardwareSelector) ([]string, error) {
var slctrs []string
for _, selector := range selectors {
s, err := selector.ToString()
if err != nil {
return nil, err
}
slctrs = append(slctrs, s)
}
return slctrs, nil
}