/* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cloudup import ( "fmt" "strconv" "strings" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "github.com/blang/semver/v4" "k8s.io/apimachinery/pkg/api/resource" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/klog/v2" "k8s.io/kops" api "k8s.io/kops/pkg/apis/kops" "k8s.io/kops/pkg/apis/kops/model" "k8s.io/kops/pkg/apis/kops/util" "k8s.io/kops/pkg/client/simple" "k8s.io/kops/pkg/clouds" "k8s.io/kops/pkg/featureflag" "k8s.io/kops/upup/pkg/fi" "k8s.io/kops/upup/pkg/fi/cloudup/awsup" "k8s.io/kops/upup/pkg/fi/cloudup/azure" "k8s.io/kops/upup/pkg/fi/cloudup/gce" "k8s.io/kops/upup/pkg/fi/cloudup/openstack" "k8s.io/kops/util/pkg/architectures" ) const ( AuthorizationFlagAlwaysAllow = "AlwaysAllow" AuthorizationFlagRBAC = "RBAC" ) type NewClusterOptions struct { // ClusterName is the name of the cluster to initialize. ClusterName string // Authorization is the authorization mode to use. The options are "RBAC" (default) and "AlwaysAllow". Authorization string // Channel is a channel location for initializing the cluster. It defaults to "stable". Channel string // ConfigBase is the location where we will store the configuration. It defaults to the state store. ConfigBase string // DiscoveryStore is the location where we will store public OIDC-compatible discovery documents, under a cluster-specific directory. It defaults to not publishing discovery documents. DiscoveryStore string // KubernetesVersion is the version of Kubernetes to deploy. It defaults to the version recommended by the channel. KubernetesVersion string // KubernetesFeatureGates is the list of Kubernetes feature gates to enable/disable. KubernetesFeatureGates []string // AdminAccess is the set of CIDR blocks permitted to connect to the Kubernetes API. It defaults to "0.0.0.0/0" and "::/0". AdminAccess []string // SSHAccess is the set of CIDR blocks permitted to connect to SSH on the nodes. It defaults to the value of AdminAccess. SSHAccess []string // CloudProvider is the name of the cloud provider. The default is to guess based on the Zones name. CloudProvider string // Zones are the availability zones in which to run the cluster. Zones []string // ControlPlaneZones are the availability zones in which to run the control-plane nodes. Defaults to the list in the Zones field. ControlPlaneZones []string // Project is the cluster's GCE project. Project string // GCEServiceAccount specifies the service account with which the GCE VM runs. GCEServiceAccount string // Spotinst options SpotinstProduct string SpotinstOrientation string // NetworkID is the ID of the shared network (VPC). // If empty, SubnetIDs are not empty, and on AWS or OpenStack, determines network ID from the first SubnetID. // If empty otherwise, creates a new network/VPC to be owned by the cluster. NetworkID string // SubnetIDs are the IDs of the shared subnets. // If empty, creates new subnets to be owned by the cluster. SubnetIDs []string // UtilitySubnetIDs are the IDs of the shared utility subnets. If empty and the topology is "private", creates new subnets to be owned by the cluster. UtilitySubnetIDs []string // Egress defines the method of traffic egress for subnets. Egress string // IPv6 adds IPv6 CIDRs to subnets IPv6 bool // OpenstackExternalNet is the name of the external network for the openstack router. OpenstackExternalNet string OpenstackExternalSubnet string OpenstackStorageIgnoreAZ bool OpenstackDNSServers string OpenstackLBSubnet string // OpenstackLBOctavia is whether to use use octavia instead of haproxy. OpenstackLBOctavia bool OpenstackOctaviaProvider string AzureSubscriptionID string AzureTenantID string AzureResourceGroupName string AzureRouteTableName string AzureAdminUser string // ControlPlaneCount is the number of control-plane nodes to create. Defaults to the length of ControlPlaneZones. // if ControlPlaneZones is explicitly nonempty, otherwise defaults to 1. ControlPlaneCount int32 // APIServerCount is the number of API servers to create. Defaults to 0. APIServerCount int32 // EncryptEtcdStorage is whether to encrypt the etcd volumes. EncryptEtcdStorage *bool // EtcdClusters contains the names of the etcd clusters. EtcdClusters []string // EtcdStorageType is the underlying cloud storage class of the etcd volumes. EtcdStorageType string // NodeCount is the number of nodes to create. Defaults to leaving the count unspecified // on the InstanceGroup, which results in a count of 2. NodeCount int32 // Bastion enables the creation of a Bastion instance. Bastion bool // BastionLoadBalancerType is the bastion loadbalancer type to use; "public" or "internal". // Defaults to "public". BastionLoadBalancerType string // Networking is the networking provider/node to use. Networking string // Topology is the network topology to use. Defaults to "public" for IPv4 clusters and "private" for IPv6 clusters. Topology string // DNSType is the DNS type to use; "public" or "private". Defaults to "public". DNSType string // APILoadBalancerClass determines whether to use classic or network load balancers for the API APILoadBalancerClass string // APILoadBalancerType is the Kubernetes API loadbalancer type to use; "public" or "internal". // Defaults to using DNS instead of a load balancer if using public topology and not gossip, otherwise "public". APILoadBalancerType string // APISSLCertificate is the SSL certificate to use for the API loadbalancer. // Currently only supported in AWS. APISSLCertificate string // InstanceManager specifies which manager to use for managing instances. InstanceManager string Image string NodeImage string ControlPlaneImage string BastionImage string ControlPlaneSize string NodeSize string } func (o *NewClusterOptions) InitDefaults() { o.Channel = api.DefaultChannel o.Authorization = AuthorizationFlagRBAC o.AdminAccess = []string{"0.0.0.0/0", "::/0"} o.EtcdClusters = []string{"main", "events"} o.Networking = "cilium" o.InstanceManager = "cloudgroups" } type NewClusterResult struct { // Cluster is the initialized Cluster resource. Cluster *api.Cluster // InstanceGroups are the initialized InstanceGroup resources. InstanceGroups []*api.InstanceGroup // Channel is the loaded Channel object. Channel *api.Channel } // NewCluster initializes cluster and instance groups specifications as // intended for newly created clusters. // It is the responsibility of the caller to call cloudup.PerformAssignments() on // the returned cluster spec. func NewCluster(opt *NewClusterOptions, clientset simple.Clientset) (*NewClusterResult, error) { if opt.ClusterName == "" { return nil, fmt.Errorf("name is required") } if opt.Channel == "" { opt.Channel = api.DefaultChannel } channel, err := api.LoadChannel(clientset.VFSContext(), opt.Channel) if err != nil { return nil, err } cluster := &api.Cluster{ ObjectMeta: v1.ObjectMeta{ Name: opt.ClusterName, }, } cluster.Spec.Channel = opt.Channel if opt.KubernetesVersion == "" { kubernetesVersion := api.RecommendedKubernetesVersion(channel, kops.Version) if kubernetesVersion != nil { cluster.Spec.KubernetesVersion = kubernetesVersion.String() } } else { cluster.Spec.KubernetesVersion = opt.KubernetesVersion } cluster.Spec.ConfigStore = api.ConfigStoreSpec{ Base: opt.ConfigBase, } configBase, err := clientset.ConfigBaseFor(cluster) if err != nil { return nil, fmt.Errorf("error building ConfigBase for cluster: %v", err) } cluster.Spec.ConfigStore.Base = configBase.Path() cluster.Spec.Authorization = &api.AuthorizationSpec{} if strings.EqualFold(opt.Authorization, AuthorizationFlagAlwaysAllow) { cluster.Spec.Authorization.AlwaysAllow = &api.AlwaysAllowAuthorizationSpec{} } else if opt.Authorization == "" || strings.EqualFold(opt.Authorization, AuthorizationFlagRBAC) { cluster.Spec.Authorization.RBAC = &api.RBACAuthorizationSpec{} } else { return nil, fmt.Errorf("unknown authorization mode %q", opt.Authorization) } cluster.Spec.IAM = &api.IAMSpec{ AllowContainerRegistry: true, } cluster.Spec.Kubelet = &api.KubeletConfigSpec{ AnonymousAuth: fi.PtrTo(false), } if len(opt.KubernetesFeatureGates) > 0 { cluster.Spec.Kubelet.FeatureGates = make(map[string]string) cluster.Spec.KubeAPIServer = &api.KubeAPIServerConfig{ FeatureGates: make(map[string]string), } cluster.Spec.KubeControllerManager = &api.KubeControllerManagerConfig{ FeatureGates: make(map[string]string), } cluster.Spec.KubeProxy = &api.KubeProxyConfig{ FeatureGates: make(map[string]string), } cluster.Spec.KubeScheduler = &api.KubeSchedulerConfig{ FeatureGates: make(map[string]string), } for _, featureGate := range opt.KubernetesFeatureGates { enabled := true if featureGate[0] == '+' { featureGate = featureGate[1:] } if featureGate[0] == '-' { enabled = false featureGate = featureGate[1:] } cluster.Spec.Kubelet.FeatureGates[featureGate] = strconv.FormatBool(enabled) cluster.Spec.KubeAPIServer.FeatureGates[featureGate] = strconv.FormatBool(enabled) cluster.Spec.KubeControllerManager.FeatureGates[featureGate] = strconv.FormatBool(enabled) cluster.Spec.KubeProxy.FeatureGates[featureGate] = strconv.FormatBool(enabled) cluster.Spec.KubeScheduler.FeatureGates[featureGate] = strconv.FormatBool(enabled) } } if len(opt.AdminAccess) == 0 { opt.AdminAccess = []string{"0.0.0.0/0", "::/0"} } cluster.Spec.API.Access = opt.AdminAccess if len(opt.SSHAccess) != 0 { cluster.Spec.SSHAccess = opt.SSHAccess } else { cluster.Spec.SSHAccess = opt.AdminAccess } if len(opt.Zones) == 0 { return nil, fmt.Errorf("must specify at least one zone for the cluster (use --zones)") } allZones := sets.NewString() allZones.Insert(opt.Zones...) allZones.Insert(opt.ControlPlaneZones...) if opt.CloudProvider == "" { cloud, err := clouds.GuessCloudForPath(cluster.Spec.ConfigStore.Base) if err != nil { return nil, fmt.Errorf("pass in the cloud provider explicitly using --cloud: %w", err) } klog.V(2).Infof("Inferred %q cloud provider from state store %q", cloud, cluster.Spec.ConfigStore.Base) opt.CloudProvider = string(cloud) } var cloud fi.Cloud switch api.CloudProviderID(opt.CloudProvider) { case api.CloudProviderAWS: cluster.Spec.CloudProvider.AWS = &api.AWSSpec{} cloudTags := map[string]string{} awsCloud, err := awsup.NewAWSCloud(opt.Zones[0][:len(opt.Zones[0])-1], cloudTags) if err != nil { return nil, err } cloud = awsCloud case api.CloudProviderAzure: cluster.Spec.CloudProvider.Azure = &api.AzureSpec{ SubscriptionID: opt.AzureSubscriptionID, TenantID: opt.AzureTenantID, ResourceGroupName: opt.AzureResourceGroupName, RouteTableName: opt.AzureRouteTableName, AdminUser: opt.AzureAdminUser, } case api.CloudProviderDO: cluster.Spec.CloudProvider.DO = &api.DOSpec{} case api.CloudProviderGCE: cluster.Spec.CloudProvider.GCE = &api.GCESpec{} case api.CloudProviderHetzner: cluster.Spec.CloudProvider.Hetzner = &api.HetznerSpec{} case api.CloudProviderOpenstack: cluster.Spec.CloudProvider.Openstack = &api.OpenstackSpec{ Router: &api.OpenstackRouter{ ExternalNetwork: fi.PtrTo(opt.OpenstackExternalNet), }, BlockStorage: &api.OpenstackBlockStorageConfig{ Version: fi.PtrTo("v3"), IgnoreAZ: fi.PtrTo(opt.OpenstackStorageIgnoreAZ), ClusterName: opt.ClusterName, }, Monitor: &api.OpenstackMonitor{ Delay: fi.PtrTo("15s"), Timeout: fi.PtrTo("10s"), MaxRetries: fi.PtrTo(3), }, } initializeOpenstack(opt, cluster) osCloud, err := openstack.NewOpenstackCloud(cluster, "openstackmodel") if err != nil { return nil, err } cloud = osCloud case api.CloudProviderScaleway: cluster.Spec.CloudProvider.Scaleway = &api.ScalewaySpec{} default: return nil, fmt.Errorf("unsupported cloud provider %s", opt.CloudProvider) } if opt.DiscoveryStore != "" { discoveryPath, err := clientset.VFSContext().BuildVfsPath(opt.DiscoveryStore) if err != nil { return nil, fmt.Errorf("error building DiscoveryStore for cluster: %v", err) } cluster.Spec.ServiceAccountIssuerDiscovery = &api.ServiceAccountIssuerDiscoveryConfig{ DiscoveryStore: discoveryPath.Join(cluster.Name).Path(), } if cluster.Spec.GetCloudProvider() == api.CloudProviderAWS { cluster.Spec.ServiceAccountIssuerDiscovery.EnableAWSOIDCProvider = true cluster.Spec.IAM.UseServiceAccountExternalPermissions = fi.PtrTo(true) } } err = setupVPC(opt, cluster, cloud) if err != nil { return nil, err } zoneToSubnetMap, err := setupZones(opt, cluster, allZones) if err != nil { return nil, err } err = setupNetworking(opt, cluster) if err != nil { return nil, err } bastions, err := setupTopology(opt, cluster, allZones) if err != nil { return nil, err } controlPlanes, err := setupControlPlane(opt, cluster, zoneToSubnetMap) if err != nil { return nil, err } var nodes []*api.InstanceGroup switch opt.InstanceManager { case "karpenter": if opt.DiscoveryStore == "" { return nil, fmt.Errorf("karpenter requires --discovery-store") } cluster.Spec.Karpenter = &api.KarpenterConfig{ Enabled: true, } nodes, err = setupKarpenterNodes(opt, cluster, zoneToSubnetMap) if err != nil { return nil, err } case "cloudgroups": nodes, err = setupNodes(opt, cluster, zoneToSubnetMap) if err != nil { return nil, err } default: return nil, fmt.Errorf("invalid value %q for --instance-manager", opt.InstanceManager) } apiservers, err := setupAPIServers(opt, cluster, zoneToSubnetMap) if err != nil { return nil, err } err = setupAPI(opt, cluster) if err != nil { return nil, err } instanceGroups := append([]*api.InstanceGroup(nil), controlPlanes...) instanceGroups = append(instanceGroups, apiservers...) instanceGroups = append(instanceGroups, nodes...) instanceGroups = append(instanceGroups, bastions...) for _, instanceGroup := range instanceGroups { g := instanceGroup ig := g if instanceGroup.Spec.Image == "" { if opt.Image != "" { instanceGroup.Spec.Image = opt.Image } else { architecture, err := MachineArchitecture(cloud, instanceGroup.Spec.MachineType) if err != nil { return nil, err } instanceGroup.Spec.Image, err = defaultImage(cluster, channel, architecture) if err != nil { return nil, err } } } // TODO: Clean up if g.IsControlPlane() { if g.Spec.MachineType == "" { g.Spec.MachineType, err = defaultMachineType(cloud, cluster, ig) if err != nil { return nil, fmt.Errorf("error assigning default machine type for control plane: %v", err) } } } else if g.Spec.Role == api.InstanceGroupRoleBastion { if g.Spec.MachineType == "" { g.Spec.MachineType, err = defaultMachineType(cloud, cluster, g) if err != nil { return nil, fmt.Errorf("error assigning default machine type for bastions: %v", err) } } } else { if g.IsAPIServerOnly() && !featureflag.APIServerNodes.Enabled() { return nil, fmt.Errorf("apiserver nodes requires the APIServerNodes feature flag to be enabled") } if g.Spec.MachineType == "" { g.Spec.MachineType, err = defaultMachineType(cloud, cluster, g) if err != nil { return nil, fmt.Errorf("error assigning default machine type for nodes: %v", err) } } } if ig.Spec.Tenancy != "" && ig.Spec.Tenancy != "default" { switch cluster.Spec.GetCloudProvider() { case api.CloudProviderAWS: if _, ok := awsDedicatedInstanceExceptions[g.Spec.MachineType]; ok { return nil, fmt.Errorf("invalid dedicated instance type: %s", g.Spec.MachineType) } default: klog.Warning("Trying to set tenancy on non-AWS environment") } } if ig.IsControlPlane() { if len(ig.Spec.Subnets) == 0 { return nil, fmt.Errorf("control-plane InstanceGroup %s did not specify any Subnets", g.ObjectMeta.Name) } } else if ig.IsAPIServerOnly() && cluster.Spec.IsIPv6Only() { if len(ig.Spec.Subnets) == 0 { for _, subnet := range cluster.Spec.Networking.Subnets { if subnet.Type != api.SubnetTypePrivate && subnet.Type != api.SubnetTypeUtility { ig.Spec.Subnets = append(g.Spec.Subnets, subnet.Name) } } } } else { if len(ig.Spec.Subnets) == 0 { for _, subnet := range cluster.Spec.Networking.Subnets { if subnet.Type != api.SubnetTypeDualStack && subnet.Type != api.SubnetTypeUtility { g.Spec.Subnets = append(g.Spec.Subnets, subnet.Name) } } } if len(g.Spec.Subnets) == 0 { for _, subnet := range cluster.Spec.Networking.Subnets { if subnet.Type != api.SubnetTypeUtility { g.Spec.Subnets = append(g.Spec.Subnets, subnet.Name) } } } } if len(g.Spec.Subnets) == 0 { return nil, fmt.Errorf("unable to infer any Subnets for InstanceGroup %s ", g.ObjectMeta.Name) } } result := NewClusterResult{ Cluster: cluster, InstanceGroups: instanceGroups, Channel: channel, } return &result, nil } func setupVPC(opt *NewClusterOptions, cluster *api.Cluster, cloud fi.Cloud) error { cluster.Spec.Networking.NetworkID = opt.NetworkID switch cluster.Spec.GetCloudProvider() { case api.CloudProviderAWS: if cluster.Spec.Networking.NetworkID == "" && len(opt.SubnetIDs) > 0 { awsCloud := cloud.(awsup.AWSCloud) res, err := awsCloud.EC2().DescribeSubnets(&ec2.DescribeSubnetsInput{ SubnetIds: []*string{aws.String(opt.SubnetIDs[0])}, }) if err != nil { return fmt.Errorf("error describing subnet %s: %v", opt.SubnetIDs[0], err) } if len(res.Subnets) == 0 || res.Subnets[0].VpcId == nil { return fmt.Errorf("failed to determine VPC id of subnet %s", opt.SubnetIDs[0]) } cluster.Spec.Networking.NetworkID = *res.Subnets[0].VpcId } if featureflag.Spotinst.Enabled() { if opt.SpotinstProduct != "" { cluster.Spec.CloudProvider.AWS.SpotinstProduct = fi.PtrTo(opt.SpotinstProduct) } if opt.SpotinstOrientation != "" { cluster.Spec.CloudProvider.AWS.SpotinstOrientation = fi.PtrTo(opt.SpotinstOrientation) } } case api.CloudProviderGCE: if cluster.Spec.CloudConfig == nil { cluster.Spec.CloudConfig = &api.CloudConfiguration{} } cluster.Spec.CloudProvider.GCE.Project = opt.Project if cluster.Spec.CloudProvider.GCE.Project == "" { project, err := gce.DefaultProject() if err != nil { klog.Warningf("unable to get default google cloud project: %v", err) } else if project == "" { klog.Warningf("default google cloud project not set (try `gcloud config set project `") } else { klog.Infof("using google cloud project: %s", project) } cluster.Spec.CloudProvider.GCE.Project = project } if opt.GCEServiceAccount != "" { // TODO remove this logging? klog.Infof("VMs will be configured to use specified Service Account: %v", opt.GCEServiceAccount) cluster.Spec.CloudProvider.GCE.ServiceAccount = opt.GCEServiceAccount } case api.CloudProviderOpenstack: if cluster.Spec.CloudConfig == nil { cluster.Spec.CloudConfig = &api.CloudConfiguration{} } if cluster.Spec.Networking.NetworkID == "" && len(opt.SubnetIDs) > 0 { osCloud, err := openstack.NewOpenstackCloud(cluster, "new-cluster-setupvpc") if err != nil { return fmt.Errorf("error loading cloud: %v", err) } res, err := osCloud.FindNetworkBySubnetID(opt.SubnetIDs[0]) if err != nil { return fmt.Errorf("error finding network: %v", err) } cluster.Spec.Networking.NetworkID = res.ID } if opt.OpenstackDNSServers != "" { cluster.Spec.CloudProvider.Openstack.Router.DNSServers = fi.PtrTo(opt.OpenstackDNSServers) } if opt.OpenstackExternalSubnet != "" { cluster.Spec.CloudProvider.Openstack.Router.ExternalSubnet = fi.PtrTo(opt.OpenstackExternalSubnet) } case api.CloudProviderAzure: // TODO(kenji): Find a right place for this. // Creating an empty CloudConfig so that --cloud-config is passed to kubelet, api-server, etc. if cluster.Spec.CloudConfig == nil { cluster.Spec.CloudConfig = &api.CloudConfiguration{} } } return nil } func setupZones(opt *NewClusterOptions, cluster *api.Cluster, allZones sets.String) (map[string]*api.ClusterSubnetSpec, error) { var err error zoneToSubnetMap := make(map[string]*api.ClusterSubnetSpec) var zoneToSubnetProviderID map[string]string switch cluster.Spec.GetCloudProvider() { case api.CloudProviderGCE: // On GCE, subnets are regional - we create one per region, not per zone for _, zoneName := range allZones.List() { region, err := gce.ZoneToRegion(zoneName) if err != nil { return nil, err } // We create default subnets named the same as the regions subnetName := region subnet := model.FindSubnet(cluster, subnetName) if subnet == nil { subnet = &api.ClusterSubnetSpec{ Name: subnetName, Region: region, } if len(opt.SubnetIDs) != 0 { // We don't support multi-region clusters, so we can't have more than one subnet if len(opt.SubnetIDs) != 1 { return nil, fmt.Errorf("expected exactly one subnet for GCE, got %d", len(opt.SubnetIDs)) } subnet.ID = opt.SubnetIDs[0] } cluster.Spec.Networking.Subnets = append(cluster.Spec.Networking.Subnets, *subnet) } zoneToSubnetMap[zoneName] = subnet } return zoneToSubnetMap, nil case api.CloudProviderDO: if len(opt.Zones) > 1 { return nil, fmt.Errorf("digitalocean cloud provider currently supports one region only.") } // For DO we just pass in the region for --zones region := opt.Zones[0] subnet := model.FindSubnet(cluster, region) // for DO, subnets are just regions subnetName := region if subnet == nil { subnet = &api.ClusterSubnetSpec{ Name: subnetName, // region and zone are the same for DO Region: region, Zone: region, } cluster.Spec.Networking.Subnets = append(cluster.Spec.Networking.Subnets, *subnet) } zoneToSubnetMap[region] = subnet return zoneToSubnetMap, nil case api.CloudProviderHetzner: if len(opt.Zones) > 1 { return nil, fmt.Errorf("hetzner cloud provider currently supports only one zone (location)") } // TODO(hakman): Add customizations for Hetzner Cloud case api.CloudProviderAzure: // On Azure, subnets are regional - we create one per region, not per zone for _, zoneName := range allZones.List() { location, err := azure.ZoneToLocation(zoneName) if err != nil { return nil, err } // We create default subnets named the same as the regions subnetName := location subnet := model.FindSubnet(cluster, subnetName) if subnet == nil { subnet = &api.ClusterSubnetSpec{ Name: subnetName, Region: location, } cluster.Spec.Networking.Subnets = append(cluster.Spec.Networking.Subnets, *subnet) } zoneToSubnetMap[zoneName] = subnet } return zoneToSubnetMap, nil case api.CloudProviderAWS: if len(opt.Zones) > 0 && len(opt.SubnetIDs) > 0 { zoneToSubnetProviderID, err = getAWSZoneToSubnetProviderID(cluster.Spec.Networking.NetworkID, opt.Zones[0][:len(opt.Zones[0])-1], opt.SubnetIDs) if err != nil { return nil, err } } case api.CloudProviderOpenstack: if len(opt.Zones) > 0 && len(opt.SubnetIDs) > 0 { zoneToSubnetProviderID, err = getOpenstackZoneToSubnetProviderID(cluster, allZones.List(), opt.SubnetIDs) if err != nil { return nil, err } } case api.CloudProviderScaleway: if len(opt.Zones) > 1 { return nil, fmt.Errorf("scaleway cloud provider currently supports only one availability zone") } } for _, zoneName := range allZones.List() { // We create default subnets named the same as the zones subnetName := zoneName subnet := model.FindSubnet(cluster, subnetName) if subnet == nil { subnet = &api.ClusterSubnetSpec{ Name: subnetName, Zone: subnetName, Egress: opt.Egress, } if subnetID, ok := zoneToSubnetProviderID[zoneName]; ok { subnet.ID = subnetID } cluster.Spec.Networking.Subnets = append(cluster.Spec.Networking.Subnets, *subnet) } zoneToSubnetMap[zoneName] = subnet } return zoneToSubnetMap, nil } func getAWSZoneToSubnetProviderID(VPCID string, region string, subnetIDs []string) (map[string]string, error) { res := make(map[string]string) cloudTags := map[string]string{} awsCloud, err := awsup.NewAWSCloud(region, cloudTags) if err != nil { return res, fmt.Errorf("error loading cloud: %v", err) } vpcInfo, err := awsCloud.FindVPCInfo(VPCID) if err != nil { return res, fmt.Errorf("error describing VPC: %v", err) } if vpcInfo == nil { return res, fmt.Errorf("VPC %q not found", VPCID) } subnetByID := make(map[string]*fi.SubnetInfo) for _, subnetInfo := range vpcInfo.Subnets { subnetByID[subnetInfo.ID] = subnetInfo } for _, subnetID := range subnetIDs { subnet, ok := subnetByID[subnetID] if !ok { return res, fmt.Errorf("subnet %s not found in VPC %s", subnetID, VPCID) } if res[subnet.Zone] != "" { return res, fmt.Errorf("subnet %s and %s have the same zone", subnetID, res[subnet.Zone]) } res[subnet.Zone] = subnetID } return res, nil } func getOpenstackZoneToSubnetProviderID(cluster *api.Cluster, zones []string, subnetIDs []string) (map[string]string, error) { res := make(map[string]string) osCloud, err := openstack.NewOpenstackCloud(cluster, "new-cluster-zone-to-subnet") if err != nil { return res, fmt.Errorf("error loading cloud: %v", err) } osCloud.UseZones(zones) networkInfo, err := osCloud.FindVPCInfo(cluster.Spec.Networking.NetworkID) if err != nil { return res, fmt.Errorf("error describing Network: %v", err) } if networkInfo == nil { return res, fmt.Errorf("network %q not found", cluster.Spec.Networking.NetworkID) } subnetByID := make(map[string]*fi.SubnetInfo) for _, subnetInfo := range networkInfo.Subnets { subnetByID[subnetInfo.ID] = subnetInfo } for _, subnetID := range subnetIDs { subnet, ok := subnetByID[subnetID] if !ok { return res, fmt.Errorf("subnet %s not found in network %s", subnetID, cluster.Spec.Networking.NetworkID) } if res[subnet.Zone] != "" { return res, fmt.Errorf("subnet %s and %s have the same zone", subnetID, res[subnet.Zone]) } res[subnet.Zone] = subnetID } return res, nil } func setupControlPlane(opt *NewClusterOptions, cluster *api.Cluster, zoneToSubnetMap map[string]*api.ClusterSubnetSpec) ([]*api.InstanceGroup, error) { cloudProvider := cluster.Spec.GetCloudProvider() var controlPlanes []*api.InstanceGroup // Build the control-plane subnets. // The control-plane zones is the default set of zones unless explicitly set. // The control-plane count is the number of control-plane zones unless explicitly set. // We then round-robin around the zones. { controlPlaneCount := opt.ControlPlaneCount controlPlaneZones := opt.ControlPlaneZones if len(controlPlaneZones) != 0 { if controlPlaneCount != 0 && controlPlaneCount < int32(len(controlPlaneZones)) { return nil, fmt.Errorf("specified %d control-plane zones, but also requested %d control-plane nodes. If specifying both, the count should match.", len(controlPlaneZones), controlPlaneCount) } if controlPlaneCount == 0 { // If control-plane count is not specified, default to the number of control-plane zones controlPlaneCount = int32(len(controlPlaneZones)) } } else { // controlPlaneZones not set; default to same as node Zones controlPlaneZones = opt.Zones if controlPlaneCount == 0 { // If control-plane count is not specified, default to 1 controlPlaneCount = 1 } } if len(controlPlaneZones) == 0 { // Should be unreachable return nil, fmt.Errorf("cannot determine control-plane zones") } for i := 0; i < int(controlPlaneCount); i++ { zone := controlPlaneZones[i%len(controlPlaneZones)] name := zone if cloudProvider == api.CloudProviderDO { if int(controlPlaneCount) >= len(controlPlaneZones) { name += "-" + strconv.Itoa(1+(i/len(controlPlaneZones))) } } else { if int(controlPlaneCount) > len(controlPlaneZones) { name += "-" + strconv.Itoa(1+(i/len(controlPlaneZones))) } } g := &api.InstanceGroup{} g.Spec.Role = api.InstanceGroupRoleControlPlane g.Spec.MinSize = fi.PtrTo(int32(1)) g.Spec.MaxSize = fi.PtrTo(int32(1)) g.ObjectMeta.Name = "control-plane-" + name subnet := zoneToSubnetMap[zone] if subnet == nil { klog.Fatalf("subnet not found in zoneToSubnetMap") } g.Spec.Subnets = []string{subnet.Name} if opt.IPv6 && opt.Topology == api.TopologyPrivate { g.Spec.Subnets = []string{"dualstack-" + subnet.Name} } if cloudProvider == api.CloudProviderGCE || cloudProvider == api.CloudProviderAzure { g.Spec.Zones = []string{zone} } if cluster.IsKubernetesLT("1.27") && cloudProvider == api.CloudProviderAWS { g.Spec.InstanceMetadata = &api.InstanceMetadataOptions{ HTTPTokens: fi.PtrTo("required"), } } g.Spec.MachineType = opt.ControlPlaneSize g.Spec.Image = opt.ControlPlaneImage controlPlanes = append(controlPlanes, g) } } // Build the Etcd clusters { controlPlaneAZs := sets.NewString() duplicateAZs := false for _, ig := range controlPlanes { zones, err := model.FindZonesForInstanceGroup(cluster, ig) if err != nil { return nil, err } for _, zone := range zones { if controlPlaneAZs.Has(zone) { duplicateAZs = true } controlPlaneAZs.Insert(zone) } } if duplicateAZs { klog.Warningf("Running with control-plane nodes in the same AZs; redundancy will be reduced") } clusters := opt.EtcdClusters if opt.Networking == "cilium-etcd" { clusters = append(clusters, "cilium") } encryptEtcdStorage := false if opt.EncryptEtcdStorage != nil { encryptEtcdStorage = fi.ValueOf(opt.EncryptEtcdStorage) } else if cloudProvider == api.CloudProviderAWS { encryptEtcdStorage = true } for _, etcdCluster := range clusters { etcd := createEtcdCluster(etcdCluster, controlPlanes, encryptEtcdStorage, opt.EtcdStorageType) cluster.Spec.EtcdClusters = append(cluster.Spec.EtcdClusters, etcd) } } return controlPlanes, nil } func trimCommonPrefix(names []string) []string { // Trim shared prefix to keep the lengths sane // (this only applies to new clusters...) for len(names) != 0 && len(names[0]) > 1 { prefix := names[0][:1] allMatch := true for _, name := range names { if !strings.HasPrefix(name, prefix) { allMatch = false } } if !allMatch { break } for i := range names { names[i] = strings.TrimPrefix(names[i], prefix) } } for i, name := range names { if len(name) > 0 && name[0] >= '0' && name[0] <= '9' { names[i] = "etcd-" + name } } return names } func setupNodes(opt *NewClusterOptions, cluster *api.Cluster, zoneToSubnetMap map[string]*api.ClusterSubnetSpec) ([]*api.InstanceGroup, error) { cloudProvider := cluster.Spec.GetCloudProvider() var nodes []*api.InstanceGroup // The node count is the number of zones unless explicitly set // We then divvy up amongst the zones numZones := len(opt.Zones) nodeCount := opt.NodeCount if nodeCount == 0 { // If node count is not specified, default to the number of zones nodeCount = int32(numZones) } countPerIG := nodeCount / int32(numZones) remainder := int(nodeCount) % numZones for i, zone := range opt.Zones { count := countPerIG if i < remainder { count++ } g := &api.InstanceGroup{} g.Spec.Role = api.InstanceGroupRoleNode g.Spec.MinSize = fi.PtrTo(count) g.Spec.MaxSize = fi.PtrTo(count) g.ObjectMeta.Name = "nodes-" + zone subnet := zoneToSubnetMap[zone] if subnet == nil { klog.Fatalf("subnet not found in zoneToSubnetMap") } g.Spec.Subnets = []string{subnet.Name} if cloudProvider == api.CloudProviderGCE || cloudProvider == api.CloudProviderAzure { g.Spec.Zones = []string{zone} } if cluster.IsKubernetesLT("1.27") { if cloudProvider == api.CloudProviderAWS { g.Spec.InstanceMetadata = &api.InstanceMetadataOptions{ HTTPPutResponseHopLimit: fi.PtrTo(int64(1)), HTTPTokens: fi.PtrTo("required"), } } } if cloudProvider == api.CloudProviderGCE { if g.Spec.NodeLabels == nil { g.Spec.NodeLabels = make(map[string]string) } g.Spec.NodeLabels["cloud.google.com/metadata-proxy-ready"] = "true" } g.Spec.MachineType = opt.NodeSize g.Spec.Image = opt.NodeImage nodes = append(nodes, g) } return nodes, nil } func setupKarpenterNodes(opt *NewClusterOptions, cluster *api.Cluster, zoneToSubnetMap map[string]*api.ClusterSubnetSpec) ([]*api.InstanceGroup, error) { g := &api.InstanceGroup{} g.Spec.Role = api.InstanceGroupRoleNode g.Spec.Manager = api.InstanceManagerKarpenter g.ObjectMeta.Name = "nodes" if cluster.IsKubernetesLT("1.27") { g.Spec.InstanceMetadata = &api.InstanceMetadataOptions{ HTTPPutResponseHopLimit: fi.PtrTo(int64(1)), HTTPTokens: fi.PtrTo("required"), } } return []*api.InstanceGroup{g}, nil } func setupAPIServers(opt *NewClusterOptions, cluster *api.Cluster, zoneToSubnetMap map[string]*api.ClusterSubnetSpec) ([]*api.InstanceGroup, error) { cloudProvider := cluster.Spec.GetCloudProvider() var nodes []*api.InstanceGroup numZones := len(opt.Zones) nodeCount := opt.APIServerCount if nodeCount == 0 { return nodes, nil } countPerIG := nodeCount / int32(numZones) remainder := int(nodeCount) % numZones for i, zone := range opt.Zones { count := countPerIG if i < remainder { count++ } g := &api.InstanceGroup{} g.Spec.Role = api.InstanceGroupRoleAPIServer g.Spec.MinSize = fi.PtrTo(count) g.Spec.MaxSize = fi.PtrTo(count) g.ObjectMeta.Name = "apiserver-" + zone subnet := zoneToSubnetMap[zone] if subnet == nil { klog.Fatalf("subnet not found in zoneToSubnetMap") } g.Spec.Subnets = []string{subnet.Name} if cloudProvider == api.CloudProviderGCE || cloudProvider == api.CloudProviderAzure { g.Spec.Zones = []string{zone} } if cluster.IsKubernetesLT("1.27") { if cloudProvider == api.CloudProviderAWS { g.Spec.InstanceMetadata = &api.InstanceMetadataOptions{ HTTPPutResponseHopLimit: fi.PtrTo(int64(1)), HTTPTokens: fi.PtrTo("required"), } } } nodes = append(nodes, g) } return nodes, nil } func setupNetworking(opt *NewClusterOptions, cluster *api.Cluster) error { switch opt.Networking { case "kubenet": cluster.Spec.Networking.Kubenet = &api.KubenetNetworkingSpec{} case "external": cluster.Spec.Networking.External = &api.ExternalNetworkingSpec{} case "cni": cluster.Spec.Networking.CNI = &api.CNINetworkingSpec{} case "kopeio-vxlan", "kopeio": cluster.Spec.Networking.Kopeio = &api.KopeioNetworkingSpec{} case "flannel", "flannel-vxlan": cluster.Spec.Networking.Flannel = &api.FlannelNetworkingSpec{ Backend: "vxlan", } case "flannel-udp": klog.Warningf("flannel UDP mode is not recommended; consider flannel-vxlan instead") cluster.Spec.Networking.Flannel = &api.FlannelNetworkingSpec{ Backend: "udp", } case "calico": cluster.Spec.Networking.Calico = &api.CalicoNetworkingSpec{} case "canal": cluster.Spec.Networking.Canal = &api.CanalNetworkingSpec{} case "kube-router": cluster.Spec.Networking.KubeRouter = &api.KuberouterNetworkingSpec{} if cluster.Spec.KubeProxy == nil { cluster.Spec.KubeProxy = &api.KubeProxyConfig{} } enabled := false cluster.Spec.KubeProxy.Enabled = &enabled case "amazonvpc", "amazon-vpc-routed-eni": cluster.Spec.Networking.AmazonVPC = &api.AmazonVPCNetworkingSpec{} case "cilium", "": addCiliumNetwork(cluster) case "cilium-etcd": addCiliumNetwork(cluster) cluster.Spec.Networking.Cilium.EtcdManaged = true case "cilium-eni": addCiliumNetwork(cluster) cluster.Spec.Networking.Cilium.IPAM = "eni" case "gcp", "gce": cluster.Spec.Networking.GCP = &api.GCPNetworkingSpec{} default: return fmt.Errorf("unknown networking mode %q", opt.Networking) } klog.V(4).Infof("networking mode=%s => %s", opt.Networking, fi.DebugAsJsonString(cluster.Spec.Networking)) return nil } func setupTopology(opt *NewClusterOptions, cluster *api.Cluster, allZones sets.String) ([]*api.InstanceGroup, error) { var bastions []*api.InstanceGroup if opt.Topology == "" { if opt.IPv6 { opt.Topology = api.TopologyPrivate } else { opt.Topology = api.TopologyPublic } } cluster.Spec.Networking.Topology = &api.TopologySpec{} switch opt.Topology { case api.TopologyPublic: if opt.Bastion { return nil, fmt.Errorf("bastion supports --topology='private' only") } for i := range cluster.Spec.Networking.Subnets { cluster.Spec.Networking.Subnets[i].Type = api.SubnetTypePublic } case api.TopologyPrivate: if cluster.Spec.Networking.Kubenet != nil { return nil, fmt.Errorf("invalid networking option %s. Kubenet does not support private topology", opt.Networking) } for i := range cluster.Spec.Networking.Subnets { cluster.Spec.Networking.Subnets[i].Type = api.SubnetTypePrivate } var zoneToSubnetProviderID map[string]string var err error if len(opt.Zones) > 0 && len(opt.UtilitySubnetIDs) > 0 { switch cluster.Spec.GetCloudProvider() { case api.CloudProviderAWS: zoneToSubnetProviderID, err = getAWSZoneToSubnetProviderID(cluster.Spec.Networking.NetworkID, opt.Zones[0][:len(opt.Zones[0])-1], opt.UtilitySubnetIDs) if err != nil { return nil, err } case api.CloudProviderOpenstack: zoneToSubnetProviderID, err = getOpenstackZoneToSubnetProviderID(cluster, allZones.List(), opt.UtilitySubnetIDs) if err != nil { return nil, err } } } if opt.IPv6 { var dualStackSubnets []api.ClusterSubnetSpec for _, s := range cluster.Spec.Networking.Subnets { if s.Type != api.SubnetTypePrivate { continue } subnet := api.ClusterSubnetSpec{ Name: "dualstack-" + s.Name, Zone: s.Zone, Type: api.SubnetTypeDualStack, Region: s.Region, } if subnetID, ok := zoneToSubnetProviderID[s.Zone]; ok { subnet.ID = subnetID } dualStackSubnets = append(dualStackSubnets, subnet) } cluster.Spec.Networking.Subnets = append(cluster.Spec.Networking.Subnets, dualStackSubnets...) } addUtilitySubnets := true switch cluster.Spec.GetCloudProvider() { case api.CloudProviderGCE: // GCE does not need utility subnets addUtilitySubnets = false } if addUtilitySubnets { var utilitySubnets []api.ClusterSubnetSpec for _, s := range cluster.Spec.Networking.Subnets { if s.Type != api.SubnetTypePrivate { continue } subnet := api.ClusterSubnetSpec{ Name: "utility-" + s.Name, Zone: s.Zone, Type: api.SubnetTypeUtility, Region: s.Region, } if subnetID, ok := zoneToSubnetProviderID[s.Zone]; ok { subnet.ID = subnetID } utilitySubnets = append(utilitySubnets, subnet) } cluster.Spec.Networking.Subnets = append(cluster.Spec.Networking.Subnets, utilitySubnets...) } if opt.Bastion { bastionGroup := &api.InstanceGroup{} bastionGroup.Spec.Role = api.InstanceGroupRoleBastion bastionGroup.ObjectMeta.Name = "bastions" bastionGroup.Spec.MaxSize = fi.PtrTo(int32(1)) bastionGroup.Spec.MinSize = fi.PtrTo(int32(1)) bastions = append(bastions, bastionGroup) if cluster.PublishesDNSRecords() { cluster.Spec.Networking.Topology.Bastion = &api.BastionSpec{ PublicName: "bastion." + cluster.Name, } } if opt.IPv6 { for _, s := range cluster.Spec.Networking.Subnets { if s.Type == api.SubnetTypeDualStack { bastionGroup.Spec.Subnets = append(bastionGroup.Spec.Subnets, s.Name) } } } if cluster.Spec.GetCloudProvider() == api.CloudProviderGCE { bastionGroup.Spec.Zones = allZones.List() } if cluster.IsKubernetesLT("1.27") { if cluster.Spec.GetCloudProvider() == api.CloudProviderAWS { bastionGroup.Spec.InstanceMetadata = &api.InstanceMetadataOptions{ HTTPPutResponseHopLimit: fi.PtrTo(int64(1)), HTTPTokens: fi.PtrTo("required"), } } } bastionGroup.Spec.Image = opt.BastionImage } default: return nil, fmt.Errorf("invalid topology %s", opt.Topology) } if opt.IPv6 { cluster.Spec.Networking.NonMasqueradeCIDR = "::/0" cluster.Spec.ExternalCloudControllerManager = &api.CloudControllerManagerConfig{} if cluster.Spec.GetCloudProvider() == api.CloudProviderAWS { for i := range cluster.Spec.Networking.Subnets { cluster.Spec.Networking.Subnets[i].IPv6CIDR = fmt.Sprintf("/64#%x", i) } } else { klog.Errorf("IPv6 support is available only on AWS") } } err := setupDNSTopology(opt, cluster) if err != nil { return nil, err } return bastions, nil } func setupDNSTopology(opt *NewClusterOptions, cluster *api.Cluster) error { switch strings.ToLower(opt.DNSType) { case "": switch cluster.Spec.GetCloudProvider() { case api.CloudProviderHetzner, api.CloudProviderDO, api.CloudProviderAzure: // Use dns=none if not specified cluster.Spec.Networking.Topology.DNS = api.DNSTypeNone default: if cluster.UsesLegacyGossip() { cluster.Spec.Networking.Topology.DNS = api.DNSTypePrivate } else { cluster.Spec.Networking.Topology.DNS = api.DNSTypePublic } } case "public": cluster.Spec.Networking.Topology.DNS = api.DNSTypePublic case "private": cluster.Spec.Networking.Topology.DNS = api.DNSTypePrivate case "none": cluster.Spec.Networking.Topology.DNS = api.DNSTypeNone default: return fmt.Errorf("unknown DNSType: %q", opt.DNSType) } return nil } func setupAPI(opt *NewClusterOptions, cluster *api.Cluster) error { // Populate the API access, so that it can be discoverable klog.Infof("Cloud Provider ID: %q", cluster.Spec.GetCloudProvider()) if opt.APILoadBalancerType != "" || opt.APISSLCertificate != "" { cluster.Spec.API.LoadBalancer = &api.LoadBalancerAccessSpec{} } else { switch opt.Topology { case api.TopologyPublic: if cluster.UsesLegacyGossip() || cluster.UsesNoneDNS() { // gossip DNS names don't work outside the cluster, so we use a LoadBalancer instead cluster.Spec.API.LoadBalancer = &api.LoadBalancerAccessSpec{} } else { cluster.Spec.API.DNS = &api.DNSAccessSpec{} } case api.TopologyPrivate: cluster.Spec.API.LoadBalancer = &api.LoadBalancerAccessSpec{} default: return fmt.Errorf("unknown topology type: %q", opt.Topology) } } if cluster.Spec.API.LoadBalancer != nil && cluster.Spec.API.LoadBalancer.Type == "" { switch opt.APILoadBalancerType { case "", "public": cluster.Spec.API.LoadBalancer.Type = api.LoadBalancerTypePublic case "internal": cluster.Spec.API.LoadBalancer.Type = api.LoadBalancerTypeInternal default: return fmt.Errorf("unknown api-loadbalancer-type: %q", opt.APILoadBalancerType) } } if cluster.Spec.API.LoadBalancer != nil && opt.APISSLCertificate != "" { cluster.Spec.API.LoadBalancer.SSLCertificate = opt.APISSLCertificate } if cluster.Spec.API.LoadBalancer != nil && cluster.Spec.API.LoadBalancer.Class == "" && cluster.Spec.GetCloudProvider() == api.CloudProviderAWS { switch opt.APILoadBalancerClass { case "classic": klog.Warning("AWS Classic Load Balancer support for API is deprecated and should not be used for newly created clusters") cluster.Spec.API.LoadBalancer.Class = api.LoadBalancerClassClassic case "", "network": cluster.Spec.API.LoadBalancer.Class = api.LoadBalancerClassNetwork default: return fmt.Errorf("unknown api-loadbalancer-class: %q", opt.APILoadBalancerClass) } } return nil } func initializeOpenstack(opt *NewClusterOptions, cluster *api.Cluster) { if opt.APILoadBalancerType != "" { cluster.Spec.API.LoadBalancer = &api.LoadBalancerAccessSpec{} provider := "haproxy" if opt.OpenstackLBOctavia { if opt.OpenstackOctaviaProvider != "" { provider = opt.OpenstackOctaviaProvider } else { provider = "octavia" } } LbMethod := "ROUND_ROBIN" if provider == "ovn" { LbMethod = "SOURCE_IP_PORT" } cluster.Spec.CloudProvider.Openstack.Loadbalancer = &api.OpenstackLoadbalancerConfig{ FloatingNetwork: fi.PtrTo(opt.OpenstackExternalNet), Method: fi.PtrTo(LbMethod), Provider: fi.PtrTo(provider), UseOctavia: fi.PtrTo(opt.OpenstackLBOctavia), } if opt.OpenstackLBSubnet != "" { cluster.Spec.CloudProvider.Openstack.Loadbalancer.FloatingSubnet = fi.PtrTo(opt.OpenstackLBSubnet) } } // this is needed in new clusters, otherwise openstack clients will automatically try to use openstack designate if strings.ToLower(opt.DNSType) == "none" { if cluster.Spec.Networking.Topology == nil { cluster.Spec.Networking.Topology = &api.TopologySpec{ DNS: api.DNSTypeNone, } } else { cluster.Spec.Networking.Topology.DNS = api.DNSTypeNone } } if cluster.Spec.ExternalCloudControllerManager == nil { cluster.Spec.ExternalCloudControllerManager = &api.CloudControllerManagerConfig{} } cluster.Spec.ExternalCloudControllerManager.ClusterName = opt.ClusterName } func createEtcdCluster(etcdCluster string, controlPlanes []*api.InstanceGroup, encryptEtcdStorage bool, etcdStorageType string) api.EtcdClusterSpec { etcd := api.EtcdClusterSpec{ Name: etcdCluster, Manager: &api.EtcdManagerSpec{ BackupRetentionDays: fi.PtrTo[uint32](90), }, } // if this is the main cluster, we use 200 millicores by default. // otherwise we use 100 millicores by default. 100Mi is always default // for event and main clusters. This is changeable in the kops cluster // configuration. if etcd.Name == "main" { cpuRequest := resource.MustParse("200m") etcd.CPURequest = &cpuRequest } else { cpuRequest := resource.MustParse("100m") etcd.CPURequest = &cpuRequest } memoryRequest := resource.MustParse("100Mi") etcd.MemoryRequest = &memoryRequest var names []string for _, ig := range controlPlanes { name := ig.ObjectMeta.Name // We expect the IG to have a `control-plane-` or `master-` prefix, but this is both superfluous // and not how we named things previously name = strings.TrimPrefix(name, "control-plane-") name = strings.TrimPrefix(name, "master-") names = append(names, name) } names = trimCommonPrefix(names) for i, ig := range controlPlanes { m := api.EtcdMemberSpec{} if encryptEtcdStorage { m.EncryptedVolume = &encryptEtcdStorage } if len(etcdStorageType) > 0 { m.VolumeType = fi.PtrTo(etcdStorageType) } m.Name = names[i] m.InstanceGroup = fi.PtrTo(ig.ObjectMeta.Name) etcd.Members = append(etcd.Members, m) } // Cilium etcd server is not compacted by the k8s API server. if etcd.Name == "cilium" { if etcd.Manager == nil { etcd.Manager = &api.EtcdManagerSpec{ Env: []api.EnvVar{ {Name: "ETCD_AUTO_COMPACTION_MODE", Value: "revision"}, {Name: "ETCD_AUTO_COMPACTION_RETENTION", Value: "2500"}, }, } } } return etcd } func addCiliumNetwork(cluster *api.Cluster) { cilium := &api.CiliumNetworkingSpec{} cluster.Spec.Networking.Cilium = cilium cilium.EnableNodePort = true if cluster.Spec.KubeProxy == nil { cluster.Spec.KubeProxy = &api.KubeProxyConfig{} } enabled := false cluster.Spec.KubeProxy.Enabled = &enabled } // defaultImage returns the default Image, based on the cloudprovider func defaultImage(cluster *api.Cluster, channel *api.Channel, architecture architectures.Architecture) (string, error) { kubernetesVersion, err := util.ParseKubernetesVersion(cluster.Spec.KubernetesVersion) if err != nil { return "", fmt.Errorf("unable to parse kubernetes version %q", cluster.Spec.KubernetesVersion) } if channel != nil { image := channel.FindImage(cluster.Spec.GetCloudProvider(), *kubernetesVersion, architecture) if image != nil { return image.Name, nil } } if kubernetesVersion.LT(semver.MustParse("1.27.0")) { switch cluster.Spec.GetCloudProvider() { case api.CloudProviderDO: return defaultDOImageFocal, nil case api.CloudProviderHetzner: return defaultHetznerImageFocal, nil case api.CloudProviderScaleway: return defaultScalewayImageFocal, nil } } else { switch cluster.Spec.GetCloudProvider() { case api.CloudProviderDO: return defaultDOImageJammy, nil case api.CloudProviderHetzner: return defaultHetznerImageJammy, nil case api.CloudProviderScaleway: return defaultScalewayImageJammy, nil } } return "", fmt.Errorf("unable to determine default image for cloud provider %q and architecture %q", cluster.Spec.GetCloudProvider(), architecture) } func MachineArchitecture(cloud fi.Cloud, machineType string) (architectures.Architecture, error) { if machineType == "" { return architectures.ArchitectureAmd64, nil } // Some calls only have AWS initialised at this point and in other cases pass in nil as cloud. if cloud == nil { return architectures.ArchitectureAmd64, nil } switch cloud.ProviderID() { case api.CloudProviderAWS: info, err := cloud.(awsup.AWSCloud).DescribeInstanceType(machineType) if err != nil { return "", fmt.Errorf("error finding instance info for instance type %q: %w", machineType, err) } if info.ProcessorInfo == nil || len(info.ProcessorInfo.SupportedArchitectures) == 0 { return "", fmt.Errorf("unable to determine architecture info for instance type %q", machineType) } var unsupported []string for _, arch := range info.ProcessorInfo.SupportedArchitectures { // Return the first found supported architecture, in order of popularity switch fi.ValueOf(arch) { case ec2.ArchitectureTypeX8664: return architectures.ArchitectureAmd64, nil case ec2.ArchitectureTypeArm64: return architectures.ArchitectureArm64, nil default: unsupported = append(unsupported, fi.ValueOf(arch)) } } return "", fmt.Errorf("unsupported architecture for instance type %q: %v", machineType, unsupported) default: // No other clouds are known to support any other architectures at this time return architectures.ArchitectureAmd64, nil } }