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

Add UpsertTrustedClusterV2 RPC #49789

Merged
merged 24 commits into from
Dec 21, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
fb760dc
Add UpsertTrustedClusterV2 rpc
bernardjkim Dec 4, 2024
bc9e0fb
Replace confusing UpsertValidationTrustedCluster name
bernardjkim Dec 5, 2024
415d72e
Use UpsertTrustedClusterV2 in tests
bernardjkim Dec 5, 2024
7049271
Address feedback
bernardjkim Dec 5, 2024
7ebe5ce
Merge branch 'master' into bernard/upsert-trusted-cluster-v2
bernardjkim Dec 5, 2024
1933ba6
Use webclient.Find
bernardjkim Dec 5, 2024
cb1f827
Fix test/lint
bernardjkim Dec 5, 2024
6243ff2
Allow label updates
bernardjkim Dec 6, 2024
4142256
Fix test
bernardjkim Dec 6, 2024
5db82b4
Merge branch 'master' into bernard/upsert-trusted-cluster-v2
bernardjkim Dec 6, 2024
330a663
Fix error handling
bernardjkim Dec 7, 2024
456a2fe
Implement CreateTrustedClusterV2 and UpdateTrustedClusterV2
bernardjkim Dec 10, 2024
d3118e2
Address feedback
bernardjkim Dec 10, 2024
aa82d22
Minor fixes
bernardjkim Dec 10, 2024
96eea47
Merge branch 'master' into bernard/upsert-trusted-cluster-v2
bernardjkim Dec 10, 2024
3036745
Move V2 RPCs to the trust service
bernardjkim Dec 11, 2024
57fc180
Merge branch 'master' into bernard/upsert-trusted-cluster-v2
bernardjkim Dec 12, 2024
4a4bb8d
Update comment
bernardjkim Dec 12, 2024
6470291
Drop V2 suffix
bernardjkim Dec 12, 2024
f8bc242
Require matching revision
bernardjkim Dec 12, 2024
fba6930
Fix upsert/update revision
bernardjkim Dec 20, 2024
519c124
Drop V2 from Create and Update APIs
bernardjkim Dec 20, 2024
3f46c89
Merge branch 'master' into bernard/upsert-trusted-cluster-v2
bernardjkim Dec 20, 2024
a290e38
Lint: Fix typo
bernardjkim Dec 20, 2024
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
24 changes: 20 additions & 4 deletions api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2278,18 +2278,34 @@ func (c *Client) GetTrustedClusters(ctx context.Context) ([]types.TrustedCluster
}

