Skip to content

Commit

Permalink
Merge pull request #10284 from AlonaKaplan/binding_plugin_api
Browse files Browse the repository at this point in the history
Binding plugin API
  • Loading branch information
kubevirt-bot committed Aug 16, 2023
2 parents cb29e9f + ba7bbb9 commit 9c14b3e
Show file tree
Hide file tree
Showing 21 changed files with 545 additions and 12 deletions.
34 changes: 34 additions & 0 deletions api/openapi-spec/swagger.json
Expand Up @@ -17256,6 +17256,10 @@
"type": "integer",
"format": "int32"
},
"binding": {
"description": "Binding specifies the binding plugin that will be used to connect the interface to the guest. It provides an alternative to InterfaceBindingMethod. version: 1alphav1",
"$ref": "#/definitions/v1.PluginBinding"
},
"bootOrder": {
"description": "BootOrder is an integer value \u003e 0, used to determine ordering of boot devices. Lower values take precedence. Each interface or disk that has a boot order must have a unique value. Interfaces without a boot order are not tried.",
"type": "integer",
Expand Down Expand Up @@ -17318,6 +17322,15 @@
}
}
},
"v1.InterfaceBindingPlugin": {
"type": "object",
"properties": {
"sidecarImage": {
"description": "SidecarImage references a container image that runs in the virt-launcher pod. The sidecar handles (libvirt) domain configuration and optional services. version: 1alphav1",
"type": "string"
}
}
},
"v1.InterfaceBridge": {
"description": "InterfaceBridge connects to a given network via a linux bridge.",
"type": "object"
Expand Down Expand Up @@ -18178,6 +18191,13 @@
"description": "NetworkConfiguration holds network options",
"type": "object",
"properties": {
"binding": {
"type": "object",
"additionalProperties": {
"default": {},
"$ref": "#/definitions/v1.InterfaceBindingPlugin"
}
},
"defaultNetworkInterface": {
"type": "string"
},
Expand Down Expand Up @@ -18400,6 +18420,20 @@
}
}
},
"v1.PluginBinding": {
"description": "PluginBinding represents a binding implemented in a plugin.",
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"description": "Name references to the binding name as denined in the kubevirt CR. version: 1alphav1",
"type": "string",
"default": ""
}
}
},
"v1.PodNetwork": {
"description": "Represents the stock pod network interface.",
"type": "object",
Expand Down
22 changes: 22 additions & 0 deletions manifests/generated/kv-resource.yaml
Expand Up @@ -604,6 +604,17 @@ spec:
network:
description: NetworkConfiguration holds network options
properties:
binding:
additionalProperties:
properties:
sidecarImage:
description: 'SidecarImage references a container image
that runs in the virt-launcher pod. The sidecar handles
(libvirt) domain configuration and optional services.
version: 1alphav1'
type: string
type: object
type: object
defaultNetworkInterface:
type: string
permitBridgeInterfaceOnPodNetwork:
Expand Down Expand Up @@ -3603,6 +3614,17 @@ spec:
network:
description: NetworkConfiguration holds network options
properties:
binding:
additionalProperties:
properties:
sidecarImage:
description: 'SidecarImage references a container image
that runs in the virt-launcher pod. The sidecar handles
(libvirt) domain configuration and optional services.
version: 1alphav1'
type: string
type: object
type: object
defaultNetworkInterface:
type: string
permitBridgeInterfaceOnPodNetwork:
Expand Down
6 changes: 3 additions & 3 deletions pkg/instancetype/instancetype.go
Expand Up @@ -1222,8 +1222,8 @@ func applyDiskPreferences(preferenceSpec *instancetypev1beta1.VirtualMachinePref
}
}

