diff --git a/apis/v1alpha3/vspheremachine_conversion.go b/apis/v1alpha3/vspheremachine_conversion.go index 79a0f1d548..4d9c80bc45 100644 --- a/apis/v1alpha3/vspheremachine_conversion.go +++ b/apis/v1alpha3/vspheremachine_conversion.go @@ -40,6 +40,7 @@ func (src *VSphereMachine) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.TagIDs = restored.Spec.TagIDs dst.Spec.PowerOffMode = restored.Spec.PowerOffMode dst.Spec.GuestSoftPowerOffTimeout = restored.Spec.GuestSoftPowerOffTimeout + dst.Spec.NamingStrategy = restored.Spec.NamingStrategy for i := range dst.Spec.Network.Devices { dst.Spec.Network.Devices[i].AddressesFromPools = restored.Spec.Network.Devices[i].AddressesFromPools dst.Spec.Network.Devices[i].DHCP4Overrides = restored.Spec.Network.Devices[i].DHCP4Overrides diff --git a/apis/v1alpha3/vspheremachinetemplate_conversion.go b/apis/v1alpha3/vspheremachinetemplate_conversion.go index 45f090e2c1..71149b05d1 100644 --- a/apis/v1alpha3/vspheremachinetemplate_conversion.go +++ b/apis/v1alpha3/vspheremachinetemplate_conversion.go @@ -43,6 +43,7 @@ func (src *VSphereMachineTemplate) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Template.Spec.AdditionalDisksGiB = restored.Spec.Template.Spec.AdditionalDisksGiB dst.Spec.Template.Spec.PowerOffMode = restored.Spec.Template.Spec.PowerOffMode dst.Spec.Template.Spec.GuestSoftPowerOffTimeout = restored.Spec.Template.Spec.GuestSoftPowerOffTimeout + dst.Spec.Template.Spec.NamingStrategy = restored.Spec.Template.Spec.NamingStrategy for i := range dst.Spec.Template.Spec.Network.Devices { dst.Spec.Template.Spec.Network.Devices[i].AddressesFromPools = restored.Spec.Template.Spec.Network.Devices[i].AddressesFromPools dst.Spec.Template.Spec.Network.Devices[i].DHCP4Overrides = restored.Spec.Template.Spec.Network.Devices[i].DHCP4Overrides diff --git a/apis/v1alpha3/zz_generated.conversion.go b/apis/v1alpha3/zz_generated.conversion.go index 2af41228a7..ea15e3f54c 100644 --- a/apis/v1alpha3/zz_generated.conversion.go +++ b/apis/v1alpha3/zz_generated.conversion.go @@ -1410,6 +1410,7 @@ func autoConvert_v1beta1_VSphereMachineSpec_To_v1alpha3_VSphereMachineSpec(in *v out.FailureDomain = (*string)(unsafe.Pointer(in.FailureDomain)) // WARNING: in.PowerOffMode requires manual conversion: does not exist in peer-type // WARNING: in.GuestSoftPowerOffTimeout requires manual conversion: does not exist in peer-type + // WARNING: in.NamingStrategy requires manual conversion: does not exist in peer-type return nil } diff --git a/apis/v1alpha4/vspheremachine_conversion.go b/apis/v1alpha4/vspheremachine_conversion.go index 22a85aaa5a..cca41dd7a9 100644 --- a/apis/v1alpha4/vspheremachine_conversion.go +++ b/apis/v1alpha4/vspheremachine_conversion.go @@ -40,6 +40,7 @@ func (src *VSphereMachine) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.TagIDs = restored.Spec.TagIDs dst.Spec.PowerOffMode = restored.Spec.PowerOffMode dst.Spec.GuestSoftPowerOffTimeout = restored.Spec.GuestSoftPowerOffTimeout + dst.Spec.NamingStrategy = restored.Spec.NamingStrategy for i := range dst.Spec.Network.Devices { dst.Spec.Network.Devices[i].AddressesFromPools = restored.Spec.Network.Devices[i].AddressesFromPools dst.Spec.Network.Devices[i].DHCP4Overrides = restored.Spec.Network.Devices[i].DHCP4Overrides diff --git a/apis/v1alpha4/vspheremachinetemplate_conversion.go b/apis/v1alpha4/vspheremachinetemplate_conversion.go index c0719758e8..8be6829cbc 100644 --- a/apis/v1alpha4/vspheremachinetemplate_conversion.go +++ b/apis/v1alpha4/vspheremachinetemplate_conversion.go @@ -43,6 +43,7 @@ func (src *VSphereMachineTemplate) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Template.Spec.AdditionalDisksGiB = restored.Spec.Template.Spec.AdditionalDisksGiB dst.Spec.Template.Spec.PowerOffMode = restored.Spec.Template.Spec.PowerOffMode dst.Spec.Template.Spec.GuestSoftPowerOffTimeout = restored.Spec.Template.Spec.GuestSoftPowerOffTimeout + dst.Spec.Template.Spec.NamingStrategy = restored.Spec.Template.Spec.NamingStrategy for i := range dst.Spec.Template.Spec.Network.Devices { dst.Spec.Template.Spec.Network.Devices[i].AddressesFromPools = restored.Spec.Template.Spec.Network.Devices[i].AddressesFromPools dst.Spec.Template.Spec.Network.Devices[i].DHCP4Overrides = restored.Spec.Template.Spec.Network.Devices[i].DHCP4Overrides diff --git a/apis/v1alpha4/zz_generated.conversion.go b/apis/v1alpha4/zz_generated.conversion.go index 25993ebacf..418bca0c78 100644 --- a/apis/v1alpha4/zz_generated.conversion.go +++ b/apis/v1alpha4/zz_generated.conversion.go @@ -1564,6 +1564,7 @@ func autoConvert_v1beta1_VSphereMachineSpec_To_v1alpha4_VSphereMachineSpec(in *v out.FailureDomain = (*string)(unsafe.Pointer(in.FailureDomain)) // WARNING: in.PowerOffMode requires manual conversion: does not exist in peer-type // WARNING: in.GuestSoftPowerOffTimeout requires manual conversion: does not exist in peer-type + // WARNING: in.NamingStrategy requires manual conversion: does not exist in peer-type return nil } diff --git a/apis/v1beta1/vspheremachine_types.go b/apis/v1beta1/vspheremachine_types.go index 40c7682ce5..e772286e44 100644 --- a/apis/v1beta1/vspheremachine_types.go +++ b/apis/v1beta1/vspheremachine_types.go @@ -68,6 +68,30 @@ type VSphereMachineSpec struct { // // +optional GuestSoftPowerOffTimeout *metav1.Duration `json:"guestSoftPowerOffTimeout,omitempty"` + + // NamingStrategy allows configuring the naming strategy used when calculating the name of the VirtualMachine. + // +optional + NamingStrategy *VirtualMachineNamingStrategy `json:"namingStrategy,omitempty"` +} + +// VirtualMachineNamingStrategy defines the naming strategy for the VirtualMachines. +type VirtualMachineNamingStrategy struct { + // Template defines the template to use for generating the name of the VirtualMachine object. + // If not defined, it will fall back to `{{ .machine.name }}`. + // The templating has the following data available: + // * `.machine.name`: The name of the Machine object. + // The templating also has the following funcs available: + // * `trimSuffix`: same as strings.TrimSuffix + // * `trunc`: truncates a string, e.g. `trunc 2 "hello"` or `trunc -2 "hello"` + // Notes: + // * While the template offers some flexibility, we would like the name to link to the Machine name + // to ensure better user experience when troubleshooting + // * Generated names must be valid Kubernetes names as they are used to create a VirtualMachine object + // and usually also as the name of the Node object. + // * Names are automatically truncated at 63 characters. Please note that this can lead to name conflicts, + // so we highly recommend to use a template which leads to a name shorter than 63 characters. + // +optional + Template *string `json:"template,omitempty"` } // VSphereMachineStatus defines the observed state of VSphereMachine. diff --git a/apis/v1beta1/zz_generated.deepcopy.go b/apis/v1beta1/zz_generated.deepcopy.go index 6398eb52e7..8cc6469b5c 100644 --- a/apis/v1beta1/zz_generated.deepcopy.go +++ b/apis/v1beta1/zz_generated.deepcopy.go @@ -1048,6 +1048,11 @@ func (in *VSphereMachineSpec) DeepCopyInto(out *VSphereMachineSpec) { *out = new(metav1.Duration) **out = **in } + if in.NamingStrategy != nil { + in, out := &in.NamingStrategy, &out.NamingStrategy + *out = new(VirtualMachineNamingStrategy) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VSphereMachineSpec. @@ -1391,3 +1396,23 @@ func (in *VirtualMachineCloneSpec) DeepCopy() *VirtualMachineCloneSpec { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualMachineNamingStrategy) DeepCopyInto(out *VirtualMachineNamingStrategy) { + *out = *in + if in.Template != nil { + in, out := &in.Template, &out.Template + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualMachineNamingStrategy. +func (in *VirtualMachineNamingStrategy) DeepCopy() *VirtualMachineNamingStrategy { + if in == nil { + return nil + } + out := new(VirtualMachineNamingStrategy) + in.DeepCopyInto(out) + return out +} diff --git a/config/default/crd/bases/infrastructure.cluster.x-k8s.io_vspheremachines.yaml b/config/default/crd/bases/infrastructure.cluster.x-k8s.io_vspheremachines.yaml index b39cad66f0..5b48aa859d 100644 --- a/config/default/crd/bases/infrastructure.cluster.x-k8s.io_vspheremachines.yaml +++ b/config/default/crd/bases/infrastructure.cluster.x-k8s.io_vspheremachines.yaml @@ -1026,6 +1026,28 @@ spec: virtual machine is cloned. format: int64 type: integer + namingStrategy: + description: NamingStrategy allows configuring the naming strategy + used when calculating the name of the VirtualMachine. + properties: + template: + description: |- + Template defines the template to use for generating the name of the VirtualMachine object. + If not defined, it will fall back to `{{ .machine.name }}`. + The templating has the following data available: + * `.machine.name`: The name of the Machine object. + The templating also has the following funcs available: + * `trimSuffix`: same as strings.TrimSuffix + * `trunc`: truncates a string, e.g. `trunc 2 "hello"` or `trunc -2 "hello"` + Notes: + * While the template offers some flexibility, we would like the name to link to the Machine name + to ensure better user experience when troubleshooting + * Generated names must be valid Kubernetes names as they are used to create a VirtualMachine object + and usually also as the name of the Node object. + * Names are automatically truncated at 63 characters. Please note that this can lead to name conflicts, + so we highly recommend to use a template which leads to a name shorter than 63 characters. + type: string + type: object network: description: Network is the network configuration for this machine's VM. diff --git a/config/default/crd/bases/infrastructure.cluster.x-k8s.io_vspheremachinetemplates.yaml b/config/default/crd/bases/infrastructure.cluster.x-k8s.io_vspheremachinetemplates.yaml index 33e9644a62..16401bad0e 100644 --- a/config/default/crd/bases/infrastructure.cluster.x-k8s.io_vspheremachinetemplates.yaml +++ b/config/default/crd/bases/infrastructure.cluster.x-k8s.io_vspheremachinetemplates.yaml @@ -896,6 +896,28 @@ spec: virtual machine is cloned. format: int64 type: integer + namingStrategy: + description: NamingStrategy allows configuring the naming + strategy used when calculating the name of the VirtualMachine. + properties: + template: + description: |- + Template defines the template to use for generating the name of the VirtualMachine object. + If not defined, it will fall back to `{{ .machine.name }}`. + The templating has the following data available: + * `.machine.name`: The name of the Machine object. + The templating also has the following funcs available: + * `trimSuffix`: same as strings.TrimSuffix + * `trunc`: truncates a string, e.g. `trunc 2 "hello"` or `trunc -2 "hello"` + Notes: + * While the template offers some flexibility, we would like the name to link to the Machine name + to ensure better user experience when troubleshooting + * Generated names must be valid Kubernetes names as they are used to create a VirtualMachine object + and usually also as the name of the Node object. + * Names are automatically truncated at 63 characters. Please note that this can lead to name conflicts, + so we highly recommend to use a template which leads to a name shorter than 63 characters. + type: string + type: object network: description: Network is the network configuration for this machine's VM. diff --git a/pkg/services/vimmachine.go b/pkg/services/vimmachine.go index 04268eaf6d..5b7830bc9f 100644 --- a/pkg/services/vimmachine.go +++ b/pkg/services/vimmachine.go @@ -18,9 +18,11 @@ limitations under the License. package services import ( + "bytes" "context" "encoding/json" "strings" + "text/template" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" @@ -197,10 +199,15 @@ func (v *VimMachineService) GetHostInfo(ctx context.Context, machineCtx capvcont return "", errors.New("received unexpected VIMMachineContext type") } + name, err := generateVMObjectName(vimMachineCtx, vimMachineCtx.Machine.Name) + if err != nil { + return "", err + } + vsphereVM := &infrav1.VSphereVM{} if err := v.Client.Get(ctx, client.ObjectKey{ Namespace: vimMachineCtx.VSphereMachine.Namespace, - Name: generateVMObjectName(vimMachineCtx, vimMachineCtx.Machine.Name), + Name: name, }, vsphereVM); err != nil { return "", err } @@ -215,9 +222,14 @@ func (v *VimMachineService) GetHostInfo(ctx context.Context, machineCtx capvcont func (v *VimMachineService) findVSphereVM(ctx context.Context, vimMachineCtx *capvcontext.VIMMachineContext) (*infrav1.VSphereVM, error) { // Get ready to find the associated VSphereVM resource. vm := &infrav1.VSphereVM{} + name, err := generateVMObjectName(vimMachineCtx, vimMachineCtx.Machine.Name) + if err != nil { + return nil, err + } + vmKey := types.NamespacedName{ Namespace: vimMachineCtx.VSphereMachine.Namespace, - Name: generateVMObjectName(vimMachineCtx, vimMachineCtx.Machine.Name), + Name: name, } // Attempt to find the associated VSphereVM resource. if err := v.Client.Get(ctx, vmKey, vm); err != nil { @@ -301,10 +313,15 @@ func (v *VimMachineService) reconcileNetwork(ctx context.Context, vimMachineCtx func (v *VimMachineService) createOrPatchVSphereVM(ctx context.Context, vimMachineCtx *capvcontext.VIMMachineContext, vsphereVM *infrav1.VSphereVM) (*infrav1.VSphereVM, error) { log := ctrl.LoggerFrom(ctx) // Create or update the VSphereVM resource. + name, err := generateVMObjectName(vimMachineCtx, vimMachineCtx.Machine.Name) + if err != nil { + return nil, err + } + vm := &infrav1.VSphereVM{ ObjectMeta: metav1.ObjectMeta{ Namespace: vimMachineCtx.VSphereMachine.Namespace, - Name: generateVMObjectName(vimMachineCtx, vimMachineCtx.Machine.Name), + Name: name, }, } mutateFn := func() (err error) { @@ -393,12 +410,72 @@ func (v *VimMachineService) createOrPatchVSphereVM(ctx context.Context, vimMachi // generateVMObjectName returns a new VM object name in specific cases, otherwise return the same // passed in the parameter. -func generateVMObjectName(vimMachineCtx *capvcontext.VIMMachineContext, machineName string) string { +func generateVMObjectName(vimMachineCtx *capvcontext.VIMMachineContext, machineName string) (string, error) { + name, err := GenerateVirtualMachineName(machineName, vimMachineCtx.VSphereMachine.Spec.NamingStrategy) + if err != nil { + return "", err + } // Windows VM names must have 15 characters length at max. if vimMachineCtx.VSphereMachine.Spec.OS == infrav1.Windows && len(machineName) > 15 { - return strings.TrimSuffix(machineName[0:9], "-") + "-" + machineName[len(machineName)-5:] + return strings.TrimSuffix(name[0:9], "-") + "-" + name[len(name)-5:], nil } - return machineName + return name, nil +} + +const ( + maxNameLength = 63 +) + +// Note: Inlining these functions from sprig to avoid introducing a dependency. +var nameTemplateFuncs = map[string]any{ + "trimSuffix": func(a, b string) string { return strings.TrimSuffix(b, a) }, + "trunc": func(c int, s string) string { + if c < 0 && len(s)+c > 0 { + return s[len(s)+c:] + } + if c >= 0 && len(s) > c { + return s[:c] + } + return s + }, +} + +var nameTpl = template.New("name generator").Funcs(nameTemplateFuncs).Option("missingkey=error") + +// GenerateVirtualMachineName generates the name of a VirtualMachine based on the naming strategy. +func GenerateVirtualMachineName(machineName string, namingStrategy *infrav1.VirtualMachineNamingStrategy) (string, error) { + // Per default the name of the VirtualMachine should be equal to the Machine name (this is the same as "{{ .machine.name }}") + if namingStrategy == nil || namingStrategy.Template == nil { + // Note: No need to trim to max length in this case as valid Machine names will also be valid VirtualMachine names. + return machineName, nil + } + + nameTemplate := *namingStrategy.Template + data := map[string]interface{}{ + "machine": map[string]interface{}{ + "name": machineName, + }, + } + + tpl, err := nameTpl.Parse(nameTemplate) + if err != nil { + return "", errors.Wrapf(err, "failed to generate name for VirtualMachine: failed to parse namingStrategy.template %q", nameTemplate) + } + + var buf bytes.Buffer + if err := tpl.Execute(&buf, data); err != nil { + return "", errors.Wrap(err, "failed to generate name for VirtualMachine") + } + + name := buf.String() + + // If the name exceeds the maxNameLength, trim to maxNameLength. + // Note: we're not adding a random suffix as the name has to be deterministic. + if len(name) > maxNameLength { + name = name[:maxNameLength] + } + + return name, nil } // generateOverrideFunc returns a function which can override the values in the VSphereVM Spec