Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v16] [gcp] support project discovery #47568

Merged
merged 1 commit into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions api/types/matchers_gcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,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 @@ -454,8 +454,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 @@ -107,6 +107,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 @@ -266,6 +268,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 @@ -324,6 +327,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 @@ -659,6 +664,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 @@ -1022,6 +1032,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 @@ -1234,6 +1245,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 @@ -4347,6 +4347,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 @@ -572,7 +572,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 @@ -734,19 +739,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
Loading