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 }