kops/pkg/nodeidentity/gce/identify.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
}