github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/e2e/connect/acls.go (about)

     1  package connect
     2  
     3  import (
     4  	"os"
     5  	"regexp"
     6  	"testing"
     7  	"time"
     8  
     9  	consulapi "github.com/hashicorp/consul/api"
    10  	nomadapi "github.com/hashicorp/nomad/api"
    11  	"github.com/hashicorp/nomad/e2e/consulacls"
    12  	"github.com/hashicorp/nomad/e2e/e2eutil"
    13  	"github.com/hashicorp/nomad/e2e/framework"
    14  	"github.com/hashicorp/nomad/helper/uuid"
    15  	"github.com/hashicorp/nomad/jobspec"
    16  	"github.com/stretchr/testify/require"
    17  )
    18  
    19  type ConnectACLsE2ETest struct {
    20  	framework.TC
    21  
    22  	// manageConsulACLs is used to 'enable' and 'disable' Consul ACLs in the
    23  	// Consul Cluster that has been setup for e2e testing.
    24  	manageConsulACLs consulacls.Manager
    25  	// consulMasterToken is set to the generated Consul ACL token after using
    26  	// the consul-acls-manage.sh script to enable ACLs.
    27  	consulMasterToken string
    28  
    29  	// things to cleanup after each test case
    30  	jobIDs          []string
    31  	consulPolicyIDs []string
    32  	consulTokenIDs  []string
    33  }
    34  
    35  func (tc *ConnectACLsE2ETest) BeforeAll(f *framework.F) {
    36  	// Wait for Nomad to be ready before doing anything.
    37  	e2eutil.WaitForLeader(f.T(), tc.Nomad())
    38  	e2eutil.WaitForNodesReady(f.T(), tc.Nomad(), 2)
    39  
    40  	// Now enable Consul ACLs, the bootstrapping process for which will be
    41  	// managed automatically if needed.
    42  	var err error
    43  	tc.manageConsulACLs, err = consulacls.New(consulacls.DefaultTFStateFile)
    44  	require.NoError(f.T(), err)
    45  	tc.enableConsulACLs(f)
    46  
    47  	// Sanity check the consul master token exists, otherwise tests are just
    48  	// going to be a train wreck.
    49  	tokenLength := len(tc.consulMasterToken)
    50  	require.Equal(f.T(), 36, tokenLength, "consul master token wrong length")
    51  
    52  	// Sanity check the CONSUL_HTTP_TOKEN is NOT set, because that will cause
    53  	// the agent checks to fail (which do not allow having a token set (!)).
    54  	consulTokenEnv := os.Getenv(envConsulToken)
    55  	require.Empty(f.T(), consulTokenEnv)
    56  
    57  	// Wait for Nomad to be ready _again_, since everything was restarted during
    58  	// the bootstrap process.
    59  	e2eutil.WaitForLeader(f.T(), tc.Nomad())
    60  	e2eutil.WaitForNodesReady(f.T(), tc.Nomad(), 2)
    61  }
    62  
    63  // enableConsulACLs effectively executes `consul-acls-manage.sh enable`, which
    64  // will activate Consul ACLs, going through the bootstrap process if necessary.
    65  func (tc *ConnectACLsE2ETest) enableConsulACLs(f *framework.F) {
    66  	tc.consulMasterToken = tc.manageConsulACLs.Enable(f.T())
    67  }
    68  
    69  // AfterAll runs after all tests are complete.
    70  //
    71  // We disable ConsulACLs in here to isolate the use of Consul ACLs only to
    72  // test suites that explicitly want to test with them enabled.
    73  func (tc *ConnectACLsE2ETest) AfterAll(f *framework.F) {
    74  	tc.disableConsulACLs(f)
    75  }
    76  
    77  // disableConsulACLs effectively executes `consul-acls-manage.sh disable`, which
    78  // will de-activate Consul ACLs.
    79  func (tc *ConnectACLsE2ETest) disableConsulACLs(f *framework.F) {
    80  	tc.manageConsulACLs.Disable(f.T())
    81  }
    82  
    83  // AfterEach does cleanup of Consul ACL objects that were created during each
    84  // test case. Each test case may assume it is starting from a "fresh" state -
    85  // as if the consul ACL bootstrap process had just taken place.
    86  func (tc *ConnectACLsE2ETest) AfterEach(f *framework.F) {
    87  	if os.Getenv("NOMAD_TEST_SKIPCLEANUP") == "1" {
    88  		return
    89  	}
    90  
    91  	t := f.T()
    92  
    93  	// cleanup jobs
    94  	for _, id := range tc.jobIDs {
    95  		t.Log("cleanup: deregister nomad job id:", id)
    96  		_, _, err := tc.Nomad().Jobs().Deregister(id, true, nil)
    97  		f.NoError(err)
    98  	}
    99  
   100  	// cleanup consul tokens
   101  	for _, id := range tc.consulTokenIDs {
   102  		t.Log("cleanup: delete consul token id:", id)
   103  		_, err := tc.Consul().ACL().TokenDelete(id, &consulapi.WriteOptions{Token: tc.consulMasterToken})
   104  		f.NoError(err)
   105  	}
   106  
   107  	// cleanup consul policies
   108  	for _, id := range tc.consulPolicyIDs {
   109  		t.Log("cleanup: delete consul policy id:", id)
   110  		_, err := tc.Consul().ACL().PolicyDelete(id, &consulapi.WriteOptions{Token: tc.consulMasterToken})
   111  		f.NoError(err)
   112  	}
   113  
   114  	// do garbage collection
   115  	err := tc.Nomad().System().GarbageCollect()
   116  	f.NoError(err)
   117  
   118  	// assert there are no leftover SI tokens, which may take a minute to be
   119  	// cleaned up
   120  	f.Eventually(func() bool {
   121  		siTokens := tc.countSITokens(t)
   122  		t.Log("cleanup: checking for remaining SI tokens:", siTokens)
   123  		return len(siTokens) == 0
   124  	}, 2*time.Minute, 2*time.Second, "SI tokens did not get removed")
   125  
   126  	tc.jobIDs = []string{}
   127  	tc.consulTokenIDs = []string{}
   128  	tc.consulPolicyIDs = []string{}
   129  }
   130  
   131  type consulPolicy struct {
   132  	Name  string // e.g. nomad-operator
   133  	Rules string // e.g. service "" { policy="write" }
   134  }
   135  
   136  func (tc *ConnectACLsE2ETest) createConsulPolicy(p consulPolicy, f *framework.F) string {
   137  	result, _, err := tc.Consul().ACL().PolicyCreate(&consulapi.ACLPolicy{
   138  		Name:        p.Name,
   139  		Description: "test policy " + p.Name,
   140  		Rules:       p.Rules,
   141  	}, &consulapi.WriteOptions{Token: tc.consulMasterToken})
   142  	f.NoError(err, "failed to create consul policy")
   143  	tc.consulPolicyIDs = append(tc.consulPolicyIDs, result.ID)
   144  	return result.ID
   145  }
   146  
   147  func (tc *ConnectACLsE2ETest) createOperatorToken(policyID string, f *framework.F) string {
   148  	token, _, err := tc.Consul().ACL().TokenCreate(&consulapi.ACLToken{
   149  		Description: "operator token",
   150  		Policies:    []*consulapi.ACLTokenPolicyLink{{ID: policyID}},
   151  	}, &consulapi.WriteOptions{Token: tc.consulMasterToken})
   152  	f.NoError(err, "failed to create operator token")
   153  	tc.consulTokenIDs = append(tc.consulTokenIDs, token.AccessorID)
   154  	return token.SecretID
   155  }
   156  
   157  func (tc *ConnectACLsE2ETest) TestConnectACLsRegisterMasterToken(f *framework.F) {
   158  	t := f.T()
   159  
   160  	t.Log("test register Connect job w/ ACLs enabled w/ master token")
   161  
   162  	jobID := "connect" + uuid.Generate()[0:8]
   163  	tc.jobIDs = append(tc.jobIDs, jobID)
   164  
   165  	jobAPI := tc.Nomad().Jobs()
   166  
   167  	job, err := jobspec.ParseFile(demoConnectJob)
   168  	f.NoError(err)
   169  
   170  	// Set the job file to use the consul master token.
   171  	// One should never do this in practice, but, it should work.
   172  	// https://www.consul.io/docs/acl/acl-system.html#builtin-tokens
   173  	job.ConsulToken = &tc.consulMasterToken
   174  
   175  	// Avoid using Register here, because that would actually create and run the
   176  	// Job which runs the task, creates the SI token, which all needs to be
   177  	// given time to settle and cleaned up. That is all covered in the big slow
   178  	// test at the bottom.
   179  	resp, _, err := jobAPI.Plan(job, false, nil)
   180  	f.NoError(err)
   181  	f.NotNil(resp)
   182  }
   183  
   184  func (tc *ConnectACLsE2ETest) TestConnectACLsRegisterMissingOperatorToken(f *framework.F) {
   185  	t := f.T()
   186  
   187  	t.Log("test register Connect job w/ ACLs enabled w/o operator token")
   188  
   189  	job, err := jobspec.ParseFile(demoConnectJob)
   190  	f.NoError(err)
   191  
   192  	jobAPI := tc.Nomad().Jobs()
   193  
   194  	// Explicitly show the ConsulToken is not set
   195  	job.ConsulToken = nil
   196  
   197  	_, _, err = jobAPI.Register(job, nil)
   198  	f.Error(err)
   199  
   200  	t.Log("job correctly rejected, with error:", err)
   201  }
   202  
   203  func (tc *ConnectACLsE2ETest) TestConnectACLsRegisterFakeOperatorToken(f *framework.F) {
   204  	t := f.T()
   205  
   206  	t.Log("test register Connect job w/ ACLs enabled w/ operator token")
   207  
   208  	policyID := tc.createConsulPolicy(consulPolicy{
   209  		Name:  "nomad-operator-policy",
   210  		Rules: `service "count-api" { policy = "write" } service "count-dashboard" { policy = "write" }`,
   211  	}, f)
   212  	t.Log("created operator policy:", policyID)
   213  
   214  	// generate a fake consul token token
   215  	fakeToken := uuid.Generate()
   216  	job := tc.parseJobSpecFile(t, demoConnectJob)
   217  
   218  	jobAPI := tc.Nomad().Jobs()
   219  
   220  	// deliberately set the fake Consul token
   221  	job.ConsulToken = &fakeToken
   222  
   223  	// should fail, because the token is fake
   224  	_, _, err := jobAPI.Register(job, nil)
   225  	f.Error(err)
   226  	t.Log("job correctly rejected, with error:", err)
   227  }
   228  
   229  func (tc *ConnectACLsE2ETest) TestConnectACLsConnectDemo(f *framework.F) {
   230  	t := f.T()
   231  
   232  	t.Log("test register Connect job w/ ACLs enabled w/ operator token")
   233  
   234  	// === Setup ACL policy and mint Operator token ===
   235  
   236  	// create a policy allowing writes of services "count-api" and "count-dashboard"
   237  	policyID := tc.createConsulPolicy(consulPolicy{
   238  		Name:  "nomad-operator-policy",
   239  		Rules: `service "count-api" { policy = "write" } service "count-dashboard" { policy = "write" }`,
   240  	}, f)
   241  	t.Log("created operator policy:", policyID)
   242  
   243  	// create a Consul "operator token" blessed with the above policy
   244  	operatorToken := tc.createOperatorToken(policyID, f)
   245  	t.Log("created operator token:", operatorToken)
   246  
   247  	jobID := connectJobID()
   248  	tc.jobIDs = append(tc.jobIDs, jobID)
   249  
   250  	allocs := e2eutil.RegisterAndWaitForAllocs(t, tc.Nomad(), demoConnectJob, jobID, operatorToken)
   251  	f.Equal(2, len(allocs), "expected 2 allocs for connect demo", allocs)
   252  	allocIDs := e2eutil.AllocIDsFromAllocationListStubs(allocs)
   253  	f.Equal(2, len(allocIDs), "expected 2 allocIDs for connect demo", allocIDs)
   254  	e2eutil.WaitForAllocsRunning(t, tc.Nomad(), allocIDs)
   255  
   256  	// === Check Consul SI tokens were generated for sidecars ===
   257  	foundSITokens := tc.countSITokens(t)
   258  	f.Equal(2, len(foundSITokens), "expected 2 SI tokens total: %v", foundSITokens)
   259  	f.Equal(1, foundSITokens["connect-proxy-count-api"], "expected 1 SI token for connect-proxy-count-api: %v", foundSITokens)
   260  	f.Equal(1, foundSITokens["connect-proxy-count-dashboard"], "expected 1 SI token for connect-proxy-count-dashboard: %v", foundSITokens)
   261  
   262  	t.Log("connect legacy job with ACLs enable finished")
   263  }
   264  
   265  func (tc *ConnectACLsE2ETest) TestConnectACLsConnectNativeDemo(f *framework.F) {
   266  	t := f.T()
   267  
   268  	t.Log("test register Connect job w/ ACLs enabled w/ operator token")
   269  
   270  	// === Setup ACL policy and mint Operator token ===
   271  
   272  	// create a policy allowing writes of services "uuid-fe" and "uuid-api"
   273  	policyID := tc.createConsulPolicy(consulPolicy{
   274  		Name:  "nomad-operator-policy",
   275  		Rules: `service "uuid-fe" { policy = "write" } service "uuid-api" { policy = "write" }`,
   276  	}, f)
   277  	t.Log("created operator policy:", policyID)
   278  
   279  	// create a Consul "operator token" blessed with the above policy
   280  	operatorToken := tc.createOperatorToken(policyID, f)
   281  	t.Log("created operator token:", operatorToken)
   282  
   283  	jobID := connectJobID()
   284  	tc.jobIDs = append(tc.jobIDs, jobID)
   285  
   286  	allocs := e2eutil.RegisterAndWaitForAllocs(t, tc.Nomad(), demoConnectNativeJob, jobID, operatorToken)
   287  	allocIDs := e2eutil.AllocIDsFromAllocationListStubs(allocs)
   288  	e2eutil.WaitForAllocsRunning(t, tc.Nomad(), allocIDs)
   289  
   290  	// === Check Consul SI tokens were generated for native tasks ===
   291  	foundSITokens := tc.countSITokens(t)
   292  	f.Equal(2, len(foundSITokens), "expected 2 SI tokens total: %v", foundSITokens)
   293  	f.Equal(1, foundSITokens["frontend"], "expected 1 SI token for frontend: %v", foundSITokens)
   294  	f.Equal(1, foundSITokens["generate"], "expected 1 SI token for generate: %v", foundSITokens)
   295  
   296  	t.Log("connect native job with ACLs enabled finished")
   297  }
   298  
   299  func (tc *ConnectACLsE2ETest) TestConnectACLsConnectIngressGatewayDemo(f *framework.F) {
   300  	t := f.T()
   301  
   302  	t.Log("test register Connect Ingress Gateway job w/ ACLs enabled")
   303  
   304  	// setup ACL policy and mint operator token
   305  
   306  	policyID := tc.createConsulPolicy(consulPolicy{
   307  		Name:  "nomad-operator-policy",
   308  		Rules: `service "my-ingress-service" { policy = "write" } service "uuid-api" { policy = "write" }`,
   309  	}, f)
   310  	operatorToken := tc.createOperatorToken(policyID, f)
   311  	t.Log("created operator token:", operatorToken)
   312  
   313  	jobID := connectJobID()
   314  	tc.jobIDs = append(tc.jobIDs, jobID)
   315  
   316  	allocs := e2eutil.RegisterAndWaitForAllocs(t, tc.Nomad(), demoConnectIngressGateway, jobID, operatorToken)
   317  	allocIDs := e2eutil.AllocIDsFromAllocationListStubs(allocs)
   318  	e2eutil.WaitForAllocsRunning(t, tc.Nomad(), allocIDs)
   319  
   320  	foundSITokens := tc.countSITokens(t)
   321  	f.Equal(2, len(foundSITokens), "expected 2 SI tokens total: %v", foundSITokens)
   322  	f.Equal(1, foundSITokens["connect-ingress-my-ingress-service"], "expected 1 SI token for connect-ingress-my-ingress-service: %v", foundSITokens)
   323  	f.Equal(1, foundSITokens["generate"], "expected 1 SI token for generate: %v", foundSITokens)
   324  
   325  	t.Log("connect ingress gateway job with ACLs enabled finished")
   326  }
   327  
   328  func (tc *ConnectACLsE2ETest) TestConnectACLsConnectTerminatingGatewayDemo(f *framework.F) {
   329  	t := f.T()
   330  
   331  	t.Log("test register Connect Terminating Gateway job w/ ACLs enabled")
   332  
   333  	// setup ACL policy and mint operator token
   334  
   335  	policyID := tc.createConsulPolicy(consulPolicy{
   336  		Name:  "nomad-operator-policy",
   337  		Rules: `service "api-gateway" { policy = "write" } service "count-dashboard" { policy = "write" }`,
   338  	}, f)
   339  	operatorToken := tc.createOperatorToken(policyID, f)
   340  	t.Log("created operator token:", operatorToken)
   341  
   342  	jobID := connectJobID()
   343  	tc.jobIDs = append(tc.jobIDs, jobID)
   344  
   345  	allocs := e2eutil.RegisterAndWaitForAllocs(t, tc.Nomad(), demoConnectTerminatingGateway, jobID, operatorToken)
   346  	allocIDs := e2eutil.AllocIDsFromAllocationListStubs(allocs)
   347  	e2eutil.WaitForAllocsRunning(t, tc.Nomad(), allocIDs)
   348  
   349  	foundSITokens := tc.countSITokens(t)
   350  	f.Equal(2, len(foundSITokens), "expected 2 SI tokens total: %v", foundSITokens)
   351  	f.Equal(1, foundSITokens["connect-terminating-api-gateway"], "expected 1 SI token for connect-terminating-api-gateway: %v", foundSITokens)
   352  	f.Equal(1, foundSITokens["connect-proxy-count-dashboard"], "expected 1 SI token for count-dashboard: %v", foundSITokens)
   353  
   354  	t.Log("connect terminating gateway job with ACLs enabled finished")
   355  }
   356  
   357  var (
   358  	siTokenRe = regexp.MustCompile(`_nomad_si \[[\w-]{36}] \[[\w-]{36}] \[([\S]+)]`)
   359  )
   360  
   361  func (tc *ConnectACLsE2ETest) serviceofSIToken(description string) string {
   362  	if m := siTokenRe.FindStringSubmatch(description); len(m) == 2 {
   363  		return m[1]
   364  	}
   365  	return ""
   366  }
   367  
   368  func (tc *ConnectACLsE2ETest) countSITokens(t *testing.T) map[string]int {
   369  	aclAPI := tc.Consul().ACL()
   370  	tokens, _, err := aclAPI.TokenList(&consulapi.QueryOptions{
   371  		Token: tc.consulMasterToken,
   372  	})
   373  	require.NoError(t, err)
   374  
   375  	// count the number of SI tokens matching each service name
   376  	foundSITokens := make(map[string]int)
   377  	for _, token := range tokens {
   378  		if service := tc.serviceofSIToken(token.Description); service != "" {
   379  			foundSITokens[service]++
   380  		}
   381  	}
   382  
   383  	return foundSITokens
   384  }
   385  
   386  func (tc *ConnectACLsE2ETest) parseJobSpecFile(t *testing.T, filename string) *nomadapi.Job {
   387  	job, err := jobspec.ParseFile(filename)
   388  	require.NoError(t, err)
   389  	return job
   390  }