Refactor imds/gcp to no longer depend on lib/cloud/gcp (#43122)

The imds pacakges are inteneded to be very lightweight
and consumable by client tools without any additional
cloud specific dependencies. By inverting dependencies
such that lib/cloud/gcp depends on lib/cloud/imds/gcp
client tools no longer transatively import the very
large `cloud.google.com/go/compute/apiv1`.

The depguard rules were also updated to prevent any
accidental regressions.
This commit is contained in:
rosstimothy
2024-06-18 00:02:18 +00:00
committed by GitHub
parent 0168ca5859
commit 8410d1e4fb
12 changed files with 265 additions and 231 deletions
+1
View File
@@ -128,6 +128,7 @@ linters-settings:
- '**/lib/service/servicecfg/**'
- '**/lib/reversetunnelclient/**'
- '**/lib/auth/authclient/**'
- '**/lib/cloud/imds/**'
allow:
- github.com/gravitational/teleport/lib/cloud/imds
deny:
+24 -1
View File
@@ -77,6 +77,9 @@ import (
"github.com/gravitational/teleport/lib/cloud/azure"
"github.com/gravitational/teleport/lib/cloud/gcp"
"github.com/gravitational/teleport/lib/cloud/imds"
awsimds "github.com/gravitational/teleport/lib/cloud/imds/aws"
azureimds "github.com/gravitational/teleport/lib/cloud/imds/azure"
gcpimds "github.com/gravitational/teleport/lib/cloud/imds/gcp"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/utils"
)
@@ -946,7 +949,27 @@ func (c *cloudClients) initInstanceMetadata(ctx context.Context) (imds.Client, e
return c.instanceMetadata, nil
}
logrus.Debug("Initializing instance metadata client.")
client, err := DiscoverInstanceMetadata(ctx)
providers := []func(ctx context.Context) (imds.Client, error){
func(ctx context.Context) (imds.Client, error) {
clt, err := awsimds.NewInstanceMetadataClient(ctx)
return clt, trace.Wrap(err)
},
func(ctx context.Context) (imds.Client, error) {
return azureimds.NewInstanceMetadataClient(), nil
},
func(ctx context.Context) (imds.Client, error) {
instancesClient, err := gcp.NewInstancesClient(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
clt, err := gcpimds.NewInstanceMetadataClient(instancesClient)
return clt, trace.Wrap(err)
},
}
client, err := DiscoverInstanceMetadata(ctx, providers)
if err != nil {
return nil, trace.Wrap(err)
}
+83 -126
View File
@@ -47,6 +47,7 @@ import (
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/utils/sshutils"
"github.com/gravitational/teleport/lib/auth/native"
gcpimds "github.com/gravitational/teleport/lib/cloud/imds/gcp"
)
// sshUser is the user to log in as on GCP VMs.
@@ -72,18 +73,18 @@ func convertAPIError(err error) error {
return err
}
// InstanceClient is a client to interact with GCP VMs.
// InstancesClient is a client to interact with GCP VMs.
type InstancesClient interface {
// ListInstances lists the GCP VMs that belong to the given project and
// zone.
// zone supports wildcard "*".
ListInstances(ctx context.Context, projectID, zone string) ([]*Instance, error)
ListInstances(ctx context.Context, projectID, zone string) ([]*gcpimds.Instance, error)
// StreamInstances streams the GCP VMs that belong to the given project and
// zone.
// zone supports wildcard "*".
StreamInstances(ctx context.Context, projectID, zone string) stream.Stream[*Instance]
StreamInstances(ctx context.Context, projectID, zone string) stream.Stream[*gcpimds.Instance]
// GetInstance gets a GCP VM.
GetInstance(ctx context.Context, req *InstanceRequest) (*Instance, error)
GetInstance(ctx context.Context, req *gcpimds.InstanceRequest) (*gcpimds.Instance, error)
// AddSSHKey adds an SSH key to a GCP VM's metadata.
AddSSHKey(ctx context.Context, req *SSHKeyRequest) error
// RemoveSSHKey removes an SSH key from a GCP VM's metadata.
@@ -91,7 +92,7 @@ type InstancesClient interface {
// GetInstanceTags gets the GCP tags associated with an instance (which are
// distinct from its labels). It is separate from GetInstance because fetching
// tags requires its own permissions.
GetInstanceTags(ctx context.Context, req *InstanceRequest) (map[string]string, error)
GetInstanceTags(ctx context.Context, req *gcpimds.InstanceRequest) (map[string]string, error)
}
// InstancesClientConfig is the client configuration for InstancesClient.
@@ -110,34 +111,6 @@ func (c *InstancesClientConfig) CheckAndSetDefaults(ctx context.Context) (err er
return nil
}
// Instance represents a GCP VM.
type Instance struct {
// Name is the instance's name.
Name string
// Zone is the instance's zone.
Zone string
// ProjectID is the ID of the project the VM is in.
ProjectID string
// ServiceAccount is the email address of the VM's service account, if any.
ServiceAccount string
// Labels is the instance's labels.
Labels map[string]string
internalIPAddress string
externalIPAddress string
hostKeys []ssh.PublicKey
metadata *computepb.Metadata
}
// InstanceRequest formats an instance request based on an instance.
func (i *Instance) InstanceRequest() InstanceRequest {
return InstanceRequest{
ProjectID: i.ProjectID,
Zone: i.Zone,
Name: i.Name,
}
}
// NewInstancesClient creates a new InstancesClient.
func NewInstancesClient(ctx context.Context) (InstancesClient, error) {
var cfg InstancesClientConfig
@@ -171,7 +144,7 @@ func isExternalNAT(s string) bool {
return slices.Contains(externalNATKeys, s)
}
func toInstance(origInstance *computepb.Instance, projectID string) *Instance {
func toInstance(origInstance *computepb.Instance, projectID string) *gcpimds.Instance {
zoneParts := strings.Split(origInstance.GetZone(), "/")
zone := zoneParts[len(zoneParts)-1]
var internalIP string
@@ -189,14 +162,24 @@ func toInstance(origInstance *computepb.Instance, projectID string) *Instance {
}
}
}
inst := &Instance{
items := make(map[string]string, len(origInstance.GetMetadata().GetItems()))
for _, item := range origInstance.GetMetadata().GetItems() {
if item.Key == nil {
continue
}
items[item.GetKey()] = item.GetValue()
}
inst := &gcpimds.Instance{
Name: origInstance.GetName(),
ProjectID: projectID,
Zone: zone,
Labels: origInstance.GetLabels(),
internalIPAddress: internalIP,
externalIPAddress: externalIP,
metadata: origInstance.GetMetadata(),
InternalIPAddress: internalIP,
ExternalIPAddress: externalIP,
Fingerprint: origInstance.GetFingerprint(),
MetadataItems: items,
}
// GCP VMs can have at most one service account.
if len(origInstance.ServiceAccounts) > 0 {
@@ -205,8 +188,8 @@ func toInstance(origInstance *computepb.Instance, projectID string) *Instance {
return inst
}
func toInstances(origInstances []*computepb.Instance, projectID string) []*Instance {
instances := make([]*Instance, 0, len(origInstances))
func toInstances(origInstances []*computepb.Instance, projectID string) []*gcpimds.Instance {
instances := make([]*gcpimds.Instance, 0, len(origInstances))
for _, inst := range origInstances {
instances = append(instances, toInstance(inst, projectID))
}
@@ -216,26 +199,26 @@ func toInstances(origInstances []*computepb.Instance, projectID string) []*Insta
// ListInstances lists the GCP VMs that belong to the given project and
// zone.
// zone supports wildcard "*".
func (clt *instancesClient) ListInstances(ctx context.Context, projectID, zone string) ([]*Instance, error) {
func (clt *instancesClient) ListInstances(ctx context.Context, projectID, zone string) ([]*gcpimds.Instance, error) {
instances, err := stream.Collect(clt.StreamInstances(ctx, projectID, zone))
return instances, trace.Wrap(err)
}
func (clt *instancesClient) StreamInstances(ctx context.Context, projectID, zone string) stream.Stream[*Instance] {
func (clt *instancesClient) StreamInstances(ctx context.Context, projectID, zone string) stream.Stream[*gcpimds.Instance] {
if len(projectID) == 0 {
return stream.Fail[*Instance](trace.BadParameter("projectID must be set"))
return stream.Fail[*gcpimds.Instance](trace.BadParameter("projectID must be set"))
}
if len(zone) == 0 {
return stream.Fail[*Instance](trace.BadParameter("location must be set"))
return stream.Fail[*gcpimds.Instance](trace.BadParameter("location must be set"))
}
var getInstances func() ([]*Instance, error)
var getInstances func() ([]*gcpimds.Instance, error)
if zone == types.Wildcard {
it := clt.InstanceClient.AggregatedList(ctx, &computepb.AggregatedListInstancesRequest{
Project: projectID,
})
getInstances = func() ([]*Instance, error) {
getInstances = func() ([]*gcpimds.Instance, error) {
resp, err := it.Next()
if resp.Value == nil {
return nil, trace.Wrap(err)
@@ -247,16 +230,16 @@ func (clt *instancesClient) StreamInstances(ctx context.Context, projectID, zone
Project: projectID,
Zone: zone,
})
getInstances = func() ([]*Instance, error) {
getInstances = func() ([]*gcpimds.Instance, error) {
resp, err := it.Next()
if resp == nil {
return nil, trace.Wrap(convertAPIError(err))
}
return []*Instance{toInstance(resp, projectID)}, trace.Wrap(convertAPIError(err))
return []*gcpimds.Instance{toInstance(resp, projectID)}, trace.Wrap(convertAPIError(err))
}
}
return stream.PageFunc(func() ([]*Instance, error) {
return stream.PageFunc(func() ([]*gcpimds.Instance, error) {
instances, err := getInstances()
if errors.Is(err, iterator.Done) {
return instances, io.EOF
@@ -265,36 +248,8 @@ func (clt *instancesClient) StreamInstances(ctx context.Context, projectID, zone
})
}
// InstanceRequest contains parameters for making a request to a specific instance.
type InstanceRequest struct {
// ProjectID is the ID of the VM's project.
ProjectID string
// Zone is the instance's zone.
Zone string
// Name is the instance's name.
Name string
// ID is the instance's ID.
ID uint64
// WithoutHostKeys indicates that the client should not request the instance's
// host keys.
WithoutHostKeys bool
}
func (req *InstanceRequest) CheckAndSetDefaults() error {
if len(req.ProjectID) == 0 {
trace.BadParameter("projectID must be set")
}
if len(req.Zone) == 0 {
trace.BadParameter("zone must be set")
}
if len(req.Name) == 0 {
trace.BadParameter("name must be set")
}
return nil
}
// getHostKeys gets the SSH host keys from the VM, if available.
func (clt *instancesClient) getHostKeys(ctx context.Context, req *InstanceRequest) ([]ssh.PublicKey, error) {
func (clt *instancesClient) getHostKeys(ctx context.Context, req *gcpimds.InstanceRequest) ([]ssh.PublicKey, error) {
guestAttributes, err := clt.InstanceClient.GetGuestAttributes(ctx, &computepb.GetGuestAttributesInstanceRequest{
Instance: req.Name,
Project: req.ProjectID,
@@ -306,20 +261,20 @@ func (clt *instancesClient) getHostKeys(ctx context.Context, req *InstanceReques
}
items := guestAttributes.GetQueryValue().GetItems()
keys := make([]ssh.PublicKey, 0, len(items))
var errors []error
var errs []error
for _, item := range items {
key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(fmt.Sprintf("%v %v", item.GetKey(), item.GetValue())))
if err == nil {
keys = append(keys, key)
} else {
errors = append(errors, err)
errs = append(errs, err)
}
}
return keys, trace.NewAggregate(errors...)
return keys, trace.NewAggregate(errs...)
}
// GetInstance gets a GCP VM.
func (clt *instancesClient) GetInstance(ctx context.Context, req *InstanceRequest) (*Instance, error) {
func (clt *instancesClient) GetInstance(ctx context.Context, req *gcpimds.InstanceRequest) (*gcpimds.Instance, error) {
if err := req.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
@@ -337,7 +292,7 @@ func (clt *instancesClient) GetInstance(ctx context.Context, req *InstanceReques
if !req.WithoutHostKeys {
hostKeys, err := clt.getHostKeys(ctx, req)
if err == nil {
inst.hostKeys = hostKeys
inst.HostKeys = hostKeys
} else if !trace.IsNotFound(err) {
return nil, trace.Wrap(err)
}
@@ -356,7 +311,7 @@ func (clt *instancesClient) getTagBindingsClient(ctx context.Context, zone strin
}
// GetInstanceTags gets the GCP tags for the instance.
func (clt *instancesClient) GetInstanceTags(ctx context.Context, req *InstanceRequest) (map[string]string, error) {
func (clt *instancesClient) GetInstanceTags(ctx context.Context, req *gcpimds.InstanceRequest) (map[string]string, error) {
tagClient, err := clt.getTagBindingsClient(ctx, req.Zone)
if err != nil {
return nil, trace.Wrap(err)
@@ -388,7 +343,7 @@ func (clt *instancesClient) GetInstanceTags(ctx context.Context, req *InstanceRe
// SSHKeyRequest contains parameters to add/removed SSH keys from an instance.
type SSHKeyRequest struct {
// Instance is the instance to add/remove keys form.
Instance *Instance
Instance *gcpimds.Instance
// PublicKey is the key to add. Ignored when removing a key.
PublicKey ssh.PublicKey
// Expires is the expiration time of the key. Ignored when removing a key.
@@ -422,26 +377,14 @@ func formatSSHKey(pubKey ssh.PublicKey, expires time.Time) string {
// keys.
const sshKeyName = "ssh-keys"
func addSSHKey(meta *computepb.Metadata, pubKey ssh.PublicKey, expires time.Time) {
var sshKeyItem *computepb.Items
for _, item := range meta.GetItems() {
if item.GetKey() == sshKeyName {
sshKeyItem = item
break
}
}
if sshKeyItem == nil {
sshKeyItem = &computepb.Items{Key: googleapi.String(sshKeyName)}
meta.Items = append(meta.Items, sshKeyItem)
func addSSHKey(instance *gcpimds.Instance, pubKey ssh.PublicKey, expires time.Time) {
var existingKeys []string
if keys, ok := instance.MetadataItems[sshKeyName]; ok {
existingKeys = strings.Split(keys, "\n")
}
var existingKeys []string
if rawKeys := sshKeyItem.GetValue(); rawKeys != "" {
existingKeys = strings.Split(rawKeys, "\n")
}
existingKeys = append(existingKeys, formatSSHKey(pubKey, expires))
newKeys := strings.Join(existingKeys, "\n")
sshKeyItem.Value = &newKeys
instance.MetadataItems[sshKeyName] = strings.Join(existingKeys, "\n")
}
// AddSSHKey adds an SSH key to a GCP VM's metadata.
@@ -452,10 +395,11 @@ func (clt *instancesClient) AddSSHKey(ctx context.Context, req *SSHKeyRequest) e
if req.PublicKey == nil {
return trace.BadParameter("public key not set")
}
addSSHKey(req.Instance.metadata, req.PublicKey, req.Expires)
addSSHKey(req.Instance, req.PublicKey, req.Expires)
op, err := clt.InstanceClient.SetMetadata(ctx, &computepb.SetMetadataInstanceRequest{
Instance: req.Instance.Name,
MetadataResource: req.Instance.metadata,
MetadataResource: convertInstanceMetadata(req.Instance),
Project: req.Instance.ProjectID,
Zone: req.Instance.Zone,
})
@@ -468,21 +412,33 @@ func (clt *instancesClient) AddSSHKey(ctx context.Context, req *SSHKeyRequest) e
return nil
}
func removeSSHKey(meta *computepb.Metadata) {
for _, item := range meta.GetItems() {
if item.GetKey() != sshKeyName {
continue
}
existingKeys := strings.Split(item.GetValue(), "\n")
newKeys := make([]string, 0, len(existingKeys))
for _, key := range existingKeys {
if !strings.HasPrefix(key, sshUser) {
newKeys = append(newKeys, key)
}
}
item.Value = googleapi.String(strings.TrimSpace(strings.Join(newKeys, "\n")))
func removeSSHKey(instance *gcpimds.Instance) {
keys, ok := instance.MetadataItems[sshKeyName]
if !ok {
return
}
existingKeys := strings.Split(keys, "\n")
newKeys := make([]string, 0, len(existingKeys))
for _, key := range existingKeys {
if !strings.HasPrefix(key, sshUser) {
newKeys = append(newKeys, key)
}
}
instance.MetadataItems[sshKeyName] = strings.TrimSpace(strings.Join(newKeys, "\n"))
}
func convertInstanceMetadata(i *gcpimds.Instance) *computepb.Metadata {
items := make([]*computepb.Items, 0, len(i.MetadataItems))
for k, v := range i.MetadataItems {
items = append(items, &computepb.Items{Key: googleapi.String(k), Value: googleapi.String(v)})
}
return &computepb.Metadata{
Fingerprint: googleapi.String(i.Fingerprint),
Items: items,
}
}
// RemoveSSHKey removes an SSH key from a GCP VM's metadata.
@@ -490,10 +446,11 @@ func (clt *instancesClient) RemoveSSHKey(ctx context.Context, req *SSHKeyRequest
if err := req.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err)
}
removeSSHKey(req.Instance.metadata)
removeSSHKey(req.Instance)
op, err := clt.InstanceClient.SetMetadata(ctx, &computepb.SetMetadataInstanceRequest{
Instance: req.Instance.Name,
MetadataResource: req.Instance.metadata,
MetadataResource: convertInstanceMetadata(req.Instance),
Project: req.Instance.ProjectID,
Zone: req.Instance.Zone,
})
@@ -511,7 +468,7 @@ type RunCommandRequest struct {
// Client is the instance client to use.
Client InstancesClient
// InstanceRequest is the set of parameters identifying the instance.
InstanceRequest
gcpimds.InstanceRequest
// Script is the script to execute.
Script string
// SSHPort is the ssh server port to connect to. Defaults to 22.
@@ -570,16 +527,16 @@ func RunCommand(ctx context.Context, req *RunCommandRequest) error {
if err != nil {
return trace.Wrap(err)
}
if len(instance.hostKeys) == 0 {
if len(instance.HostKeys) == 0 {
return trace.NotFound(`Instance %v is missing host keys. Did you enable guest attributes on the instance?
https://cloud.google.com/solutions/connecting-securely#storing_host_keys_by_enabling_guest_attributes`, req.Name)
}
var ipAddrs []string
if instance.externalIPAddress != "" {
ipAddrs = append(ipAddrs, instance.externalIPAddress)
if instance.ExternalIPAddress != "" {
ipAddrs = append(ipAddrs, instance.ExternalIPAddress)
}
if instance.internalIPAddress != "" {
ipAddrs = append(ipAddrs, instance.internalIPAddress)
if instance.InternalIPAddress != "" {
ipAddrs = append(ipAddrs, instance.InternalIPAddress)
}
if len(ipAddrs) == 0 {
return trace.NotFound("Instance %v is missing an IP address.", req.Name)
@@ -607,7 +564,7 @@ https://cloud.google.com/solutions/connecting-securely#storing_host_keys_by_enab
}()
// Configure the SSH client.
callback, err := sshutils.HostKeyCallback(instance.hostKeys, true)
callback, err := sshutils.HostKeyCallback(instance.HostKeys, true)
if err != nil {
return trace.Wrap(err)
}
+13 -13
View File
@@ -27,7 +27,6 @@ import (
"testing"
"time"
"cloud.google.com/go/compute/apiv1/computepb"
"github.com/googleapis/gax-go/v2/apierror"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
@@ -37,6 +36,7 @@ import (
"google.golang.org/grpc/status"
"github.com/gravitational/teleport/api/internalutils/stream"
gcpimds "github.com/gravitational/teleport/lib/cloud/imds/gcp"
"github.com/gravitational/teleport/lib/sshutils"
"github.com/gravitational/teleport/lib/utils"
)
@@ -91,7 +91,7 @@ type mockInstance struct {
sshServer *sshutils.Server
execCount int
instance *Instance
instance *gcpimds.Instance
}
func newMockInstance(t *testing.T, hostSigner ssh.Signer, listener net.Listener) *mockInstance {
@@ -159,19 +159,19 @@ func (m *mockInstance) userKeyAuth(conn ssh.ConnMetadata, key ssh.PublicKey) (*s
return nil, trace.AccessDenied("unknown public key for user %q", conn.User())
}
func (m *mockInstance) ListInstances(ctx context.Context, projectID, location string) ([]*Instance, error) {
return []*Instance{m.instance}, nil
func (m *mockInstance) ListInstances(ctx context.Context, projectID, location string) ([]*gcpimds.Instance, error) {
return []*gcpimds.Instance{m.instance}, nil
}
func (m *mockInstance) StreamInstances(ctx context.Context, projectID, location string) stream.Stream[*Instance] {
func (m *mockInstance) StreamInstances(ctx context.Context, projectID, location string) stream.Stream[*gcpimds.Instance] {
return stream.Once(m.instance)
}
func (m *mockInstance) GetInstance(ctx context.Context, req *InstanceRequest) (*Instance, error) {
func (m *mockInstance) GetInstance(ctx context.Context, req *gcpimds.InstanceRequest) (*gcpimds.Instance, error) {
return m.instance, nil
}
func (m *mockInstance) GetInstanceTags(ctx context.Context, req *InstanceRequest) (map[string]string, error) {
func (m *mockInstance) GetInstanceTags(ctx context.Context, req *gcpimds.InstanceRequest) (map[string]string, error) {
return nil, nil
}
@@ -185,8 +185,8 @@ func (m *mockInstance) RemoveSSHKey(ctx context.Context, req *SSHKeyRequest) err
return nil
}
func (m *mockInstance) setInstance(inst *Instance) {
inst.hostKeys = m.hostKeys
func (m *mockInstance) setInstance(inst *gcpimds.Instance) {
inst.HostKeys = m.hostKeys
m.instance = inst
}
@@ -230,18 +230,18 @@ func TestRunCommand(t *testing.T) {
t.Cleanup(mock.Stop)
mock.hostKeys = []ssh.PublicKey{publicKey}
inst := &Instance{
inst := &gcpimds.Instance{
Name: "my-instance",
Zone: "my-zone",
ProjectID: "my-project-id",
internalIPAddress: "test.example.com",
metadata: &computepb.Metadata{},
InternalIPAddress: "test.example.com",
MetadataItems: map[string]string{},
}
mock.setInstance(inst)
require.NoError(t, RunCommand(ctx, &RunCommandRequest{
Client: mock,
InstanceRequest: InstanceRequest{
InstanceRequest: gcpimds.InstanceRequest{
ProjectID: "my-project-id",
Zone: "my-zone",
Name: "my-instance",
+6 -34
View File
@@ -24,46 +24,18 @@ import (
"github.com/gravitational/trace"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/cloud/imds"
"github.com/gravitational/teleport/lib/cloud/imds/aws"
"github.com/gravitational/teleport/lib/cloud/imds/azure"
"github.com/gravitational/teleport/lib/cloud/imds/gcp"
)
const (
// discoverInstanceMetadataTimeout is the maximum amount of time allowed
// to discover an instance metadata service. The timeout is short to
// minimize Teleport's startup time when it isn't running on any cloud
// instance. Checking for instance metadata typically takes less than 30ms.
discoverInstanceMetadataTimeout = 500 * time.Millisecond
)
type imConstructor func(ctx context.Context) (imds.Client, error)
var providers = map[types.InstanceMetadataType]imConstructor{
types.InstanceMetadataTypeEC2: initEC2,
types.InstanceMetadataTypeAzure: initAzure,
types.InstanceMetadataTypeGCP: initGCP,
}
func initEC2(ctx context.Context) (imds.Client, error) {
im, err := aws.NewInstanceMetadataClient(ctx)
return im, trace.Wrap(err)
}
func initAzure(ctx context.Context) (imds.Client, error) {
return azure.NewInstanceMetadataClient(), nil
}
func initGCP(ctx context.Context) (imds.Client, error) {
im, err := gcp.NewInstanceMetadataClient(ctx)
return im, trace.Wrap(err)
}
// discoverInstanceMetadataTimeout is the maximum amount of time allowed
// to discover an instance metadata service. The timeout is short to
// minimize Teleport's startup time when it isn't running on any cloud
// instance. Checking for instance metadata typically takes less than 30ms.
const discoverInstanceMetadataTimeout = 500 * time.Millisecond
// DiscoverInstanceMetadata checks which cloud instance type Teleport is
// running on, if any.
func DiscoverInstanceMetadata(ctx context.Context) (imds.Client, error) {
func DiscoverInstanceMetadata(ctx context.Context, providers []func(ctx context.Context) (imds.Client, error)) (imds.Client, error) {
ctx, cancel := context.WithTimeout(ctx, discoverInstanceMetadataTimeout)
defer cancel()
+76 -24
View File
@@ -27,13 +27,12 @@ import (
"net/url"
"strconv"
"strings"
"sync"
"cloud.google.com/go/compute/metadata"
"github.com/gravitational/trace"
"golang.org/x/crypto/ssh"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/cloud/gcp"
"github.com/gravitational/teleport/lib/defaults"
)
@@ -53,24 +52,80 @@ func (rt contextRoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
type metadataGetter func(ctx context.Context, path string) (string, error)
// InstanceRequest contains parameters for making a request to a specific instance.
type InstanceRequest struct {
// ProjectID is the ID of the VM's project.
ProjectID string
// Zone is the instance's zone.
Zone string
// Name is the instance's name.
Name string
// ID is the instance's ID.
ID uint64
// WithoutHostKeys indicates that the client should not request the instance's
// host keys.
WithoutHostKeys bool
}
func (req *InstanceRequest) CheckAndSetDefaults() error {
if req.ProjectID == "" {
return trace.BadParameter("projectID must be set")
}
if req.Zone == "" {
return trace.BadParameter("zone must be set")
}
if req.Name == "" {
return trace.BadParameter("name must be set")
}
return nil
}
// Instance represents a GCP VM.
type Instance struct {
// Name is the instance's name.
Name string
// Zone is the instance's zone.
Zone string
// ProjectID is the ID of the project the VM is in.
ProjectID string
// ServiceAccount is the email address of the VM's service account, if any.
ServiceAccount string
// Labels is the instance's labels.
Labels map[string]string
// InternalIPAddress is the instance's private ip.
InternalIPAddress string
// ExternalIPAddress is the instance's public ip.
ExternalIPAddress string
// HostKeys contain any public keys associated with the instance.
HostKeys []ssh.PublicKey
// Fingerprint is generated server-side and used to enforce optimistic
// locking. It must be unaltered and provided to any update requests to
// prevent requests being rejected.
Fingerprint string
// MetadataItems are key value pairs associated with the instance.
MetadataItems map[string]string
}
// InstanceGetter provides a mechanism to retrieve information about a
// particular instannce.
type InstanceGetter interface {
// GetInstance gets a GCP VM.
GetInstance(ctx context.Context, req *InstanceRequest) (*Instance, error)
// GetInstanceTags gets the GCP tags for the instance.
GetInstanceTags(ctx context.Context, req *InstanceRequest) (map[string]string, error)
}
// InstanceMetadataClient is a client for GCP instance metadata.
type InstanceMetadataClient struct {
instancesClient gcp.InstancesClient
getMetadata metadataGetter
labelPermissionErrorOnce sync.Once
tagPermissionErrorOnce sync.Once
getMetadata metadataGetter
instanceGetter InstanceGetter
}
// NewInstanceMetadataClient creates a new instance metadata client.
func NewInstanceMetadataClient(ctx context.Context) (*InstanceMetadataClient, error) {
instancesClient, err := gcp.NewInstancesClient(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
func NewInstanceMetadataClient(getter InstanceGetter) (*InstanceMetadataClient, error) {
return &InstanceMetadataClient{
instancesClient: instancesClient,
getMetadata: getMetadata,
getMetadata: getMetadata,
instanceGetter: getter,
}, nil
}
@@ -92,7 +147,7 @@ func (client *InstanceMetadataClient) getNumericID(ctx context.Context) (uint64,
return id, nil
}
// GetTags gets all of the GCP instance's labels and tags.
// GetTags gets all the GCP instance's labels and tags.
func (client *InstanceMetadataClient) GetTags(ctx context.Context) (map[string]string, error) {
// Get a bunch of info from instance metadata.
projectID, err := client.GetProjectID(ctx)
@@ -112,7 +167,7 @@ func (client *InstanceMetadataClient) GetTags(ctx context.Context) (map[string]s
return nil, trace.Wrap(err)
}
req := &gcp.InstanceRequest{
req := &InstanceRequest{
ProjectID: projectID,
Zone: zone,
Name: name,
@@ -121,23 +176,20 @@ func (client *InstanceMetadataClient) GetTags(ctx context.Context) (map[string]s
}
// Get labels.
var gcpLabels map[string]string
inst, err := client.instancesClient.GetInstance(ctx, req)
inst, err := client.instanceGetter.GetInstance(ctx, req)
if err == nil {
gcpLabels = inst.Labels
} else if trace.IsAccessDenied(err) {
client.labelPermissionErrorOnce.Do(func() {
slog.WarnContext(ctx, "Access denied to instance labels, does the instance have compute.instances.get permission?")
})
slog.WarnContext(ctx, "Access denied to instance labels, does the instance have compute.instances.get permission?")
} else {
return nil, trace.Wrap(err)
}
// Get tags.
gcpTags, err := client.instancesClient.GetInstanceTags(ctx, req)
gcpTags, err := client.instanceGetter.GetInstanceTags(ctx, req)
if trace.IsAccessDenied(err) {
client.tagPermissionErrorOnce.Do(func() {
slog.WarnContext(ctx, "Access denied to resource management tags, does the instance have compute.instances.listEffectiveTags permission?")
})
slog.WarnContext(ctx, "Access denied to resource management tags, does the instance have compute.instances.listEffectiveTags permission?")
} else if err != nil {
return nil, trace.Wrap(err)
}
+8 -10
View File
@@ -25,8 +25,6 @@ import (
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/lib/cloud/gcp"
)
func makeMetadataGetter(values map[string]string) metadataGetter {
@@ -40,18 +38,18 @@ func makeMetadataGetter(values map[string]string) metadataGetter {
}
type mockInstanceGetter struct {
gcp.InstancesClient
instance *gcp.Instance
InstanceGetter
instance *Instance
instanceErr error
tags map[string]string
tagsErr error
}
func (m *mockInstanceGetter) GetInstance(ctx context.Context, req *gcp.InstanceRequest) (*gcp.Instance, error) {
func (m *mockInstanceGetter) GetInstance(ctx context.Context, req *InstanceRequest) (*Instance, error) {
return m.instance, m.instanceErr
}
func (m *mockInstanceGetter) GetInstanceTags(ctx context.Context, req *gcp.InstanceRequest) (map[string]string, error) {
func (m *mockInstanceGetter) GetInstanceTags(ctx context.Context, req *InstanceRequest) (map[string]string, error) {
return m.tags, m.tagsErr
}
@@ -106,7 +104,7 @@ func TestIsInstanceMetadataAvailable(t *testing.T) {
if os.Getenv("TELEPORT_TEST_GCP") == "" {
t.Skip("not on gcp")
}
client, err := NewInstanceMetadataClient(context.Background())
client, err := NewInstanceMetadataClient(nil)
require.NoError(t, err)
require.True(t, client.IsAvailable(context.Background()))
})
@@ -121,7 +119,7 @@ func TestGetTags(t *testing.T) {
"instance/name": "myname",
"instance/id": "12345678",
})
defaultInstance := &gcp.Instance{
defaultInstance := &Instance{
ProjectID: "myproject",
Zone: "myzone",
Name: "myname",
@@ -203,8 +201,8 @@ func TestGetTags(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := &InstanceMetadataClient{
getMetadata: tc.getMetadata,
instancesClient: tc.instancesClient,
getMetadata: tc.getMetadata,
instanceGetter: tc.instancesClient,
}
tags, err := client.GetTags(context.Background())
tc.assertErr(t, err)
+25 -1
View File
@@ -101,6 +101,11 @@ import (
"github.com/gravitational/teleport/lib/bpf"
"github.com/gravitational/teleport/lib/cache"
"github.com/gravitational/teleport/lib/cloud"
"github.com/gravitational/teleport/lib/cloud/gcp"
"github.com/gravitational/teleport/lib/cloud/imds"
awsimds "github.com/gravitational/teleport/lib/cloud/imds/aws"
"github.com/gravitational/teleport/lib/cloud/imds/azure"
gcpimds "github.com/gravitational/teleport/lib/cloud/imds/gcp"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/events/athena"
@@ -954,7 +959,26 @@ func NewTeleport(cfg *servicecfg.Config) (*TeleportProcess, error) {
// Check if we're on a cloud instance, and if we should override the node's hostname.
imClient := cfg.InstanceMetadataClient
if imClient == nil {
imClient, err = cloud.DiscoverInstanceMetadata(supervisor.ExitContext())
providers := []func(ctx context.Context) (imds.Client, error){
func(ctx context.Context) (imds.Client, error) {
clt, err := awsimds.NewInstanceMetadataClient(ctx)
return clt, trace.Wrap(err)
},
func(ctx context.Context) (imds.Client, error) {
return azure.NewInstanceMetadataClient(), nil
},
func(ctx context.Context) (imds.Client, error) {
instancesClient, err := gcp.NewInstancesClient(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
clt, err := gcpimds.NewInstanceMetadataClient(instancesClient)
return clt, trace.Wrap(err)
},
}
imClient, err = cloud.DiscoverInstanceMetadata(supervisor.ExitContext(), providers)
if err == nil {
cfg.Logger.InfoContext(supervisor.ExitContext(),
"Found an instance metadata service. Teleport will import labels from this cloud instance.",
+4 -4
View File
@@ -48,7 +48,7 @@ import (
"github.com/gravitational/teleport/api/utils/retryutils"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/cloud"
"github.com/gravitational/teleport/lib/cloud/gcp"
gcpimds "github.com/gravitational/teleport/lib/cloud/imds/gcp"
"github.com/gravitational/teleport/lib/integrations/awsoidc"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/srv/discovery/common"
@@ -788,8 +788,8 @@ func genAzureInstancesLogStr(instances []*armcompute.VirtualMachine) string {
})
}
func genGCPInstancesLogStr(instances []*gcp.Instance) string {
return genInstancesLogStr(instances, func(i *gcp.Instance) string {
func genGCPInstancesLogStr(instances []*gcpimds.Instance) string {
return genInstancesLogStr(instances, func(i *gcpimds.Instance) string {
return i.Name
})
}
@@ -1137,7 +1137,7 @@ func (s *Server) filterExistingGCPNodes(instances *server.GCPInstances) {
_, nameOK := labels[types.NameLabel]
return projectIDOK && zoneOK && nameOK
})
var filtered []*gcp.Instance
var filtered []*gcpimds.Instance
outer:
for _, inst := range instances.Instances {
for _, node := range nodes {
+11 -10
View File
@@ -77,6 +77,7 @@ import (
"github.com/gravitational/teleport/lib/cloud"
"github.com/gravitational/teleport/lib/cloud/azure"
"github.com/gravitational/teleport/lib/cloud/gcp"
gcpimds "github.com/gravitational/teleport/lib/cloud/imds/gcp"
"github.com/gravitational/teleport/lib/cloud/mocks"
libevents "github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/modules"
@@ -2526,22 +2527,22 @@ func TestAzureVMDiscovery(t *testing.T) {
}
type mockGCPClient struct {
vms []*gcp.Instance
vms []*gcpimds.Instance
}
func (m *mockGCPClient) ListInstances(_ context.Context, _, _ string) ([]*gcp.Instance, error) {
func (m *mockGCPClient) ListInstances(_ context.Context, _, _ string) ([]*gcpimds.Instance, error) {
return m.vms, nil
}
func (m *mockGCPClient) StreamInstances(_ context.Context, _, _ string) stream.Stream[*gcp.Instance] {
func (m *mockGCPClient) StreamInstances(_ context.Context, _, _ string) stream.Stream[*gcpimds.Instance] {
return stream.Slice(m.vms)
}
func (m *mockGCPClient) GetInstance(_ context.Context, _ *gcp.InstanceRequest) (*gcp.Instance, error) {
func (m *mockGCPClient) GetInstance(_ context.Context, _ *gcpimds.InstanceRequest) (*gcpimds.Instance, error) {
return nil, trace.NotFound("disabled for test")
}
func (m *mockGCPClient) GetInstanceTags(_ context.Context, _ *gcp.InstanceRequest) (map[string]string, error) {
func (m *mockGCPClient) GetInstanceTags(_ context.Context, _ *gcpimds.InstanceRequest) (map[string]string, error) {
return nil, nil
}
@@ -2606,7 +2607,7 @@ func TestGCPVMDiscovery(t *testing.T) {
tests := []struct {
name string
presentVMs []types.Server
foundGCPVMs []*gcp.Instance
foundGCPVMs []*gcpimds.Instance
discoveryConfig *discoveryconfig.DiscoveryConfig
staticMatchers Matchers
wantInstalledInstances []string
@@ -2614,7 +2615,7 @@ func TestGCPVMDiscovery(t *testing.T) {
{
name: "no nodes present, 1 found",
presentVMs: []types.Server{},
foundGCPVMs: []*gcp.Instance{
foundGCPVMs: []*gcpimds.Instance{
{
ProjectID: "myproject",
Zone: "myzone",
@@ -2644,7 +2645,7 @@ func TestGCPVMDiscovery(t *testing.T) {
},
},
staticMatchers: defaultStaticMatcher,
foundGCPVMs: []*gcp.Instance{
foundGCPVMs: []*gcpimds.Instance{
{
ProjectID: "myproject",
Zone: "myzone",
@@ -2672,7 +2673,7 @@ func TestGCPVMDiscovery(t *testing.T) {
},
},
staticMatchers: defaultStaticMatcher,
foundGCPVMs: []*gcp.Instance{
foundGCPVMs: []*gcpimds.Instance{
{
ProjectID: "myproject",
Zone: "myzone",
@@ -2687,7 +2688,7 @@ func TestGCPVMDiscovery(t *testing.T) {
{
name: "no nodes present, 1 found usind dynamic matchers",
presentVMs: []types.Server{},
foundGCPVMs: []*gcp.Instance{
foundGCPVMs: []*gcpimds.Instance{
{
ProjectID: "myproject",
Zone: "myzone",
+8 -3
View File
@@ -28,6 +28,7 @@ import (
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/lib/cloud/gcp"
gcpimds "github.com/gravitational/teleport/lib/cloud/imds/gcp"
)
// GCPInstaller handles running commands that install Teleport on GCP
@@ -40,7 +41,7 @@ type GCPInstaller struct {
// virtual machines.
type GCPRunRequest struct {
Client gcp.InstancesClient
Instances []*gcp.Instance
Instances []*gcpimds.Instance
Params []string
Zone string
ProjectID string
@@ -60,8 +61,12 @@ func (gi *GCPInstaller) Run(ctx context.Context, req GCPRunRequest) error {
inst := inst
g.Go(func() error {
runRequest := gcp.RunCommandRequest{
Client: req.Client,
InstanceRequest: inst.InstanceRequest(),
Client: req.Client,
InstanceRequest: gcpimds.InstanceRequest{
ProjectID: inst.ProjectID,
Zone: inst.Zone,
Name: inst.Name,
},
Script: getGCPInstallerScript(
req.ScriptName,
req.PublicProxyAddr,
+6 -5
View File
@@ -30,6 +30,7 @@ import (
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/installers"
"github.com/gravitational/teleport/lib/cloud/gcp"
gcpimds "github.com/gravitational/teleport/lib/cloud/imds/gcp"
"github.com/gravitational/teleport/lib/services"
)
@@ -50,7 +51,7 @@ type GCPInstances struct {
// Parameters are the parameters passed to the installation script
Parameters []string
// Instances is a list of discovered GCP virtual machines.
Instances []*gcp.Instance
Instances []*gcpimds.Instance
}
// MakeEvents generates MakeEvents for these instances.
@@ -143,16 +144,16 @@ func (*gcpInstanceFetcher) GetMatchingInstances(_ []types.Server, _ bool) ([]Ins
// GetInstances fetches all GCP virtual machines matching configured filters.
func (f *gcpInstanceFetcher) GetInstances(ctx context.Context, _ bool) ([]Instances, error) {
// Key by project ID, then by zone.
instanceMap := make(map[string]map[string][]*gcp.Instance)
instanceMap := make(map[string]map[string][]*gcpimds.Instance)
for _, projectID := range f.ProjectIDs {
instanceMap[projectID] = make(map[string][]*gcp.Instance)
instanceMap[projectID] = make(map[string][]*gcpimds.Instance)
for _, zone := range f.Zones {
instanceMap[projectID][zone] = make([]*gcp.Instance, 0)
instanceMap[projectID][zone] = make([]*gcpimds.Instance, 0)
vms, err := f.GCP.ListInstances(ctx, projectID, zone)
if err != nil {
return nil, trace.Wrap(err)
}
filteredVMs := make([]*gcp.Instance, 0, len(vms))
filteredVMs := make([]*gcpimds.Instance, 0, len(vms))
for _, vm := range vms {
if len(f.ServiceAccounts) > 0 && !slices.Contains(f.ServiceAccounts, vm.ServiceAccount) {
continue