From a67427173d92dec41fc6c2a012b93637b1314b5b Mon Sep 17 00:00:00 2001 From: Yash Raj Date: Wed, 22 May 2024 06:54:07 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8C=B1Add=20validation=20checks=20and=20t?= =?UTF-8?q?ests=20for=20hbmmt=20and=20hcloudmachine=20webhook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added validation checks for hbmm , hbmmt , hcloudmachine and hcloud machinetemplate webhooks . Also added the unit tests for the same --- api/v1beta1/hcloudmachine_validation.go | 56 +++ api/v1beta1/hcloudmachine_validation_test.go | 151 ++++++ api/v1beta1/hcloudmachine_webhook.go | 31 +- api/v1beta1/hcloudmachinetemplate_webhook.go | 4 +- .../hetznerbaremetalmachine_validation.go | 90 ++++ ...hetznerbaremetalmachine_validation_test.go | 436 ++++++++++++++++++ .../hetznerbaremetalmachine_webhook.go | 65 +-- ...hetznerbaremetalmachinetemplate_webhook.go | 18 +- controllers/controllers_suite_test.go | 1 + controllers/hcloudmachine_controller_test.go | 42 +- .../hcloudmachinetemplate_controller_test.go | 89 +++- ...hetznerbaremetalmachine_controller_test.go | 163 +++++++ 12 files changed, 1034 insertions(+), 112 deletions(-) create mode 100644 api/v1beta1/hcloudmachine_validation.go create mode 100644 api/v1beta1/hcloudmachine_validation_test.go create mode 100644 api/v1beta1/hetznerbaremetalmachine_validation.go create mode 100644 api/v1beta1/hetznerbaremetalmachine_validation_test.go diff --git a/api/v1beta1/hcloudmachine_validation.go b/api/v1beta1/hcloudmachine_validation.go new file mode 100644 index 000000000..81515ef45 --- /dev/null +++ b/api/v1beta1/hcloudmachine_validation.go @@ -0,0 +1,56 @@ +/* +Copyright 2021 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 v1beta1 + +import ( + "reflect" + + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func validateHCloudMachineSpec(oldSpec, newSpec HCloudMachineSpec) field.ErrorList { + var allErrs field.ErrorList + // Type is immutable + if !reflect.DeepEqual(oldSpec.Type, newSpec.Type) { + allErrs = append(allErrs, + field.Invalid(field.NewPath("spec", "type"), newSpec.Type, "field is immutable"), + ) + } + + // ImageName is immutable + if !reflect.DeepEqual(oldSpec.ImageName, newSpec.ImageName) { + allErrs = append(allErrs, + field.Invalid(field.NewPath("spec", "imageName"), newSpec.ImageName, "field is immutable"), + ) + } + + // SSHKeys is immutable + if !reflect.DeepEqual(oldSpec.SSHKeys, newSpec.SSHKeys) { + allErrs = append(allErrs, + field.Invalid(field.NewPath("spec", "sshKeys"), newSpec.SSHKeys, "field is immutable"), + ) + } + + // Placement group name is immutable + if !reflect.DeepEqual(oldSpec.PlacementGroupName, newSpec.PlacementGroupName) { + allErrs = append(allErrs, + field.Invalid(field.NewPath("spec", "placementGroupName"), newSpec.PlacementGroupName, "field is immutable"), + ) + } + + return allErrs +} diff --git a/api/v1beta1/hcloudmachine_validation_test.go b/api/v1beta1/hcloudmachine_validation_test.go new file mode 100644 index 000000000..cdd74df59 --- /dev/null +++ b/api/v1beta1/hcloudmachine_validation_test.go @@ -0,0 +1,151 @@ +/* +Copyright 2024 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 v1beta1 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +type args struct { + oldSpec HCloudMachineSpec + newSpec HCloudMachineSpec +} + +func TestValidateHCloudMachineSpec(t *testing.T) { + tests := []struct { + name string + args args + want *field.Error + }{ + { + name: "Immutable Type", + args: args{ + oldSpec: HCloudMachineSpec{ + Type: "cpx11", + }, + newSpec: HCloudMachineSpec{ + Type: "cx21", + }, + }, + want: field.Invalid(field.NewPath("spec", "type"), "cx21", "field is immutable"), + }, + { + name: "Immutable ImageName", + args: args{ + oldSpec: HCloudMachineSpec{ + ImageName: "ubuntu-20.04", + }, + newSpec: HCloudMachineSpec{ + ImageName: "centos-7", + }, + }, + want: field.Invalid(field.NewPath("spec", "imageName"), "centos-7", "field is immutable"), + }, + { + name: "Immutable SSHKeys", + args: args{ + oldSpec: HCloudMachineSpec{ + SSHKeys: []SSHKey{ + { + Name: "ssh-key-1", + Fingerprint: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC", + }, + }, + }, + newSpec: HCloudMachineSpec{ + SSHKeys: []SSHKey{ + { + Name: "ssh-key-1", + Fingerprint: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC", + }, + { + Name: "ssh-key-2", + Fingerprint: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC", + }, + }, + }, + }, + want: field.Invalid(field.NewPath("spec", "sshKeys"), []SSHKey{ + { + Name: "ssh-key-1", + Fingerprint: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC", + }, + { + Name: "ssh-key-2", + Fingerprint: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC", + }, + }, "field is immutable"), + }, + { + name: "Immutable PlacementGroupName", + args: args{ + oldSpec: HCloudMachineSpec{ + PlacementGroupName: createPlacementGroupName("placement-group-1"), + }, + newSpec: HCloudMachineSpec{ + PlacementGroupName: createPlacementGroupName("placement-group-2"), + }, + }, + want: field.Invalid(field.NewPath("spec", "placementGroupName"), "placement-group-2", "field is immutable"), + }, + { + name: "No Errors", + args: args{ + oldSpec: HCloudMachineSpec{ + Type: "cpx11", + ImageName: "ubuntu-20.04", + SSHKeys: []SSHKey{{Name: "ssh-key-1"}}, + PlacementGroupName: createPlacementGroupName("placement-group-1"), + }, + newSpec: HCloudMachineSpec{ + Type: "cpx11", + ImageName: "ubuntu-20.04", + SSHKeys: []SSHKey{{Name: "ssh-key-1"}}, + PlacementGroupName: createPlacementGroupName("placement-group-1"), + }, + }, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := validateHCloudMachineSpec(tt.args.oldSpec, tt.args.newSpec) + + if len(got) == 0 { + assert.Empty(t, got) + } + + if len(got) > 1 { + t.Errorf("got length: %d greater than 1", len(got)) + } + + // assert if length of got is 1 + if len(got) == 1 { + assert.Equal(t, tt.want.Type, got[0].Type) + assert.Equal(t, tt.want.Field, got[0].Field) + assert.Equal(t, tt.want.Detail, got[0].Detail) + } + }) + } +} + +func createPlacementGroupName(name string) *string { + return &name +} diff --git a/api/v1beta1/hcloudmachine_webhook.go b/api/v1beta1/hcloudmachine_webhook.go index 76c90c75c..7c816398a 100644 --- a/api/v1beta1/hcloudmachine_webhook.go +++ b/api/v1beta1/hcloudmachine_webhook.go @@ -18,7 +18,6 @@ package v1beta1 import ( "fmt" - "reflect" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" @@ -82,35 +81,7 @@ func (r *HCloudMachine) ValidateUpdate(old runtime.Object) (admission.Warnings, return nil, apierrors.NewBadRequest(fmt.Sprintf("expected an HCloudMachine but got a %T", old)) } - var allErrs field.ErrorList - - // Type is immutable - if !reflect.DeepEqual(oldM.Spec.Type, r.Spec.Type) { - allErrs = append(allErrs, - field.Invalid(field.NewPath("spec", "type"), r.Spec.Type, "field is immutable"), - ) - } - - // ImageName is immutable - if !reflect.DeepEqual(oldM.Spec.ImageName, r.Spec.ImageName) { - allErrs = append(allErrs, - field.Invalid(field.NewPath("spec", "imageName"), r.Spec.ImageName, "field is immutable"), - ) - } - - // SSHKeys is immutable - if !reflect.DeepEqual(oldM.Spec.SSHKeys, r.Spec.SSHKeys) { - allErrs = append(allErrs, - field.Invalid(field.NewPath("spec", "sshKeys"), r.Spec.SSHKeys, "field is immutable"), - ) - } - - // Placement group name is immutable - if !reflect.DeepEqual(oldM.Spec.PlacementGroupName, r.Spec.PlacementGroupName) { - allErrs = append(allErrs, - field.Invalid(field.NewPath("spec", "placementGroupName"), r.Spec.PlacementGroupName, "field is immutable"), - ) - } + allErrs := validateHCloudMachineSpec(oldM.Spec, r.Spec) return nil, aggregateObjErrors(r.GroupVersionKind().GroupKind(), r.Name, allErrs) } diff --git a/api/v1beta1/hcloudmachinetemplate_webhook.go b/api/v1beta1/hcloudmachinetemplate_webhook.go index f4b590945..393302ef2 100644 --- a/api/v1beta1/hcloudmachinetemplate_webhook.go +++ b/api/v1beta1/hcloudmachinetemplate_webhook.go @@ -66,13 +66,13 @@ func (r *HCloudMachineTemplateWebhook) ValidateUpdate(ctx context.Context, oldRa if err != nil { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a admission.Request inside context: %v", err)) } - var allErrs field.ErrorList - if !topology.ShouldSkipImmutabilityChecks(req, newHCloudMachineTemplate) && !reflect.DeepEqual(newHCloudMachineTemplate.Spec, oldHCloudMachineTemplate.Spec) { allErrs = append(allErrs, field.Invalid(field.NewPath("spec"), newHCloudMachineTemplate, "HCloudMachineTemplate.Spec is immutable")) } + allErrs = append(allErrs, validateHCloudMachineSpec(oldHCloudMachineTemplate.Spec.Template.Spec, newHCloudMachineTemplate.Spec.Template.Spec)...) + return nil, aggregateObjErrors(newHCloudMachineTemplate.GroupVersionKind().GroupKind(), newHCloudMachineTemplate.Name, allErrs) } diff --git a/api/v1beta1/hetznerbaremetalmachine_validation.go b/api/v1beta1/hetznerbaremetalmachine_validation.go new file mode 100644 index 000000000..d27c3d6bd --- /dev/null +++ b/api/v1beta1/hetznerbaremetalmachine_validation.go @@ -0,0 +1,90 @@ +/* +Copyright 2021 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 v1beta1 + +import ( + "fmt" + "reflect" + "strings" + + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func validateHetznerBareMetalMachineSpecCreate(spec HetznerBareMetalMachineSpec) field.ErrorList { + var allErrs field.ErrorList + + if (spec.InstallImage.Image.Name == "" || spec.InstallImage.Image.URL == "") && + spec.InstallImage.Image.Path == "" { + allErrs = append(allErrs, + field.Invalid(field.NewPath("spec", "installImage", "image"), spec.InstallImage.Image, + "have to specify either image name and url or path"), + ) + } + + if spec.InstallImage.Image.URL != "" { + if _, err := GetImageSuffix(spec.InstallImage.Image.URL); err != nil { + allErrs = append(allErrs, + field.Invalid(field.NewPath("spec", "installImage", "image", "url"), spec.InstallImage.Image.URL, + "unknown image type in URL"), + ) + } + } + + // validate host selector + for labelKey, labelVal := range spec.HostSelector.MatchLabels { + if _, err := labels.NewRequirement(labelKey, selection.Equals, []string{labelVal}); err != nil { + allErrs = append(allErrs, field.Invalid( + field.NewPath("spec", "hostSelector", "matchLabels"), spec.HostSelector.MatchLabels, + fmt.Sprintf("invalid match label: %s", err.Error()), + )) + } + } + for _, req := range spec.HostSelector.MatchExpressions { + lowercaseOperator := selection.Operator(strings.ToLower(string(req.Operator))) + if _, err := labels.NewRequirement(req.Key, lowercaseOperator, req.Values); err != nil { + allErrs = append(allErrs, field.Invalid( + field.NewPath("spec", "hostSelector", "matchExpressions"), spec.HostSelector.MatchExpressions, + fmt.Sprintf("invalid match expression: %s", err.Error()), + )) + } + } + + return allErrs +} + +func validateHetznerBareMetalMachineSpecUpdate(oldSpec, newSpec HetznerBareMetalMachineSpec) field.ErrorList { + var allErrs field.ErrorList + if !reflect.DeepEqual(newSpec.InstallImage, oldSpec.InstallImage) { + allErrs = append(allErrs, + field.Invalid(field.NewPath("spec", "installImage"), newSpec.InstallImage, "installImage immutable"), + ) + } + if !reflect.DeepEqual(newSpec.SSHSpec, oldSpec.SSHSpec) { + allErrs = append(allErrs, + field.Invalid(field.NewPath("spec", "sshSpec"), newSpec.SSHSpec, "sshSpec immutable"), + ) + } + if !reflect.DeepEqual(newSpec.HostSelector, oldSpec.HostSelector) { + allErrs = append(allErrs, + field.Invalid(field.NewPath("spec", "hostSelector"), newSpec.HostSelector, "hostSelector immutable"), + ) + } + + return allErrs +} diff --git a/api/v1beta1/hetznerbaremetalmachine_validation_test.go b/api/v1beta1/hetznerbaremetalmachine_validation_test.go new file mode 100644 index 000000000..58ce7c5dd --- /dev/null +++ b/api/v1beta1/hetznerbaremetalmachine_validation_test.go @@ -0,0 +1,436 @@ +/* +Copyright 2024 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 v1beta1 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/selection" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func TestValidateHetznerBareMetalMachineSpecCreate(t *testing.T) { + type args struct { + spec HetznerBareMetalMachineSpec + } + tests := []struct { + name string + args args + want *field.Error + }{ + { + name: "Valid Image", + args: args{ + spec: HetznerBareMetalMachineSpec{ + InstallImage: InstallImage{ + Image: Image{ + Name: "ubuntu-20.04", + URL: "https://example.com/ubuntu-20.04.tar.gz", + }, + }, + }, + }, + want: nil, + }, + { + name: "Valid Image Path", + args: args{ + spec: HetznerBareMetalMachineSpec{ + InstallImage: InstallImage{ + Image: Image{ + Path: "path/to/image.tar.gz", + }, + }, + }, + }, + want: nil, + }, + { + name: "Invalid Image", + args: args{ + spec: HetznerBareMetalMachineSpec{ + InstallImage: InstallImage{ + Image: Image{}, + }, + }, + }, + want: field.Invalid(field.NewPath("spec", "installImage", "image"), Image{}, "have to specify either image name and url or path"), + }, + { + name: "Invalid Image URL", + args: args{ + spec: HetznerBareMetalMachineSpec{ + InstallImage: InstallImage{ + Image: Image{ + Name: "ubuntu-20.04", + URL: "https://example.com/ubuntu-20.04.invalid", + }, + }, + }, + }, + want: field.Invalid(field.NewPath("spec", "installImage", "image", "url"), "https://example.com/ubuntu-20.04.invalid", "unknown image type in URL"), + }, + { + name: "Valid HostSelector MatchLabels", + args: args{ + spec: HetznerBareMetalMachineSpec{ + InstallImage: InstallImage{ + Image: Image{ + Name: "ubuntu-20.04", + URL: "https://example.com/ubuntu-20.04.tar.gz", + }, + }, + HostSelector: HostSelector{ + MatchLabels: map[string]string{ + "key1": "value1", + }, + }, + }, + }, + want: nil, + }, + { + name: "Valid HostSelector MatchExpressions", + args: args{ + spec: HetznerBareMetalMachineSpec{ + InstallImage: InstallImage{ + Image: Image{ + Name: "ubuntu-20.04", + URL: "https://example.com/ubuntu-20.04.tar.gz", + }, + }, + HostSelector: HostSelector{ + MatchExpressions: []HostSelectorRequirement{ + { + Key: "key1", + Operator: selection.In, + Values: []string{"value1", "value2"}, + }, + }, + }, + }, + }, + want: nil, + }, + { + name: "Invalid HostSelector MatchExpressions - Invalid Operator", + args: args{ + spec: HetznerBareMetalMachineSpec{ + InstallImage: InstallImage{ + Image: Image{ + Name: "ubuntu-20.04", + URL: "https://example.com/ubuntu-20.04.tar.gz", + }, + }, + HostSelector: HostSelector{ + MatchExpressions: []HostSelectorRequirement{ + { + Key: "key1", + Operator: selection.Operator("Invalid"), + Values: []string{"value1", "value2"}, + }, + }, + }, + }, + }, + want: field.Invalid( + field.NewPath("spec", "hostSelector", "matchExpressions"), + []HostSelectorRequirement{ + { + Key: "key1", + Operator: selection.Operator("Invalid"), + Values: []string{"value1", "value2"}, + }, + }, + `invalid match expression: operator: Unsupported value: "invalid": supported values: "in", "notin", "=", "==", "!=", "gt", "lt", "exists", "!"`, + ), + }, + { + name: "Invalid HostSelector MatchExpressions - Empty Key", + args: args{ + spec: HetznerBareMetalMachineSpec{ + InstallImage: InstallImage{ + Image: Image{ + Name: "ubuntu-20.04", + URL: "https://example.com/ubuntu-20.04.tar.gz", + }, + }, + HostSelector: HostSelector{ + MatchExpressions: []HostSelectorRequirement{ + { + Key: "", + Operator: selection.In, + Values: []string{"value1", "value2"}, + }, + }, + }, + }, + }, + want: field.Invalid( + field.NewPath("spec", "hostSelector", "matchExpressions"), + []HostSelectorRequirement{ + { + Key: "", + Operator: selection.In, + Values: []string{"value1", "value2"}, + }, + }, + `invalid match expression: key: Invalid value: "": name part must be non-empty; name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')`, + ), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := validateHetznerBareMetalMachineSpecCreate(tt.args.spec) + + if len(got) == 0 { + assert.Empty(t, got) + } + + if len(got) > 1 { + t.Errorf("got length: %d greater than 1", len(got)) + } + + // assert if length of got is 1 + if len(got) == 1 { + assert.Equal(t, tt.want.Type, got[0].Type) + assert.Equal(t, tt.want.Field, got[0].Field) + assert.Equal(t, tt.want.Detail, got[0].Detail) + } + }) + } +} + +func TestValidateHetznerBareMetalMachineSpecUpdate(t *testing.T) { + type args struct { + oldSpec HetznerBareMetalMachineSpec + newSpec HetznerBareMetalMachineSpec + } + tests := []struct { + name string + args args + want *field.Error + }{ + { + name: "Immutable InstallImage", + args: args{ + oldSpec: HetznerBareMetalMachineSpec{ + InstallImage: InstallImage{ + Image: Image{ + Name: "ubuntu-20.04", + URL: "https://example.com/ubuntu-20.04.tar.gz", + }, + }, + }, + newSpec: HetznerBareMetalMachineSpec{ + InstallImage: InstallImage{ + Image: Image{ + Name: "centos-7", + URL: "https://example.com/centos-7.tar.gz", + }, + }, + }, + }, + want: field.Invalid(field.NewPath("spec", "installImage"), InstallImage{ + Image: Image{ + Name: "centos-7", + URL: "https://example.com/centos-7.tar.gz", + }, + }, "installImage immutable"), + }, + { + name: "Immutable SSHSpec", + args: args{ + oldSpec: HetznerBareMetalMachineSpec{ + SSHSpec: SSHSpec{ + SecretRef: SSHSecretRef{ + Name: "ssh-secret", + Key: SSHSecretKeyRef{ + Name: "ssh-key-name", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC", + PrivateKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC", + }, + }, + PortAfterInstallImage: 22, + PortAfterCloudInit: 22, + }, + }, + newSpec: HetznerBareMetalMachineSpec{ + SSHSpec: SSHSpec{ + SecretRef: SSHSecretRef{ + Name: "ssh-secret-new", + Key: SSHSecretKeyRef{ + Name: "ssh-key-name-new", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC", + PrivateKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC", + }, + }, + PortAfterInstallImage: 2222, + PortAfterCloudInit: 2222, + }, + }, + }, + want: field.Invalid(field.NewPath("spec", "sshSpec"), SSHSpec{ + SecretRef: SSHSecretRef{ + Name: "ssh-secret-new", + Key: SSHSecretKeyRef{ + Name: "ssh-key-name-new", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC", + PrivateKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC", + }, + }, + PortAfterInstallImage: 2222, + PortAfterCloudInit: 2222, + }, "sshSpec immutable"), + }, + { + name: "Immutable HostSelector", + args: args{ + oldSpec: HetznerBareMetalMachineSpec{ + HostSelector: HostSelector{ + MatchLabels: map[string]string{ + "key1": "value1", + }, + MatchExpressions: []HostSelectorRequirement{ + { + Key: "key2", + Operator: selection.In, + Values: []string{"value2"}, + }, + }, + }, + }, + newSpec: HetznerBareMetalMachineSpec{ + HostSelector: HostSelector{ + MatchLabels: map[string]string{ + "key3": "value3", + }, + MatchExpressions: []HostSelectorRequirement{ + { + Key: "key4", + Operator: selection.In, + Values: []string{"value4"}, + }, + }, + }, + }, + }, + want: field.Invalid(field.NewPath("spec", "hostSelector"), HostSelector{ + MatchLabels: map[string]string{ + "key3": "value3", + }, + MatchExpressions: []HostSelectorRequirement{ + { + Key: "key4", + Operator: selection.In, + Values: []string{"value4"}, + }, + }, + }, "hostSelector immutable"), + }, + { + name: "No Errors", + args: args{ + oldSpec: HetznerBareMetalMachineSpec{ + InstallImage: InstallImage{ + Image: Image{ + Name: "ubuntu-20.04", + URL: "https://example.com/ubuntu-20.04.tar.gz", + }, + }, + SSHSpec: SSHSpec{ + SecretRef: SSHSecretRef{ + Name: "ssh-secret", + Key: SSHSecretKeyRef{ + Name: "ssh-key-name", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC", + PrivateKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC", + }, + }, + PortAfterInstallImage: 22, + PortAfterCloudInit: 22, + }, + HostSelector: HostSelector{ + MatchLabels: map[string]string{ + "key1": "value1", + }, + MatchExpressions: []HostSelectorRequirement{ + { + Key: "key2", + Operator: selection.In, + Values: []string{"value2"}, + }, + }, + }, + }, + newSpec: HetznerBareMetalMachineSpec{ + InstallImage: InstallImage{ + Image: Image{ + Name: "ubuntu-20.04", + URL: "https://example.com/ubuntu-20.04.tar.gz", + }, + }, + SSHSpec: SSHSpec{ + SecretRef: SSHSecretRef{ + Name: "ssh-secret", + Key: SSHSecretKeyRef{ + Name: "ssh-key-name", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC", + PrivateKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC", + }, + }, + PortAfterInstallImage: 22, + PortAfterCloudInit: 22, + }, + HostSelector: HostSelector{ + MatchLabels: map[string]string{ + "key1": "value1", + }, + MatchExpressions: []HostSelectorRequirement{ + { + Key: "key2", + Operator: selection.In, + Values: []string{"value2"}, + }, + }, + }, + }, + }, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := validateHetznerBareMetalMachineSpecUpdate(tt.args.oldSpec, tt.args.newSpec) + + if len(got) == 0 { + assert.Empty(t, got) + } + + if len(got) > 1 { + t.Errorf("got length: %d greater than 1", len(got)) + } + // assert if length of got is 1 + if len(got) == 1 { + assert.Equal(t, tt.want.Type, got[0].Type) + assert.Equal(t, tt.want.Field, got[0].Field) + assert.Equal(t, tt.want.Detail, got[0].Detail) + } + }) + } +} diff --git a/api/v1beta1/hetznerbaremetalmachine_webhook.go b/api/v1beta1/hetznerbaremetalmachine_webhook.go index e1d712d1b..1fc4e5fbb 100644 --- a/api/v1beta1/hetznerbaremetalmachine_webhook.go +++ b/api/v1beta1/hetznerbaremetalmachine_webhook.go @@ -17,14 +17,7 @@ limitations under the License. package v1beta1 import ( - "fmt" - "reflect" - "strings" - - "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/selection" - "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" @@ -50,71 +43,21 @@ var _ webhook.Validator = &HetznerBareMetalMachine{} // ValidateCreate implements webhook.Validator so a webhook will be registered for the type. func (bmMachine *HetznerBareMetalMachine) ValidateCreate() (admission.Warnings, error) { - var allErrs field.ErrorList - if bmMachine.Spec.SSHSpec.PortAfterCloudInit == 0 { bmMachine.Spec.SSHSpec.PortAfterCloudInit = bmMachine.Spec.SSHSpec.PortAfterInstallImage } - if (bmMachine.Spec.InstallImage.Image.Name == "" || bmMachine.Spec.InstallImage.Image.URL == "") && - bmMachine.Spec.InstallImage.Image.Path == "" { - allErrs = append(allErrs, - field.Invalid(field.NewPath("spec", "installImage", "image"), bmMachine.Spec.InstallImage.Image, - "have to specify either image name and url or path"), - ) - } - - if bmMachine.Spec.InstallImage.Image.URL != "" { - if _, err := GetImageSuffix(bmMachine.Spec.InstallImage.Image.URL); err != nil { - allErrs = append(allErrs, - field.Invalid(field.NewPath("spec", "installImage", "image", "url"), bmMachine.Spec.InstallImage.Image.URL, - "unknown image type in URL"), - ) - } - } - - // validate host selector - for labelKey, labelVal := range bmMachine.Spec.HostSelector.MatchLabels { - if _, err := labels.NewRequirement(labelKey, selection.Equals, []string{labelVal}); err != nil { - allErrs = append(allErrs, field.Invalid( - field.NewPath("spec", "hostSelector", "matchLabels"), bmMachine.Spec.HostSelector.MatchLabels, - fmt.Sprintf("invalid match label: %s", err.Error()), - )) - } - } - for _, req := range bmMachine.Spec.HostSelector.MatchExpressions { - lowercaseOperator := selection.Operator(strings.ToLower(string(req.Operator))) - if _, err := labels.NewRequirement(req.Key, lowercaseOperator, req.Values); err != nil { - allErrs = append(allErrs, field.Invalid( - field.NewPath("spec", "hostSelector", "matchExpressions"), bmMachine.Spec.HostSelector.MatchExpressions, - fmt.Sprintf("invalid match expression: %s", err.Error()), - )) - } - } + allErrs := validateHetznerBareMetalMachineSpecCreate(bmMachine.Spec) return nil, aggregateObjErrors(bmMachine.GroupVersionKind().GroupKind(), bmMachine.Name, allErrs) } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. func (bmMachine *HetznerBareMetalMachine) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { - var allErrs field.ErrorList - oldHetznerBareMetalMachine := old.(*HetznerBareMetalMachine) - if !reflect.DeepEqual(bmMachine.Spec.InstallImage, oldHetznerBareMetalMachine.Spec.InstallImage) { - allErrs = append(allErrs, - field.Invalid(field.NewPath("spec", "installImage"), bmMachine.Spec.InstallImage, "installImage immutable"), - ) - } - if !reflect.DeepEqual(bmMachine.Spec.SSHSpec, oldHetznerBareMetalMachine.Spec.SSHSpec) { - allErrs = append(allErrs, - field.Invalid(field.NewPath("spec", "sshSpec"), bmMachine.Spec.SSHSpec, "sshSpec immutable"), - ) - } - if !reflect.DeepEqual(bmMachine.Spec.HostSelector, oldHetznerBareMetalMachine.Spec.HostSelector) { - allErrs = append(allErrs, - field.Invalid(field.NewPath("spec", "hostSelector"), bmMachine.Spec.HostSelector, "hostSelector immutable"), - ) - } + + allErrs := validateHetznerBareMetalMachineSpecUpdate(oldHetznerBareMetalMachine.Spec, bmMachine.Spec) + return nil, aggregateObjErrors(bmMachine.GroupVersionKind().GroupKind(), bmMachine.Name, allErrs) } diff --git a/api/v1beta1/hetznerbaremetalmachinetemplate_webhook.go b/api/v1beta1/hetznerbaremetalmachinetemplate_webhook.go index 252c51fc3..21166c43b 100644 --- a/api/v1beta1/hetznerbaremetalmachinetemplate_webhook.go +++ b/api/v1beta1/hetznerbaremetalmachinetemplate_webhook.go @@ -47,8 +47,19 @@ type HetznerBareMetalMachineTemplateWebhook struct{} var _ webhook.CustomValidator = &HetznerBareMetalMachineTemplateWebhook{} // ValidateCreate implements webhook.Validator so a webhook will be registered for the type. -func (r *HetznerBareMetalMachineTemplateWebhook) ValidateCreate(_ context.Context, _ runtime.Object) (admission.Warnings, error) { - return nil, nil +func (r *HetznerBareMetalMachineTemplateWebhook) ValidateCreate(_ context.Context, raw runtime.Object) (admission.Warnings, error) { + hbmmt, ok := raw.(*HetznerBareMetalMachineTemplate) + if !ok { + return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a HetznerBareMetalMachineTemplate but got a %T", raw)) + } + + if hbmmt.Spec.Template.Spec.SSHSpec.PortAfterCloudInit == 0 { + hbmmt.Spec.Template.Spec.SSHSpec.PortAfterCloudInit = hbmmt.Spec.Template.Spec.SSHSpec.PortAfterInstallImage + } + + allErrs := validateHetznerBareMetalMachineSpecCreate(hbmmt.Spec.Template.Spec) + + return nil, aggregateObjErrors(hbmmt.GroupVersionKind().GroupKind(), hbmmt.Name, allErrs) } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. @@ -66,13 +77,14 @@ func (r *HetznerBareMetalMachineTemplateWebhook) ValidateUpdate(ctx context.Cont if err != nil { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a admission.Request inside context: %v", err)) } - var allErrs field.ErrorList if !topology.ShouldSkipImmutabilityChecks(req, newHetznerBareMetalMachineTemplate) && !reflect.DeepEqual(newHetznerBareMetalMachineTemplate.Spec, oldHetznerBareMetalMachineTemplate.Spec) { allErrs = append(allErrs, field.Invalid(field.NewPath("spec"), newHetznerBareMetalMachineTemplate, "HetznerBareMetalMachineTemplate.Spec is immutable")) } + allErrs = append(allErrs, validateHetznerBareMetalMachineSpecUpdate(oldHetznerBareMetalMachineTemplate.Spec.Template.Spec, newHetznerBareMetalMachineTemplate.Spec.Template.Spec)...) + return nil, aggregateObjErrors(newHetznerBareMetalMachineTemplate.GroupVersionKind().GroupKind(), newHetznerBareMetalMachineTemplate.Name, allErrs) } diff --git a/controllers/controllers_suite_test.go b/controllers/controllers_suite_test.go index c1002d207..c3459c155 100644 --- a/controllers/controllers_suite_test.go +++ b/controllers/controllers_suite_test.go @@ -41,6 +41,7 @@ import ( const ( defaultPodNamespace = "caph-system" timeout = time.Second * 5 + interval = time.Millisecond * 100 ) var ( diff --git a/controllers/hcloudmachine_controller_test.go b/controllers/hcloudmachine_controller_test.go index 6061839d7..6eed75436 100644 --- a/controllers/hcloudmachine_controller_test.go +++ b/controllers/hcloudmachine_controller_test.go @@ -18,7 +18,6 @@ package controllers import ( "testing" - "time" "github.com/hetznercloud/hcloud-go/v2/hcloud" . "github.com/onsi/ginkgo/v2" @@ -376,13 +375,13 @@ var _ = Describe("HCloudMachineReconciler", func() { } return len(servers) == 0 - }, timeout, time.Second).Should(BeTrue()) + }, timeout, interval).Should(BeTrue()) By("checking that bootstrap condition is not ready") Eventually(func() bool { return isPresentAndFalseWithReason(key, hcloudMachine, infrav1.BootstrapReadyCondition, infrav1.BootstrapNotReadyReason) - }, timeout, time.Second).Should(BeTrue()) + }, timeout, interval).Should(BeTrue()) By("setting the bootstrap data") @@ -395,13 +394,13 @@ var _ = Describe("HCloudMachineReconciler", func() { Eventually(func() error { return ph.Patch(ctx, capiMachine, patch.WithStatusObservedGeneration{}) - }, timeout, time.Second).Should(BeNil()) + }, timeout, interval).Should(BeNil()) By("checking that bootstrap condition is ready") Eventually(func() bool { return isPresentAndTrue(key, hcloudMachine, infrav1.BootstrapReadyCondition) - }, timeout, time.Second).Should(BeTrue()) + }, timeout, interval).Should(BeTrue()) By("listing hcloud servers") @@ -415,19 +414,19 @@ var _ = Describe("HCloudMachineReconciler", func() { return 0 } return len(servers) - }, timeout, time.Second).Should(BeNumerically(">", 0)) + }, timeout, interval).Should(BeNumerically(">", 0)) By("checking if server created condition is set") Eventually(func() bool { return isPresentAndTrue(key, hcloudMachine, infrav1.ServerCreateSucceededCondition) - }, timeout, time.Second).Should(BeTrue()) + }, timeout, interval).Should(BeTrue()) By("checking if server available condition is set") Eventually(func() bool { return isPresentAndTrue(key, hcloudMachine, infrav1.ServerAvailableCondition) - }, timeout, time.Second).Should(BeTrue()) + }, timeout, interval).Should(BeTrue()) }) }) @@ -470,7 +469,7 @@ var _ = Describe("HCloudMachineReconciler", func() { It("checks that ImageNotFound is visible in conditions if image does not exist", func() { Eventually(func() bool { return isPresentAndFalseWithReason(key, hcloudMachine, infrav1.ServerCreateSucceededCondition, infrav1.ImageNotFoundReason) - }, timeout, time.Second).Should(BeTrue()) + }, timeout, interval).Should(BeTrue()) }) }) }) @@ -530,7 +529,7 @@ var _ = Describe("HCloudMachineReconciler", func() { return 0 } return len(servers) - }, timeout, time.Second).Should(BeNumerically(">", 0)) + }, timeout, interval).Should(BeNumerically(">", 0)) }) }) @@ -559,7 +558,7 @@ var _ = Describe("HCloudMachineReconciler", func() { } return len(servers) - }, timeout, time.Second).Should(BeNumerically(">", 0)) + }, timeout, interval).Should(BeNumerically(">", 0)) }) }) @@ -607,7 +606,7 @@ var _ = Describe("HCloudMachineReconciler", func() { } return len(servers) - }, timeout, time.Second).Should(BeNumerically(">", 0)) + }, timeout, interval).Should(BeNumerically(">", 0)) }) }) }) @@ -734,7 +733,7 @@ var _ = Describe("Hetzner secret", func() { Eventually(func() bool { return isPresentAndFalseWithReason(key, hcloudMachine, infrav1.HCloudTokenAvailableCondition, expectedReason) - }, timeout, time.Second).Should(BeTrue()) + }, timeout, interval).Should(BeTrue()) }, Entry("no Hetzner secret/wrong reference", func() *corev1.Secret { return &corev1.Secret{ @@ -808,6 +807,23 @@ var _ = Describe("HCloudMachine validation", func() { hcloudMachine.Spec.ImageName = "" Expect(testEnv.Create(ctx, hcloudMachine)).ToNot(Succeed()) }) + + It("should allow valid HCloudMachine creation", func() { + Expect(testEnv.Create(ctx, hcloudMachine)).To(Succeed()) + }) + + It("should prevent updating immutable fields", func() { + Expect(testEnv.Create(ctx, hcloudMachine)).To(Succeed()) + + Eventually(func() error { + key := client.ObjectKey{Namespace: testNs.Name, Name: hcloudMachine.Name} + return testEnv.Client.Get(ctx, key, hcloudMachine) + }, timeout, interval).Should(BeNil()) + + hcloudMachine.Spec.Type = "cpx32" + hcloudMachine.Spec.ImageName = "fedora-control-plane" + Expect(testEnv.Update(ctx, hcloudMachine)).ToNot(Succeed()) + }) }) var _ = Describe("IgnoreInsignificantHetznerClusterUpdates Predicate", func() { diff --git a/controllers/hcloudmachinetemplate_controller_test.go b/controllers/hcloudmachinetemplate_controller_test.go index ec808aa78..af3bd04aa 100644 --- a/controllers/hcloudmachinetemplate_controller_test.go +++ b/controllers/hcloudmachinetemplate_controller_test.go @@ -17,8 +17,6 @@ limitations under the License. package controllers import ( - "time" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" @@ -244,8 +242,93 @@ var _ = Describe("HCloudMachineTemplateReconciler", func() { } return true - }, timeout, time.Second).Should(BeTrue()) + }, timeout, interval).Should(BeTrue()) + }) + }) + Context("HCloudMachineTemplate Webhook Validation", func() { + var ( + hcloudMachineTemplate *infrav1.HCloudMachineTemplate + testNs *corev1.Namespace + ) + BeforeEach(func() { + var err error + testNs, err = testEnv.CreateNamespace(ctx, "hcloudmachine-validation") + Expect(err).NotTo(HaveOccurred()) + + hcloudMachineTemplate = &infrav1.HCloudMachineTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hcloud-validation-machine", + Namespace: testNs.Name, + }, + Spec: infrav1.HCloudMachineTemplateSpec{ + Template: infrav1.HCloudMachineTemplateResource{ + Spec: infrav1.HCloudMachineSpec{ + Type: "cx41", + ImageName: "fedora", + }, + }, + }, + } + Expect(testEnv.Client.Create(ctx, hcloudMachineTemplate)).To(Succeed()) + + key = client.ObjectKey{Namespace: testNs.Name, Name: "hcloud-validation-machine"} + Eventually(func() error { + return testEnv.Client.Get(ctx, key, hcloudMachineTemplate) + }, timeout, interval).Should(BeNil()) + }) + AfterEach(func() { + Expect(testEnv.Cleanup(ctx, testNs, hcloudMachineTemplate)).To(Succeed()) + }) + + It("should prevent updating type", func() { + Expect(testEnv.Get(ctx, key, machineTemplate)).To(Succeed()) + + hcloudMachineTemplate.Spec.Template.Spec.Type = "cpx32" + Expect(testEnv.Client.Update(ctx, hcloudMachineTemplate)).ToNot(Succeed()) + }) + + It("should prevent updating Image name", func() { + Expect(testEnv.Get(ctx, key, machineTemplate)).To(Succeed()) + + hcloudMachineTemplate.Spec.Template.Spec.ImageName = "fedora-control-plane" + Expect(testEnv.Client.Update(ctx, hcloudMachineTemplate)).ToNot(Succeed()) + + }) + + It("should prevent updating SSHKey", func() { + Expect(testEnv.Get(ctx, key, machineTemplate)).To(Succeed()) + + hcloudMachineTemplate.Spec.Template.Spec.SSHKeys = []infrav1.SSHKey{{Name: "ssh-key-1"}} + Expect(testEnv.Client.Update(ctx, hcloudMachineTemplate)).ToNot(Succeed()) + + }) + + It("should prevent updating PlacementGroups", func() { + Expect(testEnv.Get(ctx, key, machineTemplate)).To(Succeed()) + + hcloudMachineTemplate.Spec.Template.Spec.PlacementGroupName = createPlacementGroupName("placement-group-1") + Expect(testEnv.Client.Update(ctx, hcloudMachineTemplate)).ToNot(Succeed()) + + }) + + It("should succeed for mutable fields", func() { + Expect(testEnv.Get(ctx, key, machineTemplate)).To(Succeed()) + + hcloudMachineTemplate.Status.Conditions = clusterv1.Conditions{ + { + Type: "TestSuccessful", + Status: corev1.ConditionTrue, + Reason: "TestPassed", + Message: "The test was successful", + }, + } + Expect(testEnv.Client.Update(ctx, hcloudMachineTemplate)).To(Succeed()) }) }) + }) }) + +func createPlacementGroupName(name string) *string { + return &name +} diff --git a/controllers/hetznerbaremetalmachine_controller_test.go b/controllers/hetznerbaremetalmachine_controller_test.go index 5aea7bb24..852c1621a 100644 --- a/controllers/hetznerbaremetalmachine_controller_test.go +++ b/controllers/hetznerbaremetalmachine_controller_test.go @@ -709,5 +709,168 @@ var _ = Describe("HetznerBareMetalMachineReconciler", func() { Expect(testEnv.Update(ctx, bmMachine)).NotTo(Succeed()) }) }) + + Context("validate create", func() { + var ( + hbmmt *infrav1.HetznerBareMetalMachineTemplate + testNs *corev1.Namespace + ) + BeforeEach(func() { + var err error + testNs, err = testEnv.CreateNamespace(ctx, "hcloudmachine-validation") + Expect(err).NotTo(HaveOccurred()) + + hbmmt = &infrav1.HetznerBareMetalMachineTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Name: bmMachineName, + Namespace: testNs.Name, + Labels: map[string]string{ + clusterv1.ClusterNameLabel: capiCluster.Name, + }, + }, + Spec: infrav1.HetznerBareMetalMachineTemplateSpec{ + Template: infrav1.HetznerBareMetalMachineTemplateResource{ + Spec: getDefaultHetznerBareMetalMachineSpec(), + }, + }, + } + + }) + + AfterEach(func() { + Expect(testEnv.Cleanup(ctx, testNs, hbmmt)).To(Succeed()) + }) + + It("should allow creation of hbmmt", func() { + Expect(testEnv.Create(ctx, hbmmt)).To(Succeed()) + }) + + }) + + Context("validate update", func() { + var ( + hbmmt *infrav1.HetznerBareMetalMachineTemplate + testNs *corev1.Namespace + ) + BeforeEach(func() { + var err error + testNs, err = testEnv.CreateNamespace(ctx, "hcloudmachine-validation") + Expect(err).NotTo(HaveOccurred()) + + hbmmt = &infrav1.HetznerBareMetalMachineTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Name: bmMachineName, + Namespace: testNs.Name, + Labels: map[string]string{ + clusterv1.ClusterNameLabel: capiCluster.Name, + }, + }, + Spec: infrav1.HetznerBareMetalMachineTemplateSpec{ + Template: infrav1.HetznerBareMetalMachineTemplateResource{ + Spec: getDefaultHetznerBareMetalMachineSpec(), + }, + }, + } + Expect(testEnv.Client.Create(ctx, hbmmt)).To(Succeed()) + + key = client.ObjectKey{Namespace: testNs.Name, Name: hbmmt.Name} + Eventually(func() error { + return testEnv.Client.Get(ctx, key, hbmmt) + }, timeout, time.Second).Should(BeNil()) + + }) + + AfterEach(func() { + Expect(testEnv.Cleanup(ctx, testNs, hbmmt)).To(Succeed()) + }) + + It("should not allow update of InstallImage", func() { + Expect(testEnv.Get(ctx, key, hbmmt)).To(Succeed()) + + hbmmt.Spec = infrav1.HetznerBareMetalMachineTemplateSpec{ + Template: infrav1.HetznerBareMetalMachineTemplateResource{ + Spec: infrav1.HetznerBareMetalMachineSpec{ + InstallImage: infrav1.InstallImage{ + Image: infrav1.Image{ + Name: "ubuntu-20.04", + URL: "https://example.com/ubuntu-20.04.tar.gz", + }, + Partitions: []infrav1.Partition{ + { + Mount: "/", + FileSystem: "ext4", + Size: "10GiB", + }, + }, + }, + }, + }, + } + Expect(testEnv.Client.Update(ctx, hbmmt)).ToNot(Succeed()) + + }) + + It("should not allow update of SSHSpec", func() { + Expect(testEnv.Get(ctx, key, hbmmt)).To(Succeed()) + + hbmmt.Spec = infrav1.HetznerBareMetalMachineTemplateSpec{ + Template: infrav1.HetznerBareMetalMachineTemplateResource{ + Spec: infrav1.HetznerBareMetalMachineSpec{ + SSHSpec: infrav1.SSHSpec{ + SecretRef: infrav1.SSHSecretRef{ + Name: "ssh-secret-new", + Key: infrav1.SSHSecretKeyRef{ + Name: "ssh-key-name-new", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC", + PrivateKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC", + }, + }, + PortAfterInstallImage: 2222, + PortAfterCloudInit: 2222, + }, + }, + }, + } + Expect(testEnv.Client.Update(ctx, hbmmt)).ToNot(Succeed()) + + }) + + It("should not allow update of Host Selectors", func() { + Expect(testEnv.Get(ctx, key, hbmmt)).To(Succeed()) + + hbmmt.Spec = infrav1.HetznerBareMetalMachineTemplateSpec{ + Template: infrav1.HetznerBareMetalMachineTemplateResource{ + Spec: infrav1.HetznerBareMetalMachineSpec{ + HostSelector: infrav1.HostSelector{ + MatchLabels: map[string]string{ + "key3": "value3", + }, + MatchExpressions: []infrav1.HostSelectorRequirement{ + { + Key: "key4", + Operator: selection.In, + Values: []string{"value4"}, + }, + }, + }, + }, + }, + } + Expect(testEnv.Client.Update(ctx, hbmmt)).ToNot(Succeed()) + + }) + + It("should allow update of mutable fields", func() { + Expect(testEnv.Get(ctx, key, hbmmt)).To(Succeed()) + + if hbmmt.ObjectMeta.Annotations == nil { + hbmmt.ObjectMeta.Annotations = make(map[string]string) + } + hbmmt.ObjectMeta.Annotations["test"] = "should_succeed" + Expect(testEnv.Client.Update(ctx, hbmmt)).To(Succeed()) + + }) + + }) }) })