Skip to content

Commit

Permalink
Integration Discovery Rules: endpoint that returns all the rules
Browse files Browse the repository at this point in the history
This PR adds a new endpoint that returns all the discovery rules that
exist for a given Integration.

It will include what was the last time the rule was used to fetch
resources, so that users can see stale rules and act upon it.

The endpoint supports filtering by the resource type to ensure the UI
only requests a subset of the rules, depending on the screen the user is
visiting (eg, EC2, RDS or EKS).
  • Loading branch information
marcoandredinis committed Nov 28, 2024
1 parent 864eca5 commit 3b3425d
Show file tree
Hide file tree
Showing 4 changed files with 269 additions and 0 deletions.
1 change: 1 addition & 0 deletions lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,7 @@ func (h *Handler) bindDefaultEndpoints() {
h.GET("/webapi/sites/:site/integrations/:name", h.WithClusterAuth(h.integrationsGet))
h.PUT("/webapi/sites/:site/integrations/:name", h.WithClusterAuth(h.integrationsUpdate))
h.GET("/webapi/sites/:site/integrations/:name/stats", h.WithClusterAuth(h.integrationStats))
h.GET("/webapi/sites/:site/integrations/:name/discoveryrules", h.WithClusterAuth(h.integrationDiscoveryRules))
h.DELETE("/webapi/sites/:site/integrations/:name_or_subkind", h.WithClusterAuth(h.integrationsDelete))

// GET the Microsoft Teams plugin app.zip file.
Expand Down
104 changes: 104 additions & 0 deletions lib/web/integrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/httplib"
"github.com/gravitational/teleport/lib/reversetunnelclient"
libui "github.com/gravitational/teleport/lib/ui"
"github.com/gravitational/teleport/lib/web/ui"
)

Expand Down Expand Up @@ -319,6 +320,109 @@ func rulesWithIntegration(dc *discoveryconfig.DiscoveryConfig, matcherType strin
return ret
}

// integrationDiscoveryRules returns the Discovery Rules that are using a given integration.
// A Discovery Rule is just like a DiscoveryConfig Matcher, except that it breaks down by region.
// So, if a Matcher exists for two regions, that will be represented as two Rules.
func (h *Handler) integrationDiscoveryRules(w http.ResponseWriter, r *http.Request, p httprouter.Params, sctx *SessionContext, site reversetunnelclient.RemoteSite) (interface{}, error) {
integrationName := p.ByName("name")
if integrationName == "" {
return nil, trace.BadParameter("an integration name is required")
}

values := r.URL.Query()
startKey := values.Get("startKey")
resourceType := values.Get("resourceType")

clt, err := sctx.GetUserClient(r.Context(), site)
if err != nil {
return nil, trace.Wrap(err)
}

ig, err := clt.GetIntegration(r.Context(), integrationName)
if err != nil {
return nil, trace.Wrap(err)
}

rules, err := collectAutoDiscoverRules(r.Context(), ig.GetName(), startKey, resourceType, clt.DiscoveryConfigClient())
if err != nil {
return nil, trace.Wrap(err)
}

return rules, nil
}