// UpsertTrustedCluster creates or updates a Trusted Cluster.
func (c *Client) UpsertTrustedCluster(ctx context.Context, trusedCluster types.TrustedCluster) (types.TrustedCluster, error) {
trustedCluster, ok := trusedCluster.(*types.TrustedClusterV2)
//
// Deprecated: Use [Client.UpsertTrustedClusterV2] instead.
func (c *Client) UpsertTrustedCluster(ctx context.Context, trustedCluster types.TrustedCluster) (types.TrustedCluster, error) {
trustedClusterV2, ok := trustedCluster.(*types.TrustedClusterV2)
if !ok {
return nil, trace.BadParameter("invalid type %T", trusedCluster)
return nil, trace.BadParameter("invalid type %T", trustedCluster)
}
resp, err := c.grpc.UpsertTrustedCluster(ctx, trustedCluster)
resp, err := c.grpc.UpsertTrustedCluster(ctx, trustedClusterV2)
if err != nil {
return nil, trace.Wrap(err)
}
return resp, nil
}

// UpsertTrustedClusterV2 creates or updates a Trusted Cluster.
func (c *Client) UpsertTrustedClusterV2(ctx context.Context, trustedCluster types.TrustedCluster) (types.TrustedCluster, error) {
trustedClusterV2, ok := trustedCluster.(*types.TrustedClusterV2)
if !ok {
return nil, trace.BadParameter("invalid type %T", trustedCluster)
}
resp, err := c.grpc.UpsertTrustedClusterV2(ctx, trustedClusterV2)
if err != nil && trace.IsNotImplemented(err) {
return nil, trace.Wrap(err, "control plane does not support this operation, "+
"consider upgrading your control plane")
}
return resp, nil
}

// DeleteTrustedCluster deletes a Trusted Cluster by name.
func (c *Client) DeleteTrustedCluster(ctx context.Context, name string) error {
if name == "" {
Expand Down
1,944 changes: 994 additions & 950 deletions api/client/proto/authservice.pb.go

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion api/proto/teleport/legacy/client/proto/authservice.proto
Original file line number Diff line number Diff line change
Expand Up @@ -3220,7 +3220,13 @@ service AuthService {
// GetTrustedClusters gets all current Trusted Cluster resources.
rpc GetTrustedClusters(google.protobuf.Empty) returns (types.TrustedClusterV2List);
// UpsertTrustedCluster upserts a Trusted Cluster in a backend.
rpc UpsertTrustedCluster(types.TrustedClusterV2) returns (types.TrustedClusterV2);
//
// Deprecated: Use UpsertTrustedClusterV2 instead.
rpc UpsertTrustedCluster(types.TrustedClusterV2) returns (types.TrustedClusterV2) {
option deprecated = true;
}
// UpsertTrustedClusterV2 upserts a Trusted Cluster in a backend.
rpc UpsertTrustedClusterV2(types.TrustedClusterV2) returns (types.TrustedClusterV2);
// DeleteTrustedCluster deletes an existing Trusted Cluster in a backend by name.
rpc DeleteTrustedCluster(types.ResourceRequest) returns (google.protobuf.Empty);

Expand Down
14 changes: 12 additions & 2 deletions api/types/trustedcluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ import (
// TrustedCluster holds information needed for a cluster that can not be directly
// accessed (maybe be behind firewall without any open ports) to join a parent cluster.
type TrustedCluster interface {
// Resource provides common resource properties
Resource
// ResourceWithOrigin provides common resource properties
ResourceWithOrigin
// SetMetadata sets object metadata
SetMetadata(meta Metadata)
// GetEnabled returns the state of the TrustedCluster.
Expand Down Expand Up @@ -184,6 +184,16 @@ func (c *TrustedClusterV2) SetName(e string) {
c.Metadata.Name = e
}

// Origin returns the origin value of the resource.
func (c *TrustedClusterV2) Origin() string {
return c.Metadata.Origin()
}

// SetOrigin sets the origin value of the resource.
func (c *TrustedClusterV2) SetOrigin(origin string) {
c.Metadata.SetOrigin(origin)
}

bernardjkim marked this conversation as resolved.
Show resolved Hide resolved
// GetEnabled returns the state of the TrustedCluster.
func (c *TrustedClusterV2) GetEnabled() bool {
return c.Spec.Enabled
Expand Down
9 changes: 7 additions & 2 deletions integration/helpers/trustedclusters.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,17 @@ func WaitForTunnelConnections(t *testing.T, authServer *auth.Server, clusterName
// propagate and services to start
//
// Duplicated in tool/tsh/tsh_test.go
func TryCreateTrustedCluster(t *testing.T, authServer *auth.Server, trustedCluster types.TrustedCluster) {
func TryCreateTrustedCluster(t *testing.T, authServer *auth.Server, trustedCluster types.TrustedCluster, skipNameValidation bool) {
t.Helper()
ctx := context.TODO()
for i := 0; i < 10; i++ {
log.Debugf("Will create trusted cluster %v, attempt %v.", trustedCluster, i)
_, err := authServer.UpsertTrustedCluster(ctx, trustedCluster)
var err error
if skipNameValidation {
_, err = authServer.UpsertTrustedCluster(ctx, trustedCluster)
} else {
_, err = authServer.UpsertTrustedClusterV2(ctx, trustedCluster)
}
if err == nil {
return
}
Expand Down
74 changes: 43 additions & 31 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ func TestIntegrations(t *testing.T) {
t.Run("TrustedDisabledClusters", suite.bind(testDisabledTrustedClusters))
t.Run("TrustedClustersRoleMapChanges", suite.bind(testTrustedClustersRoleMapChanges))
t.Run("TrustedClustersWithLabels", suite.bind(testTrustedClustersWithLabels))
t.Run("TrustedClustersSkipNameValidation", suite.bind(testTrustedClustersSkipNameValidation))
t.Run("TrustedTunnelNode", suite.bind(testTrustedTunnelNode))
t.Run("TwoClustersProxy", suite.bind(testTwoClustersProxy))
t.Run("TwoClustersTunnel", suite.bind(testTwoClustersTunnel))
Expand Down Expand Up @@ -2796,7 +2797,8 @@ func testMapRoles(t *testing.T, suite *integrationTestSuite) {
require.NoError(t, services.CheckAndSetDefaults(trustedCluster))

// try and upsert a trusted cluster
helpers.TryCreateTrustedCluster(t, aux.Process.GetAuthServer(), trustedCluster)
const skipNameValidation = false
helpers.TryCreateTrustedCluster(t, aux.Process.GetAuthServer(), trustedCluster, skipNameValidation)
helpers.WaitForTunnelConnections(t, main.Process.GetAuthServer(), clusterAux, 1)

sshPort, _, _ := aux.StartNodeAndProxy(t, "aux-node")
Expand Down Expand Up @@ -2946,6 +2948,9 @@ type trustedClusterTest struct {
// useLabels turns on trusted cluster labels and
// verifies RBAC
useLabels bool
// skipNameValidation uses the deprecated UpsertTrustedCluster and skips
// cluster name validation.
skipNameValidation bool
}

// TestTrustedClusters tests remote clusters scenarios
Expand Down Expand Up @@ -2982,6 +2987,15 @@ func testTrustedClustersWithLabels(t *testing.T, suite *integrationTestSuite) {
trustedClusters(t, suite, trustedClusterTest{multiplex: false, useLabels: true})
}

// TestTrustedClustersSkipNameValidation tests remote clusters scenarios
// skipping name validation.
func testTrustedClustersSkipNameValidation(t *testing.T, suite *integrationTestSuite) {
tr := utils.NewTracer(utils.ThisFunction()).Start()
defer tr.Stop()

trustedClusters(t, suite, trustedClusterTest{skipNameValidation: true})
}

// TestJumpTrustedClusters tests remote clusters scenarios
// using trusted clusters feature using jumphost connection
func testJumpTrustedClusters(t *testing.T, suite *integrationTestSuite) {
Expand Down Expand Up @@ -3119,17 +3133,25 @@ func trustedClusters(t *testing.T, suite *integrationTestSuite, test trustedClus
{Remote: mainOps, Local: []string{auxDevs}},
})

// modify trusted cluster resource name, so it would not
// match the cluster name to check that it does not matter
trustedCluster.SetName(main.Secrets.SiteName + "-cluster")

require.NoError(t, main.Start())
require.NoError(t, aux.Start())

require.NoError(t, services.CheckAndSetDefaults(trustedCluster))

// Note that the trusted cluster resource name must match the cluster name.
// Modify the trusted cluster resource name and expect the upsert to fail.
trustedCluster.SetName(main.Secrets.SiteName + "-cluster")

_, err = aux.Process.GetAuthServer().UpsertTrustedClusterV2(ctx, trustedCluster)
require.ErrorContains(t, err, "trusted cluster resource name must be the same as the remote cluster name", "expected failure due to tc name mismatch")

if !test.skipNameValidation {
// Modify the trusted cluster resource name back to what it was orignally.
trustedCluster.SetName(main.Secrets.SiteName)
}

// try and upsert a trusted cluster
helpers.TryCreateTrustedCluster(t, aux.Process.GetAuthServer(), trustedCluster)
helpers.TryCreateTrustedCluster(t, aux.Process.GetAuthServer(), trustedCluster, test.skipNameValidation)
helpers.WaitForTunnelConnections(t, main.Process.GetAuthServer(), clusterAux, 1)

sshPort, _, _ := aux.StartNodeAndProxy(t, "aux-node")
Expand Down Expand Up @@ -3212,7 +3234,7 @@ func trustedClusters(t *testing.T, suite *integrationTestSuite, test trustedClus
require.Error(t, err, "expected tunnel to close and SSH client to start failing")

// recreating the trusted cluster should re-establish connection
_, err = aux.Process.GetAuthServer().UpsertTrustedCluster(ctx, trustedCluster)
_, err = aux.Process.GetAuthServer().UpsertTrustedClusterV2(ctx, trustedCluster)
require.NoError(t, err)

// check that remote cluster has been re-provisioned
Expand Down Expand Up @@ -3354,9 +3376,6 @@ func trustedDisabledCluster(t *testing.T, suite *integrationTestSuite, test trus
{Remote: mainOps, Local: []string{auxDevs}},
})

// modify trusted cluster resource name, so it would not
// match the cluster name to check that it does not matter
trustedCluster.SetName(main.Secrets.SiteName + "-cluster")
// disable cluster
trustedCluster.SetEnabled(false)

Expand All @@ -3366,11 +3385,11 @@ func trustedDisabledCluster(t *testing.T, suite *integrationTestSuite, test trus
require.NoError(t, services.CheckAndSetDefaults(trustedCluster))

// try and upsert a trusted cluster while disabled
helpers.TryCreateTrustedCluster(t, aux.Process.GetAuthServer(), trustedCluster)
helpers.TryCreateTrustedCluster(t, aux.Process.GetAuthServer(), trustedCluster, test.skipNameValidation)

// try to enable disabled cluster
trustedCluster.SetEnabled(true)
helpers.TryCreateTrustedCluster(t, aux.Process.GetAuthServer(), trustedCluster)
helpers.TryCreateTrustedCluster(t, aux.Process.GetAuthServer(), trustedCluster, test.skipNameValidation)
helpers.WaitForTunnelConnections(t, main.Process.GetAuthServer(), clusterAux, 1)

helpers.CheckTrustedClustersCanConnect(ctx, t, helpers.TrustedClusterSetup{
Expand Down Expand Up @@ -3494,24 +3513,20 @@ func trustedClustersRoleMapChanges(t *testing.T, suite *integrationTestSuite, te
{Remote: mainOps, Local: []string{auxDevs}},
})

// modify trusted cluster resource name, so it would not
// match the cluster name to check that it does not matter
trustedCluster.SetName(main.Secrets.SiteName + "-cluster")

require.NoError(t, main.Start())
require.NoError(t, aux.Start())

require.NoError(t, services.CheckAndSetDefaults(trustedCluster))

helpers.TryCreateTrustedCluster(t, aux.Process.GetAuthServer(), trustedCluster)
helpers.TryCreateTrustedCluster(t, aux.Process.GetAuthServer(), trustedCluster, test.skipNameValidation)
helpers.WaitForTunnelConnections(t, main.Process.GetAuthServer(), clusterAux, 1)

// change role mapping to ensure updating works
trustedCluster.SetRoleMap(types.RoleMap{
{Remote: mainDevs, Local: []string{auxDevs}},
})

helpers.TryCreateTrustedCluster(t, aux.Process.GetAuthServer(), trustedCluster)
helpers.TryCreateTrustedCluster(t, aux.Process.GetAuthServer(), trustedCluster, test.skipNameValidation)
helpers.WaitForTunnelConnections(t, main.Process.GetAuthServer(), clusterAux, 1)

helpers.CheckTrustedClustersCanConnect(ctx, t, helpers.TrustedClusterSetup{
Expand All @@ -3524,7 +3539,7 @@ func trustedClustersRoleMapChanges(t *testing.T, suite *integrationTestSuite, te

// disable the enabled trusted cluster and ensure it no longer works
trustedCluster.SetEnabled(false)
helpers.TryCreateTrustedCluster(t, aux.Process.GetAuthServer(), trustedCluster)
helpers.TryCreateTrustedCluster(t, aux.Process.GetAuthServer(), trustedCluster, test.skipNameValidation)

// Wait for both cluster to no longer see each other via reverse tunnels.
require.Eventually(t, helpers.WaitForClusters(main.Tunnel, 0), 10*time.Second, 1*time.Second,
Expand Down Expand Up @@ -3595,17 +3610,14 @@ func testTrustedTunnelNode(t *testing.T, suite *integrationTestSuite) {
{Remote: mainDevs, Local: []string{auxDevs}},
})

// modify trusted cluster resource name, so it would not
// match the cluster name to check that it does not matter
trustedCluster.SetName(main.Secrets.SiteName + "-cluster")

require.NoError(t, main.Start())
require.NoError(t, aux.Start())

require.NoError(t, services.CheckAndSetDefaults(trustedCluster))

// try and upsert a trusted cluster
helpers.TryCreateTrustedCluster(t, aux.Process.GetAuthServer(), trustedCluster)
const skipNameValidation = false
helpers.TryCreateTrustedCluster(t, aux.Process.GetAuthServer(), trustedCluster, skipNameValidation)
helpers.WaitForTunnelConnections(t, main.Process.GetAuthServer(), clusterAux, 1)

// Create a Teleport instance with a node that dials back to the aux cluster.
Expand Down Expand Up @@ -3779,17 +3791,14 @@ func testTrustedClusterAgentless(t *testing.T, suite *integrationTestSuite) {
{Remote: devsRoleName, Local: []string{devsRoleName}},
})

// modify trusted cluster resource name, so it would not
// match the cluster name to check that it does not matter
trustedCluster.SetName(main.Secrets.SiteName + "-cluster")

require.NoError(t, main.Start())
require.NoError(t, leaf.Start())

require.NoError(t, services.CheckAndSetDefaults(trustedCluster))

// try and upsert a trusted cluster
helpers.TryCreateTrustedCluster(t, leaf.Process.GetAuthServer(), trustedCluster)
const skipNameValidation = false
helpers.TryCreateTrustedCluster(t, leaf.Process.GetAuthServer(), trustedCluster, skipNameValidation)
helpers.WaitForTunnelConnections(t, main.Process.GetAuthServer(), clusterAux, 1)

// Wait for both cluster to see each other via reverse tunnels.
Expand Down Expand Up @@ -5612,7 +5621,8 @@ func testRotateTrustedClusters(t *testing.T, suite *integrationTestSuite) {
lib.SetInsecureDevMode(true)
defer lib.SetInsecureDevMode(false)

helpers.TryCreateTrustedCluster(t, aux.Process.GetAuthServer(), trustedCluster)
const skipNameValidation = false
helpers.TryCreateTrustedCluster(t, aux.Process.GetAuthServer(), trustedCluster, skipNameValidation)
helpers.WaitForTunnelConnections(t, svc.GetAuthServer(), aux.Secrets.SiteName, 1)

// capture credentials before reload has started to simulate old client
Expand Down Expand Up @@ -7543,7 +7553,9 @@ func createTrustedClusterPair(t *testing.T, suite *integrationTestSuite, extraSe
t.Cleanup(func() { leaf.StopAll() })

require.NoError(t, services.CheckAndSetDefaults(trustedCluster))
helpers.TryCreateTrustedCluster(t, leaf.Process.GetAuthServer(), trustedCluster)

const skipNameValidation = false
helpers.TryCreateTrustedCluster(t, leaf.Process.GetAuthServer(), trustedCluster, skipNameValidation)
helpers.WaitForTunnelConnections(t, root.Process.GetAuthServer(), leafName, 1)

_, _, rootProxySSHPort := root.StartNodeAndProxy(t, "root-zero")
Expand Down
4 changes: 2 additions & 2 deletions integration/kube_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -695,7 +695,7 @@ func testKubeTrustedClustersClientCert(t *testing.T, suite *KubeSuite) {
var upsertSuccess bool
for i := 0; i < 10; i++ {
log.Debugf("Will create trusted cluster %v, attempt %v", trustedCluster, i)
_, err = aux.Process.GetAuthServer().UpsertTrustedCluster(ctx, trustedCluster)
_, err = aux.Process.GetAuthServer().UpsertTrustedClusterV2(ctx, trustedCluster)
if err != nil {
if trace.IsConnectionProblem(err) {
log.Debugf("retrying on connection problem: %v", err)
Expand Down Expand Up @@ -971,7 +971,7 @@ func testKubeTrustedClustersSNI(t *testing.T, suite *KubeSuite) {
var upsertSuccess bool
for i := 0; i < 10; i++ {
log.Debugf("Will create trusted cluster %v, attempt %v", trustedCluster, i)
_, err = aux.Process.GetAuthServer().UpsertTrustedCluster(ctx, trustedCluster)
_, err = aux.Process.GetAuthServer().UpsertTrustedClusterV2(ctx, trustedCluster)
if err != nil {
if trace.IsConnectionProblem(err) {
log.Debugf("retrying on connection problem: %v", err)
Expand Down
3 changes: 2 additions & 1 deletion integration/proxy/proxy_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,8 @@ func newSuite(t *testing.T, opts ...proxySuiteOptionsFunc) *Suite {
}

if options.trustedCluster != nil {
helpers.TryCreateTrustedCluster(t, suite.leaf.Process.GetAuthServer(), options.trustedCluster)
const skipNameValidation = false
helpers.TryCreateTrustedCluster(t, suite.leaf.Process.GetAuthServer(), options.trustedCluster, skipNameValidation)
helpers.WaitForTunnelConnections(t, suite.root.Process.GetAuthServer(), suite.leaf.Secrets.SiteName, 1)
}

Expand Down
4 changes: 2 additions & 2 deletions lib/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1262,7 +1262,7 @@ func TestTrustedClusterCRUDEventEmitted(t *testing.T) {
// test create event for switch case: when tc exists but enabled is false
tc.SetEnabled(false)

_, err = s.a.UpsertTrustedCluster(ctx, tc)
_, err = s.a.UpsertTrustedClusterV2(ctx, tc)
require.NoError(t, err)
require.Equal(t, events.TrustedClusterCreateEvent, s.mockEmitter.LastEvent().GetType())
createEvt := s.mockEmitter.LastEvent().(*apievents.TrustedClusterCreate)
Expand All @@ -1272,7 +1272,7 @@ func TestTrustedClusterCRUDEventEmitted(t *testing.T) {
// test create event for switch case: when tc exists but enabled is true
tc.SetEnabled(true)

_, err = s.a.UpsertTrustedCluster(ctx, tc)
_, err = s.a.UpsertTrustedClusterV2(ctx, tc)
require.NoError(t, err)
require.Equal(t, events.TrustedClusterCreateEvent, s.mockEmitter.LastEvent().GetType())
createEvt = s.mockEmitter.LastEvent().(*apievents.TrustedClusterCreate)
Expand Down
19 changes: 19 additions & 0 deletions lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -4926,6 +4926,25 @@ func (a *ServerWithRoles) UpsertTrustedCluster(ctx context.Context, tc types.Tru
return a.authServer.UpsertTrustedCluster(ctx, tc)
}

// UpsertTrustedClusterV2 creates or updates a trusted cluster while also ensuring that the
// resource name and cluster name match.
func (a *ServerWithRoles) UpsertTrustedClusterV2(ctx context.Context, tc types.TrustedCluster) (types.TrustedCluster, error) {
// Don't allow a Cloud tenant to be a leaf cluster.
if modules.GetModules().Features().Cloud {
return nil, trace.NotImplemented("cloud tenants cannot be leaf clusters")
}

if err := a.action(types.KindTrustedCluster, types.VerbCreate, types.VerbUpdate); err != nil {
return nil, trace.Wrap(err)
}

if err := a.context.AuthorizeAdminActionAllowReusedMFA(); err != nil {
return nil, trace.Wrap(err)
}

return a.authServer.UpsertTrustedClusterV2(ctx, tc)
bernardjkim marked this conversation as resolved.
Show resolved Hide resolved
}

func (a *ServerWithRoles) ValidateTrustedCluster(ctx context.Context, validateRequest *authclient.ValidateTrustedClusterRequest) (*authclient.ValidateTrustedClusterResponse, error) {
// Don't allow a leaf cluster to be added to a Cloud tenant.
if modules.GetModules().Features().Cloud {
Expand Down
Loading
Loading