diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/AbstractWorkspaceServiceAccount.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/AbstractWorkspaceServiceAccount.java index ce7a82e6842..223a1bff334 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/AbstractWorkspaceServiceAccount.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/AbstractWorkspaceServiceAccount.java @@ -11,6 +11,7 @@ */ package org.eclipse.che.workspace.infrastructure.kubernetes.namespace; +import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import io.fabric8.kubernetes.api.model.HasMetadata; @@ -47,6 +48,8 @@ public abstract class AbstractWorkspaceServiceAccount< public static final String EXEC_ROLE_NAME = "exec"; public static final String VIEW_ROLE_NAME = "workspace-view"; public static final String METRICS_ROLE_NAME = "workspace-metrics"; + public static final String SECRETS_ROLE_NAME = "workspace-secrets"; + public static final String CREDENTIALS_SECRET_NAME = "workspace-credentials-secret"; protected final String namespace; protected final String serviceAccountName; @@ -107,44 +110,55 @@ private void ensureImplicitRolesWithBindings(Client k8sClient) { // exec role ensureRoleWithBinding( k8sClient, - EXEC_ROLE_NAME, - singletonList("pods/exec"), - singletonList(""), - singletonList("create"), + buildRole( + EXEC_ROLE_NAME, + singletonList("pods/exec"), + emptyList(), + singletonList(""), + singletonList("create")), serviceAccountName + "-exec"); // view role ensureRoleWithBinding( k8sClient, - VIEW_ROLE_NAME, - Arrays.asList("pods", "services"), - singletonList(""), - singletonList("list"), + buildRole( + VIEW_ROLE_NAME, + Arrays.asList("pods", "services"), + emptyList(), + singletonList(""), + singletonList("list")), serviceAccountName + "-view"); // metrics role ensureRoleWithBinding( k8sClient, - METRICS_ROLE_NAME, - Arrays.asList("pods", "nodes"), - singletonList("metrics.k8s.io"), - Arrays.asList("list", "get", "watch"), + buildRole( + METRICS_ROLE_NAME, + Arrays.asList("pods", "nodes"), + emptyList(), + singletonList("metrics.k8s.io"), + Arrays.asList("list", "get", "watch")), serviceAccountName + "-metrics"); + + // credentials-secret role + ensureRoleWithBinding( + k8sClient, + buildRole( + SECRETS_ROLE_NAME, + singletonList("secrets"), + singletonList(CREDENTIALS_SECRET_NAME), + singletonList(""), + Arrays.asList("get", "patch")), + serviceAccountName + "-secrets"); } - private void ensureRoleWithBinding( - Client k8sClient, - String roleName, - List resources, - List apiGroups, - List verbs, - String bindingName) { - ensureRole(k8sClient, roleName, resources, apiGroups, verbs); + private void ensureRoleWithBinding(Client k8sClient, R role, String bindingName) { + ensureRole(k8sClient, role); //noinspection unchecked roleBindings .apply(k8sClient) .inNamespace(namespace) - .createOrReplace(createRoleBinding(roleName, bindingName, false)); + .createOrReplace(createRoleBinding(role.getMetadata().getName(), bindingName, false)); } /** @@ -180,11 +194,16 @@ private void ensureExplicitClusterRoleBindings(Client k8sClient) { * * @param name the name of the role * @param resources the resources the role grants access to + * @param resourceNames specific resource names witch the role grants access to. * @param verbs the verbs the role allows * @return the role object for the given type of Client */ protected abstract R buildRole( - String name, List resources, List apiGroups, List verbs); + String name, + List resources, + List resourceNames, + List apiGroups, + List verbs); /** * Builds a new role binding but does not persist it. @@ -209,17 +228,9 @@ private void createWorkspaceServiceAccount(Client k8sClient) { .build()); } - private void ensureRole( - Client k8sClient, - String name, - List resources, - List apiGroups, - List verbs) { + private void ensureRole(Client k8sClient, R role) { //noinspection unchecked - roles - .apply(k8sClient) - .inNamespace(namespace) - .createOrReplace(buildRole(name, resources, apiGroups, verbs)); + roles.apply(k8sClient).inNamespace(namespace).createOrReplace(role); } public interface ClientFactory { diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactory.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactory.java index 75a0d33328a..241d150e48d 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactory.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactory.java @@ -20,6 +20,7 @@ import static org.eclipse.che.api.workspace.shared.Constants.WORKSPACE_INFRASTRUCTURE_NAMESPACE_ATTRIBUTE; import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.DEFAULT_ATTRIBUTE; import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.PHASE_ATTRIBUTE; +import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.AbstractWorkspaceServiceAccount.CREDENTIALS_SECRET_NAME; import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.NamespaceNameValidator.METADATA_NAME_MAX_LENGTH; import com.google.common.annotations.VisibleForTesting; @@ -30,6 +31,8 @@ import com.google.inject.Singleton; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.Namespace; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; import io.fabric8.kubernetes.client.KubernetesClientException; import java.util.Collections; import java.util.HashMap; @@ -341,6 +344,25 @@ public KubernetesNamespace getOrCreate(RuntimeIdentity identity) throws Infrastr labelNamespaces ? namespaceLabels : emptyMap(), annotateNamespaces ? namespaceAnnotationsEvaluated : emptyMap()); + if (namespace + .secrets() + .get() + .stream() + .noneMatch(s -> s.getMetadata().getName().equals(CREDENTIALS_SECRET_NAME))) { + Secret secret = + new SecretBuilder() + .withType("opaque") + .withNewMetadata() + .withName(CREDENTIALS_SECRET_NAME) + .endMetadata() + .build(); + clientFactory + .create() + .secrets() + .inNamespace(identity.getInfrastructureNamespace()) + .create(secret); + } + if (!isNullOrEmpty(serviceAccountName)) { KubernetesWorkspaceServiceAccount workspaceServiceAccount = doCreateServiceAccount(namespace.getWorkspaceId(), namespace.getName()); diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesSecrets.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesSecrets.java index 64cc09cb120..301c7dad9b6 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesSecrets.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesSecrets.java @@ -76,6 +76,20 @@ public List get(LabelSelector labelSelector) throws InfrastructureExcept } } + /** + * Get all secrets. + * + * @return namespace secrets list + * @throws InfrastructureException when any exception occurs + */ + public List get() throws InfrastructureException { + try { + return clientFactory.create(workspaceId).secrets().inNamespace(namespace).list().getItems(); + } catch (KubernetesClientException e) { + throw new KubernetesInfrastructureException(e); + } + } + /** * Deletes all existing secrets. * diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesWorkspaceServiceAccount.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesWorkspaceServiceAccount.java index 19e955a2861..c1f4346c1d6 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesWorkspaceServiceAccount.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesWorkspaceServiceAccount.java @@ -50,7 +50,11 @@ public KubernetesWorkspaceServiceAccount( @Override protected Role buildRole( - String name, List resources, List apiGroups, List verbs) { + String name, + List resources, + List resourceNames, + List apiGroups, + List verbs) { return new RoleBuilder() .withNewMetadata() .withName(name) @@ -58,6 +62,7 @@ protected Role buildRole( .withRules( new PolicyRuleBuilder() .withResources(resources) + .withResourceNames(resourceNames) .withApiGroups(apiGroups) .withVerbs(verbs) .build()) diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactoryTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactoryTest.java index 53893dbb407..4cef78dfa89 100644 --- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactoryTest.java +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactoryTest.java @@ -16,6 +16,8 @@ import static org.eclipse.che.api.workspace.shared.Constants.WORKSPACE_INFRASTRUCTURE_NAMESPACE_ATTRIBUTE; import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.DEFAULT_ATTRIBUTE; import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.PHASE_ATTRIBUTE; +import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.AbstractWorkspaceServiceAccount.CREDENTIALS_SECRET_NAME; +import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.AbstractWorkspaceServiceAccount.SECRETS_ROLE_NAME; import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespaceFactory.NAMESPACE_TEMPLATE_ATTRIBUTE; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyMap; @@ -41,15 +43,19 @@ import io.fabric8.kubernetes.api.model.Namespace; import io.fabric8.kubernetes.api.model.NamespaceBuilder; import io.fabric8.kubernetes.api.model.NamespaceList; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.ServiceAccountList; import io.fabric8.kubernetes.api.model.Status; import io.fabric8.kubernetes.api.model.rbac.ClusterRoleBuilder; +import io.fabric8.kubernetes.api.model.rbac.PolicyRule; import io.fabric8.kubernetes.api.model.rbac.Role; import io.fabric8.kubernetes.api.model.rbac.RoleBindingList; import io.fabric8.kubernetes.api.model.rbac.RoleList; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; +import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.fabric8.kubernetes.client.server.mock.KubernetesServer; @@ -118,8 +124,7 @@ public class KubernetesNamespaceFactoryTest { @Mock private PreferenceManager preferenceManager; @Mock Appender mockedAppender; - @Mock - private NonNamespaceOperation> namespaceOperation; + @Mock private NonNamespaceOperation namespaceOperation; @Mock private Resource namespaceResource; @@ -454,6 +459,82 @@ public void shouldReturnDefaultNamespaceWhenItDoesNotExistAndUserDefinedIsNotAll .get(PHASE_ATTRIBUTE)); // no phase - means such namespace does not exist } + @Test + public void shouldCreateCredentialsSecretIfNotExists() throws Exception { + // given + namespaceFactory = + spy( + new KubernetesNamespaceFactory( + "", + "", + "-che", + true, + true, + true, + NAMESPACE_LABELS, + NAMESPACE_ANNOTATIONS, + clientFactory, + cheClientFactory, + userManager, + preferenceManager, + pool)); + KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); + KubernetesSecrets secrets = mock(KubernetesSecrets.class); + when(toReturnNamespace.secrets()).thenReturn(secrets); + when(secrets.get()).thenReturn(Collections.emptyList()); + doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespaceAccess(any(), any()); + MixedOperation mixedOperation = mock(MixedOperation.class); + lenient().when(k8sClient.secrets()).thenReturn(mixedOperation); + lenient().when(mixedOperation.inNamespace(anyString())).thenReturn(namespaceOperation); + + // when + RuntimeIdentity identity = + new RuntimeIdentityImpl("workspace123", null, USER_ID, "workspace123"); + namespaceFactory.getOrCreate(identity); + + // then + ArgumentCaptor secretsCaptor = ArgumentCaptor.forClass(Secret.class); + verify(namespaceOperation).create(secretsCaptor.capture()); + Secret secret = secretsCaptor.getValue(); + Assert.assertEquals(secret.getMetadata().getName(), CREDENTIALS_SECRET_NAME); + Assert.assertEquals(secret.getType(), "opaque"); + } + + @Test + public void shouldNotCreateCredentialsSecretIfExists() throws Exception { + // given + namespaceFactory = + spy( + new KubernetesNamespaceFactory( + "", + "", + "-che", + true, + true, + true, + NAMESPACE_LABELS, + NAMESPACE_ANNOTATIONS, + clientFactory, + cheClientFactory, + userManager, + preferenceManager, + pool)); + KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); + prepareNamespace(toReturnNamespace); + doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespaceAccess(any(), any()); + MixedOperation mixedOperation = mock(MixedOperation.class); + lenient().when(k8sClient.secrets()).thenReturn(mixedOperation); + lenient().when(mixedOperation.inNamespace(anyString())).thenReturn(namespaceOperation); + + // when + RuntimeIdentity identity = + new RuntimeIdentityImpl("workspace123", null, USER_ID, "workspace123"); + namespaceFactory.getOrCreate(identity); + + // then + verify(namespaceOperation, never()).create(any()); + } + @Test( expectedExceptions = InfrastructureException.class, expectedExceptionsMessageRegExp = @@ -532,6 +613,7 @@ public void shouldRequireNamespacePriorExistenceIfDifferentFromDefaultAndUserDef preferenceManager, pool)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); + prepareNamespace(toReturnNamespace); doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespaceAccess(any(), any()); // when @@ -564,6 +646,7 @@ public void shouldReturnDefaultNamespaceWhenCreatingIsNotIsNotAllowed() throws E preferenceManager, pool)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); + prepareNamespace(toReturnNamespace); doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespaceAccess(any(), any()); // when @@ -598,6 +681,7 @@ public void shouldPrepareWorkspaceServiceAccountIfItIsConfiguredAndNamespaceIsNo preferenceManager, pool)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); + prepareNamespace(toReturnNamespace); when(toReturnNamespace.getWorkspaceId()).thenReturn("workspace123"); when(toReturnNamespace.getName()).thenReturn("workspace123"); doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespaceAccess(any(), any()); @@ -636,6 +720,7 @@ public void shouldBindToAllConfiguredClusterRoles() throws Exception { preferenceManager, pool)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); + prepareNamespace(toReturnNamespace); when(toReturnNamespace.getWorkspaceId()).thenReturn("workspace123"); when(toReturnNamespace.getName()).thenReturn("workspace123"); doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespaceAccess(any(), any()); @@ -669,7 +754,7 @@ public void shouldBindToAllConfiguredClusterRoles() throws Exception { RoleList roles = k8sClient.rbac().roles().inNamespace("workspace123").list(); assertEquals( - Sets.newHashSet("workspace-view", "workspace-metrics", "exec"), + Sets.newHashSet("workspace-view", "workspace-metrics", "workspace-secrets", "exec"), roles.getItems().stream().map(r -> r.getMetadata().getName()).collect(Collectors.toSet())); RoleBindingList bindings = k8sClient.rbac().roleBindings().inNamespace("workspace123").list(); assertEquals( @@ -683,7 +768,67 @@ public void shouldBindToAllConfiguredClusterRoles() throws Exception { "serviceAccount-cluster0", "serviceAccount-cluster1", "serviceAccount-view", - "serviceAccount-exec")); + "serviceAccount-exec", + "serviceAccount-secrets")); + } + + @Test + public void shouldCreateAndBindCredentialsSecretRole() throws Exception { + // given + namespaceFactory = + spy( + new KubernetesNamespaceFactory( + "serviceAccount", + "cr2, cr3", + "-che", + true, + true, + true, + NAMESPACE_LABELS, + NAMESPACE_ANNOTATIONS, + clientFactory, + cheClientFactory, + userManager, + preferenceManager, + pool)); + KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); + prepareNamespace(toReturnNamespace); + when(toReturnNamespace.getWorkspaceId()).thenReturn("workspace123"); + when(toReturnNamespace.getName()).thenReturn("workspace123"); + doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespaceAccess(any(), any()); + when(clientFactory.create(any())).thenReturn(k8sClient); + + // when + RuntimeIdentity identity = + new RuntimeIdentityImpl("workspace123", null, USER_ID, "workspace123"); + namespaceFactory.getOrCreate(identity); + + // then + Optional roleOptional = + k8sClient + .rbac() + .roles() + .inNamespace("workspace123") + .list() + .getItems() + .stream() + .filter(r -> r.getMetadata().getName().equals(SECRETS_ROLE_NAME)) + .findAny(); + assertTrue(roleOptional.isPresent()); + PolicyRule rule = roleOptional.get().getRules().get(0); + assertEquals(rule.getResources(), singletonList("secrets")); + assertEquals(rule.getResourceNames(), singletonList(CREDENTIALS_SECRET_NAME)); + assertEquals(rule.getApiGroups(), singletonList("")); + assertEquals(rule.getVerbs(), Arrays.asList("get", "patch")); + assertTrue( + k8sClient + .rbac() + .roleBindings() + .inNamespace("workspace123") + .list() + .getItems() + .stream() + .anyMatch(rb -> rb.getMetadata().getName().equals("serviceAccount-secrets"))); } @Test @@ -706,6 +851,7 @@ public void shouldCreateExecAndViewRolesAndBindings() throws Exception { preferenceManager, pool)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); + prepareNamespace(toReturnNamespace); when(toReturnNamespace.getWorkspaceId()).thenReturn("workspace123"); when(toReturnNamespace.getName()).thenReturn("workspace123"); doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespaceAccess(any(), any()); @@ -725,7 +871,7 @@ public void shouldCreateExecAndViewRolesAndBindings() throws Exception { RoleList roles = k8sClient.rbac().roles().inNamespace("workspace123").list(); assertEquals( - Sets.newHashSet("workspace-view", "workspace-metrics", "exec"), + Sets.newHashSet("workspace-view", "workspace-metrics", "workspace-secrets", "exec"), roles.getItems().stream().map(r -> r.getMetadata().getName()).collect(Collectors.toSet())); Role role1 = roles.getItems().get(0); Role role2 = roles.getItems().get(1); @@ -742,7 +888,11 @@ public void shouldCreateExecAndViewRolesAndBindings() throws Exception { .stream() .map(r -> r.getMetadata().getName()) .collect(Collectors.toSet()), - Sets.newHashSet("serviceAccount-metrics", "serviceAccount-view", "serviceAccount-exec")); + Sets.newHashSet( + "serviceAccount-metrics", + "serviceAccount-view", + "serviceAccount-exec", + "serviceAccount-secrets")); } @Test @@ -1036,6 +1186,7 @@ public void shouldHandleProvision() throws InfrastructureException { preferenceManager, pool)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); + prepareNamespace(toReturnNamespace); when(toReturnNamespace.getName()).thenReturn("jondoe-che"); doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespaceAccess(any(), any()); KubernetesNamespaceMetaImpl namespaceMeta = @@ -1075,6 +1226,7 @@ public void shouldFailToProvisionIfNotAbleToFindNamespace() throws Infrastructur preferenceManager, pool)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); + prepareNamespace(toReturnNamespace); when(toReturnNamespace.getName()).thenReturn("jondoe-cha-cha-cha"); doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespaceAccess(any(), any()); KubernetesNamespaceMetaImpl namespaceMeta = @@ -1113,6 +1265,7 @@ public void shouldFail2ProvisionIfNotAbleToFindNamespace() throws Infrastructure preferenceManager, pool)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); + prepareNamespace(toReturnNamespace); when(toReturnNamespace.getName()).thenReturn("jondoe-cha-cha-cha"); doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespaceAccess(any(), any()); KubernetesNamespaceMetaImpl namespaceMeta = @@ -1189,6 +1342,7 @@ public void testUsernamePlaceholderInAnnotationsIsEvaluated() throws Infrastruct pool)); EnvironmentContext.getCurrent().setSubject(new SubjectImpl("jondoe", "123", null, false)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); + prepareNamespace(toReturnNamespace); doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespaceAccess(any(), any()); // when @@ -1310,6 +1464,16 @@ private void throwOnTryToGetNamespacesList(Throwable e) throws Exception { when(namespaceList.getItems()).thenThrow(e); } + private void prepareNamespace(KubernetesNamespace namespace) throws InfrastructureException { + KubernetesSecrets secrets = mock(KubernetesSecrets.class); + when(namespace.secrets()).thenReturn(secrets); + Secret secretMock = mock(Secret.class); + ObjectMeta objectMeta = mock(ObjectMeta.class); + when(objectMeta.getName()).thenReturn(CREDENTIALS_SECRET_NAME); + when(secretMock.getMetadata()).thenReturn(objectMeta); + when(secrets.get()).thenReturn(Collections.singletonList(secretMock)); + } + private Namespace createNamespace(String name, String phase) { return new NamespaceBuilder() .withNewMetadata() diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactory.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactory.java index b193ddec631..220f6330756 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactory.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactory.java @@ -16,11 +16,14 @@ import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.PHASE_ATTRIBUTE; +import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.AbstractWorkspaceServiceAccount.CREDENTIALS_SECRET_NAME; import com.google.common.annotations.VisibleForTesting; import com.google.inject.Inject; import com.google.inject.Singleton; import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.openshift.api.model.Project; import java.util.HashMap; @@ -120,6 +123,26 @@ public OpenShiftProject getOrCreate(RuntimeIdentity identity) throws Infrastruct labelNamespaces ? namespaceLabels : emptyMap(), annotateNamespaces ? namespaceAnnotationsEvaluated : emptyMap()); + // create credentials secret + if (osProject + .secrets() + .get() + .stream() + .noneMatch(s -> s.getMetadata().getName().equals(CREDENTIALS_SECRET_NAME))) { + Secret secret = + new SecretBuilder() + .withType("opaque") + .withNewMetadata() + .withName(CREDENTIALS_SECRET_NAME) + .endMetadata() + .build(); + clientFactory + .createOC() + .secrets() + .inNamespace(identity.getInfrastructureNamespace()) + .create(secret); + } + if (!isNullOrEmpty(getServiceAccountName())) { OpenShiftWorkspaceServiceAccount osWorkspaceServiceAccount = doCreateServiceAccount(osProject.getWorkspaceId(), osProject.getName()); diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftWorkspaceServiceAccount.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftWorkspaceServiceAccount.java index ae429123f86..7870a2d2991 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftWorkspaceServiceAccount.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftWorkspaceServiceAccount.java @@ -55,7 +55,11 @@ class OpenShiftWorkspaceServiceAccount @Override protected Role buildRole( - String name, List resources, List apiGroups, List verbs) { + String name, + List resources, + List resourceNames, + List apiGroups, + List verbs) { return new RoleBuilder() .withNewMetadata() .withName(name) @@ -63,6 +67,7 @@ protected Role buildRole( .withRules( new PolicyRuleBuilder() .withResources(resources) + .withResourceNames(resourceNames) .withApiGroups(apiGroups) .withVerbs(verbs) .build()) diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactoryTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactoryTest.java index 3d5e47dc06a..910adc758c2 100644 --- a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactoryTest.java +++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactoryTest.java @@ -16,12 +16,12 @@ import static java.util.Collections.singletonList; import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.DEFAULT_ATTRIBUTE; import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.PHASE_ATTRIBUTE; +import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.AbstractWorkspaceServiceAccount.CREDENTIALS_SECRET_NAME; import static org.eclipse.che.workspace.infrastructure.openshift.Constants.PROJECT_DESCRIPTION_ANNOTATION; import static org.eclipse.che.workspace.infrastructure.openshift.Constants.PROJECT_DESCRIPTION_ATTRIBUTE; import static org.eclipse.che.workspace.infrastructure.openshift.Constants.PROJECT_DISPLAY_NAME_ANNOTATION; import static org.eclipse.che.workspace.infrastructure.openshift.Constants.PROJECT_DISPLAY_NAME_ATTRIBUTE; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.lenient; @@ -35,9 +35,13 @@ import static org.testng.Assert.assertNull; import com.google.common.collect.ImmutableMap; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.Status; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.fabric8.openshift.api.model.Project; import io.fabric8.openshift.api.model.ProjectBuilder; @@ -45,6 +49,7 @@ import io.fabric8.openshift.client.OpenShiftClient; import io.fabric8.openshift.client.dsl.ProjectOperation; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -63,12 +68,15 @@ import org.eclipse.che.inject.ConfigurationException; import org.eclipse.che.workspace.infrastructure.kubernetes.CheServerKubernetesClientFactory; import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta; +import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesSecrets; import org.eclipse.che.workspace.infrastructure.kubernetes.util.KubernetesSharedPool; import org.eclipse.che.workspace.infrastructure.openshift.CheServerOpenshiftClientFactory; import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientFactory; import org.eclipse.che.workspace.infrastructure.openshift.provision.OpenShiftStopWorkspaceRoleProvisioner; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.testng.MockitoTestNGListener; +import org.testng.Assert; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Listeners; @@ -529,6 +537,7 @@ public void shouldRequireNamespacePriorExistenceIfDifferentFromDefaultAndUserDef pool, NO_OAUTH_IDENTITY_PROVIDER)); OpenShiftProject toReturnProject = mock(OpenShiftProject.class); + prepareProject(toReturnProject); doReturn(toReturnProject).when(projectFactory).doCreateProjectAccess(any(), any()); // when @@ -542,6 +551,92 @@ public void shouldRequireNamespacePriorExistenceIfDifferentFromDefaultAndUserDef verify(toReturnProject).prepare(eq(false), eq(false), any(), any()); } + @Test + public void shouldCreateCredentialsSecretIfNotExists() throws Exception { + // given + projectFactory = + spy( + new OpenShiftProjectFactory( + "", + null, + "-che", + true, + true, + true, + NAMESPACE_LABELS, + NAMESPACE_ANNOTATIONS, + true, + clientFactory, + cheClientFactory, + cheServerOpenshiftClientFactory, + stopWorkspaceRoleProvisioner, + userManager, + preferenceManager, + pool, + NO_OAUTH_IDENTITY_PROVIDER)); + OpenShiftProject toReturnProject = mock(OpenShiftProject.class); + doReturn(toReturnProject).when(projectFactory).doCreateProjectAccess(any(), any()); + NonNamespaceOperation namespaceOperation = mock(NonNamespaceOperation.class); + MixedOperation mixedOperation = mock(MixedOperation.class); + KubernetesSecrets secrets = mock(KubernetesSecrets.class); + when(toReturnProject.secrets()).thenReturn(secrets); + when(secrets.get()).thenReturn(Collections.emptyList()); + lenient().when(osClient.secrets()).thenReturn(mixedOperation); + lenient().when(mixedOperation.inNamespace(anyString())).thenReturn(namespaceOperation); + + // when + RuntimeIdentity identity = + new RuntimeIdentityImpl("workspace123", null, USER_ID, "workspace123"); + projectFactory.getOrCreate(identity); + + // then + ArgumentCaptor secretsCaptor = ArgumentCaptor.forClass(Secret.class); + verify(namespaceOperation).create(secretsCaptor.capture()); + Secret secret = secretsCaptor.getValue(); + Assert.assertEquals(secret.getMetadata().getName(), CREDENTIALS_SECRET_NAME); + Assert.assertEquals(secret.getType(), "opaque"); + } + + @Test + public void shouldNotCreateCredentialsSecretIfExist() throws Exception { + // given + projectFactory = + spy( + new OpenShiftProjectFactory( + "", + null, + "-che", + true, + true, + true, + NAMESPACE_LABELS, + NAMESPACE_ANNOTATIONS, + true, + clientFactory, + cheClientFactory, + cheServerOpenshiftClientFactory, + stopWorkspaceRoleProvisioner, + userManager, + preferenceManager, + pool, + NO_OAUTH_IDENTITY_PROVIDER)); + OpenShiftProject toReturnProject = mock(OpenShiftProject.class); + prepareProject(toReturnProject); + doReturn(toReturnProject).when(projectFactory).doCreateProjectAccess(any(), any()); + NonNamespaceOperation namespaceOperation = mock(NonNamespaceOperation.class); + MixedOperation mixedOperation = mock(MixedOperation.class); + lenient().when(osClient.secrets()).thenReturn(mixedOperation); + lenient().when(mixedOperation.inNamespace(anyString())).thenReturn(namespaceOperation); + + // when + RuntimeIdentity identity = + new RuntimeIdentityImpl("workspace123", null, USER_ID, "workspace123"); + projectFactory.getOrCreate(identity); + + // then + verify(namespaceOperation, never()).create(any()); + } + @Test public void shouldPrepareWorkspaceServiceAccountIfItIsConfiguredAndProjectIsNotPredefined() throws Exception { @@ -567,6 +662,7 @@ public void shouldPrepareWorkspaceServiceAccountIfItIsConfiguredAndProjectIsNotP pool, NO_OAUTH_IDENTITY_PROVIDER)); OpenShiftProject toReturnProject = mock(OpenShiftProject.class); + prepareProject(toReturnProject); when(toReturnProject.getWorkspaceId()).thenReturn("workspace123"); when(toReturnProject.getName()).thenReturn("workspace123"); doReturn(toReturnProject).when(projectFactory).doCreateProjectAccess(any(), any()); @@ -609,6 +705,7 @@ public void shouldCallStopWorkspaceRoleProvisionWhenIdentityProviderIsDefined() OpenShiftProject toReturnProject = mock(OpenShiftProject.class); when(toReturnProject.getWorkspaceId()).thenReturn("workspace123"); when(toReturnProject.getName()).thenReturn("workspace123"); + prepareProject(toReturnProject); doReturn(toReturnProject).when(projectFactory).doCreateProjectAccess(any(), any()); OpenShiftWorkspaceServiceAccount serviceAccount = mock(OpenShiftWorkspaceServiceAccount.class); @@ -649,6 +746,7 @@ public void shouldNotCallStopWorkspaceRoleProvisionWhenIdentityProviderIsDefined pool, NO_OAUTH_IDENTITY_PROVIDER)); OpenShiftProject toReturnProject = mock(OpenShiftProject.class); + prepareProject(toReturnProject); when(toReturnProject.getWorkspaceId()).thenReturn("workspace123"); when(toReturnProject.getName()).thenReturn("workspace123"); doReturn(toReturnProject).when(projectFactory).doCreateProjectAccess(any(), any()); @@ -769,6 +867,7 @@ public void testUsernamePlaceholderInAnnotationsIsEvaluated() throws Infrastruct NO_OAUTH_IDENTITY_PROVIDER)); EnvironmentContext.getCurrent().setSubject(new SubjectImpl("jondoe", "123", null, false)); OpenShiftProject toReturnProject = mock(OpenShiftProject.class); + prepareProject(toReturnProject); doReturn(toReturnProject).when(projectFactory).doCreateProjectAccess(any(), any()); // when @@ -806,6 +905,16 @@ private void prepareListedProjects(List projects) throws Exception { when(projectList.getItems()).thenReturn(projects); } + private void prepareProject(OpenShiftProject project) throws InfrastructureException { + KubernetesSecrets secrets = mock(KubernetesSecrets.class); + when(project.secrets()).thenReturn(secrets); + Secret secretMock = mock(Secret.class); + ObjectMeta objectMeta = mock(ObjectMeta.class); + when(objectMeta.getName()).thenReturn(CREDENTIALS_SECRET_NAME); + when(secretMock.getMetadata()).thenReturn(objectMeta); + when(secrets.get()).thenReturn(Collections.singletonList(secretMock)); + } + private void throwOnTryToGetProjectsList(Throwable e) throws Exception { when(projectListResource.list()).thenThrow(e); }