github.com/vmware/govmomi@v0.51.0/cli/namespace/cluster/enable.go (about)

     1  // © Broadcom. All Rights Reserved.
     2  // The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries.
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package cluster
     6  
     7  import (
     8  	"context"
     9  	"flag"
    10  	"fmt"
    11  	"strconv"
    12  	"strings"
    13  
    14  	"github.com/vmware/govmomi/cli"
    15  	"github.com/vmware/govmomi/cli/flags"
    16  	"github.com/vmware/govmomi/cli/storage/policy"
    17  	"github.com/vmware/govmomi/vapi/namespace"
    18  )
    19  
    20  type enableCluster struct {
    21  	ControlPlaneDNSSearchDomains           string
    22  	ImageStoragePolicy                     string
    23  	NcpClusterNetworkSpec                  workloadNetwork
    24  	ControlPlaneManagementNetwork          masterManagementNetwork
    25  	ControlPlaneDNSNames                   string
    26  	ControlPlaneNTPServers                 string
    27  	EphemeralStoragePolicy                 string
    28  	DefaultImageRepository                 string
    29  	ServiceCidr                            string
    30  	LoginBanner                            string
    31  	SizeHint                               string
    32  	WorkerDNS                              string
    33  	DefaultImageRegistry                   string
    34  	ControlPlaneDNS                        string
    35  	NetworkProvider                        string
    36  	ControlPlaneStoragePolicy              string
    37  	DefaultKubernetesServiceContentLibrary string
    38  
    39  	*flags.ClusterFlag
    40  }
    41  
    42  type masterManagementNetwork struct {
    43  	Mode         string
    44  	FloatingIP   string
    45  	AddressRange *namespace.AddressRange
    46  	Network      string
    47  }
    48  
    49  type workloadNetwork struct {
    50  	NsxEdgeCluster string
    51  	PodCidrs       string
    52  	EgressCidrs    string
    53  	Switch         string
    54  	IngressCidrs   string
    55  }
    56  
    57  type objectReferences struct {
    58  	Cluster                   string
    59  	Network                   string
    60  	ImageStoragePolicy        string
    61  	ControlPlaneStoragePolicy string
    62  	EphemeralStoragePolicy    string
    63  	WorkerNetworkSwitch       string
    64  	EdgeCluster               string
    65  }
    66  
    67  func init() {
    68  	newEnableCluster := &enableCluster{
    69  		ControlPlaneManagementNetwork: masterManagementNetwork{
    70  			AddressRange: &namespace.AddressRange{},
    71  		},
    72  	}
    73  	cli.Register("namespace.cluster.enable", newEnableCluster)
    74  }
    75  
    76  func (cmd *enableCluster) Register(ctx context.Context, f *flag.FlagSet) {
    77  	cmd.ClusterFlag, ctx = flags.NewClusterFlag(ctx)
    78  	// this brings in some additional flags
    79  	// Datacenter flag, which we do want in order to configure a Finder we use to do some lookups
    80  	// ClientFlag, which we do need to control auth and connection details
    81  	// OutputFlag, which doesn't have any effect in this case as there's no response to output
    82  	cmd.ClusterFlag.Register(ctx, f)
    83  	// Descriptions are mostly extracted from:
    84  	// https://vmware.github.io/vsphere-automation-sdk-rest/vsphere/operations/com/vmware/vcenter/namespace_management/clusters.enable-operation.html
    85  	f.StringVar(&cmd.SizeHint, "size", "",
    86  		"The size of the Kubernetes API server and the worker nodes. Value is one of: TINY, SMALL, MEDIUM, LARGE.")
    87  	f.StringVar(&cmd.ServiceCidr, "service-cidr", "",
    88  		"CIDR block from which Kubernetes allocates service cluster IP addresses. Shouldn't overlap with pod, ingress or egress CIDRs")
    89  	f.StringVar(&cmd.NetworkProvider, "network-provider", "NSXT_CONTAINER_PLUGIN",
    90  		"Optional. Provider of cluster networking for this vSphere Namespaces cluster. Currently only value supported is: NSXT_CONTAINER_PLUGIN.")
    91  	f.StringVar(&cmd.NcpClusterNetworkSpec.PodCidrs, "pod-cidrs", "",
    92  		"CIDR blocks from which Kubernetes allocates pod IP addresses. Comma-separated list. Shouldn't overlap with service, ingress or egress CIDRs.")
    93  	f.StringVar(&cmd.NcpClusterNetworkSpec.IngressCidrs, "workload-network.ingress-cidrs", "",
    94  		"CIDR blocks from which NSX assigns IP addresses for Kubernetes Ingresses and Kubernetes Services of type LoadBalancer. Comma-separated list. Shouldn't overlap with pod, service or egress CIDRs.")
    95  	f.StringVar(&cmd.NcpClusterNetworkSpec.EgressCidrs, "workload-network.egress-cidrs", "",
    96  		"CIDR blocks from which NSX assigns IP addresses used for performing SNAT from container IPs to external IPs. Comma-separated list. Shouldn't overlap with pod, service or ingress CIDRs.")
    97  	f.StringVar(&cmd.NcpClusterNetworkSpec.Switch, "workload-network.switch", "",
    98  		"vSphere Distributed Switch used to connect this cluster.")
    99  	f.StringVar(&cmd.NcpClusterNetworkSpec.NsxEdgeCluster, "workload-network.edge-cluster", "",
   100  		"NSX Edge Cluster to be used for Kubernetes Services of type LoadBalancer, Kubernetes Ingresses, and NSX SNAT.")
   101  	f.StringVar(&cmd.ControlPlaneManagementNetwork.Network, "mgmt-network.network", "",
   102  		"Identifier for the management network.")
   103  	f.StringVar(&cmd.ControlPlaneManagementNetwork.Mode, "mgmt-network.mode", "STATICRANGE",
   104  		"IPv4 address assignment modes. Value is one of: DHCP, STATICRANGE")
   105  	f.StringVar(&cmd.ControlPlaneManagementNetwork.FloatingIP, "mgmt-network.floating-IP", "",
   106  		"Optional. The Floating IP used by the HA master cluster in the when network Mode is DHCP.")
   107  	f.StringVar(&cmd.ControlPlaneManagementNetwork.AddressRange.StartingAddress, "mgmt-network.starting-address", "",
   108  		"Denotes the start of the IP range to be used. Optional, but required with network mode STATICRANGE.")
   109  	f.IntVar(&cmd.ControlPlaneManagementNetwork.AddressRange.AddressCount, "mgmt-network.address-count", 5,
   110  		"The number of IP addresses in the management range. Optional, but required with network mode STATICRANGE.")
   111  	f.StringVar(&cmd.ControlPlaneManagementNetwork.AddressRange.SubnetMask, "mgmt-network.subnet-mask", "",
   112  		"Subnet mask of the management network. Optional, but required with network mode STATICRANGE.")
   113  	f.StringVar(&cmd.ControlPlaneManagementNetwork.AddressRange.Gateway, "mgmt-network.gateway", "",
   114  		"Gateway to be used for the management IP range")
   115  	f.StringVar(&cmd.ControlPlaneDNS, "control-plane-dns", "",
   116  		"Comma-separated list of DNS server IP addresses to use on Kubernetes API server, specified in order of preference.")
   117  	f.StringVar(&cmd.ControlPlaneDNSNames, "control-plane-dns-names", "",
   118  		"Comma-separated list of DNS names to associate with the Kubernetes API server. These DNS names are embedded in the TLS certificate presented by the API server.")
   119  	f.StringVar(&cmd.ControlPlaneDNSSearchDomains, "control-plane-dns-search-domains", "",
   120  		"Comma-separated list of domains to be searched when trying to lookup a host name on Kubernetes API server, specified in order of preference.")
   121  	f.StringVar(&cmd.ControlPlaneNTPServers, "control-plane-ntp-servers", "",
   122  		"Optional. Comma-separated list of NTP server DNS names or IP addresses to use on Kubernetes API server, specified in order of preference. If unset, VMware Tools based time synchronization is enabled.")
   123  	f.StringVar(&cmd.WorkerDNS, "worker-dns", "",
   124  		"Comma-separated list of DNS server IP addresses to use on the worker nodes, specified in order of preference.")
   125  	f.StringVar(&cmd.ControlPlaneStoragePolicy, "control-plane-storage-policy", "",
   126  		"Storage Policy associated with Kubernetes API server.")
   127  	f.StringVar(&cmd.EphemeralStoragePolicy, "ephemeral-storage-policy", "",
   128  		"Storage Policy associated with ephemeral disks of all the Kubernetes Pods in the cluster.")
   129  	f.StringVar(&cmd.ImageStoragePolicy, "image-storage-policy", "",
   130  		"Storage Policy to be used for container images.")
   131  	f.StringVar(&cmd.LoginBanner, "login-banner", "",
   132  		"Optional. Disclaimer to be displayed prior to login via the Kubectl plugin.")
   133  	// documented API is currently ambiguous with these duplicated fields, need to wait for this to be resolved
   134  	//f.StringVar(&cmd.DefaultImageRegistry, "default-image-registry", "",
   135  	//  "Optional. Default image registry to use when unspecified in the container image name. Defaults to Docker Hub.")
   136  	//f.StringVar(&cmd.DefaultImageRepository, "default-image-repository", "",
   137  	//  "Optional. Default image registry to use when unspecified in the container image name. Defaults to Docker Hub.")
   138  	// TODO
   139  	// f.StringVar(&cmd.DefaultKubernetesServiceContentLibrary, "default-kubernetes-service-content-library", "",
   140  	//  "Optional. Content Library which holds the VM Images for vSphere Kubernetes Service. This Content Library should be subscribed to VMware's hosted vSphere Kubernetes Service Repository.")
   141  }
   142  
   143  func (cmd *enableCluster) Description() string {
   144  	return `Enable vSphere Namespaces on the cluster.
   145  This operation sets up Kubernetes instance for the cluster along with worker nodes.
   146  
   147  Examples:
   148    govc namespace.cluster.enable \
   149      -cluster "Workload-Cluster" \
   150      -service-cidr 10.96.0.0/23 \
   151      -pod-cidrs 10.244.0.0/20 \
   152      -control-plane-dns 10.10.10.10 \
   153      -control-plane-dns-names wcp.example.com \
   154      -workload-network.egress-cidrs 10.0.0.128/26 \
   155      -workload-network.ingress-cidrs "10.0.0.64/26" \
   156      -workload-network.switch VDS \
   157      -workload-network.edge-cluster Edge-Cluster-1 \
   158      -size TINY   \
   159      -mgmt-network.network "DVPG-Management Network" \
   160      -mgmt-network.gateway 10.0.0.1 \
   161      -mgmt-network.starting-address 10.0.0.45 \
   162      -mgmt-network.subnet-mask 255.255.255.0 \
   163      -ephemeral-storage-policy "vSAN Default Storage Policy" \
   164      -control-plane-storage-policy "vSAN Default Storage Policy" \
   165      -image-storage-policy "vSAN Default Storage Policy"`
   166  }
   167  
   168  func (cmd *enableCluster) Run(ctx context.Context, f *flag.FlagSet) error {
   169  	c, err := cmd.RestClient()
   170  	if err != nil {
   171  		return err
   172  	}
   173  
   174  	// Cluster object reference lookup
   175  	cluster, err := cmd.Cluster()
   176  	if err != nil {
   177  		return err
   178  	}
   179  	id := cluster.Reference().Value
   180  
   181  	finder, err := cmd.ClusterFlag.Finder()
   182  	if err != nil {
   183  		return err
   184  	}
   185  
   186  	// Network object reference lookup.
   187  	networkRef, err := finder.Network(ctx, cmd.ControlPlaneManagementNetwork.Network)
   188  	if err != nil {
   189  		return err
   190  	}
   191  
   192  	// Storage policy object references lookup
   193  	pbmc, err := cmd.PbmClient()
   194  	if err != nil {
   195  		return fmt.Errorf("error creating client for storage policy lookup: %s", err)
   196  	}
   197  
   198  	storagePolicyNames := make(map[string]string)
   199  	storagePolicyNames["control-plane"] = cmd.ControlPlaneStoragePolicy
   200  	storagePolicyNames["ephemeral"] = cmd.EphemeralStoragePolicy
   201  	storagePolicyNames["image"] = cmd.ImageStoragePolicy
   202  	// keep track of names we looked up, so we don't repeat lookups
   203  	visited := make(map[string]string)
   204  	storagePolicyRefs := make(map[string]string)
   205  	for k, v := range storagePolicyNames {
   206  		if _, exists := visited[v]; !exists {
   207  			policies, err := policy.ListProfiles(ctx, pbmc, v)
   208  			if err != nil {
   209  				return fmt.Errorf("error looking up storage policy %q: %s", v, err)
   210  			} else if len(policies) != 1 {
   211  				return fmt.Errorf("could not find a unique storage policy ID for query %q", v)
   212  			}
   213  			visited[v] = policies[0].GetPbmProfile().ProfileId.UniqueId
   214  			storagePolicyRefs[k] = policies[0].GetPbmProfile().ProfileId.UniqueId
   215  		} else {
   216  			storagePolicyRefs[k] = visited[v]
   217  		}
   218  
   219  	}
   220  
   221  	// DVS Object reference lookup
   222  	// We need an id returned from the namespace lookup here, not a regular managed object reference.
   223  	// Similar approach in powerCLI here:
   224  	// https://github.com/lamw/PowerCLI-Example-Scripts/blob/7e4b9b9c93c5ffaa0ac2fefa8e02e5f751c044b7/Modules/VMware.WorkloadManagement/VMware.WorkloadManagement.psm1#L123
   225  	// Note that the data model returned means we get no chance to choose the switch by name.
   226  	// We assume there's just one switch per cluster and bail out otherwise.
   227  	m := namespace.NewManager(c)
   228  	clusterId := cluster.Reference().Value
   229  	switches, err := m.ListCompatibleDistributedSwitches(ctx, clusterId)
   230  	if err != nil {
   231  		return fmt.Errorf("error in compatible switch lookup: %s", err)
   232  	} else if len(switches) != 1 {
   233  		return fmt.Errorf("expected to find 1 namespace compatible switch in cluster %q, found %d",
   234  			clusterId, len(switches))
   235  	}
   236  
   237  	switchId := switches[0].DistributedSwitch
   238  
   239  	edgeClusterDisplayName := cmd.NcpClusterNetworkSpec.NsxEdgeCluster
   240  	edgeClusters, err := m.ListCompatibleEdgeClusters(ctx, clusterId, switchId)
   241  	if err != nil {
   242  		return fmt.Errorf("error in compatible edge cluster lookup: %s", err)
   243  	}
   244  
   245  	matchingEdgeClusters := make([]string, 0)
   246  	for _, v := range edgeClusters {
   247  		if v.DisplayName == edgeClusterDisplayName {
   248  			matchingEdgeClusters = append(matchingEdgeClusters, v.EdgeCluster)
   249  		}
   250  	}
   251  	if len(matchingEdgeClusters) != 1 {
   252  		return fmt.Errorf("Didn't find unique match for edge cluster %q, found %d objects",
   253  			edgeClusterDisplayName, len(matchingEdgeClusters))
   254  	}
   255  
   256  	resolvedObjectRefs := objectReferences{
   257  		Cluster:                   cluster.Reference().Value,
   258  		Network:                   networkRef.Reference().Value,
   259  		ControlPlaneStoragePolicy: storagePolicyRefs["control-plane"],
   260  		EphemeralStoragePolicy:    storagePolicyRefs["ephemeral"],
   261  		ImageStoragePolicy:        storagePolicyRefs["image"],
   262  		WorkerNetworkSwitch:       switchId,
   263  		EdgeCluster:               matchingEdgeClusters[0],
   264  	}
   265  
   266  	enableClusterSpec, err := cmd.toVapiSpec(resolvedObjectRefs)
   267  	if err != nil {
   268  		return err
   269  	}
   270  
   271  	err = m.EnableCluster(ctx, id, enableClusterSpec)
   272  	if err != nil {
   273  		return err
   274  	}
   275  
   276  	return nil
   277  }
   278  
   279  func (cmd *enableCluster) toVapiSpec(refs objectReferences) (*namespace.EnableClusterSpec, error) {
   280  
   281  	podCidrs, err := splitCidrList(splitCommaSeparatedList(cmd.NcpClusterNetworkSpec.PodCidrs))
   282  	if err != nil {
   283  		return nil, fmt.Errorf("invalid workload-network.pod-cidrs value: %s", err)
   284  	}
   285  	egressCidrs, err := splitCidrList(splitCommaSeparatedList(cmd.NcpClusterNetworkSpec.EgressCidrs))
   286  	if err != nil {
   287  		return nil, fmt.Errorf("invalid workload-network.egress-cidrs value: %s", err)
   288  	}
   289  	ingressCidrs, err := splitCidrList(splitCommaSeparatedList(cmd.NcpClusterNetworkSpec.IngressCidrs))
   290  	if err != nil {
   291  		return nil, fmt.Errorf("invalid workload-network.ingress-cidrs value: %s", err)
   292  	}
   293  	serviceCidr, err := splitCidr(cmd.ServiceCidr)
   294  	if err != nil {
   295  		return nil, fmt.Errorf("invalid service-cidr value: %s", err)
   296  	}
   297  	var masterManagementNetwork *namespace.MasterManagementNetwork
   298  	if (cmd.ControlPlaneManagementNetwork.Mode != "") ||
   299  		(cmd.ControlPlaneManagementNetwork.FloatingIP != "") ||
   300  		(cmd.ControlPlaneManagementNetwork.Network != "") {
   301  		masterManagementNetwork = &namespace.MasterManagementNetwork{}
   302  		masterManagementNetwork.AddressRange = cmd.ControlPlaneManagementNetwork.AddressRange
   303  		masterManagementNetwork.FloatingIP = cmd.ControlPlaneManagementNetwork.FloatingIP
   304  		ipam := namespace.IpAssignmentModeFromString(cmd.ControlPlaneManagementNetwork.Mode)
   305  		masterManagementNetwork.Mode = &ipam
   306  		masterManagementNetwork.Network = cmd.ControlPlaneManagementNetwork.Network
   307  	}
   308  	if masterManagementNetwork != nil {
   309  		if (masterManagementNetwork.AddressRange.SubnetMask == "") &&
   310  			(masterManagementNetwork.AddressRange.StartingAddress == "") &&
   311  			(masterManagementNetwork.AddressRange.Gateway == "") &&
   312  			(masterManagementNetwork.AddressRange.AddressCount == 0) {
   313  			masterManagementNetwork.AddressRange = nil
   314  		}
   315  		masterManagementNetwork.Network = refs.Network
   316  	}
   317  
   318  	sh := namespace.SizingHintFromString(cmd.SizeHint)
   319  	np := namespace.ClusterNetworkProviderFromString(cmd.NetworkProvider)
   320  
   321  	spec := namespace.EnableClusterSpec{
   322  		MasterDNSSearchDomains: splitCommaSeparatedList(cmd.ControlPlaneDNSSearchDomains),
   323  		ImageStorage:           namespace.ImageStorageSpec{StoragePolicy: refs.ImageStoragePolicy},
   324  		NcpClusterNetworkSpec: &namespace.NcpClusterNetworkSpec{
   325  			NsxEdgeCluster:           refs.EdgeCluster,
   326  			PodCidrs:                 podCidrs,
   327  			EgressCidrs:              egressCidrs,
   328  			ClusterDistributedSwitch: refs.WorkerNetworkSwitch,
   329  			IngressCidrs:             ingressCidrs,
   330  		},
   331  		MasterManagementNetwork:                masterManagementNetwork,
   332  		MasterDNSNames:                         splitCommaSeparatedList(cmd.ControlPlaneDNSNames),
   333  		MasterNTPServers:                       splitCommaSeparatedList(cmd.ControlPlaneNTPServers),
   334  		EphemeralStoragePolicy:                 refs.EphemeralStoragePolicy,
   335  		DefaultImageRepository:                 cmd.DefaultImageRepository,
   336  		ServiceCidr:                            serviceCidr,
   337  		LoginBanner:                            cmd.LoginBanner,
   338  		SizeHint:                               &sh,
   339  		WorkerDNS:                              splitCommaSeparatedList(cmd.WorkerDNS),
   340  		DefaultImageRegistry:                   nil,
   341  		MasterDNS:                              splitCommaSeparatedList(cmd.ControlPlaneDNS),
   342  		NetworkProvider:                        &np,
   343  		MasterStoragePolicy:                    refs.ControlPlaneStoragePolicy,
   344  		DefaultKubernetesServiceContentLibrary: cmd.DefaultKubernetesServiceContentLibrary,
   345  	}
   346  	return &spec, nil
   347  }
   348  
   349  func splitCidr(input string) (*namespace.Cidr, error) {
   350  	parts := strings.Split(input, "/")
   351  	if !(len(parts) == 2) || parts[0] == "" {
   352  		return nil, fmt.Errorf("invalid cidr %q supplied, needs to be in form '192.168.1.0/24'", input)
   353  	}
   354  
   355  	prefix, err := strconv.Atoi(parts[1])
   356  	if err != nil {
   357  		return nil, err
   358  	}
   359  
   360  	result := namespace.Cidr{
   361  		Address: parts[0],
   362  		Prefix:  prefix,
   363  	}
   364  	return &result, nil
   365  }
   366  
   367  func splitCidrList(input []string) ([]namespace.Cidr, error) {
   368  	var result []namespace.Cidr
   369  	for i, cidrIn := range input {
   370  		cidr, err := splitCidr(cidrIn)
   371  		if err != nil {
   372  			return nil, fmt.Errorf("parsing cidr %q in list position %d : %q", cidrIn, i, err)
   373  		}
   374  		result = append(result, *cidr)
   375  	}
   376  	return result, nil
   377  }
   378  
   379  func splitCommaSeparatedList(cslist string) []string {
   380  	return deleteEmpty(strings.Split(cslist, ","))
   381  }
   382  
   383  func deleteEmpty(s []string) []string {
   384  	var r []string
   385  	for _, str := range s {
   386  		if str != "" {
   387  			r = append(r, str)
   388  		}
   389  	}
   390  	return r
   391  }