Merge pull request #14999 from zetaab/feature/oskopscontroller

Use kops-controller to boostrap nodes in OpenStack
This commit is contained in:
Kubernetes Prow Robot 2023-01-14 07:24:12 -08:00 committed by GitHub
commit f4780157b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 421 additions and 193 deletions

View File

@ -43,6 +43,7 @@ import (
"k8s.io/kops/upup/pkg/fi/cloudup/awsup"
"k8s.io/kops/upup/pkg/fi/cloudup/gce/tpm/gcetpmverifier"
"k8s.io/kops/upup/pkg/fi/cloudup/hetzner"
"k8s.io/kops/upup/pkg/fi/cloudup/openstack"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/yaml"
@ -127,6 +128,12 @@ func main() {
setupLog.Error(err, "unable to create verifier")
os.Exit(1)
}
} else if opt.Server.Provider.OpenStack != nil {
verifier, err = openstack.NewOpenstackVerifier(opt.Server.Provider.OpenStack)
if err != nil {
setupLog.Error(err, "unable to create verifier")
os.Exit(1)
}
} else {
klog.Fatalf("server cloud provider config not provided")
}

View File

@ -20,6 +20,7 @@ import (
"k8s.io/kops/upup/pkg/fi/cloudup/awsup"
gcetpm "k8s.io/kops/upup/pkg/fi/cloudup/gce/tpm"
"k8s.io/kops/upup/pkg/fi/cloudup/hetzner"
"k8s.io/kops/upup/pkg/fi/cloudup/openstack"
)
type Options struct {
@ -64,9 +65,10 @@ type ServerOptions struct {
}
type ServerProviderOptions struct {
AWS *awsup.AWSVerifierOptions `json:"aws,omitempty"`
GCE *gcetpm.TPMVerifierOptions `json:"gce,omitempty"`
Hetzner *hetzner.HetznerVerifierOptions `json:"hetzner,omitempty"`
AWS *awsup.AWSVerifierOptions `json:"aws,omitempty"`
GCE *gcetpm.TPMVerifierOptions `json:"gce,omitempty"`
Hetzner *hetzner.HetznerVerifierOptions `json:"hetzner,omitempty"`
OpenStack *openstack.OpenStackVerifierOptions `json:"openstack,omitempty"`
}
// DiscoveryOptions configures our support for discovery, particularly gossip DNS (i.e. k8s.local)

View File

@ -30,6 +30,7 @@ import (
"k8s.io/kops/upup/pkg/fi/cloudup/awsup"
"k8s.io/kops/upup/pkg/fi/cloudup/gce/tpm/gcetpmsigner"
"k8s.io/kops/upup/pkg/fi/cloudup/hetzner"
"k8s.io/kops/upup/pkg/fi/cloudup/openstack"
"k8s.io/kops/upup/pkg/fi/nodeup/nodetasks"
)
@ -54,6 +55,8 @@ func (b BootstrapClientBuilder) Build(c *fi.NodeupModelBuilderContext) error {
// instead we use this as a check that protokube has now started.
case kops.CloudProviderHetzner:
authenticator, err = hetzner.NewHetznerAuthenticator()
case kops.CloudProviderOpenstack:
authenticator, err = openstack.NewOpenstackAuthenticator()
default:
return fmt.Errorf("unsupported cloud provider for authenticator %q", b.BootConfig.CloudProvider)

View File

@ -29,6 +29,8 @@ func UseKopsControllerForNodeBootstrap(cluster *kops.Cluster) bool {
return true
case kops.CloudProviderHetzner:
return true
case kops.CloudProviderOpenstack:
return true
default:
return false
}

View File

@ -167,10 +167,10 @@ func (b *BootstrapScript) buildEnvironmentVariables(cluster *kops.Cluster) (map[
)
}
// credentials needed always when using swift but when using None dns only in control plane
passEnvs := true
if !strings.HasPrefix(cluster.Spec.ConfigBase, "swift://") && cluster.UsesNoneDNS() && !b.ig.IsControlPlane() {
passEnvs = false
// credentials needed always in control-plane and when using gossip also in nodes
passEnvs := false
if b.ig.IsControlPlane() || cluster.IsGossip() {
passEnvs = true
}
// Pass in required credentials when using user-defined swift endpoint
if os.Getenv("OS_AUTH_URL") != "" && passEnvs {

View File

@ -497,6 +497,24 @@ func (b *FirewallModelBuilder) addCNIRules(c *fi.CloudupModelBuilderContext, sgM
return nil
}
// addKopsControllerRules - Add rules for kops-controller for node bootstrap
func (b *FirewallModelBuilder) addKopsControllerRules(c *fi.CloudupModelBuilderContext, sgMap map[string]*openstacktasks.SecurityGroup) error {
masterName := b.SecurityGroupName(kops.InstanceGroupRoleControlPlane)
nodeName := b.SecurityGroupName(kops.InstanceGroupRoleNode)
masterSG := sgMap[masterName]
nodeSG := sgMap[nodeName]
kopsControllerRule := &openstacktasks.SecurityGroupRule{
Lifecycle: b.Lifecycle,
Direction: s(string(rules.DirIngress)),
Protocol: s(string(rules.ProtocolTCP)),
EtherType: s(string(rules.EtherType4)),
PortRangeMin: i(wellknownports.KopsControllerPort),
PortRangeMax: i(wellknownports.KopsControllerPort),
}
b.addDirectionalGroupRule(c, masterSG, nodeSG, kopsControllerRule)
return nil
}
// addProtokubeRules - Add rules for protokube if gossip DNS is enabled
func (b *FirewallModelBuilder) addProtokubeRules(c *fi.CloudupModelBuilderContext, sgMap map[string]*openstacktasks.SecurityGroup) error {
if b.Cluster.IsGossip() {
@ -668,6 +686,8 @@ func (b *FirewallModelBuilder) Build(c *fi.CloudupModelBuilderContext) error {
b.addNodeExporterAndOccmRules(c, sgMap)
// Protokube Rules
b.addProtokubeRules(c, sgMap)
// Kops-controller Rules
b.addKopsControllerRules(c, sgMap)
// Allow necessary local traffic
b.addCNIRules(c, sgMap)
// ETCD Leader Election

View File

@ -17,65 +17,21 @@ limitations under the License.
package protokube
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"os"
"path"
"strings"
"k8s.io/klog/v2"
"k8s.io/kops/protokube/pkg/gossip"
gossipos "k8s.io/kops/protokube/pkg/gossip/openstack"
"k8s.io/kops/upup/pkg/fi/cloudup/openstack"
"k8s.io/mount-utils"
utilexec "k8s.io/utils/exec"
)
const (
// MetadataLatestPath is the path to the metadata on the config drive
MetadataLatestPath string = "openstack/latest/meta_data.json"
// MetadataID is the identifier for the metadata service
MetadataID string = "metadataService"
// MetadataLastestServiceURL points to the latest metadata of the metadata service
MetadataLatestServiceURL string = "http://169.254.169.254/" + MetadataLatestPath
// ConfigDriveID is the identifier for the config drive containing metadata
ConfigDriveID string = "configDrive"
// ConfigDriveLabel identifies the config drive by label on the OS
ConfigDriveLabel string = "config-2"
// DefaultMetadataSearchOrder defines the default order in which the metadata services are queried
DefaultMetadataSearchOrder string = ConfigDriveID + ", " + MetadataID
DiskByLabelPath string = "/dev/disk/by-label/"
)
type Metadata struct {
// Matches openstack.TagClusterName
ClusterName string `json:"KubernetesCluster"`
}
type InstanceMetadata struct {
Name string `json:"name"`
UserMeta *Metadata `json:"meta"`
ProjectID string `json:"project_id"`
AvailabilityZone string `json:"availability_zone"`
Hostname string `json:"hostname"`
ServerID string `json:"uuid"`
}
// OpenStackCloudProvider is the CloudProvider implementation for OpenStack
type OpenStackCloudProvider struct {
cloud openstack.OpenstackCloud
meta *InstanceMetadata
meta *openstack.InstanceMetadata
clusterName string
project string
@ -84,149 +40,11 @@ type OpenStackCloudProvider struct {
storageZone string
}
type MetadataService struct {
serviceURL string
configDrivePath string
mounter *mount.SafeFormatAndMount
mountTarget string
searchOrder string
}
var _ CloudProvider = &OpenStackCloudProvider{}
// getFromConfigDrive tries to get metadata by mounting a config drive and returns it as InstanceMetadata
// It will return an error if there is no disk labelled as ConfigDriveLabel or other errors while mounting the disk, or reading the file occur.
func (mds MetadataService) getFromConfigDrive() (*InstanceMetadata, error) {
dev := path.Join(DiskByLabelPath, ConfigDriveLabel)
if _, err := os.Stat(dev); os.IsNotExist(err) {
out, err := mds.mounter.Exec.Command(
"blkid", "-l",
"-t", fmt.Sprintf("LABEL=%s", ConfigDriveLabel),
"-o", "device",
).CombinedOutput()
if err != nil {
return nil, fmt.Errorf("unable to run blkid: %v", err)
}
dev = strings.TrimSpace(string(out))
}
err := mds.mounter.Mount(dev, mds.mountTarget, "iso9660", []string{"ro"})
if err != nil {
err = mds.mounter.Mount(dev, mds.mountTarget, "vfat", []string{"ro"})
}
if err != nil {
return nil, fmt.Errorf("error mounting configdrive '%s': %v", dev, err)
}
defer mds.mounter.Unmount(mds.mountTarget)
f, err := os.Open(
path.Join(mds.mountTarget, mds.configDrivePath))
if err != nil {
return nil, fmt.Errorf("error reading '%s' on config drive: %v", mds.configDrivePath, err)
}
defer f.Close()
return mds.parseMetadata(f)
}
// getFromMetadataService tries to get metadata from a metadata service endpoint and returns it as InstanceMetadata.
// If the service endpoint cannot be contacted or reports a different status than StatusOK it will return an error.
func (mds MetadataService) getFromMetadataService() (*InstanceMetadata, error) {
var client http.Client
resp, err := client.Get(mds.serviceURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return mds.parseMetadata(resp.Body)
}
err = fmt.Errorf("fetching metadata from '%s' returned status code '%d'", mds.serviceURL, resp.StatusCode)
return nil, err
}
// parseMetadata reads JSON data from a Reader and returns it as InstanceMetadata.
func (mds MetadataService) parseMetadata(r io.Reader) (*InstanceMetadata, error) {
var meta InstanceMetadata
data, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
err = json.Unmarshal(data, &meta)
if err != nil {
return nil, err
}
return &meta, nil
}
// getMetadata tries to get metadata for the instance by mounting the config drive and/or querying the metadata service endpoint.
// Depending on the searchOrder it will return data from the first source which successfully returns.
// If all the sources in searchOrder are erroneous it will propagate the last error to its caller.
func (mds MetadataService) getMetadata() (*InstanceMetadata, error) {
// Note(ederst): I used and modified code for getting the config drive metadata to work from here:
// * https://github.com/kubernetes/cloud-provider-openstack/blob/27b6fc483451b6df2112a6a4a40a34ffc9093635/pkg/util/metadata/metadata.go
var meta *InstanceMetadata
var err error
ids := strings.Split(mds.searchOrder, ",")
for _, id := range ids {
id = strings.TrimSpace(id)
switch id {
case ConfigDriveID:
meta, err = mds.getFromConfigDrive()
case MetadataID:
meta, err = mds.getFromMetadataService()
default:
err = fmt.Errorf("%s is not a valid metadata search order option. Supported options are %s and %s", id, ConfigDriveID, MetadataID)
}
if err == nil {
break
}
}
return meta, err
}
func newMetadataService(serviceURL string, configDrivePath string, mounter *mount.SafeFormatAndMount, mountTarget string, searchOrder string) *MetadataService {
return &MetadataService{
serviceURL: serviceURL,
configDrivePath: configDrivePath,
mounter: mounter,
mountTarget: mountTarget,
searchOrder: searchOrder,
}
}
// getDefaultMounter returns a mount and executor interface to use for getting metadata from a config drive
func getDefaultMounter() *mount.SafeFormatAndMount {
mounter := mount.New("")
exec := utilexec.New()
return &mount.SafeFormatAndMount{
Interface: mounter,
Exec: exec,
}
}
func getLocalMetadata() (*InstanceMetadata, error) {
mountTarget, err := ioutil.TempDir("", "configdrive")
if err != nil {
return nil, err
}
defer os.Remove(mountTarget)
return newMetadataService(MetadataLatestServiceURL, MetadataLatestPath, getDefaultMounter(), mountTarget, DefaultMetadataSearchOrder).getMetadata()
}
// NewOpenStackCloudProvider builds a OpenStackCloudProvider
func NewOpenStackCloudProvider() (*OpenStackCloudProvider, error) {
metadata, err := getLocalMetadata()
metadata, err := openstack.GetLocalMetadata()
if err != nil {
return nil, fmt.Errorf("Failed to get server metadata: %v", err)
}

View File

@ -0,0 +1,42 @@
/*
Copyright 2023 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 openstack
import (
"fmt"
"k8s.io/kops/pkg/bootstrap"
)
const OpenstackAuthenticationTokenPrefix = "x-openstack-id "
type openstackAuthenticator struct {
}
var _ bootstrap.Authenticator = &openstackAuthenticator{}
func NewOpenstackAuthenticator() (bootstrap.Authenticator, error) {
return &openstackAuthenticator{}, nil
}
func (o openstackAuthenticator) CreateToken(body []byte) (string, error) {
metadata, err := GetLocalMetadata()
if err != nil {
return "", fmt.Errorf("unable to fetch metadata: %w", err)
}
return OpenstackAuthenticationTokenPrefix + metadata.ServerID, nil
}

View File

@ -0,0 +1,206 @@
/*
Copyright 2023 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 openstack
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path"
"strings"
"k8s.io/mount-utils"
utilexec "k8s.io/utils/exec"
)
const (
// MetadataLatestPath is the path to the metadata on the config drive
MetadataLatestPath string = "openstack/latest/meta_data.json"
// MetadataID is the identifier for the metadata service
MetadataID string = "metadataService"
// MetadataLastestServiceURL points to the latest metadata of the metadata service
MetadataLatestServiceURL string = "http://169.254.169.254/" + MetadataLatestPath
// ConfigDriveID is the identifier for the config drive containing metadata
ConfigDriveID string = "configDrive"
// ConfigDriveLabel identifies the config drive by label on the OS
ConfigDriveLabel string = "config-2"
// DefaultMetadataSearchOrder defines the default order in which the metadata services are queried
DefaultMetadataSearchOrder string = ConfigDriveID + ", " + MetadataID
DiskByLabelPath string = "/dev/disk/by-label/"
)
type Metadata struct {
// Matches openstack.TagClusterName
ClusterName string `json:"KubernetesCluster"`
}
type InstanceMetadata struct {
Name string `json:"name"`
UserMeta *Metadata `json:"meta"`
ProjectID string `json:"project_id"`
AvailabilityZone string `json:"availability_zone"`
Hostname string `json:"hostname"`
ServerID string `json:"uuid"`
}
type MetadataService struct {
serviceURL string
configDrivePath string
mounter *mount.SafeFormatAndMount
mountTarget string
searchOrder string
}
func newMetadataService(serviceURL string, configDrivePath string, mounter *mount.SafeFormatAndMount, mountTarget string, searchOrder string) *MetadataService {
return &MetadataService{
serviceURL: serviceURL,
configDrivePath: configDrivePath,
mounter: mounter,
mountTarget: mountTarget,
searchOrder: searchOrder,
}
}
// GetLocalMetadata returns a local metadata for the server
func GetLocalMetadata() (*InstanceMetadata, error) {
mountTarget, err := ioutil.TempDir("", "configdrive")
if err != nil {
return nil, err
}
defer os.Remove(mountTarget)
return newMetadataService(MetadataLatestServiceURL, MetadataLatestPath, getDefaultMounter(), mountTarget, DefaultMetadataSearchOrder).getMetadata()
}
// getFromConfigDrive tries to get metadata by mounting a config drive and returns it as InstanceMetadata
// It will return an error if there is no disk labelled as ConfigDriveLabel or other errors while mounting the disk, or reading the file occur.
func (mds MetadataService) getFromConfigDrive() (*InstanceMetadata, error) {
dev := path.Join(DiskByLabelPath, ConfigDriveLabel)
if _, err := os.Stat(dev); os.IsNotExist(err) {
out, err := mds.mounter.Exec.Command(
"blkid", "-l",
"-t", fmt.Sprintf("LABEL=%s", ConfigDriveLabel),
"-o", "device",
).CombinedOutput()
if err != nil {
return nil, fmt.Errorf("unable to run blkid: %v", err)
}
dev = strings.TrimSpace(string(out))
}
err := mds.mounter.Mount(dev, mds.mountTarget, "iso9660", []string{"ro"})
if err != nil {
err = mds.mounter.Mount(dev, mds.mountTarget, "vfat", []string{"ro"})
}
if err != nil {
return nil, fmt.Errorf("error mounting configdrive '%s': %v", dev, err)
}
defer mds.mounter.Unmount(mds.mountTarget)
f, err := os.Open(
path.Join(mds.mountTarget, mds.configDrivePath))
if err != nil {
return nil, fmt.Errorf("error reading '%s' on config drive: %v", mds.configDrivePath, err)
}
defer f.Close()
return mds.parseMetadata(f)
}
// getFromMetadataService tries to get metadata from a metadata service endpoint and returns it as InstanceMetadata.
// If the service endpoint cannot be contacted or reports a different status than StatusOK it will return an error.
func (mds MetadataService) getFromMetadataService() (*InstanceMetadata, error) {
var client http.Client
resp, err := client.Get(mds.serviceURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return mds.parseMetadata(resp.Body)
}
err = fmt.Errorf("fetching metadata from '%s' returned status code '%d'", mds.serviceURL, resp.StatusCode)
return nil, err
}
// parseMetadata reads JSON data from a Reader and returns it as InstanceMetadata.
func (mds MetadataService) parseMetadata(r io.Reader) (*InstanceMetadata, error) {
var meta InstanceMetadata
data, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
err = json.Unmarshal(data, &meta)
if err != nil {
return nil, err
}
return &meta, nil
}
// getMetadata tries to get metadata for the instance by mounting the config drive and/or querying the metadata service endpoint.
// Depending on the searchOrder it will return data from the first source which successfully returns.
// If all the sources in searchOrder are erroneous it will propagate the last error to its caller.
func (mds MetadataService) getMetadata() (*InstanceMetadata, error) {
// Note(ederst): I used and modified code for getting the config drive metadata to work from here:
// * https://github.com/kubernetes/cloud-provider-openstack/blob/27b6fc483451b6df2112a6a4a40a34ffc9093635/pkg/util/metadata/metadata.go
var meta *InstanceMetadata
var err error
ids := strings.Split(mds.searchOrder, ",")
for _, id := range ids {
id = strings.TrimSpace(id)
switch id {
case ConfigDriveID:
meta, err = mds.getFromConfigDrive()
case MetadataID:
meta, err = mds.getFromMetadataService()
default:
err = fmt.Errorf("%s is not a valid metadata search order option. Supported options are %s and %s", id, ConfigDriveID, MetadataID)
}
if err == nil {
break
}
}
return meta, err
}
// getDefaultMounter returns a mount and executor interface to use for getting metadata from a config drive
func getDefaultMounter() *mount.SafeFormatAndMount {
mounter := mount.New("")
exec := utilexec.New()
return &mount.SafeFormatAndMount{
Interface: mounter,
Exec: exec,
}
}

View File

@ -1,5 +1,5 @@
/*
Copyright 2022 The Kubernetes Authors.
Copyright 2023 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.
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package protokube
package openstack
import (
"fmt"

View File

@ -0,0 +1,117 @@
/*
Copyright 2023 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 openstack
import (
"context"
"fmt"
"os"
"strings"
"github.com/gophercloud/gophercloud"
gos "github.com/gophercloud/gophercloud/openstack"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/mitchellh/mapstructure"
"k8s.io/klog/v2"
"k8s.io/kops/pkg/bootstrap"
)
type OpenStackVerifierOptions struct {
}
type openstackVerifier struct {
novaClient *gophercloud.ServiceClient
}
var _ bootstrap.Verifier = &openstackVerifier{}
func NewOpenstackVerifier(opt *OpenStackVerifierOptions) (bootstrap.Verifier, error) {
env, err := gos.AuthOptionsFromEnv()
if err != nil {
return nil, err
}
region := os.Getenv("OS_REGION_NAME")
if region == "" {
return nil, fmt.Errorf("unable to find region")
}
provider, err := gos.NewClient(env.IdentityEndpoint)
if err != nil {
return nil, err
}
ua := gophercloud.UserAgent{}
ua.Prepend("kops/kopscontrollerverifier")
provider.UserAgent = ua
klog.V(4).Infof("Using user-agent %s", ua.Join())
// node-controller should be able to renew it tokens against OpenStack API
env.AllowReauth = true
err = gos.Authenticate(provider, env)
if err != nil {
return nil, err
}
novaClient, err := gos.NewComputeV2(provider, gophercloud.EndpointOpts{
Type: "compute",
Region: region,
})
if err != nil {
return nil, fmt.Errorf("error building nova client: %v", err)
}
return &openstackVerifier{
novaClient: novaClient,
}, nil
}
func (o openstackVerifier) VerifyToken(ctx context.Context, token string, body []byte, useInstanceIDForNodeName bool) (*bootstrap.VerifyResult, error) {
if !strings.HasPrefix(token, OpenstackAuthenticationTokenPrefix) {
return nil, fmt.Errorf("incorrect authorization type")
}
serverID := strings.TrimPrefix(token, OpenstackAuthenticationTokenPrefix)
instance, err := servers.Get(o.novaClient, serverID).Extract()
if err != nil {
return nil, fmt.Errorf("failed to get info for server %q: %w", token, err)
}
var addrs []string
var addresses map[string][]Address
err = mapstructure.Decode(instance.Addresses, &addresses)
if err != nil {
return nil, fmt.Errorf("unable to decode addresses: %w", err)
}
for _, addrList := range addresses {
for _, props := range addrList {
addrs = append(addrs, props.Addr)
}
}
result := &bootstrap.VerifyResult{
NodeName: instance.Name,
CertificateNames: addrs,
}
value, ok := instance.Metadata[TagKopsInstanceGroup]
if ok {
result.InstanceGroupName = value
}
return result, nil
}

View File

@ -163,6 +163,7 @@ func newPortTaskFromCloud(cloud openstack.OpenstackCloud, lifecycle fi.Lifecycle
find.ID = actual.ID
actual.InstanceGroupName = find.InstanceGroupName
actual.AdditionalSecurityGroups = find.AdditionalSecurityGroups
actual.ForAPIServer = find.ForAPIServer
}
return actual, nil
}

View File

@ -716,6 +716,9 @@ func (tf *TemplateFunctions) KopsControllerConfig() (string, error) {
case kops.CloudProviderHetzner:
config.Server.Provider.Hetzner = &hetzner.HetznerVerifierOptions{}
case kops.CloudProviderOpenstack:
config.Server.Provider.OpenStack = &openstack.OpenStackVerifierOptions{}
default:
return "", fmt.Errorf("unsupported cloud provider %s", cluster.Spec.GetCloudProvider())
}

View File

@ -54,6 +54,7 @@ import (
"k8s.io/kops/upup/pkg/fi/cloudup/gce/gcediscovery"
"k8s.io/kops/upup/pkg/fi/cloudup/gce/tpm/gcetpmsigner"
"k8s.io/kops/upup/pkg/fi/cloudup/hetzner"
"k8s.io/kops/upup/pkg/fi/cloudup/openstack"
"k8s.io/kops/upup/pkg/fi/nodeup/local"
"k8s.io/kops/upup/pkg/fi/nodeup/nodetasks"
"k8s.io/kops/upup/pkg/fi/secrets"
@ -751,6 +752,12 @@ func getNodeConfigFromServer(ctx context.Context, bootConfig *nodeup.BootConfig,
return nil, err
}
authenticator = a
case api.CloudProviderOpenstack:
a, err := openstack.NewOpenstackAuthenticator()
if err != nil {
return nil, err
}
authenticator = a
default:
return nil, fmt.Errorf("unsupported cloud provider for node configuration %s", bootConfig.CloudProvider)
}