mirror of https://github.com/kubernetes/kops.git
401 lines
13 KiB
Go
401 lines
13 KiB
Go
/*
|
|
Copyright 2019 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 openstackmodel
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports"
|
|
"k8s.io/klog/v2"
|
|
"k8s.io/kops/pkg/apis/kops"
|
|
"k8s.io/kops/pkg/model"
|
|
"k8s.io/kops/pkg/truncate"
|
|
"k8s.io/kops/pkg/wellknownports"
|
|
"k8s.io/kops/pkg/wellknownservices"
|
|
"k8s.io/kops/upup/pkg/fi"
|
|
"k8s.io/kops/upup/pkg/fi/cloudup/openstack"
|
|
"k8s.io/kops/upup/pkg/fi/cloudup/openstacktasks"
|
|
"k8s.io/utils/net"
|
|
)
|
|
|
|
// ServerGroupModelBuilder configures server group objects
|
|
type ServerGroupModelBuilder struct {
|
|
*OpenstackModelContext
|
|
BootstrapScriptBuilder *model.BootstrapScriptBuilder
|
|
Lifecycle fi.Lifecycle
|
|
}
|
|
|
|
var _ fi.CloudupModelBuilder = &ServerGroupModelBuilder{}
|
|
|
|
// See https://specs.openstack.org/openstack/nova-specs/specs/newton/approved/lowercase-metadata-keys.html for details
|
|
var instanceMetadataNotAllowedCharacters = regexp.MustCompile("[^a-zA-Z0-9-_:. ]")
|
|
|
|
// Constants for truncating Tags
|
|
const MAX_TAG_LENGTH_OPENSTACK = 60
|
|
|
|
var TRUNCATE_OPT = truncate.TruncateStringOptions{
|
|
MaxLength: MAX_TAG_LENGTH_OPENSTACK,
|
|
AlwaysAddHash: false,
|
|
HashLength: 6,
|
|
}
|
|
|
|
func (b *ServerGroupModelBuilder) buildAllowedAddressPairs(annotations map[string]string) []ports.AddressPair {
|
|
keyPrefix := openstack.OS_ANNOTATION + openstack.ALLOWED_ADDRESS_PAIR + "/"
|
|
|
|
var allowedAddressPairs []ports.AddressPair
|
|
for key := range annotations {
|
|
if strings.HasPrefix(key, keyPrefix) {
|
|
ipAddress, macAddress, _ := strings.Cut(annotations[key], ",")
|
|
|
|
allowedAddressPair := ports.AddressPair{
|
|
IPAddress: ipAddress,
|
|
}
|
|
if macAddress != "" {
|
|
allowedAddressPair.MACAddress = macAddress
|
|
}
|
|
|
|
allowedAddressPairs = append(allowedAddressPairs, allowedAddressPair)
|
|
}
|
|
}
|
|
|
|
sort.Slice(allowedAddressPairs, func(i, j int) bool {
|
|
return allowedAddressPairs[i].IPAddress < allowedAddressPairs[j].IPAddress
|
|
})
|
|
|
|
return allowedAddressPairs
|
|
}
|
|
|
|
func (b *ServerGroupModelBuilder) buildInstances(c *fi.CloudupModelBuilderContext, sg *openstacktasks.ServerGroup, ig *kops.InstanceGroup) error {
|
|
sshKeyNameFull, err := b.SSHKeyName()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sshKeyName := strings.ReplaceAll(sshKeyNameFull, ":", "_")
|
|
|
|
igMeta := make(map[string]string)
|
|
cloudTags, err := b.KopsModelContext.CloudTagsForInstanceGroup(ig)
|
|
if err != nil {
|
|
return fmt.Errorf("could not get cloud tags for instance group %s: %v", ig.Name, err)
|
|
}
|
|
for label, labelVal := range cloudTags {
|
|
sanitizedLabel := strings.ToLower(
|
|
instanceMetadataNotAllowedCharacters.ReplaceAllLiteralString(label, "_"),
|
|
)
|
|
igMeta[sanitizedLabel] = labelVal
|
|
}
|
|
if ig.Spec.Role != kops.InstanceGroupRoleBastion {
|
|
// Bastion does not belong to the cluster and will not be running protokube.
|
|
|
|
igMeta[openstack.TagClusterName] = b.ClusterName()
|
|
}
|
|
igMeta["k8s"] = b.ClusterName()
|
|
netName, err := b.GetNetworkName()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
igMeta[openstack.TagKopsNetwork] = netName
|
|
igMeta[openstack.TagKopsInstanceGroup] = ig.Name
|
|
igMeta[openstack.TagKopsRole] = string(ig.Spec.Role)
|
|
igMeta[openstack.INSTANCE_GROUP_GENERATION] = fmt.Sprintf("%d", ig.GetGeneration())
|
|
igMeta[openstack.CLUSTER_GENERATION] = fmt.Sprintf("%d", b.Cluster.GetGeneration())
|
|
|
|
if e, ok := ig.ObjectMeta.Annotations[openstack.OS_ANNOTATION+openstack.BOOT_FROM_VOLUME]; ok {
|
|
igMeta[openstack.BOOT_FROM_VOLUME] = e
|
|
}
|
|
|
|
if v, ok := ig.ObjectMeta.Annotations[openstack.OS_ANNOTATION+openstack.BOOT_VOLUME_SIZE]; ok {
|
|
igMeta[openstack.BOOT_VOLUME_SIZE] = v
|
|
}
|
|
|
|
startupScript, err := b.BootstrapScriptBuilder.ResourceNodeUp(c, ig)
|
|
if err != nil {
|
|
return fmt.Errorf("could not create startup script for instance group %s: %v", ig.Name, err)
|
|
}
|
|
|
|
var securityGroups []*openstacktasks.SecurityGroup
|
|
securityGroupName := b.SecurityGroupName(ig.Spec.Role)
|
|
securityGroups = append(securityGroups, b.LinkToSecurityGroup(securityGroupName))
|
|
|
|
if b.Cluster.Spec.CloudProvider.Openstack.Loadbalancer == nil && ig.Spec.Role == kops.InstanceGroupRoleControlPlane {
|
|
securityGroups = append(securityGroups, b.LinkToSecurityGroup(b.APIResourceName()))
|
|
}
|
|
|
|
r := strings.NewReplacer("_", "-", ".", "-")
|
|
groupName := r.Replace(strings.ToLower(ig.Name))
|
|
// In the future, OpenStack will use Machine API to manage groups,
|
|
// for now create d.InstanceGroups.Spec.MinSize amount of servers
|
|
for i := int32(0); i < *ig.Spec.MinSize; i++ {
|
|
// FIXME: Must ensure 63 or less characters
|
|
// replace all dots and _ with -, this is needed to get external cloudprovider working
|
|
iName := strings.ReplaceAll(strings.ToLower(fmt.Sprintf("%s-%d.%s", ig.Name, i+1, b.ClusterName())), "_", "-")
|
|
instanceName := fi.PtrTo(strings.ReplaceAll(iName, ".", "-"))
|
|
|
|
var az *string
|
|
var subnets []*openstacktasks.Subnet
|
|
havePublicSubnet := false
|
|
if len(ig.Spec.Subnets) > 0 {
|
|
subnet := ig.Spec.Subnets[int(i)%len(ig.Spec.Subnets)]
|
|
// bastion subnet name might contain a "utility-" prefix
|
|
if ig.Spec.Role == kops.InstanceGroupRoleBastion {
|
|
az = fi.PtrTo(strings.Replace(subnet, "utility-", "", 1))
|
|
} else {
|
|
az = fi.PtrTo(subnet)
|
|
}
|
|
|
|
subnetName, subnetType, err := b.findSubnetClusterSpec(subnet)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
subnets = append(subnets, b.LinkToSubnet(s(subnetName)))
|
|
if subnetType == kops.SubnetTypePublic || subnetType == kops.SubnetTypeUtility {
|
|
havePublicSubnet = true
|
|
}
|
|
}
|
|
if len(ig.Spec.Zones) > 0 {
|
|
zone := ig.Spec.Zones[int(i)%len(ig.Spec.Zones)]
|
|
az = fi.PtrTo(zone)
|
|
}
|
|
// Create instance port task
|
|
portName := fmt.Sprintf("%s-%s", "port", *instanceName)
|
|
portTagKopsName := strings.ReplaceAll(
|
|
strings.ReplaceAll(
|
|
strings.ToLower(
|
|
fmt.Sprintf("port-%s-%d", ig.Name, i+1),
|
|
),
|
|
"_", "-",
|
|
), ".", "-",
|
|
)
|
|
portTask := &openstacktasks.Port{
|
|
Name: fi.PtrTo(portName),
|
|
InstanceGroupName: &groupName,
|
|
Network: b.LinkToNetwork(),
|
|
Tags: []string{
|
|
truncate.TruncateString(fmt.Sprintf("%s=%s", openstack.TagKopsInstanceGroup, groupName), TRUNCATE_OPT),
|
|
truncate.TruncateString(fmt.Sprintf("%s=%s", openstack.TagKopsName, portTagKopsName), TRUNCATE_OPT),
|
|
truncate.TruncateString(fmt.Sprintf("%s=%s", openstack.TagClusterName, b.ClusterName()), TRUNCATE_OPT),
|
|
},
|
|
SecurityGroups: securityGroups,
|
|
AdditionalSecurityGroups: ig.Spec.AdditionalSecurityGroups,
|
|
Subnets: subnets,
|
|
AllowedAddressPairs: b.buildAllowedAddressPairs(ig.ObjectMeta.Annotations),
|
|
Lifecycle: b.Lifecycle,
|
|
}
|
|
c.AddTask(portTask)
|
|
|
|
if b.Cluster.UsesNoneDNS() && ig.Spec.Role == kops.InstanceGroupRoleControlPlane {
|
|
portTask.WellKnownServices = append(portTask.WellKnownServices, wellknownservices.KubeAPIServer)
|
|
}
|
|
|
|
metaWithName := make(map[string]string)
|
|
for k, v := range igMeta {
|
|
metaWithName[k] = v
|
|
}
|
|
metaWithName[openstack.TagKopsName] = fi.ValueOf(instanceName)
|
|
instanceTask := &openstacktasks.Instance{
|
|
Name: instanceName,
|
|
Lifecycle: b.Lifecycle,
|
|
GroupName: s(groupName),
|
|
Region: fi.PtrTo(b.Cluster.Spec.Networking.Subnets[0].Region),
|
|
Flavor: fi.PtrTo(ig.Spec.MachineType),
|
|
Image: fi.PtrTo(ig.Spec.Image),
|
|
SSHKey: fi.PtrTo(sshKeyName),
|
|
ServerGroup: sg,
|
|
Role: fi.PtrTo(string(ig.Spec.Role)),
|
|
Port: portTask,
|
|
UserData: startupScript,
|
|
Metadata: metaWithName,
|
|
SecurityGroups: ig.Spec.AdditionalSecurityGroups,
|
|
AvailabilityZone: az,
|
|
ConfigDrive: b.Cluster.Spec.CloudProvider.Openstack.Metadata.ConfigDrive,
|
|
}
|
|
c.AddTask(instanceTask)
|
|
|
|
// Associate a floating IP to the instances if we have external network in router
|
|
// and respective subnet is "Public" or "Utility".
|
|
if b.Cluster.Spec.CloudProvider.Openstack.Router != nil {
|
|
if ig.Spec.AssociatePublicIP != nil && !fi.ValueOf(ig.Spec.AssociatePublicIP) {
|
|
continue
|
|
}
|
|
if havePublicSubnet || ig.Spec.Role == kops.InstanceGroupRoleBastion {
|
|
t := &openstacktasks.FloatingIP{
|
|
Name: fi.PtrTo(fmt.Sprintf("%s-%s", "fip", *instanceTask.Name)),
|
|
Lifecycle: b.Lifecycle,
|
|
}
|
|
c.AddTask(t)
|
|
if ig.Spec.Role == kops.InstanceGroupRoleControlPlane {
|
|
// Ensure the floating IP is included in the TLS certificate,
|
|
// if we're not going to use an alias for it
|
|
t.WellKnownServices = append(t.WellKnownServices, wellknownservices.KubeAPIServer, wellknownservices.KopsController)
|
|
}
|
|
instanceTask.FloatingIP = t
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *ServerGroupModelBuilder) Build(c *fi.CloudupModelBuilderContext) error {
|
|
clusterName := b.ClusterName()
|
|
|
|
sgs := make(map[string]*openstacktasks.ServerGroup)
|
|
for _, ig := range b.InstanceGroups {
|
|
klog.V(2).Infof("Found instance group with name %s and role %v.", ig.Name, ig.Spec.Role)
|
|
affinityPolicies := []string{}
|
|
if v, ok := ig.ObjectMeta.Annotations[openstack.OS_ANNOTATION+openstack.SERVER_GROUP_AFFINITY]; ok {
|
|
affinityPolicies = append(affinityPolicies, v)
|
|
} else {
|
|
affinityPolicies = append(affinityPolicies, "anti-affinity")
|
|
}
|
|
|
|
sgName := fmt.Sprintf("%s-%s", clusterName, ig.Name)
|
|
if name, ok := ig.ObjectMeta.Annotations[openstack.OS_ANNOTATION+openstack.SERVER_GROUP_NAME]; ok {
|
|
sgName = fmt.Sprintf("%s-%s", clusterName, name)
|
|
}
|
|
|
|
sgTask, ok := sgs[sgName]
|
|
if !ok {
|
|
igMap := make(map[string]*int32)
|
|
igMap[ig.Name] = ig.Spec.MaxSize
|
|
sgTask = &openstacktasks.ServerGroup{
|
|
Name: s(sgName),
|
|
ClusterName: s(clusterName),
|
|
IGMap: igMap,
|
|
Policies: affinityPolicies,
|
|
Lifecycle: b.Lifecycle,
|
|
}
|
|
sgs[sgName] = sgTask
|
|
} else {
|
|
sgTask.IGMap[ig.Name] = ig.Spec.MaxSize
|
|
}
|
|
|
|
err := b.buildInstances(c, sgTask, ig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, s := range sgs {
|
|
c.AddTask(s)
|
|
}
|
|
|
|
if b.Cluster.Spec.CloudProvider.Openstack.Loadbalancer != nil {
|
|
var lbSubnetName string
|
|
var err error
|
|
for _, sp := range b.Cluster.Spec.Networking.Subnets {
|
|
if sp.Type == kops.SubnetTypeDualStack || sp.Type == kops.SubnetTypePrivate {
|
|
lbSubnetName, err = b.findSubnetNameByID(sp.ID, sp.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if lbSubnetName == "" {
|
|
return fmt.Errorf("could not find subnet for Kubernetes API loadbalancer")
|
|
}
|
|
|
|
lbTask := &openstacktasks.LB{
|
|
Name: fi.PtrTo(b.APIResourceName()),
|
|
Subnet: fi.PtrTo(lbSubnetName),
|
|
Lifecycle: b.Lifecycle,
|
|
}
|
|
|
|
if b.Cluster.Spec.CloudProvider.Openstack.Loadbalancer.FlavorID != nil {
|
|
lbTask.FlavorID = b.Cluster.Spec.CloudProvider.Openstack.Loadbalancer.FlavorID
|
|
}
|
|
|
|
useVIPACL := b.UseVIPACL()
|
|
if !useVIPACL {
|
|
lbTask.SecurityGroup = b.LinkToSecurityGroup(b.APIResourceName())
|
|
}
|
|
|
|
c.AddTask(lbTask)
|
|
|
|
lbfipTask := &openstacktasks.FloatingIP{
|
|
Name: fi.PtrTo(fmt.Sprintf("%s-%s", "fip", *lbTask.Name)),
|
|
LB: lbTask,
|
|
Lifecycle: b.Lifecycle,
|
|
}
|
|
c.AddTask(lbfipTask)
|
|
|
|
lbfipTask.WellKnownServices = append(lbfipTask.WellKnownServices, wellknownservices.KubeAPIServer)
|
|
|
|
poolTask := &openstacktasks.LBPool{
|
|
Name: fi.PtrTo(fmt.Sprintf("%s-https", fi.ValueOf(lbTask.Name))),
|
|
Loadbalancer: lbTask,
|
|
Lifecycle: b.Lifecycle,
|
|
}
|
|
c.AddTask(poolTask)
|
|
|
|
nameForResource := fi.ValueOf(lbTask.Name)
|
|
listenerTask := &openstacktasks.LBListener{
|
|
Name: fi.PtrTo(nameForResource),
|
|
Port: fi.PtrTo(wellknownports.KubeAPIServer),
|
|
Lifecycle: b.Lifecycle,
|
|
Pool: poolTask,
|
|
}
|
|
if useVIPACL {
|
|
var AllowedCIDRs []string
|
|
// currently kOps openstack supports only ipv4 addresses
|
|
for _, CIDR := range b.Cluster.Spec.API.Access {
|
|
if net.IsIPv4CIDRString(CIDR) {
|
|
AllowedCIDRs = append(AllowedCIDRs, CIDR)
|
|
}
|
|
}
|
|
sort.Strings(AllowedCIDRs)
|
|
listenerTask.AllowedCIDRs = AllowedCIDRs
|
|
}
|
|
c.AddTask(listenerTask)
|
|
|
|
monitorTask := &openstacktasks.PoolMonitor{
|
|
Name: fi.PtrTo(nameForResource),
|
|
Pool: poolTask,
|
|
Lifecycle: b.Lifecycle,
|
|
}
|
|
c.AddTask(monitorTask)
|
|
|
|
ifName, err := b.GetNetworkName()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, ig := range b.InstanceGroups {
|
|
if ig.Spec.Role == kops.InstanceGroupRoleControlPlane {
|
|
associateTask := &openstacktasks.PoolAssociation{
|
|
Name: fi.PtrTo(fmt.Sprintf("%s-%s", clusterName, ig.Name)),
|
|
ServerPrefix: fi.PtrTo(ig.Name),
|
|
ClusterName: s(clusterName),
|
|
Pool: poolTask,
|
|
InterfaceName: fi.PtrTo(ifName),
|
|
ProtocolPort: fi.PtrTo(wellknownports.KubeAPIServer),
|
|
Lifecycle: b.Lifecycle,
|
|
Weight: fi.PtrTo(1),
|
|
}
|
|
c.AddTask(associateTask)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
}
|