-
Notifications
You must be signed in to change notification settings - Fork 124
/
machines.go
139 lines (115 loc) · 5.17 KB
/
machines.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
/*
Copyright 2019 The Machine Controller 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 admission
import (
"encoding/json"
"fmt"
"golang.org/x/crypto/ssh"
clusterv1alpha1 "github.com/kubermatic/machine-controller/pkg/apis/cluster/v1alpha1"
"github.com/kubermatic/machine-controller/pkg/cloudprovider"
"github.com/kubermatic/machine-controller/pkg/providerconfig"
providerconfigtypes "github.com/kubermatic/machine-controller/pkg/providerconfig/types"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/klog"
)
// BypassSpecNoModificationRequirementAnnotation is used to bypass the "no machine.spec modification" allowed
// restriction from the webhook in order to change the spec in some special cases, e.G. for the migration of
// the `providerConfig` field to `providerSpec`
const BypassSpecNoModificationRequirementAnnotation = "kubermatic.io/bypass-no-spec-mutation-requirement"
func (ad *admissionData) mutateMachines(ar admissionv1beta1.AdmissionReview) (*admissionv1beta1.AdmissionResponse, error) {
machine := clusterv1alpha1.Machine{}
if err := json.Unmarshal(ar.Request.Object.Raw, &machine); err != nil {
return nil, fmt.Errorf("failed to unmarshal: %v", err)
}
machineOriginal := machine.DeepCopy()
klog.V(3).Infof("Defaulting and validating machine %s/%s", machine.Namespace, machine.Name)
// Mutating .Spec is never allowed
// Only hidden exception: the machine-controller may set the .Spec.Name to .Metadata.Name
// because otherwise it can never add the delete finalizer as it internally defaults the Name
// as well, since on the CREATE request for machines, there is only Metadata.GenerateName set
// so we can't default it initially
if ar.Request.Operation == admissionv1beta1.Update {
oldMachine := clusterv1alpha1.Machine{}
if err := json.Unmarshal(ar.Request.OldObject.Raw, &oldMachine); err != nil {
return nil, fmt.Errorf("failed to unmarshal OldObject: %v", err)
}
if oldMachine.Spec.Name != machine.Spec.Name && machine.Spec.Name == machine.Name {
oldMachine.Spec.Name = machine.Spec.Name
}
// Allow mutation when:
// * oldMachine has Initializers on it
// * machine has the `MigrationBypassSpecNoModificationRequirementAnnotation` annotation (used for type migration)
bypassValidationForMigration := machine.Annotations[BypassSpecNoModificationRequirementAnnotation] == "true"
if (oldMachine.Initializers == nil || len(oldMachine.Initializers.Pending) == 0) && !bypassValidationForMigration {
if equal := apiequality.Semantic.DeepEqual(machine.Spec, oldMachine.Spec); !equal {
return nil, fmt.Errorf("machine.spec is immutable")
}
}
}
// Delete the `BypassSpecNoModificationRequirementAnnotation` annotation, it should be valid only once
delete(machine.Annotations, BypassSpecNoModificationRequirementAnnotation)
// Default name
if machine.Spec.Name == "" {
machine.Spec.Name = machine.Name
}
// Default and verify .Spec on CREATE only, its expensive and not required to do it on UPDATE
// as we disallow .Spec changes anyways
if ar.Request.Operation == admissionv1beta1.Create {
if err := ad.defaultAndValidateMachineSpec(&machine.Spec); err != nil {
return nil, err
}
}
return createAdmissionResponse(machineOriginal, &machine)
}
func (ad *admissionData) defaultAndValidateMachineSpec(spec *clusterv1alpha1.MachineSpec) error {
providerConfig, err := providerconfigtypes.GetConfig(spec.ProviderSpec)
if err != nil {
return fmt.Errorf("failed to read machine.spec.providerSpec: %v", err)
}
skg := providerconfig.NewConfigVarResolver(ad.ctx, ad.client)
prov, err := cloudprovider.ForProvider(providerConfig.CloudProvider, skg)
if err != nil {
return fmt.Errorf("failed to get cloud provider %q: %v", providerConfig.CloudProvider, err)
}
// Verify operating system.
if _, err := ad.userDataManager.ForOS(providerConfig.OperatingSystem); err != nil {
return fmt.Errorf("failed to get OS '%s': %v", providerConfig.OperatingSystem, err)
}
// Check kubelet version
if spec.Versions.Kubelet == "" {
return fmt.Errorf("Kubelet version must be set")
}
// Validate SSH keys
if err := validatePublicKeys(providerConfig.SSHPublicKeys); err != nil {
return fmt.Errorf("Invalid public keys specified: %v", err)
}
defaultedSpec, err := prov.AddDefaults(*spec)
if err != nil {
return fmt.Errorf("failed to default machineSpec: %v", err)
}
spec = &defaultedSpec
if err := prov.Validate(*spec); err != nil {
return fmt.Errorf("validation failed: %v", err)
}
return nil
}
func validatePublicKeys(keys []string) error {
for _, s := range keys {
_, _, _, _, err := ssh.ParseAuthorizedKey([]byte(s))
if err != nil {
return fmt.Errorf("invalid public key %q: %v", s, err)
}
}
return nil
}