Skip to content

Commit

Permalink
[v14] [gcp] support project discovery (#47566)
Browse files Browse the repository at this point in the history
* [gcp] support project discovery

This PR extends Teleport discovery service to be able to support find all GKE and VM servers in every project a user has access to.

```
discovery_service:
  enabled: true
  discovery_group: "test"
  gcp:
  - types: ["gke"]
    locations: ["*"]
    project_ids: ["*"]
    tags:
     '*': '*'
```

* simplify docs by adding examples
  • Loading branch information
tigrato authored Oct 16, 2024
1 parent ff0d651 commit 97dc48e
Show file tree
Hide file tree
Showing 11 changed files with 482 additions and 40 deletions.
4 changes: 2 additions & 2 deletions api/types/matchers_gcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ func (m *GCPMatcher) CheckAndSetDefaults() error {
m.Locations = []string{Wildcard}
}

if slices.Contains(m.ProjectIDs, Wildcard) {
return trace.BadParameter("GCP discovery service project_ids does not support wildcards; please specify at least one value in project_ids.")
if slices.Contains(m.ProjectIDs, Wildcard) && len(m.ProjectIDs) > 1 {
return trace.BadParameter("GCP discovery service either supports wildcard project_ids or multiple values, but not both.")
}
if len(m.ProjectIDs) == 0 {
return trace.BadParameter("GCP discovery service project_ids does cannot be empty; please specify at least one value in project_ids.")
Expand Down
4 changes: 2 additions & 2 deletions api/types/matchers_gcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,12 @@ func TestGCPMatcherCheckAndSetDefaults(t *testing.T) {
errCheck: isBadParameterErr,
},
{
name: "wildcard is invalid for project ids",
name: "wildcard is valid for project ids",
in: &GCPMatcher{
Types: []string{"gce"},
ProjectIDs: []string{"*"},
},
errCheck: isBadParameterErr,
errCheck: require.NoError,
},
{
name: "invalid type",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -452,8 +452,18 @@ value, `gke`.
#### `discovery_service.gcp[0].project_ids`

In your matcher, replace `myproject` with the ID of your Google Cloud project.
The `project_ids` field must include at least one value, and it must not be the
wildcard character (`*`).

Ensure that the `project_ids` field follows these rules:
- It must include at least one value.
- It must not combine the wildcard character (`*`) with other values.

##### Examples of valid configurations
- `["p1", "p2"]`
- `["*"]`
- `["p1"]`

##### Example of an invalid configuration
- `["p1", "*"]`

#### `discovery_service.gcp[0].locations`

Expand Down
16 changes: 16 additions & 0 deletions lib/cloud/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ type GCPClients interface {
GetGCPSQLAdminClient(context.Context) (gcp.SQLAdminClient, error)
// GetGCPGKEClient returns GKE client.
GetGCPGKEClient(context.Context) (gcp.GKEClient, error)
// GetGCPProjectsClient returns Projects client.
GetGCPProjectsClient(context.Context) (gcp.ProjectsClient, error)
// GetGCPInstancesClient returns instances client.
GetGCPInstancesClient(context.Context) (gcp.InstancesClient, error)
}
Expand Down Expand Up @@ -255,6 +257,7 @@ func NewClients(opts ...ClientsOption) (Clients, error) {
gcpClients: gcpClients{
gcpSQLAdmin: newClientCache[gcp.SQLAdminClient](gcp.NewSQLAdminClient),
gcpGKE: newClientCache[gcp.GKEClient](gcp.NewGKEClient),
gcpProjects: newClientCache[gcp.ProjectsClient](gcp.NewProjectsClient),
gcpInstances: newClientCache[gcp.InstancesClient](gcp.NewInstancesClient),
},
azureClients: azClients,
Expand Down Expand Up @@ -313,6 +316,8 @@ type gcpClients struct {
gcpSQLAdmin *clientCache[gcp.SQLAdminClient]
// gcpGKE is the cached GCP Cloud GKE client.
gcpGKE *clientCache[gcp.GKEClient]
// gcpProjects is the cached GCP Cloud Projects client.
gcpProjects *clientCache[gcp.ProjectsClient]
// gcpInstances is the cached GCP instances client.
gcpInstances *clientCache[gcp.InstancesClient]
}
Expand Down Expand Up @@ -639,6 +644,11 @@ func (c *cloudClients) GetGCPGKEClient(ctx context.Context) (gcp.GKEClient, erro
return c.gcpGKE.GetClient(ctx)
}

// GetGCPProjectsClient returns Project client.
func (c *cloudClients) GetGCPProjectsClient(ctx context.Context) (gcp.ProjectsClient, error) {
return c.gcpProjects.GetClient(ctx)
}

// GetGCPInstancesClient returns instances client.
func (c *cloudClients) GetGCPInstancesClient(ctx context.Context) (gcp.InstancesClient, error) {
return c.gcpInstances.GetClient(ctx)
Expand Down Expand Up @@ -982,6 +992,7 @@ type TestCloudClients struct {
STS stsiface.STSAPI
GCPSQL gcp.SQLAdminClient
GCPGKE gcp.GKEClient
GCPProjects gcp.ProjectsClient
GCPInstances gcp.InstancesClient
EC2 ec2iface.EC2API
SSM ssmiface.SSMAPI
Expand Down Expand Up @@ -1178,6 +1189,11 @@ func (c *TestCloudClients) GetGCPGKEClient(ctx context.Context) (gcp.GKEClient,
return c.GCPGKE, nil
}

// GetGCPGKEClient returns GKE client.
func (c *TestCloudClients) GetGCPProjectsClient(ctx context.Context) (gcp.ProjectsClient, error) {
return c.GCPProjects, nil
}

// GetGCPInstancesClient returns instances client.
func (c *TestCloudClients) GetGCPInstancesClient(ctx context.Context) (gcp.InstancesClient, error) {
return c.GCPInstances, nil
Expand Down
105 changes: 105 additions & 0 deletions lib/cloud/gcp/projects.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Teleport
* Copyright (C) 2024 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package gcp

import (
"context"

"github.com/gravitational/trace"
"google.golang.org/api/cloudresourcemanager/v1"
)

// Project is a GCP project.
type Project struct {
// ID is the project ID.
ID string
// Name is the project name.
Name string
}

// ProjectsClient is an interface to interact with GCP Projects API.
type ProjectsClient interface {
// ListProjects lists the GCP projects that the authenticated user has access to.
ListProjects(ctx context.Context) ([]Project, error)
}

// ProjectsClientConfig is the client configuration for ProjectsClient.
type ProjectsClientConfig struct {
// Client is the GCP client for resourcemanager service.
Client *cloudresourcemanager.Service
}

// CheckAndSetDefaults check and set defaults for ProjectsClientConfig.
func (c *ProjectsClientConfig) CheckAndSetDefaults(ctx context.Context) (err error) {
if c.Client == nil {
c.Client, err = cloudresourcemanager.NewService(ctx)
if err != nil {
return trace.Wrap(err)
}
}
return nil
}

// NewProjectsClient returns a ProjectsClient interface wrapping resourcemanager.ProjectsClient
// for interacting with GCP Projects API.
func NewProjectsClient(ctx context.Context) (ProjectsClient, error) {
var cfg ProjectsClientConfig
client, err := NewProjectsClientWithConfig(ctx, cfg)
return client, trace.Wrap(err)
}

// NewProjectsClientWithConfig returns a ProjectsClient interface wrapping resourcemanager.ProjectsClient
// for interacting with GCP Projects API.
func NewProjectsClientWithConfig(ctx context.Context, cfg ProjectsClientConfig) (ProjectsClient, error) {
if err := cfg.CheckAndSetDefaults(ctx); err != nil {
return nil, trace.Wrap(err)
}
return &projectsClient{cfg}, nil
}

type projectsClient struct {
ProjectsClientConfig
}

// ListProjects lists the GCP Projects that the authenticated user has access to.
func (g *projectsClient) ListProjects(ctx context.Context) ([]Project, error) {

var pageToken string
var projects []Project
for {
projectsCall, err := g.Client.Projects.List().PageToken(pageToken).Do()
if err != nil {
return nil, trace.Wrap(err)
}
for _, project := range projectsCall.Projects {
projects = append(projects,
Project{
ID: project.ProjectId,
Name: project.Name,
},
)
}
if projectsCall.NextPageToken == "" {
break
}
pageToken = projectsCall.NextPageToken
}

return projects, nil
}
47 changes: 47 additions & 0 deletions lib/config/configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4346,6 +4346,53 @@ func TestDiscoveryConfig(t *testing.T) {
ProjectIDs: []string{"p1", "p2"},
}},
},
{
desc: "GCP section is filled with wildcard project ids",
expectError: require.NoError,
expectEnabled: require.True,
mutate: func(cfg cfgMap) {
cfg["discovery_service"].(cfgMap)["enabled"] = "yes"
cfg["discovery_service"].(cfgMap)["gcp"] = []cfgMap{
{
"types": []string{"gke"},
"locations": []string{"eucentral1"},
"tags": cfgMap{
"discover_teleport": "yes",
},
"project_ids": []string{"*"},
},
}
},
expectedGCPMatchers: []types.GCPMatcher{{
Types: []string{"gke"},
Locations: []string{"eucentral1"},
Labels: map[string]apiutils.Strings{
"discover_teleport": []string{"yes"},
},
Tags: map[string]apiutils.Strings{
"discover_teleport": []string{"yes"},
},
ProjectIDs: []string{"*"},
}},
},
{
desc: "GCP section mixes wildcard and specific project ids",
expectError: require.Error,
expectEnabled: require.True,
mutate: func(cfg cfgMap) {
cfg["discovery_service"].(cfgMap)["enabled"] = "yes"
cfg["discovery_service"].(cfgMap)["gcp"] = []cfgMap{
{
"types": []string{"gke"},
"locations": []string{"eucentral1"},
"tags": cfgMap{
"discover_teleport": "yes",
},
"project_ids": []string{"p1", "*"},
},
}
},
},
{
desc: "GCP section is filled with installer",
expectError: require.NoError,
Expand Down
28 changes: 20 additions & 8 deletions lib/srv/discovery/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,12 @@ func (s *Server) gcpServerFetchersFromMatchers(ctx context.Context, matchers []t
return nil, trace.Wrap(err)
}

return server.MatchersToGCPInstanceFetchers(serverMatchers, client), nil
projectsClient, err := s.CloudClients.GetGCPProjectsClient(ctx)
if err != nil {
return nil, trace.Wrap(err)
}

return server.MatchersToGCPInstanceFetchers(serverMatchers, client, projectsClient), nil
}

// databaseFetchersFromMatchers converts Matchers into a set of Database Fetchers.
Expand Down Expand Up @@ -704,19 +709,26 @@ func (s *Server) initGCPWatchers(ctx context.Context, matchers []types.GCPMatche
if err != nil {
return trace.Wrap(err)
}
projectClient, err := s.CloudClients.GetGCPProjectsClient(ctx)
if err != nil {
return trace.Wrap(err, "unable to create gcp project client")
}
for _, matcher := range otherMatchers {
for _, projectID := range matcher.ProjectIDs {
for _, location := range matcher.Locations {
for _, t := range matcher.Types {
switch t {
case types.GCPMatcherKubernetes:
fetcher, err := fetchers.NewGKEFetcher(fetchers.GKEFetcherConfig{
Client: kubeClient,
Location: location,
FilterLabels: matcher.GetLabels(),
ProjectID: projectID,
Log: s.Log,
})
fetcher, err := fetchers.NewGKEFetcher(
ctx,
fetchers.GKEFetcherConfig{
GKEClient: kubeClient,
ProjectClient: projectClient,
Location: location,
FilterLabels: matcher.GetLabels(),
ProjectID: projectID,
Log: s.Log,
})
if err != nil {
return trace.Wrap(err)
}
Expand Down
Loading

0 comments on commit 97dc48e

Please sign in to comment.