mirror of https://github.com/kubernetes/kops.git
232 lines
7.3 KiB
Go
232 lines
7.3 KiB
Go
/*
|
|
Copyright 2019 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package gce
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"cloud.google.com/go/compute/metadata"
|
|
compute "google.golang.org/api/compute/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/klog/v2"
|
|
"k8s.io/kops/pkg/nodeidentity"
|
|
)
|
|
|
|
// MetadataKeyInstanceGroupName is the key for the metadata that specifies the instance group name
|
|
// This is used by the gce nodeidentifier to securely identify the node instancegroup
|
|
const MetadataKeyInstanceGroupName = "kops-k8s-io-instance-group-name"
|
|
|
|
// nodeIdentifier identifies a node from GCE
|
|
type nodeIdentifier struct {
|
|
// computeService is the GCE client
|
|
computeService *compute.Service
|
|
|
|
// project is our GCE project; we require that instances be in this project
|
|
project string
|
|
}
|
|
|
|
// New creates and returns a nodeidentity.LegacyIdentifier for Nodes running on GCE
|
|
func New() (nodeidentity.LegacyIdentifier, error) {
|
|
ctx := context.Background()
|
|
|
|
computeService, err := compute.NewService(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error building compute API client: %v", err)
|
|
}
|
|
|
|
// Project ID
|
|
project := os.Getenv("GCP_PROJECT")
|
|
if project != "" {
|
|
klog.Infof("using project=%q from GCP_PROJECT env var", project)
|
|
} else {
|
|
project, err = metadata.ProjectID()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error reading project from GCE: %v", err)
|
|
}
|
|
project = strings.TrimSpace(project)
|
|
if project == "" {
|
|
return nil, fmt.Errorf("project metadata was empty")
|
|
}
|
|
klog.Infof("Found project=%q", project)
|
|
}
|
|
|
|
return &nodeIdentifier{
|
|
computeService: computeService,
|
|
project: project,
|
|
}, nil
|
|
}
|
|
|
|
// IdentifyNode queries GCE for the node identity information
|
|
func (i *nodeIdentifier) IdentifyNode(ctx context.Context, node *corev1.Node) (*nodeidentity.LegacyInfo, error) {
|
|
providerID := node.Spec.ProviderID
|
|
if providerID == "" {
|
|
return nil, fmt.Errorf("providerID was not set for node %s", node.Name)
|
|
}
|
|
if !strings.HasPrefix(providerID, "gce://") {
|
|
return nil, fmt.Errorf("providerID %q not recognized for node %s", providerID, node.Name)
|
|
}
|
|
|
|
tokens := strings.Split(strings.TrimPrefix(providerID, "gce://"), "/")
|
|
if len(tokens) != 3 {
|
|
return nil, fmt.Errorf("providerID %q not recognized for node %s", providerID, node.Name)
|
|
}
|
|
|
|
project := tokens[0]
|
|
zone := tokens[1]
|
|
instanceName := tokens[2]
|
|
|
|
if project != i.project {
|
|
return nil, fmt.Errorf("providerID %q did not match our project %q", providerID, i.project)
|
|
}
|
|
|
|
instance, err := i.getInstance(zone, instanceName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
instanceStatus := instance.Status
|
|
if instanceStatus != "RUNNING" {
|
|
return nil, fmt.Errorf("found instance %q, but status is %q", instanceName, instanceStatus)
|
|
}
|
|
|
|
// The metadata itself is potentially mutable from the instance
|
|
// We instead look at the MIG configuration
|
|
createdBy := getMetadataValue(instance.Metadata, "created-by")
|
|
if createdBy == "" {
|
|
return nil, fmt.Errorf("instance %q did not have created-by metadata label set", instanceName)
|
|
}
|
|
|
|
// We need to double-check the MIG configuration, in case created-by was changed
|
|
migName := lastComponent(createdBy)
|
|
|
|
mig, err := i.getMIG(zone, migName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// We now double check that the instance is indeed managed by the MIG
|
|
// this can't be spoofed without GCE API access
|
|
migMember, err := i.getManagedInstance(ctx, mig, instance.Id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if migMember.Version == nil {
|
|
return nil, fmt.Errorf("instance %s did not have Version set", instance.Name)
|
|
}
|
|
|
|
instanceTemplate, err := i.getInstanceTemplate(lastComponent(migMember.Version.InstanceTemplate))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
igName := getMetadataValue(instanceTemplate.Properties.Metadata, MetadataKeyInstanceGroupName)
|
|
if igName == "" {
|
|
return nil, fmt.Errorf("ig name not set on instance template %s", instanceTemplate.Name)
|
|
}
|
|
|
|
info := &nodeidentity.LegacyInfo{}
|
|
info.InstanceGroup = igName
|
|
return info, nil
|
|
}
|
|
|
|
// getInstance queries GCE for the instance with the specified name, returning an error if not found
|
|
func (i *nodeIdentifier) getInstance(zone string, instanceName string) (*compute.Instance, error) {
|
|
instance, err := i.computeService.Instances.Get(i.project, zone, instanceName).Do()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error fetching GCE instance: %v", err)
|
|
}
|
|
|
|
return instance, nil
|
|
}
|
|
|
|
// getInstanceTemplate queries GCE for the IG Template with the specified name, returning an error if not found
|
|
func (i *nodeIdentifier) getInstanceTemplate(name string) (*compute.InstanceTemplate, error) {
|
|
t, err := i.computeService.InstanceTemplates.Get(i.project, name).Do()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error fetching GCE instance group template %q: %v", name, err)
|
|
}
|
|
|
|
return t, nil
|
|
}
|
|
|
|
// getMIG queries GCE for the MIG with the specified name, returning an error if not found
|
|
func (i *nodeIdentifier) getMIG(zone string, migName string) (*compute.InstanceGroupManager, error) {
|
|
mig, err := i.computeService.InstanceGroupManagers.Get(i.project, zone, migName).Do()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error fetching GCE managed instance group %q: %v", migName, err)
|
|
}
|
|
|
|
return mig, nil
|
|
}
|
|
|
|
// getManagedInstance queries GCE for the instance from the MIG
|
|
func (i *nodeIdentifier) getManagedInstance(ctx context.Context, mig *compute.InstanceGroupManager, instanceID uint64) (*compute.ManagedInstance, error) {
|
|
var matches []*compute.ManagedInstance
|
|
|
|
filter := "id=" + strconv.FormatUint(instanceID, 10)
|
|
zone := lastComponent(mig.Zone)
|
|
if err := i.computeService.InstanceGroupManagers.ListManagedInstances(i.project, zone, mig.Name).Filter(filter).Pages(ctx, func(page *compute.InstanceGroupManagersListManagedInstancesResponse) error {
|
|
// Post-filter... filters aren't implemented (b/27605549)
|
|
for _, instance := range page.ManagedInstances {
|
|
if instance.Id != instanceID {
|
|
continue
|
|
}
|
|
matches = append(matches, instance)
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return nil, fmt.Errorf("error fetching GCE managed instance group members for %q: %v", mig.Name, err)
|
|
}
|
|
|
|
if len(matches) == 0 {
|
|
return nil, fmt.Errorf("instance %v not managed by mig %s", instanceID, mig.Name)
|
|
}
|
|
if len(matches) > 1 {
|
|
// Should be impossible - shows that filters / post-filters are not working
|
|
return nil, fmt.Errorf("found multiple instances with id %v managed by mig %s", instanceID, mig.Name)
|
|
}
|
|
|
|
return matches[0], nil
|
|
}
|
|
|
|
// lastComponent returns the last component of a URL, i.e. anything after the last slash
|
|
// If there is no slash, returns the whole string
|
|
func lastComponent(s string) string {
|
|
lastSlash := strings.LastIndex(s, "/")
|
|
if lastSlash != -1 {
|
|
s = s[lastSlash+1:]
|
|
}
|
|
return s
|
|
}
|
|
|
|
func getMetadataValue(metadata *compute.Metadata, key string) string {
|
|
value := ""
|
|
if metadata != nil {
|
|
for _, item := range metadata.Items {
|
|
if item.Key == key && item.Value != nil {
|
|
value = *item.Value
|
|
}
|
|
}
|
|
}
|
|
return value
|
|
}
|