github.com/thomasobenaus/nomad@v0.11.1/nomad/consul_test.go (about)

     1  package nomad
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"sync"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/hashicorp/nomad/command/agent/consul"
    11  	"github.com/hashicorp/nomad/helper"
    12  	"github.com/hashicorp/nomad/helper/testlog"
    13  	"github.com/hashicorp/nomad/helper/uuid"
    14  	"github.com/hashicorp/nomad/nomad/structs"
    15  	"github.com/stretchr/testify/require"
    16  	"golang.org/x/time/rate"
    17  )
    18  
    19  var _ ConsulACLsAPI = (*consulACLsAPI)(nil)
    20  var _ ConsulACLsAPI = (*mockConsulACLsAPI)(nil)
    21  
    22  type revokeRequest struct {
    23  	accessorID string
    24  	committed  bool
    25  }
    26  
    27  type mockConsulACLsAPI struct {
    28  	lock           sync.Mutex
    29  	revokeRequests []revokeRequest
    30  	stopped        bool
    31  }
    32  
    33  func (m *mockConsulACLsAPI) CheckSIPolicy(_ context.Context, _, _ string) error {
    34  	panic("not implemented yet")
    35  }
    36  
    37  func (m *mockConsulACLsAPI) CreateToken(_ context.Context, _ ServiceIdentityIndex) (*structs.SIToken, error) {
    38  	panic("not implemented yet")
    39  }
    40  
    41  func (m *mockConsulACLsAPI) ListTokens() ([]string, error) {
    42  	panic("not implemented yet")
    43  }
    44  
    45  func (m *mockConsulACLsAPI) Stop() {
    46  	m.lock.Lock()
    47  	defer m.lock.Unlock()
    48  	m.stopped = true
    49  }
    50  
    51  type mockPurgingServer struct {
    52  	purgedAccessorIDs []string
    53  	failure           error
    54  }
    55  
    56  func (mps *mockPurgingServer) purgeFunc(accessors []*structs.SITokenAccessor) error {
    57  	if mps.failure != nil {
    58  		return mps.failure
    59  	}
    60  
    61  	for _, accessor := range accessors {
    62  		mps.purgedAccessorIDs = append(mps.purgedAccessorIDs, accessor.AccessorID)
    63  	}
    64  	return nil
    65  }
    66  
    67  func (m *mockConsulACLsAPI) RevokeTokens(_ context.Context, accessors []*structs.SITokenAccessor, committed bool) bool {
    68  	m.lock.Lock()
    69  	defer m.lock.Unlock()
    70  
    71  	for _, accessor := range accessors {
    72  		m.revokeRequests = append(m.revokeRequests, revokeRequest{
    73  			accessorID: accessor.AccessorID,
    74  			committed:  committed,
    75  		})
    76  	}
    77  	return false
    78  }
    79  
    80  func TestConsulACLsAPI_CreateToken(t *testing.T) {
    81  	t.Parallel()
    82  
    83  	try := func(t *testing.T, expErr error) {
    84  		logger := testlog.HCLogger(t)
    85  		aclAPI := consul.NewMockACLsAPI(logger)
    86  		aclAPI.SetError(expErr)
    87  
    88  		c := NewConsulACLsAPI(aclAPI, logger, nil)
    89  
    90  		ctx := context.Background()
    91  		sii := ServiceIdentityIndex{
    92  			AllocID:   uuid.Generate(),
    93  			ClusterID: uuid.Generate(),
    94  			TaskName:  "my-task1",
    95  		}
    96  
    97  		token, err := c.CreateToken(ctx, sii)
    98  
    99  		if expErr != nil {
   100  			require.Equal(t, expErr, err)
   101  			require.Nil(t, token)
   102  		} else {
   103  			require.NoError(t, err)
   104  			require.Equal(t, "my-task1", token.TaskName)
   105  			require.True(t, helper.IsUUID(token.AccessorID))
   106  			require.True(t, helper.IsUUID(token.SecretID))
   107  		}
   108  	}
   109  
   110  	t.Run("create token success", func(t *testing.T) {
   111  		try(t, nil)
   112  	})
   113  
   114  	t.Run("create token error", func(t *testing.T) {
   115  		try(t, errors.New("consul broke"))
   116  	})
   117  }
   118  
   119  func TestConsulACLsAPI_RevokeTokens(t *testing.T) {
   120  	t.Parallel()
   121  
   122  	setup := func(t *testing.T, exp error) (context.Context, ConsulACLsAPI, *structs.SIToken) {
   123  		logger := testlog.HCLogger(t)
   124  		aclAPI := consul.NewMockACLsAPI(logger)
   125  
   126  		c := NewConsulACLsAPI(aclAPI, logger, nil)
   127  
   128  		ctx := context.Background()
   129  		generated, err := c.CreateToken(ctx, ServiceIdentityIndex{
   130  			ClusterID: uuid.Generate(),
   131  			AllocID:   uuid.Generate(),
   132  			TaskName:  "task1",
   133  		})
   134  		require.NoError(t, err)
   135  
   136  		// set the mock error after calling CreateToken for setting up
   137  		aclAPI.SetError(exp)
   138  
   139  		return context.Background(), c, generated
   140  	}
   141  
   142  	accessors := func(ids ...string) (result []*structs.SITokenAccessor) {
   143  		for _, id := range ids {
   144  			result = append(result, &structs.SITokenAccessor{AccessorID: id})
   145  		}
   146  		return
   147  	}
   148  
   149  	t.Run("revoke token success", func(t *testing.T) {
   150  		ctx, c, token := setup(t, nil)
   151  		retryLater := c.RevokeTokens(ctx, accessors(token.AccessorID), false)
   152  		require.False(t, retryLater)
   153  	})
   154  
   155  	t.Run("revoke token non-existent", func(t *testing.T) {
   156  		ctx, c, _ := setup(t, nil)
   157  		retryLater := c.RevokeTokens(ctx, accessors(uuid.Generate()), false)
   158  		require.False(t, retryLater)
   159  	})
   160  
   161  	t.Run("revoke token error", func(t *testing.T) {
   162  		exp := errors.New("consul broke")
   163  		ctx, c, token := setup(t, exp)
   164  		retryLater := c.RevokeTokens(ctx, accessors(token.AccessorID), false)
   165  		require.True(t, retryLater)
   166  	})
   167  }
   168  
   169  func TestConsulACLsAPI_bgRetryRevoke(t *testing.T) {
   170  	t.Parallel()
   171  
   172  	// manually create so the bg daemon does not run, letting us explicitly
   173  	// call and test bgRetryRevoke
   174  	setup := func(t *testing.T) (*consulACLsAPI, *mockPurgingServer) {
   175  		logger := testlog.HCLogger(t)
   176  		aclAPI := consul.NewMockACLsAPI(logger)
   177  		server := new(mockPurgingServer)
   178  		shortWait := rate.Limit(1 * time.Millisecond)
   179  
   180  		return &consulACLsAPI{
   181  			aclClient: aclAPI,
   182  			purgeFunc: server.purgeFunc,
   183  			limiter:   rate.NewLimiter(shortWait, int(shortWait)),
   184  			stopC:     make(chan struct{}),
   185  			logger:    logger,
   186  		}, server
   187  	}
   188  
   189  	t.Run("retry revoke no items", func(t *testing.T) {
   190  		c, server := setup(t)
   191  		c.bgRetryRevoke()
   192  		require.Empty(t, server)
   193  	})
   194  
   195  	t.Run("retry revoke success", func(t *testing.T) {
   196  		c, server := setup(t)
   197  		accessorID := uuid.Generate()
   198  		c.bgRetryRevocation = append(c.bgRetryRevocation, &structs.SITokenAccessor{
   199  			NodeID:     uuid.Generate(),
   200  			AllocID:    uuid.Generate(),
   201  			AccessorID: accessorID,
   202  			TaskName:   "task1",
   203  		})
   204  		require.Empty(t, server.purgedAccessorIDs)
   205  		c.bgRetryRevoke()
   206  		require.Equal(t, 1, len(server.purgedAccessorIDs))
   207  		require.Equal(t, accessorID, server.purgedAccessorIDs[0])
   208  		require.Empty(t, c.bgRetryRevocation) // should be empty now
   209  	})
   210  
   211  	t.Run("retry revoke failure", func(t *testing.T) {
   212  		c, server := setup(t)
   213  		server.failure = errors.New("revocation fail")
   214  		accessorID := uuid.Generate()
   215  		c.bgRetryRevocation = append(c.bgRetryRevocation, &structs.SITokenAccessor{
   216  			NodeID:     uuid.Generate(),
   217  			AllocID:    uuid.Generate(),
   218  			AccessorID: accessorID,
   219  			TaskName:   "task1",
   220  		})
   221  		require.Empty(t, server.purgedAccessorIDs)
   222  		c.bgRetryRevoke()
   223  		require.Equal(t, 1, len(c.bgRetryRevocation)) // non-empty because purge failed
   224  		require.Equal(t, accessorID, c.bgRetryRevocation[0].AccessorID)
   225  	})
   226  }
   227  
   228  func TestConsulACLsAPI_Stop(t *testing.T) {
   229  	t.Parallel()
   230  
   231  	setup := func(t *testing.T) *consulACLsAPI {
   232  		logger := testlog.HCLogger(t)
   233  		return NewConsulACLsAPI(nil, logger, nil)
   234  	}
   235  
   236  	c := setup(t)
   237  	c.Stop()
   238  	_, err := c.CreateToken(context.Background(), ServiceIdentityIndex{
   239  		ClusterID: "",
   240  		AllocID:   "",
   241  		TaskName:  "",
   242  	})
   243  	require.Error(t, err)
   244  }
   245  
   246  func TestConsulACLsAPI_CheckSIPolicy(t *testing.T) {
   247  	t.Parallel()
   248  
   249  	try := func(t *testing.T, service, token string, expErr string) {
   250  		logger := testlog.HCLogger(t)
   251  		aclAPI := consul.NewMockACLsAPI(logger)
   252  		cAPI := NewConsulACLsAPI(aclAPI, logger, nil)
   253  
   254  		err := cAPI.CheckSIPolicy(context.Background(), service, token)
   255  		if expErr != "" {
   256  			require.EqualError(t, err, expErr)
   257  		} else {
   258  			require.NoError(t, err)
   259  		}
   260  	}
   261  
   262  	t.Run("operator has service write", func(t *testing.T) {
   263  		try(t, "service1", consul.ExampleOperatorTokenID1, "")
   264  	})
   265  
   266  	t.Run("operator has service_prefix write", func(t *testing.T) {
   267  		try(t, "foo-service1", consul.ExampleOperatorTokenID2, "")
   268  	})
   269  
   270  	t.Run("operator permissions insufficient", func(t *testing.T) {
   271  		try(t, "service1", consul.ExampleOperatorTokenID3,
   272  			"permission denied for \"service1\"",
   273  		)
   274  	})
   275  
   276  	t.Run("no token provided", func(t *testing.T) {
   277  		try(t, "service1", "", "missing consul token")
   278  	})
   279  
   280  	t.Run("nonsense token provided", func(t *testing.T) {
   281  		try(t, "service1", "f1682bde-1e71-90b1-9204-85d35467ba61",
   282  			"unable to validate operator consul token: no such token",
   283  		)
   284  	})
   285  }