github.com/vmware/govmomi@v0.37.2/govc/namespace/cluster/enable.go (about)

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