// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package nomad import ( "context" "errors" "sync" "testing" "time" "github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/command/agent/consul" "github.com/hashicorp/nomad/helper" "github.com/hashicorp/nomad/helper/testlog" "github.com/hashicorp/nomad/helper/uuid" "github.com/hashicorp/nomad/nomad/structs" "github.com/stretchr/testify/require" "golang.org/x/time/rate" ) var _ ConsulACLsAPI = (*consulACLsAPI)(nil) var _ ConsulACLsAPI = (*mockConsulACLsAPI)(nil) var _ ConsulConfigsAPI = (*consulConfigsAPI)(nil) func TestConsulConfigsAPI_SetCE(t *testing.T) { ci.Parallel(t) try := func(t *testing.T, expect error, f func(ConsulConfigsAPI) error) { logger := testlog.HCLogger(t) configsAPI := consul.NewMockConfigsAPI(logger) configsAPI.SetError(expect) c := NewConsulConfigsAPI(configsAPI, logger) err := f(c) // set the config entry switch expect { case nil: require.NoError(t, err) default: require.Equal(t, expect, err) } } ctx := context.Background() // existing behavior is no set namespace consulNamespace := "" ingressCE := new(structs.ConsulIngressConfigEntry) t.Run("ingress ok", func(t *testing.T) { try(t, nil, func(c ConsulConfigsAPI) error { return c.SetIngressCE(ctx, consulNamespace, "ig", ingressCE) }) }) t.Run("ingress fail", func(t *testing.T) { try(t, errors.New("consul broke"), func(c ConsulConfigsAPI) error { return c.SetIngressCE(ctx, consulNamespace, "ig", ingressCE) }) }) terminatingCE := new(structs.ConsulTerminatingConfigEntry) t.Run("terminating ok", func(t *testing.T) { try(t, nil, func(c ConsulConfigsAPI) error { return c.SetTerminatingCE(ctx, consulNamespace, "tg", terminatingCE) }) }) t.Run("terminating fail", func(t *testing.T) { try(t, errors.New("consul broke"), func(c ConsulConfigsAPI) error { return c.SetTerminatingCE(ctx, consulNamespace, "tg", terminatingCE) }) }) // also mesh } type revokeRequest struct { accessorID string committed bool } type mockConsulACLsAPI struct { lock sync.Mutex revokeRequests []revokeRequest stopped bool } func (m *mockConsulACLsAPI) CheckPermissions(context.Context, string, *structs.ConsulUsage, string) error { panic("not implemented yet") } func (m *mockConsulACLsAPI) CreateToken(context.Context, ServiceIdentityRequest) (*structs.SIToken, error) { panic("not implemented yet") } func (m *mockConsulACLsAPI) ListTokens() ([]string, error) { panic("not implemented yet") } func (m *mockConsulACLsAPI) Stop() { m.lock.Lock() defer m.lock.Unlock() m.stopped = true } type mockPurgingServer struct { purgedAccessorIDs []string failure error } func (mps *mockPurgingServer) purgeFunc(accessors []*structs.SITokenAccessor) error { if mps.failure != nil { return mps.failure } for _, accessor := range accessors { mps.purgedAccessorIDs = append(mps.purgedAccessorIDs, accessor.AccessorID) } return nil } func (m *mockConsulACLsAPI) RevokeTokens(_ context.Context, accessors []*structs.SITokenAccessor, committed bool) bool { return m.storeForRevocation(accessors, committed) } func (m *mockConsulACLsAPI) MarkForRevocation(accessors []*structs.SITokenAccessor) { m.storeForRevocation(accessors, true) } func (m *mockConsulACLsAPI) storeForRevocation(accessors []*structs.SITokenAccessor, committed bool) bool { m.lock.Lock() defer m.lock.Unlock() for _, accessor := range accessors { m.revokeRequests = append(m.revokeRequests, revokeRequest{ accessorID: accessor.AccessorID, committed: committed, }) } return false } func TestConsulACLsAPI_CreateToken(t *testing.T) { ci.Parallel(t) try := func(t *testing.T, expErr error) { logger := testlog.HCLogger(t) aclAPI := consul.NewMockACLsAPI(logger) aclAPI.SetError(expErr) c := NewConsulACLsAPI(aclAPI, logger, nil) ctx := context.Background() sii := ServiceIdentityRequest{ ConsulNamespace: "foo-namespace", AllocID: uuid.Generate(), ClusterID: uuid.Generate(), TaskName: "my-task1-sidecar-proxy", TaskKind: structs.NewTaskKind(structs.ConnectProxyPrefix, "my-service"), } token, err := c.CreateToken(ctx, sii) if expErr != nil { require.Equal(t, expErr, err) require.Nil(t, token) } else { require.NoError(t, err) require.Equal(t, "foo-namespace", token.ConsulNamespace) require.Equal(t, "my-task1-sidecar-proxy", token.TaskName) require.True(t, helper.IsUUID(token.AccessorID)) require.True(t, helper.IsUUID(token.SecretID)) } } t.Run("create token success", func(t *testing.T) { try(t, nil) }) t.Run("create token error", func(t *testing.T) { try(t, errors.New("consul broke")) }) } func TestConsulACLsAPI_RevokeTokens(t *testing.T) { ci.Parallel(t) setup := func(t *testing.T, exp error) (context.Context, ConsulACLsAPI, *structs.SIToken) { logger := testlog.HCLogger(t) aclAPI := consul.NewMockACLsAPI(logger) c := NewConsulACLsAPI(aclAPI, logger, nil) ctx := context.Background() generated, err := c.CreateToken(ctx, ServiceIdentityRequest{ ConsulNamespace: "foo-namespace", ClusterID: uuid.Generate(), AllocID: uuid.Generate(), TaskName: "task1-sidecar-proxy", TaskKind: structs.NewTaskKind(structs.ConnectProxyPrefix, "service1"), }) require.NoError(t, err) // set the mock error after calling CreateToken for setting up aclAPI.SetError(exp) return context.Background(), c, generated } accessors := func(ids ...string) (result []*structs.SITokenAccessor) { for _, id := range ids { result = append(result, &structs.SITokenAccessor{ AccessorID: id, ConsulNamespace: "foo-namespace", }) } return } t.Run("revoke token success", func(t *testing.T) { ctx, c, token := setup(t, nil) retryLater := c.RevokeTokens(ctx, accessors(token.AccessorID), false) require.False(t, retryLater) }) t.Run("revoke token non-existent", func(t *testing.T) { ctx, c, _ := setup(t, nil) retryLater := c.RevokeTokens(ctx, accessors(uuid.Generate()), false) require.False(t, retryLater) }) t.Run("revoke token error", func(t *testing.T) { exp := errors.New("consul broke") ctx, c, token := setup(t, exp) retryLater := c.RevokeTokens(ctx, accessors(token.AccessorID), false) require.True(t, retryLater) }) } func TestConsulACLsAPI_MarkForRevocation(t *testing.T) { ci.Parallel(t) logger := testlog.HCLogger(t) aclAPI := consul.NewMockACLsAPI(logger) c := NewConsulACLsAPI(aclAPI, logger, nil) generated, err := c.CreateToken(context.Background(), ServiceIdentityRequest{ ConsulNamespace: "foo-namespace", ClusterID: uuid.Generate(), AllocID: uuid.Generate(), TaskName: "task1-sidecar-proxy", TaskKind: structs.NewTaskKind(structs.ConnectProxyPrefix, "service1"), }) require.NoError(t, err) // set the mock error after calling CreateToken for setting up aclAPI.SetError(nil) accessors := []*structs.SITokenAccessor{{ ConsulNamespace: "foo-namespace", AccessorID: generated.AccessorID, }} c.MarkForRevocation(accessors) require.Len(t, c.bgRetryRevocation, 1) require.Contains(t, c.bgRetryRevocation, accessors[0]) } func TestConsulACLsAPI_bgRetryRevoke(t *testing.T) { ci.Parallel(t) // manually create so the bg daemon does not run, letting us explicitly // call and test bgRetryRevoke setup := func(t *testing.T) (*consulACLsAPI, *mockPurgingServer) { logger := testlog.HCLogger(t) aclAPI := consul.NewMockACLsAPI(logger) server := new(mockPurgingServer) shortWait := rate.Limit(1 * time.Millisecond) return &consulACLsAPI{ aclClient: aclAPI, purgeFunc: server.purgeFunc, limiter: rate.NewLimiter(shortWait, int(shortWait)), stopC: make(chan struct{}), logger: logger, }, server } t.Run("retry revoke no items", func(t *testing.T) { c, server := setup(t) c.bgRetryRevoke() require.Empty(t, server) }) t.Run("retry revoke success", func(t *testing.T) { c, server := setup(t) accessorID := uuid.Generate() c.bgRetryRevocation = append(c.bgRetryRevocation, &structs.SITokenAccessor{ ConsulNamespace: "foo-namespace", NodeID: uuid.Generate(), AllocID: uuid.Generate(), AccessorID: accessorID, TaskName: "task1", }) require.Empty(t, server.purgedAccessorIDs) c.bgRetryRevoke() require.Equal(t, 1, len(server.purgedAccessorIDs)) require.Equal(t, accessorID, server.purgedAccessorIDs[0]) require.Empty(t, c.bgRetryRevocation) // should be empty now }) t.Run("retry revoke failure", func(t *testing.T) { c, server := setup(t) server.failure = errors.New("revocation fail") accessorID := uuid.Generate() c.bgRetryRevocation = append(c.bgRetryRevocation, &structs.SITokenAccessor{ ConsulNamespace: "foo-namespace", NodeID: uuid.Generate(), AllocID: uuid.Generate(), AccessorID: accessorID, TaskName: "task1", }) require.Empty(t, server.purgedAccessorIDs) c.bgRetryRevoke() require.Equal(t, 1, len(c.bgRetryRevocation)) // non-empty because purge failed require.Equal(t, accessorID, c.bgRetryRevocation[0].AccessorID) }) } func TestConsulACLsAPI_Stop(t *testing.T) { ci.Parallel(t) setup := func(t *testing.T) *consulACLsAPI { logger := testlog.HCLogger(t) return NewConsulACLsAPI(nil, logger, nil) } c := setup(t) c.Stop() _, err := c.CreateToken(context.Background(), ServiceIdentityRequest{ ClusterID: "", AllocID: "", TaskName: "", }) require.Error(t, err) }