Repository: kubevirt/kubevirt (Go)

Issue:
Validation of VM machine type not strict enough
<!-- Make sure that you visit our User Guide at https://kubevirt.io/user-guide.
-->

**What happened**:
When creating a VM, the field `spec.domain.machine.type` is validated against the target architectures valid machine types.
However in this validation, all strings containing the valid machine type are accepted.

For example on s390x, it is validated against `s390-ccw-virtio*`, so `s390-ccw-virtio` or `s390-ccw-virtio-rhel-8` are valid. What should not be valid, but is currently accepted is `something-s390-ccw-virtio`. This is the same for other architectures.

**What you expected to happen**:
To accept wildcard patterns and fail when something in the machine type does not match the pattern.

**How to reproduce it (as minimally and precisely as possible)**:
Create a VMI with an invalid machine type like `something-q35` for x86.

**Additional context**:
Add any other context about the problem here.

**Environment**:
- KubeVirt version (use `virtctl version`): Latest main as of 2025-03-11
- Kubernetes version (use `kubectl version`): N/A
- VM or VMI specifications: N/A
- Cloud provider or hardware configuration: N/A
- OS (e.g. from /etc/os-release): N/A
- Kernel (e.g. `uname -a`): N/A
- Install tools: N/A
- Others: N/A



Discussion:
<!-- Greeting -->
Hello @jschintag! I'm here to assist you with any bugs, questions, or contributions. Let's dive into this issue together! If you need anything, feel free to ask.

<!-- Answer -->
To address the issue of overly permissive validation for the `spec.domain.machine.type` field, you can enhance the validation logic to ensure only exact matches to valid machine types are accepted. This can be achieved by modifying the `ValidateVirtualMachineInstanceSpec` function to check for exact matches against a predefined list of valid machine types, rather than using patterns that allow partial matches.

Here's an example of how you might implement stricter validation:

```go
func ValidateVirtualMachineInstanceSpec(path *k8sfield.Path, spec *v1.VirtualMachineInstanceSpec, config *virtconfig.ClusterConfig) []metav1.StatusCause {
    var causes []metav1.StatusCause

    validMachineTypes := map[string]bool{
        "pseries": true,
        "virt": true,
        "s390-ccw-virtio": true,
        "q35": true,
    }

    if spec.Domain.Machine != nil {
        machineType := spec.Domain.Machine.Type
        if !validMachineTypes[machineType] {
            causes = append(causes, metav1.StatusCause{
                Type:    metav1.CauseTypeFieldValueInvalid,
                Message: fmt.Sprintf("%s is not supported: %s (allowed values: %v)", path.Child("domain.machine.type").String(), machineType, getValidMachineTypes(validMachineTypes)),
                Field:   path.Child("domain.machine.type").String(),
            })
        }
    }

    // Other validation logic...

    return causes
}

func getValidMachineTypes(validMachineTypes map[string]bool) []string {
    keys := make([]string, 0, len(validMachineTypes))
    for k := range validMachineTypes {
        keys = append(keys, k)
    }
    return keys
}
```