// collectAutoDiscoverRules will iterate over all DiscoveryConfigs's Matchers and collect the Discover Rules that exist in them for the given integration.
// It can also be filtered by Matcher Type (eg ec2, rds, eks)
// A Discover Rule is a close match to a DiscoveryConfig's Matcher, except that it will count as many rules as regions exist.
// Eg if a DiscoveryConfig's Matcher has two regions, then it will output two (almost equal) Rules, one for each Region.
func collectAutoDiscoverRules(
ctx context.Context,
integrationName string,
nextPage string,
resourceTypeFilter string,
clt interface {
ListDiscoveryConfigs(ctx context.Context, pageSize int, nextToken string) ([]*discoveryconfig.DiscoveryConfig, string, error)
},
) (ui.IntegrationDiscoveryRules, error) {
const (
maxPerPage = 100
)
var ret ui.IntegrationDiscoveryRules
for {
discoveryConfigs, nextToken, err := clt.ListDiscoveryConfigs(ctx, 0, nextPage)
if err != nil {
return ret, trace.Wrap(err)
}
for _, dc := range discoveryConfigs {
lastSync := &dc.Status.LastSyncTime
if lastSync.IsZero() {
lastSync = nil
}

for _, matcher := range dc.Spec.AWS {
if matcher.Integration != integrationName {
continue
}

for _, resourceType := range matcher.Types {
if resourceTypeFilter != "" && resourceType != resourceTypeFilter {
continue
}

for _, region := range matcher.Regions {
uiLables := make([]libui.Label, 0, len(matcher.Tags))
for labelKey, labelValues := range matcher.Tags {
for _, labelValue := range labelValues {
uiLables = append(uiLables, libui.Label{
Name: labelKey,
Value: labelValue,
})
}
}
ret.Rules = append(ret.Rules, ui.IntegrationDiscoveryRule{
ResourceType: resourceType,
Region: region,
LabelMatcher: uiLables,
DiscoveryConfig: dc.GetName(),
LastSync: lastSync,
})
}
}
}
}

ret.NextKey = nextToken

if nextToken == "" || len(ret.Rules) > maxPerPage {
break
}

nextPage = nextToken
}

return ret, nil
}

// integrationsList returns a page of Integrations
func (h *Handler) integrationsList(w http.ResponseWriter, r *http.Request, p httprouter.Params, sctx *SessionContext, site reversetunnelclient.RemoteSite) (interface{}, error) {
clt, err := sctx.GetUserClient(r.Context(), site)
Expand Down
139 changes: 139 additions & 0 deletions lib/web/intgrations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ import (
"testing"
"time"

"github.com/google/uuid"
"github.com/stretchr/testify/require"

discoveryconfigv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/discoveryconfig/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/discoveryconfig"
"github.com/gravitational/teleport/api/types/header"
"github.com/gravitational/teleport/lib/services"
libui "github.com/gravitational/teleport/lib/ui"
"github.com/gravitational/teleport/lib/web/ui"
)

Expand Down Expand Up @@ -212,6 +215,142 @@ func TestCollectAWSOIDCAutoDiscoverStats(t *testing.T) {
})
}

