mirror of https://github.com/kubernetes/kops.git
Merge pull request #14999 from zetaab/feature/oskopscontroller
Use kops-controller to boostrap nodes in OpenStack
This commit is contained in:
commit
f4780157b5
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue