KubernetesGCPServiceAccountSecretManager { String ensureServiceAccountKeySecret(String workflowId, String serviceAccount) { final long epoch = epochProvider.epoch(clock.millis(), serviceAccount); final String secretName = buildSecretName(serviceAccount, epoch); LOG.info("[AUDIT] Workflow {} refers to secret {} storing keys of {}", workflowId, secretName, serviceAccount); try { return serviceAccountSecretCache.get(serviceAccount, () -> getOrCreateSecret(workflowId, serviceAccount, epoch, secretName)); } catch (ExecutionException | UncheckedExecutionException e) { final Throwable cause = e.getCause(); if (cause instanceof InvalidExecutionException) { throw (InvalidExecutionException) cause; } else if (GcpUtil.isPermissionDenied(cause)) { throw new InvalidExecutionException(String.format( "Permission denied when creating keys for service account: %s. Styx needs to be Service Account Key Admin.", serviceAccount)); } else if (GcpUtil.isResourceExhausted(cause)) { throw new InvalidExecutionException(String.format( "Maximum number of keys on service account reached: %s. Styx requires 4 keys to operate.", serviceAccount)); } else { throw new RuntimeException(e); } } } KubernetesGCPServiceAccountSecretManager(
      Fabric8KubernetesClient client,
      ServiceAccountKeyManager keyManager,
      Stats stats,
      EpochProvider epochProvider,
      Clock clock);   KubernetesGCPServiceAccountSecretManager(
      Fabric8KubernetesClient client,
      ServiceAccountKeyManager keyManager,
      Stats stats); void cleanup();  }
@Test public void shouldCreateServiceAccountKeysAndSecret() throws IOException { when(serviceAccountKeyManager.serviceAccountExists(SERVICE_ACCOUNT)).thenReturn(true); ServiceAccountKey jsonKey = new ServiceAccountKey(); jsonKey.setName("key.json"); jsonKey.setPrivateKeyData("json-private-key-data"); ServiceAccountKey p12Key = new ServiceAccountKey(); p12Key.setName("key.p12"); p12Key.setPrivateKeyData("p12-private-key-data"); when(serviceAccountKeyManager.createJsonKey(any(String.class))).thenReturn(jsonKey); when(serviceAccountKeyManager.createP12Key(any(String.class))).thenReturn(p12Key); sut.ensureServiceAccountKeySecret(WORKFLOW_ID.toString(), SERVICE_ACCOUNT); verify(serviceAccountKeyManager).createJsonKey(SERVICE_ACCOUNT); verify(serviceAccountKeyManager).createP12Key(SERVICE_ACCOUNT); verify(k8sClient).createSecret(secretCaptor.capture()); final Secret createdSecret = secretCaptor.getValue(); assertThat(createdSecret.getMetadata().getAnnotations(), hasEntry("styx-wf-sa", SERVICE_ACCOUNT)); assertThat(createdSecret.getData(), hasEntry("styx-wf-sa.json", jsonKey.getPrivateKeyData())); assertThat(createdSecret.getData(), hasEntry("styx-wf-sa.p12", p12Key.getPrivateKeyData())); }
@Test(expected = RuntimeException.class) public void shouldCleanupKeysIfKeyCreationFails() throws IOException { when(serviceAccountKeyManager.serviceAccountExists(SERVICE_ACCOUNT)).thenReturn(true); ServiceAccountKey jsonKey = new ServiceAccountKey(); jsonKey.setName("key.json"); jsonKey.setPrivateKeyData("json-private-key-data"); when(serviceAccountKeyManager.createJsonKey(any(String.class))).thenReturn(jsonKey); when(serviceAccountKeyManager.createP12Key(any(String.class))).thenThrow(new IOException()); sut.ensureServiceAccountKeySecret(WORKFLOW_ID.toString(), SERVICE_ACCOUNT); verify(serviceAccountKeyManager).createJsonKey(SERVICE_ACCOUNT); verify(serviceAccountKeyManager).createP12Key(SERVICE_ACCOUNT); verify(serviceAccountKeyManager).tryDeleteKey(jsonKey.getName()); }
@Test public void shouldDeleteGCPKeysIfSecretAlreadyExists() throws IOException { when(serviceAccountKeyManager.serviceAccountExists(SERVICE_ACCOUNT)).thenReturn(true); ServiceAccountKey jsonKey = new ServiceAccountKey(); jsonKey.setName("key.json"); jsonKey.setPrivateKeyData("json-private-key-data"); ServiceAccountKey p12Key = new ServiceAccountKey(); p12Key.setName("key.p12"); p12Key.setPrivateKeyData("p12-private-key-data"); when(serviceAccountKeyManager.createJsonKey(any(String.class))).thenReturn(jsonKey); when(serviceAccountKeyManager.createP12Key(any(String.class))).thenReturn(p12Key); when(k8sClient.createSecret(any())).thenThrow(new KubernetesClientException( "Already exists", 409, new Status())); sut.ensureServiceAccountKeySecret(WORKFLOW_ID.toString(), SERVICE_ACCOUNT); verify(serviceAccountKeyManager).createJsonKey(SERVICE_ACCOUNT); verify(serviceAccountKeyManager).createP12Key(SERVICE_ACCOUNT); verify(serviceAccountKeyManager).tryDeleteKey(jsonKey.getName()); verify(serviceAccountKeyManager).tryDeleteKey(p12Key.getName()); }
@Test public void shouldNotConcurrentlyCreateServiceAccountKeysAndSecrets() throws IOException, ExecutionException, InterruptedException { final ServiceAccountKey jsonKey = new ServiceAccountKey(); jsonKey.setName("key.json"); jsonKey.setPrivateKeyData("json-private-key-data"); final ServiceAccountKey p12Key = new ServiceAccountKey(); p12Key.setName("key.p12"); p12Key.setPrivateKeyData("p12-private-key-data"); CompletableFuture<Boolean> accountExistsFuture = new CompletableFuture<>(); when(serviceAccountKeyManager.serviceAccountExists(SERVICE_ACCOUNT)).thenAnswer(a -> accountExistsFuture.get()); when(serviceAccountKeyManager.createJsonKey(any(String.class))).thenReturn(jsonKey); when(serviceAccountKeyManager.createP12Key(any(String.class))).thenReturn(p12Key); final Future<String> f1 = executor.submit( () -> sut.ensureServiceAccountKeySecret(WORKFLOW_ID.toString(), SERVICE_ACCOUNT)); final Future<String> f2 = executor.submit( () -> sut.ensureServiceAccountKeySecret(WORKFLOW_ID.toString(), SERVICE_ACCOUNT)); verify(serviceAccountKeyManager, timeout(30_000)).serviceAccountExists(SERVICE_ACCOUNT); Thread.sleep(5000); verify(serviceAccountKeyManager, times(1)).serviceAccountExists(SERVICE_ACCOUNT); accountExistsFuture.complete(true); final String secret1 = f1.get(); final String secret2 = f2.get(); verify(k8sClient, times(1)).createSecret(secretCaptor.capture()); final Secret createdSecret = secretCaptor.getValue(); assertThat(secret1, is(createdSecret.getMetadata().getName())); assertThat(secret2, is(createdSecret.getMetadata().getName())); verify(serviceAccountKeyManager, times(1)).createJsonKey(SERVICE_ACCOUNT); verify(serviceAccountKeyManager, times(1)).createP12Key(SERVICE_ACCOUNT); }
@Test public void shouldHandlePermissionDenied() throws IOException { when(serviceAccountKeyManager.serviceAccountExists(anyString())).thenReturn(true); final GoogleJsonResponseException permissionDenied = new GoogleJsonResponseException( new HttpResponseException.Builder(403, "Forbidden", new HttpHeaders()), new GoogleJsonError().set("status", "PERMISSION_DENIED")); doThrow(permissionDenied).when(serviceAccountKeyManager).createJsonKey(any()); doThrow(permissionDenied).when(serviceAccountKeyManager).createP12Key(any()); exception.expect(InvalidExecutionException.class); exception.expectMessage(String.format( "Permission denied when creating keys for service account: %s. Styx needs to be Service Account Key Admin.", SERVICE_ACCOUNT)); sut.ensureServiceAccountKeySecret(WORKFLOW_ID.toString(), SERVICE_ACCOUNT); }
@Test public void shouldHandleTooManyKeysCreated() throws IOException { when(serviceAccountKeyManager.serviceAccountExists(anyString())).thenReturn(true); final GoogleJsonResponseException resourceExhausted = new GoogleJsonResponseException( new HttpResponseException.Builder(429, "RESOURCE_EXHAUSTED", new HttpHeaders()), new GoogleJsonError().set("status", "RESOURCE_EXHAUSTED")); doThrow(resourceExhausted).when(serviceAccountKeyManager).createJsonKey(any()); doThrow(resourceExhausted).when(serviceAccountKeyManager).createP12Key(any()); exception.expect(InvalidExecutionException.class); exception.expectMessage(String.format( "Maximum number of keys on service account reached: %s. Styx requires 4 keys to operate.", SERVICE_ACCOUNT)); sut.ensureServiceAccountKeySecret(WORKFLOW_ID.toString(), SERVICE_ACCOUNT); }
@Test public void shouldFailIfServiceAccountDoesNotExist() throws IOException { when(serviceAccountKeyManager.serviceAccountExists(SERVICE_ACCOUNT)).thenReturn(false); exception.expect(InvalidExecutionException.class); exception.expectMessage("Referenced service account " + SERVICE_ACCOUNT + " was not found"); sut.ensureServiceAccountKeySecret(WORKFLOW_INSTANCE.workflowId().toString(), SERVICE_ACCOUNT); }