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 }