func TestCollectAutoDiscoverRules(t *testing.T) {
ctx := context.Background()
integrationName := "my-integration"

t.Run("without discovery configs, returns no rules", func(t *testing.T) {
clt := &mockDiscoveryConfigsGetter{
discoveryConfigs: make([]*discoveryconfig.DiscoveryConfig, 0),
}

gotRules, err := collectAutoDiscoverRules(ctx, integrationName, "", "", clt)
require.NoError(t, err)
expectedRules := ui.IntegrationDiscoveryRules{}
require.Equal(t, expectedRules, gotRules)
})

t.Run("collects multiple discovery configs", func(t *testing.T) {
syncTime := time.Now()
dcForEC2 := &discoveryconfig.DiscoveryConfig{
ResourceHeader: header.ResourceHeader{Metadata: header.Metadata{
Name: uuid.NewString(),
}},
Spec: discoveryconfig.Spec{AWS: []types.AWSMatcher{{
Integration: integrationName,
Types: []string{"ec2"},
Regions: []string{"us-east-1"},
Tags: types.Labels{"*": []string{"*"}},
}}},
Status: discoveryconfig.Status{
LastSyncTime: syncTime,
},
}
dcForRDS := &discoveryconfig.DiscoveryConfig{
ResourceHeader: header.ResourceHeader{Metadata: header.Metadata{
Name: uuid.NewString(),
}},
Spec: discoveryconfig.Spec{AWS: []types.AWSMatcher{{
Integration: integrationName,
Types: []string{"rds"},
Regions: []string{"us-east-1", "us-east-2"},
Tags: types.Labels{
"env": []string{"dev", "prod"},
},
}}},
Status: discoveryconfig.Status{
LastSyncTime: syncTime,
},
}
dcForEKS := &discoveryconfig.DiscoveryConfig{
ResourceHeader: header.ResourceHeader{Metadata: header.Metadata{
Name: uuid.NewString(),
}},
Spec: discoveryconfig.Spec{AWS: []types.AWSMatcher{{
Integration: integrationName,
Types: []string{"eks"},
Regions: []string{"us-east-1"},
Tags: types.Labels{"*": []string{"*"}},
}}},
Status: discoveryconfig.Status{
LastSyncTime: syncTime,
},
}
dcForEKSWithoutStatus := &discoveryconfig.DiscoveryConfig{
ResourceHeader: header.ResourceHeader{Metadata: header.Metadata{
Name: uuid.NewString(),
}},
Spec: discoveryconfig.Spec{AWS: []types.AWSMatcher{{
Integration: integrationName,
Types: []string{"eks"},
Regions: []string{"eu-west-1"},
Tags: types.Labels{"*": []string{"*"}},
}}},
}
clt := &mockDiscoveryConfigsGetter{
discoveryConfigs: []*discoveryconfig.DiscoveryConfig{
dcForEC2,
dcForRDS,
dcForEKS,
dcForEKSWithoutStatus,
},
}

got, err := collectAutoDiscoverRules(ctx, integrationName, "", "", clt)
require.NoError(t, err)
expectedRules := []ui.IntegrationDiscoveryRule{
{
ResourceType: "ec2",
Region: "us-east-1",
LabelMatcher: []libui.Label{
{Name: "*", Value: "*"},
},
DiscoveryConfig: dcForEC2.GetName(),
LastSync: &syncTime,
},
{
ResourceType: "eks",
Region: "us-east-1",
LabelMatcher: []libui.Label{
{Name: "*", Value: "*"},
},
DiscoveryConfig: dcForEKS.GetName(),
LastSync: &syncTime,
},
{
ResourceType: "eks",
Region: "eu-west-1",
LabelMatcher: []libui.Label{
{Name: "*", Value: "*"},
},
DiscoveryConfig: dcForEKSWithoutStatus.GetName(),
},
{
ResourceType: "rds",
Region: "us-east-1",
LabelMatcher: []libui.Label{
{Name: "env", Value: "dev"},
{Name: "env", Value: "prod"},
},
DiscoveryConfig: dcForRDS.GetName(),
LastSync: &syncTime,
},
{
ResourceType: "rds",
Region: "us-east-2",
LabelMatcher: []libui.Label{
{Name: "env", Value: "dev"},
{Name: "env", Value: "prod"},
},
DiscoveryConfig: dcForRDS.GetName(),
LastSync: &syncTime,
},
}
require.Empty(t, got.NextKey)
require.ElementsMatch(t, expectedRules, got.Rules)
})
}

type mockDiscoveryConfigsGetter struct {
discoveryConfigs []*discoveryconfig.DiscoveryConfig
}
Expand Down
25 changes: 25 additions & 0 deletions lib/web/ui/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,31 @@ type ResourceTypeSummary struct {
ECSDatabaseServiceCount int `json:"ecsDatabaseServiceCount,omitempty"`
}

// IntegrationDiscoveryRule describes a discovery rule associated with an integration.
type IntegrationDiscoveryRule struct {
// ResourceType indicates the type of resource that this rule targets.
// This is the same value that is set in DiscoveryConfig.AWS.<Matcher>.Types
// Example: ec2, rds, eks
ResourceType string `json:"resourceType,omitempty"`
// Region where this rule applies to.
Region string `json:"region,omitempty"`
// LabelMatcher is the set of labels that are used to filter the resources before trying to auto-enroll them.
LabelMatcher []ui.Label `json:"labelMatcher,omitempty"`
// DiscoveryConfig is the name of the DiscoveryConfig that created this rule.
DiscoveryConfig string `json:"discoveryConfig,omitempty"`
// LastSync contains the time when this rule was used.
// If empty, it indicates that the rule is not being used.
LastSync *time.Time `json:"lastSync,omitempty"`
}

// IntegrationDiscoveryRules contains the list of discovery rules for a given Integration.
type IntegrationDiscoveryRules struct {
// Rules is the list of integration rules.
Rules []IntegrationDiscoveryRule `json:"rules"`
// NextKey is the position to resume listing rules.
NextKey string `json:"nextKey,omitempty"`
}

// Integration describes Integration fields
type Integration struct {
// Name is the Integration name.
Expand Down

0 comments on commit 3b3425d

Please sign in to comment.