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

Integration Discovery Rules: endpoint that returns all the rules #49514

Merged
merged 2 commits into from
Dec 6, 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
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 := collectAutoDiscoveryRules(r.Context(), ig.GetName(), startKey, resourceType, clt.DiscoveryConfigClient())
if err != nil {
return nil, trace.Wrap(err)
}

return rules, nil
}

// collectAutoDiscoveryRules will iterate over all DiscoveryConfigs's Matchers and collect the Discovery Rules that exist in them for the given integration.
// It can also be filtered by Matcher Type (eg ec2, rds, eks)
// A Discovery 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 collectAutoDiscoveryRules(
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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I suppose it can fetch more rules than maxPerPage, but I assume we're okay with that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think so.
This works as a soft limit.
Otherwise, we might need to stop adding elements in the middle of the loop, which would turn the nextToken something composed of the DiscoveryConfig previous nextToken and some other value indicating where we stopped (some index).

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 → lib/web/integrations_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 TestCollectAutoDiscoveryRules(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 := collectAutoDiscoveryRules(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 := collectAutoDiscoveryRules(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 @@ -101,6 +101,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
Loading