This code snippet defines a map of valid machine types and checks if the provided machine type is an exact match to one of the keys in the map. If not, it adds a validation error to the `causes` slice <sup>[[1]](https://github.com/kubevirt/kubevirt/blob/main/pkg/virt-api/webhooks/validating-webhook/admitters/vmi-create-admitter_test.go)</sup>.


<!-- Dosu Comment Footer -->
*To continue talking to [Dosu](https://dosu.dev), mention @dosu.*

---
<sup>Help Dosu learn! Give it feedback: [Great Response](https://app.dosu.dev/response-feedback/3d4016d2-0193-488b-80a1-f00752550813?feedback_type=great_response) | [Irrelevant Answer](https://app.dosu.dev/response-feedback/3d4016d2-0193-488b-80a1-f00752550813?feedback_type=irrelevant_answer) | [Incorrect Sources](https://app.dosu.dev/response-feedback/3d4016d2-0193-488b-80a1-f00752550813?feedback_type=incorrect_sources) | [Too Verbose](https://app.dosu.dev/response-feedback/3d4016d2-0193-488b-80a1-f00752550813?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/3d4016d2-0193-488b-80a1-f00752550813?feedback_type=hallucination) | [Bug Report](https://app.dosu.dev/response-feedback/3d4016d2-0193-488b-80a1-f00752550813?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/3d4016d2-0193-488b-80a1-f00752550813?feedback_type=other)</sup>

@jschintag I couldn't have reproduced it on main's kubevirtci cluster:
```shell
> vi examples/vmi-fedora.yaml  # to add the non-supported machine type
> grep "something" -B 3  examples/vmi-fedora.yaml
spec:
  domain:
    machine:
      type: "something-q35"

> k create -f examples/vmi-fedora.yaml
virtualmachineinstance.kubevirt.io/vmi-fedora created

> k get vmi -w
NAME         AGE    PHASE        IP    NODENAME   READY
vmi-fedora   112s   Scheduling                    False
```

What am I missing?

/cc @dasionov 
That is the problem `something-q35` should not be a valid machine type. It should return an admission error and not create the vmi.
Since it is validated against `q35*`
We are already validating the default patterns for each arch in vmi-create, IMO let's just adjust the patterns to be more strict.
Yes, i have checked the code and currently it is done via regex, while the pattern is a glob pattern. I think something like `filepath.Match` might be more suited to our use case. Especially since we already have the machine types as glob patterns instead of regex patterns.
> Yes, i have checked the code and currently it is done via regex, while the pattern is a glob pattern. I think something like `filepath.Match` might be more suited to our use case. Especially since we already have the machine types as glob patterns instead of regex patterns.

Thank you for clarifying @jschintag!
The problem is documented in the MatchString() function doc ("contains **any** match.."):
```go
// MatchString reports whether the string s
// contains any match of the regular expression re.
func (re *Regexp) MatchString(s string) bool {
	return re.doMatch(nil, nil, s)
}
```

/triage accepted

@jschintag would you be able to implement a PR for this?
Sure, i can do that
I have created an initial fix here: https://github.com/jschintag/kubevirt/tree/api-machine-type-validation
I'll wait a bit for #13852 to merge, as it expands the same unit tests. That way i can easier test i didn't break anything.



Patch:
diff --git a/pkg/virt-api/webhooks/validating-webhook/admitters/vmi-create-admitter.go b/pkg/virt-api/webhooks/validating-webhook/admitters/vmi-create-admitter.go
index bedddfef9ac6..c587058e3d16 100644
--- a/pkg/virt-api/webhooks/validating-webhook/admitters/vmi-create-admitter.go
+++ b/pkg/virt-api/webhooks/validating-webhook/admitters/vmi-create-admitter.go
@@ -970,7 +970,8 @@ func validateEmulatedMachine(field *k8sfield.Path, spec *v1.VirtualMachineInstan
 		supportedMachines := config.GetEmulatedMachines(spec.Architecture)
 		var match = false
 		for _, val := range supportedMachines {
-			if regexp.MustCompile(val).MatchString(machine.Type) {
+			// The pattern are hardcoded, so this should not throw an error
+			if ok, _ := filepath.Match(val, machine.Type); ok {
 				match = true
 				break
 			}


Test patch:
diff --git a/pkg/virt-api/webhooks/validating-webhook/admitters/vmi-create-admitter_test.go b/pkg/virt-api/webhooks/validating-webhook/admitters/vmi-create-admitter_test.go
index bc35b3768c51..ae5b6849bcb3 100644
--- a/pkg/virt-api/webhooks/validating-webhook/admitters/vmi-create-admitter_test.go
+++ b/pkg/virt-api/webhooks/validating-webhook/admitters/vmi-create-admitter_test.go
@@ -495,16 +495,26 @@ var _ = Describe("Validating VMICreate Admitter", func() {
 			Entry("when architecture is s390x", "s390x", "s390-ccw-virtio"),
 		)
 
-		It("should reject invalid machine type", func() {
+		DescribeTable("should reject invalid machine type", func(arch string, machineType string) {
+			enableFeatureGate(featuregate.MultiArchitecture)
+
 			vmi := api.NewMinimalVMI("testvmi")
-			vmi.Spec.Domain.Machine = &v1.Machine{Type: "test"}
+			vmi.Spec.Architecture = arch
+			vmi.Spec.Domain.Machine = &v1.Machine{Type: machineType}
 
 			causes := ValidateVirtualMachineInstanceSpec(k8sfield.NewPath("fake"), &vmi.Spec, config)
 			Expect(causes).To(HaveLen(1))
 			Expect(string(causes[0].Type)).To(Equal("FieldValueInvalid"))
 			Expect(causes[0].Field).To(Equal("fake.domain.machine.type"))
-			Expect(causes[0].Message).To(ContainSubstring("fake.domain.machine.type is not supported: test (allowed values:"))
-		})
+			Expect(causes[0].Message).To(ContainSubstring(fmt.Sprintf("fake.domain.machine.type is not supported: %s (allowed values:", machineType)))
+		},
+			Entry("Simple wrong value", "amd64", "test"),
+			Entry("Wrong prefix amd64 q35", "amd64", "test-q35"),
+			Entry("Wrong prefix amd64 pc-q35", "amd64", "test-pc-q35"),
+			Entry("Wrong prefix arm64", "arm64", "test-virt"),
+			Entry("Wrong prefix ppc64le", "ppc64le", "test-pseries"),
+			Entry("Wrong prefix s390x", "s390x", "test-s390-ccw-virtio"),
+		)
 
 		It("should accept valid hostname", func() {
 			vmi := api.NewMinimalVMI("testvmi")