diff --git a/api/client/client.go b/api/client/client.go index 1d9c46ce3c869..7602e0824d9a6 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -43,6 +43,7 @@ import ( "github.com/gravitational/teleport/api/breaker" "github.com/gravitational/teleport/api/client/accesslist" + "github.com/gravitational/teleport/api/client/discoveryconfig" "github.com/gravitational/teleport/api/client/okta" "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/client/userloginstate" @@ -52,6 +53,7 @@ import ( accesslistv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/accesslist/v1" auditlogpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/auditlog/v1" devicepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/devicetrust/v1" + discoveryconfigv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/discoveryconfig/v1" integrationpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/integration/v1" kubeproto "github.com/gravitational/teleport/api/gen/proto/go/teleport/kube/v1" loginrulepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/loginrule/v1" @@ -4034,6 +4036,14 @@ func (c *Client) AccessListClient() *accesslist.Client { return accesslist.NewClient(accesslistv1.NewAccessListServiceClient(c.conn)) } +// DiscoveryConfigClient returns a DiscoveryConfig client. +// Clients connecting to older Teleport versions, still get an DiscoveryConfig client +// when calling this method, but all RPCs will return "not implemented" errors +// (as per the default gRPC behavior). +func (c *Client) DiscoveryConfigClient() *discoveryconfig.Client { + return discoveryconfig.NewClient(discoveryconfigv1.NewDiscoveryConfigServiceClient(c.conn)) +} + // UserLoginStateClient returns a user login state client. // Clients connecting to older Teleport versions, still get a user login state client // when calling this method, but all RPCs will return "not implemented" errors diff --git a/api/client/discoveryconfig/discoveryconfig.go b/api/client/discoveryconfig/discoveryconfig.go new file mode 100644 index 0000000000000..ca531609e5096 --- /dev/null +++ b/api/client/discoveryconfig/discoveryconfig.go @@ -0,0 +1,111 @@ +// Copyright 2023 Gravitational, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package discoveryconfig + +import ( + "context" + + "github.com/gravitational/trace" + + discoveryconfigv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/discoveryconfig/v1" + "github.com/gravitational/teleport/api/types/discoveryconfig" + conv "github.com/gravitational/teleport/api/types/discoveryconfig/convert/v1" +) + +// Client is an DiscoveryConfig client that conforms to the following lib/services interfaces: +// - services.DiscoveryConfigs +type Client struct { + grpcClient discoveryconfigv1.DiscoveryConfigServiceClient +} + +// NewClient creates a new Discovery Config client. +func NewClient(grpcClient discoveryconfigv1.DiscoveryConfigServiceClient) *Client { + return &Client{ + grpcClient: grpcClient, + } +} + +// ListDiscoveryConfigs returns a paginated list of DiscoveryConfigs. +func (c *Client) ListDiscoveryConfigs(ctx context.Context, pageSize int, nextToken string) ([]*discoveryconfig.DiscoveryConfig, string, error) { + resp, err := c.grpcClient.ListDiscoveryConfigs(ctx, &discoveryconfigv1.ListDiscoveryConfigsRequest{ + PageSize: int32(pageSize), + NextToken: nextToken, + }) + if err != nil { + return nil, "", trace.Wrap(err) + } + + discoveryConfigs := make([]*discoveryconfig.DiscoveryConfig, len(resp.DiscoveryConfigs)) + for i, discoveryConfig := range resp.DiscoveryConfigs { + var err error + discoveryConfigs[i], err = conv.FromProto(discoveryConfig) + if err != nil { + return nil, "", trace.Wrap(err) + } + } + + return discoveryConfigs, resp.GetNextKey(), nil +} + +// GetDiscoveryConfig returns the specified DiscoveryConfig resource. +func (c *Client) GetDiscoveryConfig(ctx context.Context, name string) (*discoveryconfig.DiscoveryConfig, error) { + resp, err := c.grpcClient.GetDiscoveryConfig(ctx, &discoveryconfigv1.GetDiscoveryConfigRequest{ + Name: name, + }) + if err != nil { + return nil, trace.Wrap(err) + } + + discoveryConfig, err := conv.FromProto(resp) + return discoveryConfig, trace.Wrap(err) +} + +// CreateDiscoveryConfig creates the DiscoveryConfig. +func (c *Client) CreateDiscoveryConfig(ctx context.Context, discoveryConfig *discoveryconfig.DiscoveryConfig) (*discoveryconfig.DiscoveryConfig, error) { + resp, err := c.grpcClient.CreateDiscoveryConfig(ctx, &discoveryconfigv1.CreateDiscoveryConfigRequest{ + DiscoveryConfig: conv.ToProto(discoveryConfig), + }) + if err != nil { + return nil, trace.Wrap(err) + } + dc, err := conv.FromProto(resp) + return dc, trace.Wrap(err) +} + +// UpdateDiscoveryConfig updates the DiscoveryConfig. +func (c *Client) UpdateDiscoveryConfig(ctx context.Context, discoveryConfig *discoveryconfig.DiscoveryConfig) (*discoveryconfig.DiscoveryConfig, error) { + resp, err := c.grpcClient.UpdateDiscoveryConfig(ctx, &discoveryconfigv1.UpdateDiscoveryConfigRequest{ + DiscoveryConfig: conv.ToProto(discoveryConfig), + }) + if err != nil { + return nil, trace.Wrap(err) + } + dc, err := conv.FromProto(resp) + return dc, trace.Wrap(err) +} + +// DeleteDiscoveryConfig removes the specified DiscoveryConfig resource. +func (c *Client) DeleteDiscoveryConfig(ctx context.Context, name string) error { + _, err := c.grpcClient.DeleteDiscoveryConfig(ctx, &discoveryconfigv1.DeleteDiscoveryConfigRequest{ + Name: name, + }) + return trace.Wrap(err) +} + +// DeleteAllDiscoveryConfigs removes all DiscoveryConfigs. +func (c *Client) DeleteAllDiscoveryConfigs(ctx context.Context) error { + _, err := c.grpcClient.DeleteAllDiscoveryConfigs(ctx, &discoveryconfigv1.DeleteAllDiscoveryConfigsRequest{}) + return trace.Wrap(err) +} diff --git a/api/types/constants.go b/api/types/constants.go index ddf07591d23ac..4d3d6bb08e186 100644 --- a/api/types/constants.go +++ b/api/types/constants.go @@ -460,6 +460,10 @@ const ( // KindAccessListMember is an AccessListMember resource KindAccessListMember = "access_list_member" + // KindDiscoveryConfig is a DiscoveryConfig resource. + // Used for adding additional matchers in Discovery Service. + KindDiscoveryConfig = "discovery_config" + // V7 is the seventh version of resources. V7 = "v7" diff --git a/api/types/discoveryconfig/convert/v1/discoveryconfig.go b/api/types/discoveryconfig/convert/v1/discoveryconfig.go new file mode 100644 index 0000000000000..396c03335b8fe --- /dev/null +++ b/api/types/discoveryconfig/convert/v1/discoveryconfig.go @@ -0,0 +1,108 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "github.com/gravitational/trace" + + 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" + headerv1 "github.com/gravitational/teleport/api/types/header/convert/v1" +) + +// FromProto converts a v1 discovery config into an internal discovery config object. +func FromProto(msg *discoveryconfigv1.DiscoveryConfig) (*discoveryconfig.DiscoveryConfig, error) { + if msg == nil { + return nil, trace.BadParameter("discovery config message is nil") + } + + if msg.Spec == nil { + return nil, trace.BadParameter("spec is missing") + } + if msg.Spec.DiscoveryGroup == "" { + return nil, trace.BadParameter("discovery group is missing") + } + + awsMatchers := make([]types.AWSMatcher, 0, len(msg.Spec.Aws)) + for _, m := range msg.Spec.Aws { + awsMatchers = append(awsMatchers, *m) + } + + azureMatchers := make([]types.AzureMatcher, 0, len(msg.Spec.Azure)) + for _, m := range msg.Spec.Azure { + azureMatchers = append(azureMatchers, *m) + } + + gcpMatchers := make([]types.GCPMatcher, 0, len(msg.Spec.Gcp)) + for _, m := range msg.Spec.Gcp { + gcpMatchers = append(gcpMatchers, *m) + } + + kubeMatchers := make([]types.KubernetesMatcher, 0, len(msg.Spec.Kube)) + for _, m := range msg.Spec.Kube { + kubeMatchers = append(kubeMatchers, *m) + } + + discoveryConfig, err := discoveryconfig.NewDiscoveryConfig( + headerv1.FromMetadataProto(msg.Header.Metadata), + discoveryconfig.Spec{ + DiscoveryGroup: msg.Spec.DiscoveryGroup, + AWS: awsMatchers, + Azure: azureMatchers, + GCP: gcpMatchers, + Kube: kubeMatchers, + }, + ) + + return discoveryConfig, trace.Wrap(err) +} + +// ToProto converts an internal discovery config into a v1 discovery config object. +func ToProto(discoveryConfig *discoveryconfig.DiscoveryConfig) *discoveryconfigv1.DiscoveryConfig { + awsMatchers := make([]*types.AWSMatcher, 0, len(discoveryConfig.Spec.AWS)) + for _, m := range discoveryConfig.Spec.AWS { + m := m + awsMatchers = append(awsMatchers, &m) + } + + azureMatchers := make([]*types.AzureMatcher, 0, len(discoveryConfig.Spec.Azure)) + for _, m := range discoveryConfig.Spec.Azure { + azureMatchers = append(azureMatchers, &m) + } + + gcpMatchers := make([]*types.GCPMatcher, 0, len(discoveryConfig.Spec.GCP)) + for _, m := range discoveryConfig.Spec.GCP { + gcpMatchers = append(gcpMatchers, &m) + } + + kubeMatchers := make([]*types.KubernetesMatcher, 0, len(discoveryConfig.Spec.Kube)) + for _, m := range discoveryConfig.Spec.Kube { + kubeMatchers = append(kubeMatchers, &m) + } + + return &discoveryconfigv1.DiscoveryConfig{ + Header: headerv1.ToResourceHeaderProto(discoveryConfig.ResourceHeader), + Spec: &discoveryconfigv1.DiscoveryConfigSpec{ + DiscoveryGroup: discoveryConfig.GetDiscoveryGroup(), + Aws: awsMatchers, + Azure: azureMatchers, + Gcp: gcpMatchers, + Kube: kubeMatchers, + }, + } +} diff --git a/api/types/discoveryconfig/convert/v1/discoveryconfig_test.go b/api/types/discoveryconfig/convert/v1/discoveryconfig_test.go new file mode 100644 index 0000000000000..8095eee3c7e66 --- /dev/null +++ b/api/types/discoveryconfig/convert/v1/discoveryconfig_test.go @@ -0,0 +1,72 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/types/discoveryconfig" + "github.com/gravitational/teleport/api/types/header" +) + +func TestRoundtrip(t *testing.T) { + discoveryConfig := newDiscoveryConfig(t, "discovery-config-01") + + converted, err := FromProto(ToProto(discoveryConfig)) + require.NoError(t, err) + + require.Empty(t, cmp.Diff(discoveryConfig, converted)) +} + +// Make sure that we don't panic if any of the message fields are missing. +func TestFromProtoNils(t *testing.T) { + // Spec is nil + discoveryConfig := ToProto(newDiscoveryConfig(t, "discovery-config-01")) + discoveryConfig.Spec = nil + + _, err := FromProto(discoveryConfig) + require.Error(t, err) +} + +func newDiscoveryConfig(t *testing.T, name string) *discoveryconfig.DiscoveryConfig { + t.Helper() + + discoveryConfig, err := discoveryconfig.NewDiscoveryConfig( + header.Metadata{ + Name: name, + }, + discoveryconfig.Spec{ + DiscoveryGroup: "discovery-group-01", + AWS: []types.AWSMatcher{ + { + Types: []string{"rds"}, + Regions: []string{"us-west-2"}, + }, + { + Types: []string{"ec2"}, + Regions: []string{"eu-west-2"}, + }, + }, + }, + ) + require.NoError(t, err) + return discoveryConfig +} diff --git a/api/types/discoveryconfig/discoveryconfig.go b/api/types/discoveryconfig/discoveryconfig.go new file mode 100644 index 0000000000000..551bd23e37b27 --- /dev/null +++ b/api/types/discoveryconfig/discoveryconfig.go @@ -0,0 +1,142 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package discoveryconfig + +import ( + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/types/header" + "github.com/gravitational/teleport/api/types/header/convert/legacy" + "github.com/gravitational/teleport/api/utils" +) + +// DiscoveryConfig describes extra discovery matchers that are added to DiscoveryServices that share the same Discovery Group. +type DiscoveryConfig struct { + // ResourceHeader is the common resource header for all resources. + header.ResourceHeader + + // Spec is the specification for the discovery config. + Spec Spec `json:"spec" yaml:"spec"` +} + +// Spec is the specification for a discovery config. +type Spec struct { + // DiscoveryGroup is the Discovery Group for the current DiscoveryConfig. + // DiscoveryServices should include all the matchers if the DiscoveryGroup matches with their own group. + DiscoveryGroup string `json:"discovery_group" yaml:"discovery_group"` + + // AWS is a list of matchers for the supported resources in AWS. + AWS []types.AWSMatcher `json:"aws,omitempty" yaml:"aws"` + // Azure is a list of matchers for the supported resources in Azure. + Azure []types.AzureMatcher `json:"azure,omitempty" yaml:"azure"` + // GCP is a list of matchers for the supported resources in GCP. + GCP []types.GCPMatcher `json:"gcp,omitempty" yaml:"gcp"` + // Kube is a list of matchers for the supported resources in Kubernetes. + Kube []types.KubernetesMatcher `json:"kube,omitempty" yaml:"kube"` +} + +// NewDiscoveryConfig will create a new discovery config. +func NewDiscoveryConfig(metadata header.Metadata, spec Spec) (*DiscoveryConfig, error) { + discoveryConfig := &DiscoveryConfig{ + ResourceHeader: header.ResourceHeaderFromMetadata(metadata), + Spec: spec, + } + + if err := discoveryConfig.CheckAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) + } + + return discoveryConfig, nil +} + +// CheckAndSetDefaults validates fields and populates empty fields with default values. +func (a *DiscoveryConfig) CheckAndSetDefaults() error { + a.SetKind(types.KindDiscoveryConfig) + a.SetVersion(types.V1) + + if err := a.ResourceHeader.CheckAndSetDefaults(); err != nil { + return trace.Wrap(err) + } + + if a.Spec.DiscoveryGroup == "" { + return trace.BadParameter("discovery config group required") + } + + if a.Spec.AWS == nil { + a.Spec.AWS = make([]types.AWSMatcher, 0) + } + for _, m := range a.Spec.AWS { + if err := m.CheckAndSetDefaults(); err != nil { + return trace.Wrap(err) + } + } + + if a.Spec.Azure == nil { + a.Spec.Azure = make([]types.AzureMatcher, 0) + } + for _, m := range a.Spec.Azure { + if err := m.CheckAndSetDefaults(); err != nil { + return trace.Wrap(err) + } + } + + if a.Spec.GCP == nil { + a.Spec.GCP = make([]types.GCPMatcher, 0) + } + for _, m := range a.Spec.GCP { + if err := m.CheckAndSetDefaults(); err != nil { + return trace.Wrap(err) + } + } + + if a.Spec.Kube == nil { + a.Spec.Kube = make([]types.KubernetesMatcher, 0) + } + for _, m := range a.Spec.Kube { + if err := m.CheckAndSetDefaults(); err != nil { + return trace.Wrap(err) + } + } + + return nil +} + +// GetDiscoveryGroup returns the DiscoveryGroup from the discovery config. +func (a *DiscoveryConfig) GetDiscoveryGroup() string { + return a.Spec.DiscoveryGroup +} + +// GetMetadata returns metadata. This is specifically for conforming to the Resource interface, +// and should be removed when possible. +func (a *DiscoveryConfig) GetMetadata() types.Metadata { + return legacy.FromHeaderMetadata(a.Metadata) +} + +// MatchSearch goes through select field values of a resource +// and tries to match against the list of search values. +func (a *DiscoveryConfig) MatchSearch(values []string) bool { + fieldVals := append(utils.MapToStrings(a.GetAllLabels()), a.GetName(), a.GetDiscoveryGroup()) + return types.MatchSearch(fieldVals, values, nil) +} + +// CloneResource returns a copy of the resource as types.ResourceWithLabels. +func (a *DiscoveryConfig) CloneResource() types.ResourceWithLabels { + var copy *DiscoveryConfig + utils.StrictObjectToStruct(a, ©) + return copy +} diff --git a/api/types/discoveryconfig/discoveryconfig_test.go b/api/types/discoveryconfig/discoveryconfig_test.go new file mode 100644 index 0000000000000..7b618421e23ed --- /dev/null +++ b/api/types/discoveryconfig/discoveryconfig_test.go @@ -0,0 +1,103 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package discoveryconfig + +import ( + "testing" + + "github.com/gravitational/trace" + "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/types/header" +) + +func requireBadParameter(t require.TestingT, err error, i ...interface{}) { + require.True( + t, + trace.IsBadParameter(err), + "err should be bad parameter, was: %s", err, + ) +} + +func TestNewDiscoveryConfig(t *testing.T) { + for _, tt := range []struct { + name string + inMetadata header.Metadata + inSpec Spec + expected *DiscoveryConfig + errCheck require.ErrorAssertionFunc + }{ + { + name: "valid", + inMetadata: header.Metadata{ + Name: "my-first-dc", + }, + inSpec: Spec{ + DiscoveryGroup: "dg1", + }, + expected: &DiscoveryConfig{ + ResourceHeader: header.ResourceHeader{ + Kind: types.KindDiscoveryConfig, + Version: types.V1, + Metadata: header.Metadata{ + Name: "my-first-dc", + }, + }, + Spec: Spec{ + DiscoveryGroup: "dg1", + AWS: make([]types.AWSMatcher, 0), + Azure: make([]types.AzureMatcher, 0), + GCP: make([]types.GCPMatcher, 0), + Kube: make([]types.KubernetesMatcher, 0), + }, + }, + errCheck: require.NoError, + }, + { + name: "error when name is not present", + inMetadata: header.Metadata{ + Name: "", + }, + inSpec: Spec{ + DiscoveryGroup: "dg1", + }, + errCheck: requireBadParameter, + }, + { + name: "error when discovery group is not present", + inMetadata: header.Metadata{ + Name: "my-first-dc", + }, + inSpec: Spec{ + DiscoveryGroup: "", + }, + errCheck: requireBadParameter, + }, + } { + t.Run(tt.name, func(t *testing.T) { + got, err := NewDiscoveryConfig(tt.inMetadata, tt.inSpec) + if tt.errCheck != nil { + tt.errCheck(t, err) + } + + if tt.expected != nil { + require.Equal(t, tt.expected, got) + } + }) + } +} diff --git a/lib/services/discoveryconfig.go b/lib/services/discoveryconfig.go new file mode 100644 index 0000000000000..4d40e2d506497 --- /dev/null +++ b/lib/services/discoveryconfig.go @@ -0,0 +1,95 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + "context" + + "github.com/gravitational/trace" + + discoveryconfigclient "github.com/gravitational/teleport/api/client/discoveryconfig" + "github.com/gravitational/teleport/api/types/discoveryconfig" + "github.com/gravitational/teleport/lib/utils" +) + +var _ DiscoveryConfigs = (*discoveryconfigclient.Client)(nil) + +// DiscoveryConfigs defines an interface for managing DiscoveryConfigs. +type DiscoveryConfigs interface { + DiscoveryConfigsGetter + // CreateDiscoveryConfig creates a new DiscoveryConfig resource. + CreateDiscoveryConfig(context.Context, *discoveryconfig.DiscoveryConfig) (*discoveryconfig.DiscoveryConfig, error) + // UpdateDiscoveryConfig updates an existing DiscoveryConfig resource. + UpdateDiscoveryConfig(context.Context, *discoveryconfig.DiscoveryConfig) (*discoveryconfig.DiscoveryConfig, error) + // DeleteDiscoveryConfig removes the specified DiscoveryConfig resource. + DeleteDiscoveryConfig(ctx context.Context, name string) error + // DeleteAllDiscoveryConfigs removes all DiscoveryConfigs. + DeleteAllDiscoveryConfigs(context.Context) error +} + +// DiscoveryConfigsGetter defines methods for List/Read operations on DiscoveryConfig Resources. +type DiscoveryConfigsGetter interface { + // ListDiscoveryConfigs returns a paginated list of all DiscoveryConfig resources. + // An optional DiscoveryGroup can be provided to filter. + ListDiscoveryConfigs(ctx context.Context, pageSize int, nextToken string) ([]*discoveryconfig.DiscoveryConfig, string, error) + // GetDiscoveryConfig returns the specified DiscoveryConfig resources. + GetDiscoveryConfig(ctx context.Context, name string) (*discoveryconfig.DiscoveryConfig, error) +} + +// MarshalDiscoveryConfig marshals the DiscoveryCOnfig resource to JSON. +func MarshalDiscoveryConfig(discoveryConfig *discoveryconfig.DiscoveryConfig, opts ...MarshalOption) ([]byte, error) { + if err := discoveryConfig.CheckAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) + } + + cfg, err := CollectOptions(opts) + if err != nil { + return nil, trace.Wrap(err) + } + + if !cfg.PreserveResourceID { + copy := *discoveryConfig + copy.SetResourceID(0) + discoveryConfig = © + } + return utils.FastMarshal(discoveryConfig) +} + +// UnmarshalDiscoveryConfig unmarshals the DiscoveryConfig resource from JSON. +func UnmarshalDiscoveryConfig(data []byte, opts ...MarshalOption) (*discoveryconfig.DiscoveryConfig, error) { + if len(data) == 0 { + return nil, trace.BadParameter("missing discovery config data") + } + cfg, err := CollectOptions(opts) + if err != nil { + return nil, trace.Wrap(err) + } + var discoveryConfig *discoveryconfig.DiscoveryConfig + if err := utils.FastUnmarshal(data, &discoveryConfig); err != nil { + return nil, trace.BadParameter(err.Error()) + } + if err := discoveryConfig.CheckAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) + } + if cfg.ID != 0 { + discoveryConfig.SetResourceID(cfg.ID) + } + if !cfg.Expires.IsZero() { + discoveryConfig.SetExpiry(cfg.Expires) + } + return discoveryConfig, nil +} diff --git a/lib/services/discoveryconfig_test.go b/lib/services/discoveryconfig_test.go new file mode 100644 index 0000000000000..1c26cdd1f2e8e --- /dev/null +++ b/lib/services/discoveryconfig_test.go @@ -0,0 +1,88 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "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/utils" +) + +// TestDiscoveryConfigUnmarshal verifies a DiscoveryConfig resource can be unmarshaled. +func TestDiscoveryConfigUnmarshal(t *testing.T) { + expected, err := discoveryconfig.NewDiscoveryConfig( + header.Metadata{ + Name: "test-discovery-config", + }, + discoveryconfig.Spec{ + DiscoveryGroup: "dg01", + AWS: []types.AWSMatcher{ + { + Types: []string{"ec2"}, + Regions: []string{"eu-west-2"}, + }, + }, + }, + ) + require.NoError(t, err) + data, err := utils.ToJSON([]byte(discoveryConfigYAML)) + require.NoError(t, err) + actual, err := UnmarshalDiscoveryConfig(data) + require.NoError(t, err) + require.Equal(t, expected, actual) +} + +// TestDiscoveryConfigMarshal verifies a marshaled DiscoveryConfig resource can be unmarshaled back. +func TestDiscoveryConfigMarshal(t *testing.T) { + expected, err := discoveryconfig.NewDiscoveryConfig( + header.Metadata{ + Name: "test-discovery-config", + }, + discoveryconfig.Spec{ + DiscoveryGroup: "dg01", + AWS: []types.AWSMatcher{ + { + Types: []string{"ec2"}, + Regions: []string{"eu-west-2"}, + }, + }, + }, + ) + require.NoError(t, err) + data, err := MarshalDiscoveryConfig(expected) + require.NoError(t, err) + actual, err := UnmarshalDiscoveryConfig(data) + require.NoError(t, err) + require.Equal(t, expected, actual) +} + +var discoveryConfigYAML = `--- +kind: discovery_group +version: v1 +metadata: + name: test-discovery-config +spec: + discovery_group: dg01 + aws: + - types: ["ec2"] + regions: ["eu-west-2"] +` diff --git a/lib/services/local/discoveryconfig.go b/lib/services/local/discoveryconfig.go new file mode 100644 index 0000000000000..218312693f75d --- /dev/null +++ b/lib/services/local/discoveryconfig.go @@ -0,0 +1,114 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package local + +import ( + "context" + + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/types/discoveryconfig" + "github.com/gravitational/teleport/lib/backend" + "github.com/gravitational/teleport/lib/defaults" + "github.com/gravitational/teleport/lib/services" + "github.com/gravitational/teleport/lib/services/local/generic" +) + +const ( + discoveryConfigPrefix = "discovery_config" +) + +// DiscoveryConfigService manages DiscoveryConfigs in the Backend. +type DiscoveryConfigService struct { + svc generic.Service[*discoveryconfig.DiscoveryConfig] +} + +// NewDiscoveryConfigService creates a new DiscoveryConfigService. +func NewDiscoveryConfigService(backend backend.Backend) (*DiscoveryConfigService, error) { + svc, err := generic.NewService(&generic.ServiceConfig[*discoveryconfig.DiscoveryConfig]{ + Backend: backend, + PageLimit: defaults.MaxIterationLimit, + ResourceKind: types.KindDiscoveryConfig, + BackendPrefix: discoveryConfigPrefix, + MarshalFunc: services.MarshalDiscoveryConfig, + UnmarshalFunc: services.UnmarshalDiscoveryConfig, + }) + if err != nil { + return nil, trace.Wrap(err) + } + + return &DiscoveryConfigService{ + svc: *svc, + }, nil +} + +// ListDiscoveryConfigss returns a paginated list of DiscoveryConfig resources. +func (s *DiscoveryConfigService) ListDiscoveryConfigs(ctx context.Context, pageSize int, pageToken string) ([]*discoveryconfig.DiscoveryConfig, string, error) { + dcs, nextKey, err := s.svc.ListResources(ctx, pageSize, pageToken) + if err != nil { + return nil, "", trace.Wrap(err) + } + + return dcs, nextKey, nil +} + +// GetDiscoveryConfig returns the specified DiscoveryConfig resource. +func (s *DiscoveryConfigService) GetDiscoveryConfig(ctx context.Context, name string) (*discoveryconfig.DiscoveryConfig, error) { + dc, err := s.svc.GetResource(ctx, name) + if err != nil { + return nil, trace.Wrap(err) + } + + return dc, nil +} + +// CreateDiscoveryConfigs creates a new DiscoveryConfig resource. +func (s *DiscoveryConfigService) CreateDiscoveryConfig(ctx context.Context, dc *discoveryconfig.DiscoveryConfig) (*discoveryconfig.DiscoveryConfig, error) { + if err := dc.CheckAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) + } + + if err := s.svc.CreateResource(ctx, dc); err != nil { + return nil, trace.Wrap(err) + } + + return dc, nil +} + +// UpdateDiscoveryConfigs updates an existing DiscoveryConfig resource. +func (s *DiscoveryConfigService) UpdateDiscoveryConfig(ctx context.Context, dc *discoveryconfig.DiscoveryConfig) (*discoveryconfig.DiscoveryConfig, error) { + if err := dc.CheckAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) + } + + if err := s.svc.UpdateResource(ctx, dc); err != nil { + return nil, trace.Wrap(err) + } + + return dc, nil +} + +// DeleteDiscoveryConfigs removes the specified DiscoveryConfig resource. +func (s *DiscoveryConfigService) DeleteDiscoveryConfig(ctx context.Context, name string) error { + return trace.Wrap(s.svc.DeleteResource(ctx, name)) +} + +// DeleteAllDiscoveryConfigss removes all DiscoveryConfig resources. +func (s *DiscoveryConfigService) DeleteAllDiscoveryConfigs(ctx context.Context) error { + return trace.Wrap(s.svc.DeleteAllResources(ctx)) +} diff --git a/lib/services/local/discoveryconfig_test.go b/lib/services/local/discoveryconfig_test.go new file mode 100644 index 0000000000000..3f1a9edb53d04 --- /dev/null +++ b/lib/services/local/discoveryconfig_test.go @@ -0,0 +1,140 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package local + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/gravitational/trace" + "github.com/jonboulle/clockwork" + "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport/api/types/discoveryconfig" + "github.com/gravitational/teleport/api/types/header" + "github.com/gravitational/teleport/lib/backend" + "github.com/gravitational/teleport/lib/backend/memory" +) + +// TestDiscoveryConfigCRUD tests backend operations with discovery config resources. +func TestDiscoveryConfigCRUD(t *testing.T) { + ctx := context.Background() + clock := clockwork.NewFakeClock() + + mem, err := memory.New(memory.Config{ + Context: ctx, + Clock: clock, + }) + require.NoError(t, err) + + service, err := NewDiscoveryConfigService(backend.NewSanitizer(mem)) + require.NoError(t, err) + + // Create a couple discovery configs. + discoveryConfig1 := newDiscoveryConfig(t, "discovery-config-1") + discoveryConfig2 := newDiscoveryConfig(t, "discovery-config-2") + + // Initially we expect no discovery configs. + out, nextToken, err := service.ListDiscoveryConfigs(ctx, 0, "") + require.NoError(t, err) + require.Empty(t, nextToken) + require.Empty(t, out) + + cmpOpts := []cmp.Option{ + cmpopts.IgnoreFields(header.Metadata{}, "ID"), + } + + // Create both discovery configs. + discoveryConfig, err := service.CreateDiscoveryConfig(ctx, discoveryConfig1) + require.NoError(t, err) + require.Empty(t, cmp.Diff(discoveryConfig1, discoveryConfig, cmpOpts...)) + discoveryConfig, err = service.CreateDiscoveryConfig(ctx, discoveryConfig2) + require.NoError(t, err) + require.Empty(t, cmp.Diff(discoveryConfig2, discoveryConfig, cmpOpts...)) + + // Fetch a paginated list of discovery configs + paginatedOut := make([]*discoveryconfig.DiscoveryConfig, 0, 2) + for { + out, nextToken, err = service.ListDiscoveryConfigs(ctx, 1, nextToken) + require.NoError(t, err) + + paginatedOut = append(paginatedOut, out...) + if nextToken == "" { + break + } + } + + require.Len(t, paginatedOut, 2) + require.Empty(t, cmp.Diff([]*discoveryconfig.DiscoveryConfig{discoveryConfig1, discoveryConfig2}, paginatedOut, cmpOpts...)) + + // Fetch a specific discovery config. + discoveryConfig, err = service.GetDiscoveryConfig(ctx, discoveryConfig2.GetName()) + require.NoError(t, err) + require.Empty(t, cmp.Diff(discoveryConfig2, discoveryConfig, cmpOpts...)) + + // Try to fetch a discovery config that doesn't exist. + _, err = service.GetDiscoveryConfig(ctx, "doesnotexist") + require.True(t, trace.IsNotFound(err), "expected not found error, got %v", err) + + // Update a discovery config. + discoveryConfig1.SetExpiry(clock.Now().Add(30 * time.Minute)) + discoveryConfig, err = service.UpdateDiscoveryConfig(ctx, discoveryConfig1) + require.NoError(t, err) + require.Empty(t, cmp.Diff(discoveryConfig1, discoveryConfig, cmpOpts...)) + discoveryConfig, err = service.GetDiscoveryConfig(ctx, discoveryConfig1.GetName()) + require.NoError(t, err) + require.Empty(t, cmp.Diff(discoveryConfig1, discoveryConfig, cmpOpts...)) + + // Delete a discovery config. + err = service.DeleteDiscoveryConfig(ctx, discoveryConfig1.GetName()) + require.NoError(t, err) + out, nextToken, err = service.ListDiscoveryConfigs(ctx, 0, "") + require.NoError(t, err) + require.Empty(t, nextToken) + require.Empty(t, cmp.Diff([]*discoveryconfig.DiscoveryConfig{discoveryConfig2}, out, cmpOpts...)) + + // Try to delete a discovery config that doesn't exist. + err = service.DeleteDiscoveryConfig(ctx, "doesnotexist") + require.True(t, trace.IsNotFound(err), "expected not found error, got %v", err) + + // Delete all discovery configs. + err = service.DeleteAllDiscoveryConfigs(ctx) + require.NoError(t, err) + out, nextToken, err = service.ListDiscoveryConfigs(ctx, 0, "") + require.NoError(t, err) + require.Empty(t, nextToken) + require.Empty(t, out) +} + +func newDiscoveryConfig(t *testing.T, name string) *discoveryconfig.DiscoveryConfig { + t.Helper() + + discoveryConfig, err := discoveryconfig.NewDiscoveryConfig( + header.Metadata{ + Name: name, + }, + discoveryconfig.Spec{ + DiscoveryGroup: "dg-1", + }, + ) + require.NoError(t, err) + + return discoveryConfig +}