func isInterfaceBindingUnset(interfaceBindingMethod virtv1.InterfaceBindingMethod) bool {
return reflect.ValueOf(interfaceBindingMethod).IsZero()
func isInterfaceBindingUnset(iface *virtv1.Interface) bool {
return reflect.ValueOf(iface.InterfaceBindingMethod).IsZero() && iface.Binding == nil
}

func isInterfaceOnPodNetwork(interfaceName string, vmiSpec *virtv1.VirtualMachineInstanceSpec) bool {
Expand All @@ -1241,7 +1241,7 @@ func applyInterfacePreferences(preferenceSpec *instancetypev1beta1.VirtualMachin
if preferenceSpec.Devices.PreferredInterfaceModel != "" && vmiIface.Model == "" {
vmiIface.Model = preferenceSpec.Devices.PreferredInterfaceModel
}
if preferenceSpec.Devices.PreferredInterfaceMasquerade != nil && isInterfaceBindingUnset(vmiIface.InterfaceBindingMethod) && isInterfaceOnPodNetwork(vmiIface.Name, vmiSpec) {
if preferenceSpec.Devices.PreferredInterfaceMasquerade != nil && isInterfaceBindingUnset(vmiIface) && isInterfaceOnPodNetwork(vmiIface.Name, vmiSpec) {
vmiIface.Masquerade = preferenceSpec.Devices.PreferredInterfaceMasquerade.DeepCopy()
}
}
Expand Down
1 change: 1 addition & 0 deletions pkg/network/setup/netpod.go
Expand Up @@ -173,6 +173,7 @@ func (n NetPod) composeDesiredSpec(currentStatus *nmstate.Status) (*nmstate.Spec
case iface.Macvtap != nil:
case iface.SRIOV != nil:
case iface.Slirp != nil:
case iface.Binding != nil:
default:
return nil, fmt.Errorf("undefined binding method: %v", iface)
}
Expand Down
8 changes: 4 additions & 4 deletions pkg/network/setup/network.go
Expand Up @@ -84,8 +84,8 @@ func (v VMNetworkConfigurator) getPhase1NICs(launcherPID *int, networks []v1.Net
return nil, fmt.Errorf("no iface matching with network %s", networks[i].Name)
}

// SR-IOV devices are not part of the phases
if iface.SRIOV != nil {
// Binding plugin and SR-IOV devices are not part of the phases
if iface.Binding != nil || iface.SRIOV != nil {
continue
}

Expand All @@ -107,8 +107,8 @@ func (v VMNetworkConfigurator) getPhase2NICs(domain *api.Domain, networks []v1.N
return nil, fmt.Errorf("no iface matching with network %s", networks[i].Name)
}

// SR-IOV devices are not part of the phases
if iface.SRIOV != nil {
// Binding plugin and SR-IOV devices are not part of the phases
if iface.Binding != nil || iface.SRIOV != nil {
continue
}

Expand Down
25 changes: 25 additions & 0 deletions pkg/virt-api/webhooks/validating-webhook/admitters/network.go
Expand Up @@ -58,3 +58,28 @@ func validateInterfaceStateValue(field *k8sfield.Path, spec *v1.VirtualMachineIn
}
return causes
}

func validateInterfaceBinding(field *k8sfield.Path, spec *v1.VirtualMachineInstanceSpec) []metav1.StatusCause {
var causes []metav1.StatusCause
for idx, iface := range spec.Domain.Devices.Interfaces {
if iface.Binding != nil {
if hasInterfaceBindingMethod(iface) {
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Message: fmt.Sprintf("logical %s interface cannot have both binding plugin and interface binding method", iface.Name),
Field: field.Child("domain", "devices", "interfaces").Index(idx).Child("binding").String(),
})
}
}
}
return causes
}

func hasInterfaceBindingMethod(iface v1.Interface) bool {
return iface.InterfaceBindingMethod.Bridge != nil ||
iface.InterfaceBindingMethod.Slirp != nil ||
iface.InterfaceBindingMethod.Masquerade != nil ||
iface.InterfaceBindingMethod.SRIOV != nil ||
iface.InterfaceBindingMethod.Macvtap != nil ||
iface.InterfaceBindingMethod.Passt != nil
}
33 changes: 33 additions & 0 deletions pkg/virt-api/webhooks/validating-webhook/admitters/network_test.go
Expand Up @@ -87,4 +87,37 @@ var _ = Describe("Validating VMI network spec", func() {
Field: "fake.domain.devices.interfaces[0].state",
}))
})

It("network interface has both binding plugin and interface binding method", func() {
vm := api.NewMinimalVMI("testvm")
vm.Spec.Domain.Devices.Interfaces = []v1.Interface{{
Name: "foo",
InterfaceBindingMethod: v1.InterfaceBindingMethod{Bridge: &v1.InterfaceBridge{}},
Binding: &v1.PluginBinding{Name: "boo"},
}}
Expect(validateInterfaceBinding(k8sfield.NewPath("fake"), &vm.Spec)).To(
ConsistOf(metav1.StatusCause{
Type: "FieldValueInvalid",
Message: "logical foo interface cannot have both binding plugin and interface binding method",
Field: "fake.domain.devices.interfaces[0].binding",
}))
})

It("network interface has only plugin binding", func() {
vm := api.NewMinimalVMI("testvm")
vm.Spec.Domain.Devices.Interfaces = []v1.Interface{{
Name: "foo",
Binding: &v1.PluginBinding{Name: "boo"},
}}
Expect(validateInterfaceBinding(k8sfield.NewPath("fake"), &vm.Spec)).To(BeEmpty())
})

It("network interface has only binding method", func() {
vm := api.NewMinimalVMI("testvm")
vm.Spec.Domain.Devices.Interfaces = []v1.Interface{{
Name: "foo",
InterfaceBindingMethod: v1.InterfaceBindingMethod{Bridge: &v1.InterfaceBridge{}},
}}
Expect(validateInterfaceBinding(k8sfield.NewPath("fake"), &vm.Spec)).To(BeEmpty())
})
})
Expand Up @@ -196,6 +196,7 @@ func ValidateVirtualMachineInstanceSpec(field *k8sfield.Path, spec *v1.VirtualMa

causes = append(causes, validateNetworksAssignedToInterfaces(field, spec, networkInterfaceMap)...)
causes = append(causes, validateInterfaceStateValue(field, spec)...)
causes = append(causes, validateInterfaceBinding(field, spec)...)

causes = append(causes, validateInputDevices(field, spec)...)
causes = append(causes, validateIOThreadsPolicy(field, spec)...)
Expand Down Expand Up @@ -323,6 +324,8 @@ func validateInterfaceNetworkBasics(field *k8sfield.Path, networkExists bool, id
causes = appendStatusCauseForPasstWithoutPodNetwork(field, causes, idx)
} else if iface.Passt != nil && numOfInterfaces > 1 {
causes = appendStatusCauseForPasstWithMultipleInterfaces(field, causes, idx)
} else if iface.Binding != nil && !config.NetworkBindingPlugingsEnabled() {
causes = appendStatusCauseForBindingPluginsFeatureGateNotEnabled(field, causes, idx)
}
return causes
}
Expand Down Expand Up @@ -621,6 +624,15 @@ func appendStatusCauseForPasstWithMultipleInterfaces(field *k8sfield.Path, cause
})
}

func appendStatusCauseForBindingPluginsFeatureGateNotEnabled(field *k8sfield.Path, causes []metav1.StatusCause, idx int) []metav1.StatusCause {
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Message: "Binding plugins feature gate is not enabled",
Field: field.Child("domain", "devices", "interfaces").Index(idx).Child("name").String(),
})
return causes
}

func validateInterfaceNameFormat(field *k8sfield.Path, iface v1.Interface, idx int) (causes []metav1.StatusCause) {
isValid := regexp.MustCompile(`^[A-Za-z0-9-_]+$`).MatchString
if !isValid(iface.Name) {
Expand Down
6 changes: 6 additions & 0 deletions pkg/virt-config/feature-gates.go
Expand Up @@ -72,6 +72,8 @@ const (
VMLiveUpdateFeaturesGate = "VMLiveUpdateFeatures"
// When BochsDisplayForEFIGuests is enabled, EFI guests will be started with Bochs display instead of VGA
BochsDisplayForEFIGuests = "BochsDisplayForEFIGuests"
// NetworkBindingPlugingsGate enables using a plugin to bind the pod and the VM network
NetworkBindingPlugingsGate = "NetworkBindingPlugins"
)

var deprecatedFeatureGates = [...]string{
Expand Down Expand Up @@ -239,3 +241,7 @@ func (config *ClusterConfig) VMLiveUpdateFeaturesEnabled() bool {
func (config *ClusterConfig) BochsDisplayForEFIGuestsEnabled() bool {
return config.isFeatureGateEnabled(BochsDisplayForEFIGuests)
}

func (config *ClusterConfig) NetworkBindingPlugingsEnabled() bool {
return config.isFeatureGateEnabled(NetworkBindingPlugingsGate)
}
3 changes: 3 additions & 0 deletions pkg/virt-controller/services/BUILD.bazel
Expand Up @@ -4,6 +4,7 @@ go_library(
name = "go_default_library",
srcs = [
"multus_annotations.go",
"net_binding.go",
"nodeselectorrenderer.go",
"rendercontainer.go",
"renderresources.go",
Expand Down Expand Up @@ -54,6 +55,7 @@ go_test(
name = "go_default_test",
srcs = [
"multus_annotations_test.go",
"net_binding_test.go",
"nodeselectorrenderer_test.go",
"rendercontainer_test.go",
"renderresources_test.go",
Expand All @@ -76,6 +78,7 @@ go_test(
"//staging/src/kubevirt.io/client-go/generated/network-attachment-definition-client/clientset/versioned/fake:go_default_library",
"//staging/src/kubevirt.io/client-go/kubecli:go_default_library",
"//staging/src/kubevirt.io/client-go/testutils:go_default_library",
"//tests/libvmi:go_default_library",
"//tools/vms-generator/utils:go_default_library",
"//vendor/github.com/golang/mock/gomock:go_default_library",
"//vendor/github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1:go_default_library",
Expand Down
53 changes: 53 additions & 0 deletions pkg/virt-controller/services/net_binding.go
@@ -0,0 +1,53 @@
/*
* This file is part of the KubeVirt project
*
* 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.
*
* Copyright 2023 Red Hat, Inc.
*
*/

package services

import (
"fmt"

v1 "kubevirt.io/api/core/v1"

"kubevirt.io/kubevirt/pkg/hooks"
)

func NetBindingPluginSidecarList(vmi *v1.VirtualMachineInstance, config *v1.KubeVirtConfiguration) (hooks.HookSidecarList, error) {
var pluginSidecars hooks.HookSidecarList
bindingByName := map[string]v1.InterfaceBindingPlugin{}
for _, iface := range vmi.Spec.Domain.Devices.Interfaces {
if iface.Binding != nil {
var exist bool
var pluginInfo v1.InterfaceBindingPlugin
if config.NetworkConfiguration != nil && config.NetworkConfiguration.Binding != nil {
pluginInfo, exist = config.NetworkConfiguration.Binding[iface.Binding.Name]
bindingByName[iface.Binding.Name] = pluginInfo
}

if !exist {
return nil, fmt.Errorf("couldn't find configuration for network bindining: %s", iface.Binding.Name)
}
}
}
for _, pluginInfo := range bindingByName {
if pluginInfo.SidecarImage != "" {
pluginSidecars = append(pluginSidecars, hooks.HookSidecar{Image: pluginInfo.SidecarImage})
}
}
return pluginSidecars, nil
}

0 comments on commit 9c14b3e

Please sign in to comment.