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/awsup"
|
||||||
"k8s.io/kops/upup/pkg/fi/cloudup/gce/tpm/gcetpmverifier"
|
"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/hetzner"
|
||||||
|
"k8s.io/kops/upup/pkg/fi/cloudup/openstack"
|
||||||
ctrl "sigs.k8s.io/controller-runtime"
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||||
"sigs.k8s.io/yaml"
|
"sigs.k8s.io/yaml"
|
||||||
|
|
@ -127,6 +128,12 @@ func main() {
|
||||||
setupLog.Error(err, "unable to create verifier")
|
setupLog.Error(err, "unable to create verifier")
|
||||||
os.Exit(1)
|
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 {
|
} else {
|
||||||
klog.Fatalf("server cloud provider config not provided")
|
klog.Fatalf("server cloud provider config not provided")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"k8s.io/kops/upup/pkg/fi/cloudup/awsup"
|
"k8s.io/kops/upup/pkg/fi/cloudup/awsup"
|
||||||
gcetpm "k8s.io/kops/upup/pkg/fi/cloudup/gce/tpm"
|
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/hetzner"
|
||||||
|
"k8s.io/kops/upup/pkg/fi/cloudup/openstack"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
|
|
@ -64,9 +65,10 @@ type ServerOptions struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerProviderOptions struct {
|
type ServerProviderOptions struct {
|
||||||
AWS *awsup.AWSVerifierOptions `json:"aws,omitempty"`
|
AWS *awsup.AWSVerifierOptions `json:"aws,omitempty"`
|
||||||
GCE *gcetpm.TPMVerifierOptions `json:"gce,omitempty"`
|
GCE *gcetpm.TPMVerifierOptions `json:"gce,omitempty"`
|
||||||
Hetzner *hetzner.HetznerVerifierOptions `json:"hetzner,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)
|
// 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/awsup"
|
||||||
"k8s.io/kops/upup/pkg/fi/cloudup/gce/tpm/gcetpmsigner"
|
"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/hetzner"
|
||||||
|
"k8s.io/kops/upup/pkg/fi/cloudup/openstack"
|
||||||
"k8s.io/kops/upup/pkg/fi/nodeup/nodetasks"
|
"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.
|
// instead we use this as a check that protokube has now started.
|
||||||
case kops.CloudProviderHetzner:
|
case kops.CloudProviderHetzner:
|
||||||
authenticator, err = hetzner.NewHetznerAuthenticator()
|
authenticator, err = hetzner.NewHetznerAuthenticator()
|
||||||
|
case kops.CloudProviderOpenstack:
|
||||||
|
authenticator, err = openstack.NewOpenstackAuthenticator()
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported cloud provider for authenticator %q", b.BootConfig.CloudProvider)
|
return fmt.Errorf("unsupported cloud provider for authenticator %q", b.BootConfig.CloudProvider)
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ func UseKopsControllerForNodeBootstrap(cluster *kops.Cluster) bool {
|
||||||
return true
|
return true
|
||||||
case kops.CloudProviderHetzner:
|
case kops.CloudProviderHetzner:
|
||||||
return true
|
return true
|
||||||
|
case kops.CloudProviderOpenstack:
|
||||||
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
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
|
// credentials needed always in control-plane and when using gossip also in nodes
|
||||||
passEnvs := true
|
passEnvs := false
|
||||||
if !strings.HasPrefix(cluster.Spec.ConfigBase, "swift://") && cluster.UsesNoneDNS() && !b.ig.IsControlPlane() {
|
if b.ig.IsControlPlane() || cluster.IsGossip() {
|
||||||
passEnvs = false
|
passEnvs = true
|
||||||
}
|
}
|
||||||
// Pass in required credentials when using user-defined swift endpoint
|
// Pass in required credentials when using user-defined swift endpoint
|
||||||
if os.Getenv("OS_AUTH_URL") != "" && passEnvs {
|
if os.Getenv("OS_AUTH_URL") != "" && passEnvs {
|
||||||
|
|
|
||||||
|
|
@ -497,6 +497,24 @@ func (b *FirewallModelBuilder) addCNIRules(c *fi.CloudupModelBuilderContext, sgM
|
||||||
return nil
|
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
|
// addProtokubeRules - Add rules for protokube if gossip DNS is enabled
|
||||||
func (b *FirewallModelBuilder) addProtokubeRules(c *fi.CloudupModelBuilderContext, sgMap map[string]*openstacktasks.SecurityGroup) error {
|
func (b *FirewallModelBuilder) addProtokubeRules(c *fi.CloudupModelBuilderContext, sgMap map[string]*openstacktasks.SecurityGroup) error {
|
||||||
if b.Cluster.IsGossip() {
|
if b.Cluster.IsGossip() {
|
||||||
|
|
@ -668,6 +686,8 @@ func (b *FirewallModelBuilder) Build(c *fi.CloudupModelBuilderContext) error {
|
||||||
b.addNodeExporterAndOccmRules(c, sgMap)
|
b.addNodeExporterAndOccmRules(c, sgMap)
|
||||||
// Protokube Rules
|
// Protokube Rules
|
||||||
b.addProtokubeRules(c, sgMap)
|
b.addProtokubeRules(c, sgMap)
|
||||||
|
// Kops-controller Rules
|
||||||
|
b.addKopsControllerRules(c, sgMap)
|
||||||
// Allow necessary local traffic
|
// Allow necessary local traffic
|
||||||
b.addCNIRules(c, sgMap)
|
b.addCNIRules(c, sgMap)
|
||||||
// ETCD Leader Election
|
// ETCD Leader Election
|
||||||
|
|
|
||||||
|
|
@ -17,65 +17,21 @@ limitations under the License.
|
||||||
package protokube
|
package protokube
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
"k8s.io/kops/protokube/pkg/gossip"
|
"k8s.io/kops/protokube/pkg/gossip"
|
||||||
gossipos "k8s.io/kops/protokube/pkg/gossip/openstack"
|
gossipos "k8s.io/kops/protokube/pkg/gossip/openstack"
|
||||||
"k8s.io/kops/upup/pkg/fi/cloudup/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
|
// OpenStackCloudProvider is the CloudProvider implementation for OpenStack
|
||||||
type OpenStackCloudProvider struct {
|
type OpenStackCloudProvider struct {
|
||||||
cloud openstack.OpenstackCloud
|
cloud openstack.OpenstackCloud
|
||||||
|
|
||||||
meta *InstanceMetadata
|
meta *openstack.InstanceMetadata
|
||||||
|
|
||||||
clusterName string
|
clusterName string
|
||||||
project string
|
project string
|
||||||
|
|
@ -84,149 +40,11 @@ type OpenStackCloudProvider struct {
|
||||||
storageZone string
|
storageZone string
|
||||||
}
|
}
|
||||||
|
|
||||||
type MetadataService struct {
|
|
||||||
serviceURL string
|
|
||||||
configDrivePath string
|
|
||||||
mounter *mount.SafeFormatAndMount
|
|
||||||
mountTarget string
|
|
||||||
searchOrder string
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ CloudProvider = &OpenStackCloudProvider{}
|
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
|
// NewOpenStackCloudProvider builds a OpenStackCloudProvider
|
||||||
func NewOpenStackCloudProvider() (*OpenStackCloudProvider, error) {
|
func NewOpenStackCloudProvider() (*OpenStackCloudProvider, error) {
|
||||||
metadata, err := getLocalMetadata()
|
metadata, err := openstack.GetLocalMetadata()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to get server metadata: %v", err)
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package protokube
|
package openstack
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"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
|
find.ID = actual.ID
|
||||||
actual.InstanceGroupName = find.InstanceGroupName
|
actual.InstanceGroupName = find.InstanceGroupName
|
||||||
actual.AdditionalSecurityGroups = find.AdditionalSecurityGroups
|
actual.AdditionalSecurityGroups = find.AdditionalSecurityGroups
|
||||||
|
actual.ForAPIServer = find.ForAPIServer
|
||||||
}
|
}
|
||||||
return actual, nil
|
return actual, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -716,6 +716,9 @@ func (tf *TemplateFunctions) KopsControllerConfig() (string, error) {
|
||||||
case kops.CloudProviderHetzner:
|
case kops.CloudProviderHetzner:
|
||||||
config.Server.Provider.Hetzner = &hetzner.HetznerVerifierOptions{}
|
config.Server.Provider.Hetzner = &hetzner.HetznerVerifierOptions{}
|
||||||
|
|
||||||
|
case kops.CloudProviderOpenstack:
|
||||||
|
config.Server.Provider.OpenStack = &openstack.OpenStackVerifierOptions{}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("unsupported cloud provider %s", cluster.Spec.GetCloudProvider())
|
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/gcediscovery"
|
||||||
"k8s.io/kops/upup/pkg/fi/cloudup/gce/tpm/gcetpmsigner"
|
"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/hetzner"
|
||||||
|
"k8s.io/kops/upup/pkg/fi/cloudup/openstack"
|
||||||
"k8s.io/kops/upup/pkg/fi/nodeup/local"
|
"k8s.io/kops/upup/pkg/fi/nodeup/local"
|
||||||
"k8s.io/kops/upup/pkg/fi/nodeup/nodetasks"
|
"k8s.io/kops/upup/pkg/fi/nodeup/nodetasks"
|
||||||
"k8s.io/kops/upup/pkg/fi/secrets"
|
"k8s.io/kops/upup/pkg/fi/secrets"
|
||||||
|
|
@ -751,6 +752,12 @@ func getNodeConfigFromServer(ctx context.Context, bootConfig *nodeup.BootConfig,
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
authenticator = a
|
authenticator = a
|
||||||
|
case api.CloudProviderOpenstack:
|
||||||
|
a, err := openstack.NewOpenstackAuthenticator()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
authenticator = a
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported cloud provider for node configuration %s", bootConfig.CloudProvider)
|
return nil, fmt.Errorf("unsupported cloud provider for node configuration %s", bootConfig.CloudProvider)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue