diff --git a/cmd/kops-controller/main.go b/cmd/kops-controller/main.go index e41cb37724..b0224743e8 100644 --- a/cmd/kops-controller/main.go +++ b/cmd/kops-controller/main.go @@ -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") } diff --git a/cmd/kops-controller/pkg/config/options.go b/cmd/kops-controller/pkg/config/options.go index b75600eb96..2eeb32b862 100644 --- a/cmd/kops-controller/pkg/config/options.go +++ b/cmd/kops-controller/pkg/config/options.go @@ -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) diff --git a/nodeup/pkg/model/bootstrap_client.go b/nodeup/pkg/model/bootstrap_client.go index 81ccbf4ef4..9974840bdd 100644 --- a/nodeup/pkg/model/bootstrap_client.go +++ b/nodeup/pkg/model/bootstrap_client.go @@ -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.CloudProvider) diff --git a/pkg/apis/kops/model/features.go b/pkg/apis/kops/model/features.go index 0fc9f9659d..01885f9471 100644 --- a/pkg/apis/kops/model/features.go +++ b/pkg/apis/kops/model/features.go @@ -29,6 +29,8 @@ func UseKopsControllerForNodeBootstrap(cluster *kops.Cluster) bool { return cluster.IsKubernetesGTE("1.22") case kops.CloudProviderHetzner: return true + case kops.CloudProviderOpenstack: + return true default: return false } diff --git a/pkg/model/bootstrapscript.go b/pkg/model/bootstrapscript.go index 364f4e4f43..d5ef695990 100644 --- a/pkg/model/bootstrapscript.go +++ b/pkg/model/bootstrapscript.go @@ -168,10 +168,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 { diff --git a/pkg/model/openstackmodel/firewall.go b/pkg/model/openstackmodel/firewall.go index 46c575b2d4..6dea265f1a 100644 --- a/pkg/model/openstackmodel/firewall.go +++ b/pkg/model/openstackmodel/firewall.go @@ -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 diff --git a/protokube/pkg/protokube/openstack_volume.go b/protokube/pkg/protokube/openstack_volume.go index ddcf4dd7b6..4c62eaee64 100644 --- a/protokube/pkg/protokube/openstack_volume.go +++ b/protokube/pkg/protokube/openstack_volume.go @@ -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) } diff --git a/upup/pkg/fi/cloudup/openstack/authenticator.go b/upup/pkg/fi/cloudup/openstack/authenticator.go new file mode 100644 index 0000000000..a0957020ab --- /dev/null +++ b/upup/pkg/fi/cloudup/openstack/authenticator.go @@ -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 +} diff --git a/upup/pkg/fi/cloudup/openstack/metadata.go b/upup/pkg/fi/cloudup/openstack/metadata.go new file mode 100644 index 0000000000..85fce1345b --- /dev/null +++ b/upup/pkg/fi/cloudup/openstack/metadata.go @@ -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, + } +} diff --git a/protokube/pkg/protokube/openstack_volume_test.go b/upup/pkg/fi/cloudup/openstack/metadata_test.go similarity index 99% rename from protokube/pkg/protokube/openstack_volume_test.go rename to upup/pkg/fi/cloudup/openstack/metadata_test.go index d57907cb39..96af94ae4f 100644 --- a/protokube/pkg/protokube/openstack_volume_test.go +++ b/upup/pkg/fi/cloudup/openstack/metadata_test.go @@ -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" diff --git a/protokube/pkg/protokube/testdata/metadata_drive.json b/upup/pkg/fi/cloudup/openstack/testdata/metadata_drive.json similarity index 100% rename from protokube/pkg/protokube/testdata/metadata_drive.json rename to upup/pkg/fi/cloudup/openstack/testdata/metadata_drive.json diff --git a/protokube/pkg/protokube/testdata/metadata_service.json b/upup/pkg/fi/cloudup/openstack/testdata/metadata_service.json similarity index 100% rename from protokube/pkg/protokube/testdata/metadata_service.json rename to upup/pkg/fi/cloudup/openstack/testdata/metadata_service.json diff --git a/upup/pkg/fi/cloudup/openstack/verifier.go b/upup/pkg/fi/cloudup/openstack/verifier.go new file mode 100644 index 0000000000..f4f848465d --- /dev/null +++ b/upup/pkg/fi/cloudup/openstack/verifier.go @@ -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 +} diff --git a/upup/pkg/fi/cloudup/openstacktasks/port.go b/upup/pkg/fi/cloudup/openstacktasks/port.go index 3bf33783b8..4b2b85861c 100644 --- a/upup/pkg/fi/cloudup/openstacktasks/port.go +++ b/upup/pkg/fi/cloudup/openstacktasks/port.go @@ -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 } diff --git a/upup/pkg/fi/cloudup/template_functions.go b/upup/pkg/fi/cloudup/template_functions.go index 623a198ee3..b549957a53 100644 --- a/upup/pkg/fi/cloudup/template_functions.go +++ b/upup/pkg/fi/cloudup/template_functions.go @@ -696,6 +696,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()) } diff --git a/upup/pkg/fi/nodeup/command.go b/upup/pkg/fi/nodeup/command.go index 448f970cd3..cdf43df715 100644 --- a/upup/pkg/fi/nodeup/command.go +++ b/upup/pkg/fi/nodeup/command.go @@ -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" @@ -758,6 +759,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) }