sigs.k8s.io/cluster-api-provider-azure@v1.17.0/azure/defaults.go (about)

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package azure
    18  
    19  import (
    20  	"fmt"
    21  	"net/http"
    22  	"regexp"
    23  	"strings"
    24  
    25  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
    26  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
    27  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
    28  	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5"
    29  	"sigs.k8s.io/cluster-api-provider-azure/util/tele"
    30  	"sigs.k8s.io/cluster-api-provider-azure/version"
    31  )
    32  
    33  const (
    34  	// DefaultUserName is the default username for a created VM.
    35  	DefaultUserName = "capi"
    36  	// DefaultAKSUserName is the default username for a created AKS VM.
    37  	DefaultAKSUserName = "azureuser"
    38  	// PublicCloudName is the name of the Azure public cloud.
    39  	PublicCloudName = "AzurePublicCloud"
    40  	// ChinaCloudName is the name of the Azure China cloud.
    41  	ChinaCloudName = "AzureChinaCloud"
    42  	// USGovernmentCloudName is the name of the Azure US Government cloud.
    43  	USGovernmentCloudName = "AzureUSGovernmentCloud"
    44  )
    45  
    46  const (
    47  	// DefaultImageOfferID is the default Azure Marketplace offer ID.
    48  	DefaultImageOfferID = "capi"
    49  	// DefaultWindowsImageOfferID is the default Azure Marketplace offer ID for Windows.
    50  	DefaultWindowsImageOfferID = "capi-windows"
    51  	// DefaultImagePublisherID is the default Azure Marketplace publisher ID.
    52  	DefaultImagePublisherID = "cncf-upstream"
    53  	// LatestVersion is the image version latest.
    54  	LatestVersion = "latest"
    55  )
    56  
    57  const (
    58  	// LinuxOS is Linux OS value for OSDisk.OSType.
    59  	LinuxOS = "Linux"
    60  	// WindowsOS is Windows OS value for OSDisk.OSType.
    61  	WindowsOS = "Windows"
    62  )
    63  
    64  const (
    65  	// BootstrappingExtensionLinux is the name of the Linux CAPZ bootstrapping VM extension.
    66  	BootstrappingExtensionLinux = "CAPZ.Linux.Bootstrapping"
    67  	// BootstrappingExtensionWindows is the name of the Windows CAPZ bootstrapping VM extension.
    68  	BootstrappingExtensionWindows = "CAPZ.Windows.Bootstrapping"
    69  )
    70  
    71  const (
    72  	// DefaultWindowsOsAndVersion is the default Windows Server version to use when
    73  	// generating default images for Windows nodes.
    74  	DefaultWindowsOsAndVersion = "windows-2019"
    75  )
    76  
    77  const (
    78  	// Global is the Azure global location value.
    79  	Global = "global"
    80  )
    81  
    82  const (
    83  	// PrivateAPIServerHostname will be used as the api server hostname for private clusters.
    84  	PrivateAPIServerHostname = "apiserver"
    85  )
    86  
    87  const (
    88  	// ControlPlaneNodeGroup will be used to create availability set for control plane machines.
    89  	ControlPlaneNodeGroup = "control-plane"
    90  )
    91  
    92  const (
    93  	// bootstrapExtensionRetries is the number of retries in the BootstrapExtensionCommand.
    94  	// NOTE: the overall timeout will be number of retries * retry sleep, in this case 60 * 5s = 300s.
    95  	bootstrapExtensionRetries = 60
    96  	// bootstrapExtensionSleep is the duration in seconds to sleep before each retry in the BootstrapExtensionCommand.
    97  	bootstrapExtensionSleep = 5
    98  	// bootstrapSentinelFile is the file written by bootstrap provider on machines to indicate successful bootstrapping,
    99  	// as defined by the Cluster API Bootstrap Provider contract (https://cluster-api.sigs.k8s.io/developer/providers/bootstrap.html).
   100  	bootstrapSentinelFile = "/run/cluster-api/bootstrap-success.complete"
   101  )
   102  
   103  const (
   104  	// CustomHeaderPrefix is the prefix of annotations that enable additional cluster / node pool features.
   105  	// Whatever follows the prefix will be passed as a header to cluster/node pool creation/update requests.
   106  	// E.g. add `"infrastructure.cluster.x-k8s.io/custom-header-UseGPUDedicatedVHD": "true"` annotation to
   107  	// AzureManagedMachinePool CR to enable creating GPU nodes by the node pool.
   108  	CustomHeaderPrefix = "infrastructure.cluster.x-k8s.io/custom-header-"
   109  )
   110  
   111  var (
   112  	// LinuxBootstrapExtensionCommand is the command the VM bootstrap extension will execute to verify Linux nodes bootstrap completes successfully.
   113  	LinuxBootstrapExtensionCommand = fmt.Sprintf("for i in $(seq 1 %d); do test -f %s && break; if [ $i -eq %d ]; then exit 1; else sleep %d; fi; done", bootstrapExtensionRetries, bootstrapSentinelFile, bootstrapExtensionRetries, bootstrapExtensionSleep)
   114  	// WindowsBootstrapExtensionCommand is the command the VM bootstrap extension will execute to verify Windows nodes bootstrap completes successfully.
   115  	WindowsBootstrapExtensionCommand = fmt.Sprintf("powershell.exe -Command \"for ($i = 0; $i -lt %d; $i++) {if (Test-Path '%s') {exit 0} else {Start-Sleep -Seconds %d}} exit -2\"",
   116  		bootstrapExtensionRetries, bootstrapSentinelFile, bootstrapExtensionSleep)
   117  )
   118  
   119  // GenerateBackendAddressPoolName generates a load balancer backend address pool name.
   120  func GenerateBackendAddressPoolName(lbName string) string {
   121  	return fmt.Sprintf("%s-%s", lbName, "backendPool")
   122  }
   123  
   124  // GenerateOutboundBackendAddressPoolName generates a load balancer outbound backend address pool name.
   125  func GenerateOutboundBackendAddressPoolName(lbName string) string {
   126  	return fmt.Sprintf("%s-%s", lbName, "outboundBackendPool")
   127  }
   128  
   129  // GenerateFrontendIPConfigName generates a load balancer frontend IP config name.
   130  func GenerateFrontendIPConfigName(lbName string) string {
   131  	return fmt.Sprintf("%s-%s", lbName, "frontEnd")
   132  }
   133  
   134  // GenerateNodeOutboundIPName generates a public IP name, based on the cluster name.
   135  func GenerateNodeOutboundIPName(clusterName string) string {
   136  	return fmt.Sprintf("pip-%s-node-outbound", clusterName)
   137  }
   138  
   139  // GenerateNodePublicIPName generates a node public IP name, based on the machine name.
   140  func GenerateNodePublicIPName(machineName string) string {
   141  	return fmt.Sprintf("pip-%s", machineName)
   142  }
   143  
   144  // GenerateControlPlaneOutboundLBName generates the name of the control plane outbound LB.
   145  func GenerateControlPlaneOutboundLBName(clusterName string) string {
   146  	return fmt.Sprintf("%s-outbound-lb", clusterName)
   147  }
   148  
   149  // GenerateControlPlaneOutboundIPName generates a public IP name, based on the cluster name.
   150  func GenerateControlPlaneOutboundIPName(clusterName string) string {
   151  	return fmt.Sprintf("pip-%s-controlplane-outbound", clusterName)
   152  }
   153  
   154  // GeneratePrivateDNSZoneName generates the name of a private DNS zone based on the cluster name.
   155  func GeneratePrivateDNSZoneName(clusterName string) string {
   156  	return fmt.Sprintf("%s.capz.io", clusterName)
   157  }
   158  
   159  // GeneratePrivateFQDN generates the FQDN for a private API Server based on the private DNS zone name.
   160  func GeneratePrivateFQDN(zoneName string) string {
   161  	return fmt.Sprintf("%s.%s", PrivateAPIServerHostname, zoneName)
   162  }
   163  
   164  // GenerateVNetLinkName generates the name of a virtual network link name based on the vnet name.
   165  func GenerateVNetLinkName(vnetName string) string {
   166  	return fmt.Sprintf("%s-link", vnetName)
   167  }
   168  
   169  // GenerateNICName generates the name of a network interface based on the name of a VM.
   170  func GenerateNICName(machineName string, multiNIC bool, index int) string {
   171  	if multiNIC {
   172  		return fmt.Sprintf("%s-nic-%d", machineName, index)
   173  	}
   174  	return fmt.Sprintf("%s-nic", machineName)
   175  }
   176  
   177  // GeneratePublicNICName generates the name of a public network interface based on the name of a VM.
   178  func GeneratePublicNICName(machineName string) string {
   179  	return fmt.Sprintf("%s-public-nic", machineName)
   180  }
   181  
   182  // GenerateOSDiskName generates the name of an OS disk based on the name of a VM.
   183  func GenerateOSDiskName(machineName string) string {
   184  	return fmt.Sprintf("%s_OSDisk", machineName)
   185  }
   186  
   187  // GenerateDataDiskName generates the name of a data disk based on the name of a VM.
   188  func GenerateDataDiskName(machineName, nameSuffix string) string {
   189  	return fmt.Sprintf("%s_%s", machineName, nameSuffix)
   190  }
   191  
   192  // GenerateVnetPeeringName generates the name for a peering between two vnets.
   193  func GenerateVnetPeeringName(sourceVnetName string, remoteVnetName string) string {
   194  	return fmt.Sprintf("%s-To-%s", sourceVnetName, remoteVnetName)
   195  }
   196  
   197  // GenerateAvailabilitySetName generates the name of a availability set based on the cluster name and the node group.
   198  // node group identifies the set of nodes that belong to this availability set:
   199  // For control plane nodes, this will be `control-plane`.
   200  // For worker nodes, this will be the machine deployment name.
   201  func GenerateAvailabilitySetName(clusterName, nodeGroup string) string {
   202  	return fmt.Sprintf("%s_%s-as", clusterName, nodeGroup)
   203  }
   204  
   205  // WithIndex appends the index as suffix to a generated name.
   206  func WithIndex(name string, n int) string {
   207  	return fmt.Sprintf("%s-%d", name, n)
   208  }
   209  
   210  // ResourceGroupID returns the azure resource ID for a given resource group.
   211  func ResourceGroupID(subscriptionID, resourceGroup string) string {
   212  	return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s", subscriptionID, resourceGroup)
   213  }
   214  
   215  // VMID returns the azure resource ID for a given VM.
   216  func VMID(subscriptionID, resourceGroup, vmName string) string {
   217  	return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/virtualMachines/%s", subscriptionID, resourceGroup, vmName)
   218  }
   219  
   220  // VMSSID returns the azure resource ID for a given VMSS.
   221  func VMSSID(subscriptionID, resourceGroup, vmssName string) string {
   222  	return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/virtualMachineScaleSets/%s", subscriptionID, resourceGroup, vmssName)
   223  }
   224  
   225  // VNetID returns the azure resource ID for a given VNet.
   226  func VNetID(subscriptionID, resourceGroup, vnetName string) string {
   227  	return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/virtualNetworks/%s", subscriptionID, resourceGroup, vnetName)
   228  }
   229  
   230  // SubnetID returns the azure resource ID for a given subnet.
   231  func SubnetID(subscriptionID, resourceGroup, vnetName, subnetName string) string {
   232  	return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/virtualNetworks/%s/subnets/%s", subscriptionID, resourceGroup, vnetName, subnetName)
   233  }
   234  
   235  // PublicIPID returns the azure resource ID for a given public IP.
   236  func PublicIPID(subscriptionID, resourceGroup, ipName string) string {
   237  	return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/publicIPAddresses/%s", subscriptionID, resourceGroup, ipName)
   238  }
   239  
   240  // PublicIPPrefixID returns the azure resource ID for a given public IP prefix.
   241  func PublicIPPrefixID(subscriptionID, resourceGroup, ipName string) string {
   242  	return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/publicipprefixes/%s", subscriptionID, resourceGroup, ipName)
   243  }
   244  
   245  // RouteTableID returns the azure resource ID for a given route table.
   246  func RouteTableID(subscriptionID, resourceGroup, routeTableName string) string {
   247  	return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/routeTables/%s", subscriptionID, resourceGroup, routeTableName)
   248  }
   249  
   250  // SecurityGroupID returns the azure resource ID for a given security group.
   251  func SecurityGroupID(subscriptionID, resourceGroup, nsgName string) string {
   252  	return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/networkSecurityGroups/%s", subscriptionID, resourceGroup, nsgName)
   253  }
   254  
   255  // NatGatewayID returns the azure resource ID for a given NAT gateway.
   256  func NatGatewayID(subscriptionID, resourceGroup, natgatewayName string) string {
   257  	return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/natGateways/%s", subscriptionID, resourceGroup, natgatewayName)
   258  }
   259  
   260  // NetworkInterfaceID returns the azure resource ID for a given network interface.
   261  func NetworkInterfaceID(subscriptionID, resourceGroup, nicName string) string {
   262  	return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/networkInterfaces/%s", subscriptionID, resourceGroup, nicName)
   263  }
   264  
   265  // FrontendIPConfigID returns the azure resource ID for a given frontend IP config.
   266  func FrontendIPConfigID(subscriptionID, resourceGroup, loadBalancerName, configName string) string {
   267  	return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/loadBalancers/%s/frontendIPConfigurations/%s", subscriptionID, resourceGroup, loadBalancerName, configName)
   268  }
   269  
   270  // AddressPoolID returns the azure resource ID for a given backend address pool.
   271  func AddressPoolID(subscriptionID, resourceGroup, loadBalancerName, backendPoolName string) string {
   272  	return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/loadBalancers/%s/backendAddressPools/%s", subscriptionID, resourceGroup, loadBalancerName, backendPoolName)
   273  }
   274  
   275  // ProbeID returns the azure resource ID for a given probe.
   276  func ProbeID(subscriptionID, resourceGroup, loadBalancerName, probeName string) string {
   277  	return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/loadBalancers/%s/probes/%s", subscriptionID, resourceGroup, loadBalancerName, probeName)
   278  }
   279  
   280  // NATRuleID returns the azure resource ID for a inbound NAT rule.
   281  func NATRuleID(subscriptionID, resourceGroup, loadBalancerName, natRuleName string) string {
   282  	return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/loadBalancers/%s/inboundNatRules/%s", subscriptionID, resourceGroup, loadBalancerName, natRuleName)
   283  }
   284  
   285  // AvailabilitySetID returns the azure resource ID for a given availability set.
   286  func AvailabilitySetID(subscriptionID, resourceGroup, availabilitySetName string) string {
   287  	return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/availabilitySets/%s", subscriptionID, resourceGroup, availabilitySetName)
   288  }
   289  
   290  // PrivateDNSZoneID returns the azure resource ID for a given private DNS zone.
   291  func PrivateDNSZoneID(subscriptionID, resourceGroup, privateDNSZoneName string) string {
   292  	return fmt.Sprintf("subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/privateDnsZones/%s", subscriptionID, resourceGroup, privateDNSZoneName)
   293  }
   294  
   295  // VirtualNetworkLinkID returns the azure resource ID for a given virtual network link.
   296  func VirtualNetworkLinkID(subscriptionID, resourceGroup, privateDNSZoneName, virtualNetworkLinkName string) string {
   297  	return fmt.Sprintf("subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/privateDnsZones/%s/virtualNetworkLinks/%s", subscriptionID, resourceGroup, privateDNSZoneName, virtualNetworkLinkName)
   298  }
   299  
   300  // ManagedClusterID returns the azure resource ID for a given managed cluster.
   301  func ManagedClusterID(subscriptionID, resourceGroup, managedClusterName string) string {
   302  	return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.ContainerService/managedClusters/%s", subscriptionID, resourceGroup, managedClusterName)
   303  }
   304  
   305  // FleetID returns the azure resource ID for a given fleet manager.
   306  func FleetID(subscriptionID, resourceGroup, fleetName string) string {
   307  	return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.ContainerService/fleets/%s", subscriptionID, resourceGroup, fleetName)
   308  }
   309  
   310  // GetBootstrappingVMExtension returns the CAPZ Bootstrapping VM extension.
   311  // The CAPZ Bootstrapping extension is a simple clone of https://github.com/Azure/custom-script-extension-linux for Linux or
   312  // https://learn.microsoft.com/azure/virtual-machines/extensions/custom-script-windows for Windows.
   313  // This extension allows running arbitrary scripts on the VM.
   314  // Its role is to detect and report Kubernetes bootstrap failure or success.
   315  func GetBootstrappingVMExtension(osType string, cloud string, vmName string, cpuArchitectureType string) *ExtensionSpec {
   316  	// currently, the bootstrap extension is only available in AzurePublicCloud.
   317  	if osType == LinuxOS && cloud == PublicCloudName {
   318  		// The command checks for the existence of the bootstrapSentinelFile on the machine, with retries and sleep between retries.
   319  		// We set the version to 1.1 (will target 1.1.1) for arm64 machines and 1.0 for x64. This is due to a known issue with newer versions of
   320  		// Go on Ubuntu 20.04. The issue is being tracked here: https://github.com/golang/go/issues/58550
   321  		// TODO: Remove this once the issue is fixed, or when Ubuntu 20.04 is no longer supported.
   322  		// We are using 1.1 instead of 1.1.1 for Arm64 as AzureAPI do not allow us to specify the full version.
   323  		extensionVersion := "1.0"
   324  		if cpuArchitectureType == string(armcompute.ArchitectureTypesArm64) {
   325  			extensionVersion = "1.1"
   326  		}
   327  		return &ExtensionSpec{
   328  			Name:      BootstrappingExtensionLinux,
   329  			VMName:    vmName,
   330  			Publisher: "Microsoft.Azure.ContainerUpstream",
   331  			Version:   extensionVersion,
   332  			ProtectedSettings: map[string]string{
   333  				"commandToExecute": LinuxBootstrapExtensionCommand,
   334  			},
   335  		}
   336  	} else if osType == WindowsOS && cloud == PublicCloudName {
   337  		// This command for the existence of the bootstrapSentinelFile on the machine, with retries and sleep between reties.
   338  		// If the file is not present after the retries are exhausted the extension fails with return code '-2' - ERROR_FILE_NOT_FOUND.
   339  		return &ExtensionSpec{
   340  			Name:      BootstrappingExtensionWindows,
   341  			VMName:    vmName,
   342  			Publisher: "Microsoft.Azure.ContainerUpstream",
   343  			Version:   "1.0",
   344  			ProtectedSettings: map[string]string{
   345  				"commandToExecute": WindowsBootstrapExtensionCommand,
   346  			},
   347  		}
   348  	}
   349  
   350  	return nil
   351  }
   352  
   353  // UserAgent specifies a string to append to the agent identifier.
   354  func UserAgent() string {
   355  	return fmt.Sprintf("cluster-api-provider-azure/%s", version.Get().String())
   356  }
   357  
   358  // ARMClientOptions returns default ARM client options for CAPZ SDK v2 requests.
   359  func ARMClientOptions(azureEnvironment string, extraPolicies ...policy.Policy) (*arm.ClientOptions, error) {
   360  	opts := &arm.ClientOptions{}
   361  
   362  	switch azureEnvironment {
   363  	case PublicCloudName:
   364  		opts.Cloud = cloud.AzurePublic
   365  	case ChinaCloudName:
   366  		opts.Cloud = cloud.AzureChina
   367  	case USGovernmentCloudName:
   368  		opts.Cloud = cloud.AzureGovernment
   369  	case "":
   370  		// No cloud name provided, so leave at defaults.
   371  	default:
   372  		return nil, fmt.Errorf("invalid cloud name %q", azureEnvironment)
   373  	}
   374  	opts.PerCallPolicies = []policy.Policy{
   375  		correlationIDPolicy{},
   376  		userAgentPolicy{},
   377  	}
   378  	opts.PerCallPolicies = append(opts.PerCallPolicies, extraPolicies...)
   379  	opts.Retry.MaxRetries = -1 // Less than zero means one try and no retries.
   380  
   381  	return opts, nil
   382  }
   383  
   384  // correlationIDPolicy adds the "x-ms-correlation-request-id" header to requests.
   385  // It implements the policy.Policy interface.
   386  type correlationIDPolicy struct{}
   387  
   388  // Do adds the "x-ms-correlation-request-id" header if a request has a correlation ID in its context.
   389  func (p correlationIDPolicy) Do(req *policy.Request) (*http.Response, error) {
   390  	if corrID, ok := tele.CorrIDFromCtx(req.Raw().Context()); ok {
   391  		req.Raw().Header.Set(string(tele.CorrIDKeyVal), string(corrID))
   392  	}
   393  	return req.Next()
   394  }
   395  
   396  // userAgentPolicy extends the "User-Agent" header on requests.
   397  // It implements the policy.Policy interface.
   398  type userAgentPolicy struct{}
   399  
   400  // Do extends the "User-Agent" header of a request by appending CAPZ's user agent.
   401  func (p userAgentPolicy) Do(req *policy.Request) (*http.Response, error) {
   402  	req.Raw().Header.Set("User-Agent", req.Raw().UserAgent()+" "+UserAgent())
   403  	return req.Next()
   404  }
   405  
   406  // CustomPutPatchHeaderPolicy adds custom headers to a PUT or PATCH request.
   407  // It implements the policy.Policy interface.
   408  type CustomPutPatchHeaderPolicy struct {
   409  	Headers map[string]string
   410  }
   411  
   412  // Do adds any custom headers to a PUT or PATCH request.
   413  func (p CustomPutPatchHeaderPolicy) Do(req *policy.Request) (*http.Response, error) {
   414  	if req.Raw().Method == http.MethodPut || req.Raw().Method == http.MethodPatch {
   415  		for key, element := range p.Headers {
   416  			req.Raw().Header.Set(key, element)
   417  		}
   418  	}
   419  
   420  	return req.Next()
   421  }
   422  
   423  // GetNormalizedKubernetesName returns a normalized name for a Kubernetes resource.
   424  func GetNormalizedKubernetesName(name string) string {
   425  	// Remove non-alphanumeric characters, convert to lowercase, and replace underscores with hyphens
   426  	name = strings.ToLower(name)
   427  	re := regexp.MustCompile(`[^a-z0-9\-]+`)
   428  	name = re.ReplaceAllString(name, "-")
   429  
   430  	// Remove leading and trailing hyphens
   431  	name = strings.Trim(name, "-")
   432  	return name
   433  }