github.com/kaisenlinux/docker.io@v0.0.0-20230510090727-ea55db55fac7/swarmkit/manager/controlapi/cluster_test.go (about)

     1  package controlapi
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/docker/swarmkit/api"
    10  	"github.com/docker/swarmkit/ca"
    11  	"github.com/docker/swarmkit/ca/testutils"
    12  	"github.com/docker/swarmkit/manager/state/store"
    13  	"github.com/docker/swarmkit/protobuf/ptypes"
    14  	grpcutils "github.com/docker/swarmkit/testutils"
    15  	gogotypes "github.com/gogo/protobuf/types"
    16  	"github.com/stretchr/testify/assert"
    17  	"github.com/stretchr/testify/require"
    18  	"google.golang.org/grpc/codes"
    19  )
    20  
    21  func createClusterSpec(name string) *api.ClusterSpec {
    22  	return &api.ClusterSpec{
    23  		Annotations: api.Annotations{
    24  			Name: name,
    25  		},
    26  		CAConfig: api.CAConfig{
    27  			NodeCertExpiry: gogotypes.DurationProto(ca.DefaultNodeCertExpiration),
    28  		},
    29  	}
    30  }
    31  
    32  func createClusterObj(id, name string, policy api.AcceptancePolicy, rootCA *ca.RootCA) *api.Cluster {
    33  	spec := createClusterSpec(name)
    34  	spec.AcceptancePolicy = policy
    35  
    36  	var key []byte
    37  	if s, err := rootCA.Signer(); err == nil {
    38  		key = s.Key
    39  	}
    40  
    41  	return &api.Cluster{
    42  		ID:   id,
    43  		Spec: *spec,
    44  		RootCA: api.RootCA{
    45  			CACert:     rootCA.Certs,
    46  			CAKey:      key,
    47  			CACertHash: rootCA.Digest.String(),
    48  			JoinTokens: api.JoinTokens{
    49  				Worker:  ca.GenerateJoinToken(rootCA, false),
    50  				Manager: ca.GenerateJoinToken(rootCA, false),
    51  			},
    52  		},
    53  	}
    54  }
    55  
    56  func createCluster(t *testing.T, ts *testServer, id, name string, policy api.AcceptancePolicy, rootCA *ca.RootCA) *api.Cluster {
    57  	cluster := createClusterObj(id, name, policy, rootCA)
    58  	assert.NoError(t, ts.Store.Update(func(tx store.Tx) error {
    59  		return store.CreateCluster(tx, cluster)
    60  	}))
    61  	return cluster
    62  }
    63  
    64  func TestValidateClusterSpec(t *testing.T) {
    65  	type BadClusterSpec struct {
    66  		spec *api.ClusterSpec
    67  		c    codes.Code
    68  	}
    69  
    70  	for _, bad := range []BadClusterSpec{
    71  		{
    72  			spec: nil,
    73  			c:    codes.InvalidArgument,
    74  		},
    75  		{
    76  			spec: &api.ClusterSpec{
    77  				Annotations: api.Annotations{
    78  					Name: store.DefaultClusterName,
    79  				},
    80  				CAConfig: api.CAConfig{
    81  					NodeCertExpiry: gogotypes.DurationProto(29 * time.Minute),
    82  				},
    83  			},
    84  			c: codes.InvalidArgument,
    85  		},
    86  		{
    87  			spec: &api.ClusterSpec{
    88  				Annotations: api.Annotations{
    89  					Name: store.DefaultClusterName,
    90  				},
    91  				Dispatcher: api.DispatcherConfig{
    92  					HeartbeatPeriod: gogotypes.DurationProto(-29 * time.Minute),
    93  				},
    94  			},
    95  			c: codes.InvalidArgument,
    96  		},
    97  		{
    98  			spec: &api.ClusterSpec{
    99  				Annotations: api.Annotations{
   100  					Name: "",
   101  				},
   102  			},
   103  			c: codes.InvalidArgument,
   104  		},
   105  		{
   106  			spec: &api.ClusterSpec{
   107  				Annotations: api.Annotations{
   108  					Name: "blah",
   109  				},
   110  			},
   111  			c: codes.InvalidArgument,
   112  		},
   113  	} {
   114  		err := validateClusterSpec(bad.spec)
   115  		assert.Error(t, err)
   116  		assert.Equal(t, bad.c, grpcutils.ErrorCode(err))
   117  	}
   118  
   119  	for _, good := range []*api.ClusterSpec{
   120  		createClusterSpec(store.DefaultClusterName),
   121  	} {
   122  		err := validateClusterSpec(good)
   123  		assert.NoError(t, err)
   124  	}
   125  
   126  }
   127  
   128  func TestGetCluster(t *testing.T) {
   129  	ts := newTestServer(t)
   130  	defer ts.Stop()
   131  	_, err := ts.Client.GetCluster(context.Background(), &api.GetClusterRequest{})
   132  	assert.Error(t, err)
   133  	assert.Equal(t, codes.InvalidArgument, grpcutils.ErrorCode(err))
   134  
   135  	_, err = ts.Client.GetCluster(context.Background(), &api.GetClusterRequest{ClusterID: "invalid"})
   136  	assert.Error(t, err)
   137  	assert.Equal(t, codes.NotFound, grpcutils.ErrorCode(err))
   138  
   139  	cluster := createCluster(t, ts, "name", "name", api.AcceptancePolicy{}, ts.Server.securityConfig.RootCA())
   140  	r, err := ts.Client.GetCluster(context.Background(), &api.GetClusterRequest{ClusterID: cluster.ID})
   141  	assert.NoError(t, err)
   142  	cluster.Meta.Version = r.Cluster.Meta.Version
   143  	// Only public fields should be available
   144  	assert.Equal(t, cluster.ID, r.Cluster.ID)
   145  	assert.Equal(t, cluster.Meta, r.Cluster.Meta)
   146  	assert.Equal(t, cluster.Spec, r.Cluster.Spec)
   147  	assert.Equal(t, cluster.RootCA.CACert, r.Cluster.RootCA.CACert)
   148  	assert.Equal(t, cluster.RootCA.CACertHash, r.Cluster.RootCA.CACertHash)
   149  	// CAKey and network keys should be nil
   150  	assert.Nil(t, r.Cluster.RootCA.CAKey)
   151  	assert.Nil(t, r.Cluster.NetworkBootstrapKeys)
   152  }
   153  
   154  func TestGetClusterWithSecret(t *testing.T) {
   155  	ts := newTestServer(t)
   156  	defer ts.Stop()
   157  	_, err := ts.Client.GetCluster(context.Background(), &api.GetClusterRequest{})
   158  	assert.Error(t, err)
   159  	assert.Equal(t, codes.InvalidArgument, grpcutils.ErrorCode(err))
   160  
   161  	_, err = ts.Client.GetCluster(context.Background(), &api.GetClusterRequest{ClusterID: "invalid"})
   162  	assert.Error(t, err)
   163  	assert.Equal(t, codes.NotFound, grpcutils.ErrorCode(err))
   164  
   165  	policy := api.AcceptancePolicy{Policies: []*api.AcceptancePolicy_RoleAdmissionPolicy{{Secret: &api.AcceptancePolicy_RoleAdmissionPolicy_Secret{Data: []byte("secret")}}}}
   166  	cluster := createCluster(t, ts, "name", "name", policy, ts.Server.securityConfig.RootCA())
   167  	r, err := ts.Client.GetCluster(context.Background(), &api.GetClusterRequest{ClusterID: cluster.ID})
   168  	assert.NoError(t, err)
   169  	cluster.Meta.Version = r.Cluster.Meta.Version
   170  	assert.NotEqual(t, cluster, r.Cluster)
   171  	assert.NotContains(t, r.Cluster.String(), "secret")
   172  	assert.NotContains(t, r.Cluster.String(), "PRIVATE")
   173  	assert.NotNil(t, r.Cluster.Spec.AcceptancePolicy.Policies[0].Secret.Data)
   174  }
   175  
   176  func TestUpdateCluster(t *testing.T) {
   177  	ts := newTestServer(t)
   178  	defer ts.Stop()
   179  	cluster := createCluster(t, ts, "name", store.DefaultClusterName, api.AcceptancePolicy{}, ts.Server.securityConfig.RootCA())
   180  
   181  	_, err := ts.Client.UpdateCluster(context.Background(), &api.UpdateClusterRequest{})
   182  	assert.Error(t, err)
   183  	assert.Equal(t, codes.InvalidArgument, grpcutils.ErrorCode(err))
   184  
   185  	_, err = ts.Client.UpdateCluster(context.Background(), &api.UpdateClusterRequest{ClusterID: "invalid", Spec: &cluster.Spec, ClusterVersion: &api.Version{}})
   186  	assert.Error(t, err)
   187  	assert.Equal(t, codes.NotFound, grpcutils.ErrorCode(err))
   188  
   189  	// No update options.
   190  	_, err = ts.Client.UpdateCluster(context.Background(), &api.UpdateClusterRequest{ClusterID: cluster.ID, Spec: &cluster.Spec})
   191  	assert.Error(t, err)
   192  	assert.Equal(t, codes.InvalidArgument, grpcutils.ErrorCode(err))
   193  
   194  	_, err = ts.Client.UpdateCluster(context.Background(), &api.UpdateClusterRequest{ClusterID: cluster.ID, Spec: &cluster.Spec, ClusterVersion: &cluster.Meta.Version})
   195  	assert.NoError(t, err)
   196  
   197  	r, err := ts.Client.ListClusters(context.Background(), &api.ListClustersRequest{
   198  		Filters: &api.ListClustersRequest_Filters{
   199  			NamePrefixes: []string{store.DefaultClusterName},
   200  		},
   201  	})
   202  	assert.NoError(t, err)
   203  	assert.Len(t, r.Clusters, 1)
   204  	assert.Equal(t, cluster.Spec.Annotations.Name, r.Clusters[0].Spec.Annotations.Name)
   205  	assert.Len(t, r.Clusters[0].Spec.AcceptancePolicy.Policies, 0)
   206  
   207  	r.Clusters[0].Spec.AcceptancePolicy = api.AcceptancePolicy{Policies: []*api.AcceptancePolicy_RoleAdmissionPolicy{{Secret: &api.AcceptancePolicy_RoleAdmissionPolicy_Secret{Alg: "bcrypt", Data: []byte("secret")}}}}
   208  	_, err = ts.Client.UpdateCluster(context.Background(), &api.UpdateClusterRequest{
   209  		ClusterID:      cluster.ID,
   210  		Spec:           &r.Clusters[0].Spec,
   211  		ClusterVersion: &r.Clusters[0].Meta.Version,
   212  	})
   213  	assert.NoError(t, err)
   214  
   215  	r, err = ts.Client.ListClusters(context.Background(), &api.ListClustersRequest{
   216  		Filters: &api.ListClustersRequest_Filters{
   217  			NamePrefixes: []string{store.DefaultClusterName},
   218  		},
   219  	})
   220  	assert.NoError(t, err)
   221  	assert.Len(t, r.Clusters, 1)
   222  	assert.Equal(t, cluster.Spec.Annotations.Name, r.Clusters[0].Spec.Annotations.Name)
   223  	assert.Len(t, r.Clusters[0].Spec.AcceptancePolicy.Policies, 1)
   224  
   225  	r.Clusters[0].Spec.AcceptancePolicy = api.AcceptancePolicy{Policies: []*api.AcceptancePolicy_RoleAdmissionPolicy{{Secret: &api.AcceptancePolicy_RoleAdmissionPolicy_Secret{Alg: "bcrypt", Data: []byte("secret")}}}}
   226  	returnedCluster, err := ts.Client.UpdateCluster(context.Background(), &api.UpdateClusterRequest{
   227  		ClusterID:      cluster.ID,
   228  		Spec:           &r.Clusters[0].Spec,
   229  		ClusterVersion: &r.Clusters[0].Meta.Version,
   230  	})
   231  	assert.NoError(t, err)
   232  	assert.NotContains(t, returnedCluster.String(), "secret")
   233  	assert.NotContains(t, returnedCluster.String(), "PRIVATE")
   234  	assert.NotNil(t, returnedCluster.Cluster.Spec.AcceptancePolicy.Policies[0].Secret.Data)
   235  
   236  	// Versioning.
   237  	assert.NoError(t, err)
   238  	version := &returnedCluster.Cluster.Meta.Version
   239  
   240  	_, err = ts.Client.UpdateCluster(context.Background(), &api.UpdateClusterRequest{
   241  		ClusterID:      cluster.ID,
   242  		Spec:           &r.Clusters[0].Spec,
   243  		ClusterVersion: version,
   244  	})
   245  	assert.NoError(t, err)
   246  
   247  	// Perform an update with the "old" version.
   248  	_, err = ts.Client.UpdateCluster(context.Background(), &api.UpdateClusterRequest{
   249  		ClusterID:      cluster.ID,
   250  		Spec:           &r.Clusters[0].Spec,
   251  		ClusterVersion: version,
   252  	})
   253  	assert.Error(t, err)
   254  }
   255  
   256  func TestUpdateClusterRotateToken(t *testing.T) {
   257  	ts := newTestServer(t)
   258  	defer ts.Stop()
   259  	cluster := createCluster(t, ts, "name", store.DefaultClusterName, api.AcceptancePolicy{}, ts.Server.securityConfig.RootCA())
   260  
   261  	r, err := ts.Client.ListClusters(context.Background(), &api.ListClustersRequest{
   262  		Filters: &api.ListClustersRequest_Filters{
   263  			NamePrefixes: []string{store.DefaultClusterName},
   264  		},
   265  	})
   266  
   267  	assert.NoError(t, err)
   268  	assert.Len(t, r.Clusters, 1)
   269  	workerToken := r.Clusters[0].RootCA.JoinTokens.Worker
   270  	managerToken := r.Clusters[0].RootCA.JoinTokens.Manager
   271  
   272  	// Rotate worker token
   273  	_, err = ts.Client.UpdateCluster(context.Background(), &api.UpdateClusterRequest{
   274  		ClusterID:      cluster.ID,
   275  		Spec:           &cluster.Spec,
   276  		ClusterVersion: &cluster.Meta.Version,
   277  		Rotation: api.KeyRotation{
   278  			WorkerJoinToken: true,
   279  		},
   280  	})
   281  	assert.NoError(t, err)
   282  
   283  	r, err = ts.Client.ListClusters(context.Background(), &api.ListClustersRequest{
   284  		Filters: &api.ListClustersRequest_Filters{
   285  			NamePrefixes: []string{store.DefaultClusterName},
   286  		},
   287  	})
   288  	assert.NoError(t, err)
   289  	assert.Len(t, r.Clusters, 1)
   290  	assert.NotEqual(t, workerToken, r.Clusters[0].RootCA.JoinTokens.Worker)
   291  	assert.Equal(t, managerToken, r.Clusters[0].RootCA.JoinTokens.Manager)
   292  	workerToken = r.Clusters[0].RootCA.JoinTokens.Worker
   293  
   294  	// Rotate manager token
   295  	_, err = ts.Client.UpdateCluster(context.Background(), &api.UpdateClusterRequest{
   296  		ClusterID:      cluster.ID,
   297  		Spec:           &cluster.Spec,
   298  		ClusterVersion: &r.Clusters[0].Meta.Version,
   299  		Rotation: api.KeyRotation{
   300  			ManagerJoinToken: true,
   301  		},
   302  	})
   303  	assert.NoError(t, err)
   304  
   305  	r, err = ts.Client.ListClusters(context.Background(), &api.ListClustersRequest{
   306  		Filters: &api.ListClustersRequest_Filters{
   307  			NamePrefixes: []string{store.DefaultClusterName},
   308  		},
   309  	})
   310  	assert.NoError(t, err)
   311  	assert.Len(t, r.Clusters, 1)
   312  	assert.Equal(t, workerToken, r.Clusters[0].RootCA.JoinTokens.Worker)
   313  	assert.NotEqual(t, managerToken, r.Clusters[0].RootCA.JoinTokens.Manager)
   314  	managerToken = r.Clusters[0].RootCA.JoinTokens.Manager
   315  
   316  	// Rotate both tokens
   317  	_, err = ts.Client.UpdateCluster(context.Background(), &api.UpdateClusterRequest{
   318  		ClusterID:      cluster.ID,
   319  		Spec:           &cluster.Spec,
   320  		ClusterVersion: &r.Clusters[0].Meta.Version,
   321  		Rotation: api.KeyRotation{
   322  			WorkerJoinToken:  true,
   323  			ManagerJoinToken: true,
   324  		},
   325  	})
   326  	assert.NoError(t, err)
   327  
   328  	r, err = ts.Client.ListClusters(context.Background(), &api.ListClustersRequest{
   329  		Filters: &api.ListClustersRequest_Filters{
   330  			NamePrefixes: []string{store.DefaultClusterName},
   331  		},
   332  	})
   333  	assert.NoError(t, err)
   334  	assert.Len(t, r.Clusters, 1)
   335  	assert.NotEqual(t, workerToken, r.Clusters[0].RootCA.JoinTokens.Worker)
   336  	assert.NotEqual(t, managerToken, r.Clusters[0].RootCA.JoinTokens.Manager)
   337  }
   338  
   339  func TestUpdateClusterRotateUnlockKey(t *testing.T) {
   340  	ts := newTestServer(t)
   341  	defer ts.Stop()
   342  	// create a cluster with extra encryption keys, to make sure they exist
   343  	cluster := createClusterObj("id", store.DefaultClusterName, api.AcceptancePolicy{}, ts.Server.securityConfig.RootCA())
   344  	expected := make(map[string]*api.EncryptionKey)
   345  	for i := 1; i <= 2; i++ {
   346  		value := fmt.Sprintf("fake%d", i)
   347  		expected[value] = &api.EncryptionKey{Subsystem: value, Key: []byte(value)}
   348  		cluster.UnlockKeys = append(cluster.UnlockKeys, expected[value])
   349  	}
   350  	require.NoError(t, ts.Store.Update(func(tx store.Tx) error {
   351  		return store.CreateCluster(tx, cluster)
   352  	}))
   353  
   354  	// we have to get the key from the memory store, since the cluster returned by the API is redacted
   355  	getManagerKey := func() (managerKey *api.EncryptionKey) {
   356  		ts.Store.View(func(tx store.ReadTx) {
   357  			viewCluster := store.GetCluster(tx, cluster.ID)
   358  			// no matter whether there's a manager key or not, the other keys should not have been affected
   359  			foundKeys := make(map[string]*api.EncryptionKey)
   360  			for _, eKey := range viewCluster.UnlockKeys {
   361  				foundKeys[eKey.Subsystem] = eKey
   362  			}
   363  			for v, key := range expected {
   364  				foundKey, ok := foundKeys[v]
   365  				require.True(t, ok)
   366  				require.Equal(t, key, foundKey)
   367  			}
   368  			managerKey = foundKeys[ca.ManagerRole]
   369  		})
   370  		return
   371  	}
   372  
   373  	validateListResult := func(expectedLocked bool) api.Version {
   374  		r, err := ts.Client.ListClusters(context.Background(), &api.ListClustersRequest{
   375  			Filters: &api.ListClustersRequest_Filters{
   376  				NamePrefixes: []string{store.DefaultClusterName},
   377  			},
   378  		})
   379  
   380  		require.NoError(t, err)
   381  		require.Len(t, r.Clusters, 1)
   382  		require.Equal(t, expectedLocked, r.Clusters[0].Spec.EncryptionConfig.AutoLockManagers)
   383  		require.Nil(t, r.Clusters[0].UnlockKeys) // redacted
   384  
   385  		return r.Clusters[0].Meta.Version
   386  	}
   387  
   388  	// we start off with manager autolocking turned off
   389  	version := validateListResult(false)
   390  	require.Nil(t, getManagerKey())
   391  
   392  	// Rotate unlock key without turning auto-lock on - key should still be nil
   393  	_, err := ts.Client.UpdateCluster(context.Background(), &api.UpdateClusterRequest{
   394  		ClusterID:      cluster.ID,
   395  		Spec:           &cluster.Spec,
   396  		ClusterVersion: &version,
   397  		Rotation: api.KeyRotation{
   398  			ManagerUnlockKey: true,
   399  		},
   400  	})
   401  	require.NoError(t, err)
   402  	version = validateListResult(false)
   403  	require.Nil(t, getManagerKey())
   404  
   405  	// Enable auto-lock only, no rotation boolean
   406  	spec := cluster.Spec.Copy()
   407  	spec.EncryptionConfig.AutoLockManagers = true
   408  	_, err = ts.Client.UpdateCluster(context.Background(), &api.UpdateClusterRequest{
   409  		ClusterID:      cluster.ID,
   410  		Spec:           spec,
   411  		ClusterVersion: &version,
   412  	})
   413  	require.NoError(t, err)
   414  	version = validateListResult(true)
   415  	managerKey := getManagerKey()
   416  	require.NotNil(t, managerKey)
   417  
   418  	// Rotate the manager key
   419  	_, err = ts.Client.UpdateCluster(context.Background(), &api.UpdateClusterRequest{
   420  		ClusterID:      cluster.ID,
   421  		Spec:           spec,
   422  		ClusterVersion: &version,
   423  		Rotation: api.KeyRotation{
   424  			ManagerUnlockKey: true,
   425  		},
   426  	})
   427  	require.NoError(t, err)
   428  	version = validateListResult(true)
   429  	newManagerKey := getManagerKey()
   430  	require.NotNil(t, managerKey)
   431  	require.NotEqual(t, managerKey, newManagerKey)
   432  	managerKey = newManagerKey
   433  
   434  	// Just update the cluster without modifying unlock keys
   435  	_, err = ts.Client.UpdateCluster(context.Background(), &api.UpdateClusterRequest{
   436  		ClusterID:      cluster.ID,
   437  		Spec:           spec,
   438  		ClusterVersion: &version,
   439  	})
   440  	require.NoError(t, err)
   441  	version = validateListResult(true)
   442  	newManagerKey = getManagerKey()
   443  	require.Equal(t, managerKey, newManagerKey)
   444  
   445  	// Disable auto lock
   446  	_, err = ts.Client.UpdateCluster(context.Background(), &api.UpdateClusterRequest{
   447  		ClusterID:      cluster.ID,
   448  		Spec:           &cluster.Spec, // set back to original spec
   449  		ClusterVersion: &version,
   450  		Rotation: api.KeyRotation{
   451  			ManagerUnlockKey: true, // this will be ignored because we disable the auto-lock
   452  		},
   453  	})
   454  	require.NoError(t, err)
   455  	validateListResult(false)
   456  	require.Nil(t, getManagerKey())
   457  }
   458  
   459  // root rotation tests have already been covered by ca_rotation_test.go - this test only makes sure that the function tested in those
   460  // tests is actually called by `UpdateCluster`, and that the results of GetCluster and ListCluster have the CA keys
   461  // and the spec key and cert redacted
   462  func TestUpdateClusterRootRotation(t *testing.T) {
   463  	ts := newTestServer(t)
   464  	defer ts.Stop()
   465  
   466  	cluster := createCluster(t, ts, "id", store.DefaultClusterName, api.AcceptancePolicy{}, ts.Server.securityConfig.RootCA())
   467  	response, err := ts.Client.GetCluster(context.Background(), &api.GetClusterRequest{ClusterID: cluster.ID})
   468  	require.NoError(t, err)
   469  	require.NotNil(t, response.Cluster)
   470  	cluster = response.Cluster
   471  
   472  	updatedSpec := cluster.Spec.Copy()
   473  	updatedSpec.CAConfig.SigningCACert = testutils.ECDSA256SHA256Cert
   474  	updatedSpec.CAConfig.SigningCAKey = testutils.ECDSA256Key
   475  	updatedSpec.CAConfig.ForceRotate = 5
   476  
   477  	_, err = ts.Client.UpdateCluster(context.Background(), &api.UpdateClusterRequest{
   478  		ClusterID:      cluster.ID,
   479  		Spec:           updatedSpec,
   480  		ClusterVersion: &cluster.Meta.Version,
   481  	})
   482  	require.NoError(t, err)
   483  
   484  	checkCluster := func() *api.Cluster {
   485  		response, err = ts.Client.GetCluster(context.Background(), &api.GetClusterRequest{ClusterID: cluster.ID})
   486  		require.NoError(t, err)
   487  		require.NotNil(t, response.Cluster)
   488  
   489  		listResponse, err := ts.Client.ListClusters(context.Background(), &api.ListClustersRequest{})
   490  		require.NoError(t, err)
   491  		require.Len(t, listResponse.Clusters, 1)
   492  
   493  		require.Equal(t, response.Cluster, listResponse.Clusters[0])
   494  
   495  		c := response.Cluster
   496  		require.NotNil(t, c.RootCA.RootRotation)
   497  
   498  		// check that all keys are redacted, and that the spec signing cert is also redacted (not because
   499  		// the cert is a secret, but because that makes it easier to get-and-update)
   500  		require.Len(t, c.RootCA.CAKey, 0)
   501  		require.Len(t, c.RootCA.RootRotation.CAKey, 0)
   502  		require.Len(t, c.Spec.CAConfig.SigningCAKey, 0)
   503  		require.Len(t, c.Spec.CAConfig.SigningCACert, 0)
   504  
   505  		return c
   506  	}
   507  
   508  	getUnredactedRootCA := func() (rootCA *api.RootCA) {
   509  		ts.Store.View(func(tx store.ReadTx) {
   510  			c := store.GetCluster(tx, cluster.ID)
   511  			require.NotNil(t, c)
   512  			rootCA = &c.RootCA
   513  		})
   514  		return
   515  	}
   516  
   517  	cluster = checkCluster()
   518  	unredactedRootCA := getUnredactedRootCA()
   519  
   520  	// update something else, but make sure this doesn't the root CA rotation doesn't change
   521  	updatedSpec = cluster.Spec.Copy()
   522  	updatedSpec.CAConfig.NodeCertExpiry = gogotypes.DurationProto(time.Hour)
   523  	_, err = ts.Client.UpdateCluster(context.Background(), &api.UpdateClusterRequest{
   524  		ClusterID:      cluster.ID,
   525  		Spec:           updatedSpec,
   526  		ClusterVersion: &cluster.Meta.Version,
   527  	})
   528  	require.NoError(t, err)
   529  
   530  	updatedCluster := checkCluster()
   531  	require.NotEqual(t, cluster.Spec.CAConfig.NodeCertExpiry, updatedCluster.Spec.CAConfig.NodeCertExpiry)
   532  	updatedUnredactedRootCA := getUnredactedRootCA()
   533  
   534  	require.Equal(t, unredactedRootCA, updatedUnredactedRootCA)
   535  }
   536  
   537  func TestListClusters(t *testing.T) {
   538  	ts := newTestServer(t)
   539  	defer ts.Stop()
   540  	r, err := ts.Client.ListClusters(context.Background(), &api.ListClustersRequest{})
   541  	assert.NoError(t, err)
   542  	assert.Empty(t, r.Clusters)
   543  
   544  	createCluster(t, ts, "id1", "name1", api.AcceptancePolicy{}, ts.Server.securityConfig.RootCA())
   545  	r, err = ts.Client.ListClusters(context.Background(), &api.ListClustersRequest{})
   546  	assert.NoError(t, err)
   547  	assert.Equal(t, 1, len(r.Clusters))
   548  
   549  	createCluster(t, ts, "id2", "name2", api.AcceptancePolicy{}, ts.Server.securityConfig.RootCA())
   550  	createCluster(t, ts, "id3", "name3", api.AcceptancePolicy{}, ts.Server.securityConfig.RootCA())
   551  	r, err = ts.Client.ListClusters(context.Background(), &api.ListClustersRequest{})
   552  	assert.NoError(t, err)
   553  	assert.Equal(t, 3, len(r.Clusters))
   554  }
   555  
   556  func TestListClustersWithSecrets(t *testing.T) {
   557  	ts := newTestServer(t)
   558  	defer ts.Stop()
   559  	r, err := ts.Client.ListClusters(context.Background(), &api.ListClustersRequest{})
   560  	assert.NoError(t, err)
   561  	assert.Empty(t, r.Clusters)
   562  
   563  	policy := api.AcceptancePolicy{Policies: []*api.AcceptancePolicy_RoleAdmissionPolicy{{Secret: &api.AcceptancePolicy_RoleAdmissionPolicy_Secret{Alg: "bcrypt", Data: []byte("secret")}}}}
   564  
   565  	createCluster(t, ts, "id1", "name1", policy, ts.Server.securityConfig.RootCA())
   566  	r, err = ts.Client.ListClusters(context.Background(), &api.ListClustersRequest{})
   567  	assert.NoError(t, err)
   568  	assert.Equal(t, 1, len(r.Clusters))
   569  
   570  	createCluster(t, ts, "id2", "name2", policy, ts.Server.securityConfig.RootCA())
   571  	createCluster(t, ts, "id3", "name3", policy, ts.Server.securityConfig.RootCA())
   572  	r, err = ts.Client.ListClusters(context.Background(), &api.ListClustersRequest{})
   573  	assert.NoError(t, err)
   574  	assert.Equal(t, 3, len(r.Clusters))
   575  	for _, cluster := range r.Clusters {
   576  		assert.NotContains(t, cluster.String(), policy.Policies[0].Secret)
   577  		assert.NotContains(t, cluster.String(), "PRIVATE")
   578  		assert.NotNil(t, cluster.Spec.AcceptancePolicy.Policies[0].Secret.Data)
   579  	}
   580  }
   581  
   582  func TestExpireBlacklistedCerts(t *testing.T) {
   583  	now := time.Now()
   584  
   585  	longAgo := now.Add(-24 * time.Hour * 1000)
   586  	justBeforeGrace := now.Add(-expiredCertGrace - 5*time.Minute)
   587  	justAfterGrace := now.Add(-expiredCertGrace + 5*time.Minute)
   588  	future := now.Add(time.Hour)
   589  
   590  	cluster := &api.Cluster{
   591  		BlacklistedCertificates: map[string]*api.BlacklistedCertificate{
   592  			"longAgo":         {Expiry: ptypes.MustTimestampProto(longAgo)},
   593  			"justBeforeGrace": {Expiry: ptypes.MustTimestampProto(justBeforeGrace)},
   594  			"justAfterGrace":  {Expiry: ptypes.MustTimestampProto(justAfterGrace)},
   595  			"future":          {Expiry: ptypes.MustTimestampProto(future)},
   596  		},
   597  	}
   598  
   599  	expireBlacklistedCerts(cluster)
   600  
   601  	assert.Len(t, cluster.BlacklistedCertificates, 2)
   602  
   603  	_, hasJustAfterGrace := cluster.BlacklistedCertificates["justAfterGrace"]
   604  	assert.True(t, hasJustAfterGrace)
   605  
   606  	_, hasFuture := cluster.BlacklistedCertificates["future"]
   607  	assert.True(t, hasFuture)
   608  }