diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/GitConfigProvisioner.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/GitConfigProvisioner.java index c2936265c46..de576bbbcf7 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/GitConfigProvisioner.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/GitConfigProvisioner.java @@ -52,10 +52,10 @@ @Singleton public class GitConfigProvisioner implements ConfigurationProvisioner { - private final String GIT_CONFIG_MAP_NAME_SUFFIX = "-gitconfig"; + public static final String GIT_CONFIG_MAP_NAME_SUFFIX = "-gitconfig"; private static final String GIT_BASE_CONFIG_PATH = "/etc/"; - private static final String GIT_CONFIG = "gitconfig"; + public static final String GIT_CONFIG = "gitconfig"; private static final String GIT_CONFIG_PATH = GIT_BASE_CONFIG_PATH + GIT_CONFIG; private static final String PREFERENCES_KEY_FILTER = "theia-user-preferences"; private static final String GIT_USER_NAME_PROPERTY = "git.user.name"; diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/GitCredentialStorageFileSecretApplier.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/GitCredentialStorageFileSecretApplier.java new file mode 100644 index 00000000000..a807329b5fb --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/GitCredentialStorageFileSecretApplier.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.workspace.infrastructure.kubernetes.provision.secret; + +import static java.lang.String.format; +import static org.eclipse.che.workspace.infrastructure.kubernetes.provision.secret.SecretAsContainerResourceProvisioner.ANNOTATION_PREFIX; + +import com.google.common.annotations.Beta; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.Secret; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import javax.inject.Inject; +import javax.inject.Singleton; +import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; +import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.K8sVersion; +import org.eclipse.che.workspace.infrastructure.kubernetes.provision.GitConfigProvisioner; + +/** + * An instance of {@link FileSecretApplier} that is trying to adjust the content of git-config that + * was added by {@link GitConfigProvisioner}. The adjustment is adding configuration of git + * credentials store, which is pointing to the file that is going to be mount to the container from + * the secret. + */ +@Beta +@Singleton +public class GitCredentialStorageFileSecretApplier extends FileSecretApplier { + + public static final String ANNOTATION_GIT_CREDENTIALS = + ANNOTATION_PREFIX + "/" + "git-credential"; + + private static final String GIT_CREDENTIALS_FILE_STORE_PATTERN = + "\n[credential]\n\thelper = store --file %s\n"; + + @Inject + public GitCredentialStorageFileSecretApplier(K8sVersion k8sVersion) { + super(k8sVersion); + } + + @Override + public void applySecret(KubernetesEnvironment env, RuntimeIdentity runtimeIdentity, Secret secret) + throws InfrastructureException { + super.applySecret(env, runtimeIdentity, secret); + final String secretMountPath = secret.getMetadata().getAnnotations().get(ANNOTATION_MOUNT_PATH); + Set keys = secret.getData().keySet(); + if (keys.size() != 1) { + throw new InfrastructureException( + format( + "Invalid git credential secret data. It should contain only 1 data item but it have %d", + keys.size())); + } + Path gitSecretFilePath = Paths.get(secretMountPath, keys.iterator().next()); + ConfigMap gitConfigMap = + env.getConfigMaps() + .get( + runtimeIdentity.getWorkspaceId() + GitConfigProvisioner.GIT_CONFIG_MAP_NAME_SUFFIX); + if (gitConfigMap != null) { + Map gitConfigMapData = gitConfigMap.getData(); + String gitConfig = gitConfigMapData.get(GitConfigProvisioner.GIT_CONFIG); + if (gitConfig != null) { + if (gitConfig.contains("helper = store --file") && gitConfig.contains("[credential]")) { + throw new InfrastructureException( + "Multiple git credentials secrets found. Please remove duplication."); + } + + HashMap newGitConfigMapData = new HashMap<>(gitConfigMapData); + newGitConfigMapData.put( + GitConfigProvisioner.GIT_CONFIG, + String.format(GIT_CREDENTIALS_FILE_STORE_PATTERN, gitSecretFilePath.toString())); + gitConfigMap.setData(newGitConfigMapData); + } + } + } +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/SecretAsContainerResourceProvisioner.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/SecretAsContainerResourceProvisioner.java index 95af2c87685..05b1ecd661d 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/SecretAsContainerResourceProvisioner.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/SecretAsContainerResourceProvisioner.java @@ -11,8 +11,10 @@ */ package org.eclipse.che.workspace.infrastructure.kubernetes.provision.secret; +import static java.lang.Boolean.parseBoolean; import static java.lang.String.format; import static java.util.stream.Collectors.toMap; +import static org.eclipse.che.workspace.infrastructure.kubernetes.provision.secret.GitCredentialStorageFileSecretApplier.ANNOTATION_GIT_CREDENTIALS; import com.google.common.annotations.Beta; import io.fabric8.kubernetes.api.model.LabelSelector; @@ -44,15 +46,18 @@ public class SecretAsContainerResourceProvisioner secretLabels; @Inject public SecretAsContainerResourceProvisioner( FileSecretApplier fileSecretApplier, EnvironmentVariableSecretApplier environmentVariableSecretApplier, + GitCredentialStorageFileSecretApplier gitCredentialStorageFileSecretApplier, @Named("che.workspace.provision.secret.labels") String[] labels) { this.fileSecretApplier = fileSecretApplier; this.environmentVariableSecretApplier = environmentVariableSecretApplier; + this.gitCredentialStorageFileSecretApplier = gitCredentialStorageFileSecretApplier; this.secretLabels = Arrays.stream(labels) .map(item -> item.split("=", 2)) @@ -73,7 +78,12 @@ public void provision(E env, RuntimeIdentity runtimeIdentity, KubernetesNamespac if ("env".equalsIgnoreCase(mountType)) { environmentVariableSecretApplier.applySecret(env, runtimeIdentity, secret); } else if ("file".equalsIgnoreCase(mountType)) { - fileSecretApplier.applySecret(env, runtimeIdentity, secret); + if (parseBoolean(secret.getMetadata().getAnnotations().get(ANNOTATION_GIT_CREDENTIALS))) { + gitCredentialStorageFileSecretApplier.applySecret(env, runtimeIdentity, secret); + } else { + fileSecretApplier.applySecret(env, runtimeIdentity, secret); + } + } else { throw new InfrastructureException( format( diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/GitCredentialStorageFileSecretApplierTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/GitCredentialStorageFileSecretApplierTest.java new file mode 100644 index 00000000000..6e04942e534 --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/GitCredentialStorageFileSecretApplierTest.java @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.workspace.infrastructure.kubernetes.provision.secret; + +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; +import static org.eclipse.che.workspace.infrastructure.kubernetes.provision.secret.FileSecretApplier.ANNOTATION_MOUNT_PATH; +import static org.eclipse.che.workspace.infrastructure.kubernetes.provision.secret.KubernetesSecretApplier.ANNOTATION_AUTOMOUNT; +import static org.eclipse.che.workspace.infrastructure.kubernetes.provision.secret.SecretAsContainerResourceProvisioner.ANNOTATION_MOUNT_AS; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertTrue; + +import com.google.common.collect.ImmutableMap; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.PodSpec; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; +import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.K8sVersion; +import org.eclipse.che.workspace.infrastructure.kubernetes.provision.GitConfigProvisioner; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +@Listeners(MockitoTestNGListener.class) +public class GitCredentialStorageFileSecretApplierTest { + + @Mock private KubernetesEnvironment environment; + + @Mock private KubernetesEnvironment.PodData podData; + + @Mock private PodSpec podSpec; + + @Mock private RuntimeIdentity runtimeIdentity; + + @Mock private K8sVersion kubernetesVersion; + + GitCredentialStorageFileSecretApplier secretApplier; + + public static final String GIT_CONFIG_CONTENT = + "[user]\n\tname = Michelangelo Merisi da Caravaggio\n\tmail = mcaravag@email.not.exists.com"; + + @BeforeMethod + public void setUp() throws Exception { + lenient().when(kubernetesVersion.newerOrEqualThan(1, 13)).thenReturn(true); + lenient().when(kubernetesVersion.olderThan(1, 13)).thenReturn(false); + secretApplier = new GitCredentialStorageFileSecretApplier(kubernetesVersion); + when(environment.getPodsData()).thenReturn(singletonMap("pod1", podData)); + when(podData.getRole()).thenReturn(KubernetesEnvironment.PodRole.DEPLOYMENT); + when(podData.getSpec()).thenReturn(podSpec); + when(runtimeIdentity.getWorkspaceId()).thenReturn("ws-1234598"); + } + + @Test( + expectedExceptions = InfrastructureException.class, + expectedExceptionsMessageRegExp = + "Invalid git credential secret data. It should contain only 1 data item but it have 2") + public void shouldThrowInfrastructureExceptionIfSecretsHasMoreOrLessWhen1Data() + throws InfrastructureException { + // given + Secret secret = + new SecretBuilder() + .withData(ImmutableMap.of("credentials", "random", "credentials2", "random")) + .withMetadata( + new ObjectMetaBuilder() + .withName("test_secret") + .withAnnotations( + ImmutableMap.of( + ANNOTATION_MOUNT_AS, + "file", + ANNOTATION_MOUNT_PATH, + "/home/user/.git", + GitCredentialStorageFileSecretApplier.ANNOTATION_GIT_CREDENTIALS, + "true", + ANNOTATION_AUTOMOUNT, + "true")) + .withLabels(emptyMap()) + .build()) + .build(); + // when + secretApplier.applySecret(environment, runtimeIdentity, secret); + } + + @Test + public void shouldBeAbleToAdjustGiConfigConfigMap() throws InfrastructureException { + // given + Secret secret = + new SecretBuilder() + .withData(ImmutableMap.of("credentials", "random")) + .withMetadata( + new ObjectMetaBuilder() + .withName("test_secret") + .withAnnotations( + ImmutableMap.of( + ANNOTATION_MOUNT_AS, + "file", + ANNOTATION_MOUNT_PATH, + "/home/user/.git", + GitCredentialStorageFileSecretApplier.ANNOTATION_GIT_CREDENTIALS, + "true", + ANNOTATION_AUTOMOUNT, + "true")) + .withLabels(emptyMap()) + .build()) + .build(); + + ConfigMap configMap = + new ConfigMapBuilder() + .withData(ImmutableMap.of(GitConfigProvisioner.GIT_CONFIG, GIT_CONFIG_CONTENT)) + .build(); + when(environment.getConfigMaps()) + .thenReturn( + ImmutableMap.of( + "ws-1234598" + GitConfigProvisioner.GIT_CONFIG_MAP_NAME_SUFFIX, configMap)); + // when + secretApplier.applySecret(environment, runtimeIdentity, secret); + // then + String data = configMap.getData().get(GitConfigProvisioner.GIT_CONFIG); + assertTrue( + data.endsWith("[credential]\n\thelper = store --file /home/user/.git/credentials\n")); + } + + @Test( + expectedExceptions = InfrastructureException.class, + expectedExceptionsMessageRegExp = + "Multiple git credentials secrets found. Please remove duplication.") + public void shouldThrowInfrastructureExceptionIfGitConfigAlreadyContainsSecretConfig() + throws InfrastructureException { + // given + Secret secret = + new SecretBuilder() + .withData(ImmutableMap.of("credentials", "random")) + .withMetadata( + new ObjectMetaBuilder() + .withName("test_secret") + .withAnnotations( + ImmutableMap.of( + ANNOTATION_MOUNT_AS, + "file", + ANNOTATION_MOUNT_PATH, + "/home/user/.git", + GitCredentialStorageFileSecretApplier.ANNOTATION_GIT_CREDENTIALS, + "true", + ANNOTATION_AUTOMOUNT, + "true")) + .withLabels(emptyMap()) + .build()) + .build(); + + ConfigMap configMap = + new ConfigMapBuilder() + .withData( + ImmutableMap.of( + GitConfigProvisioner.GIT_CONFIG, + GIT_CONFIG_CONTENT + + "[credential]\n\thelper = store --file /home/user/.git/credentials\n")) + .build(); + when(environment.getConfigMaps()) + .thenReturn( + ImmutableMap.of( + "ws-1234598" + GitConfigProvisioner.GIT_CONFIG_MAP_NAME_SUFFIX, configMap)); + // when + secretApplier.applySecret(environment, runtimeIdentity, secret); + } +} diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/SecretAsContainerResourceProvisionerTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/SecretAsContainerResourceProvisionerTest.java index 08b702557b0..4565390fb88 100644 --- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/SecretAsContainerResourceProvisionerTest.java +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/SecretAsContainerResourceProvisionerTest.java @@ -13,7 +13,15 @@ import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.eclipse.che.workspace.infrastructure.kubernetes.provision.secret.EnvironmentVariableSecretApplier.ANNOTATION_ENV_NAME; +import static org.eclipse.che.workspace.infrastructure.kubernetes.provision.secret.FileSecretApplier.ANNOTATION_MOUNT_PATH; +import static org.eclipse.che.workspace.infrastructure.kubernetes.provision.secret.KubernetesSecretApplier.ANNOTATION_AUTOMOUNT; +import static org.eclipse.che.workspace.infrastructure.kubernetes.provision.secret.SecretAsContainerResourceProvisioner.ANNOTATION_MOUNT_AS; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableMap; @@ -36,6 +44,7 @@ public class SecretAsContainerResourceProvisionerTest { @Mock EnvironmentVariableSecretApplier environmentVariableSecretApplier; + @Mock GitCredentialStorageFileSecretApplier gitCredentialStorageFileSecretApplier; @Mock FileSecretApplier fileSecretApplier; private SecretAsContainerResourceProvisioner provisioner; @@ -53,7 +62,10 @@ public void setUp() throws Exception { when(namespace.secrets()).thenReturn(secrets); provisioner = new SecretAsContainerResourceProvisioner<>( - fileSecretApplier, environmentVariableSecretApplier, new String[] {"app:che"}); + fileSecretApplier, + environmentVariableSecretApplier, + gitCredentialStorageFileSecretApplier, + new String[] {"app:che"}); } @Test( @@ -61,6 +73,7 @@ public void setUp() throws Exception { expectedExceptionsMessageRegExp = "Unable to mount secret 'test_secret': it has missing or unknown type of the mount. Please make sure that 'che.eclipse.org/mount-as' annotation has value either 'env' or 'file'.") public void shouldThrowExceptionWhenNoMountTypeSpecified() throws Exception { + // given Secret secret = new SecretBuilder() .withData(ImmutableMap.of("settings.xml", "random", "another.xml", "freedom")) @@ -72,6 +85,101 @@ public void shouldThrowExceptionWhenNoMountTypeSpecified() throws Exception { .build()) .build(); when(secrets.get(any(LabelSelector.class))).thenReturn(singletonList(secret)); + // when provisioner.provision(environment, runtimeIdentity, namespace); } + + @Test + public void shouldCallEnvironmentVariableSecretApplier() throws InfrastructureException { + // given + Secret secret = + new SecretBuilder() + .withData(singletonMap("foo", "random")) + .withMetadata( + new ObjectMetaBuilder() + .withName("test_secret") + .withAnnotations( + ImmutableMap.of( + ANNOTATION_ENV_NAME, + "MY_FOO", + ANNOTATION_MOUNT_AS, + "env", + ANNOTATION_AUTOMOUNT, + "true")) + .withLabels(emptyMap()) + .build()) + .build(); + + when(secrets.get(any(LabelSelector.class))).thenReturn(singletonList(secret)); + // when + provisioner.provision(environment, runtimeIdentity, namespace); + // then + verify(environmentVariableSecretApplier) + .applySecret(eq(environment), eq(runtimeIdentity), eq(secret)); + verifyZeroInteractions(fileSecretApplier); + verifyZeroInteractions(gitCredentialStorageFileSecretApplier); + } + + @Test + public void shouldCallFileSecretApplier() throws InfrastructureException { + // given + Secret secret = + new SecretBuilder() + .withData(ImmutableMap.of("settings.xml", "random", "another.xml", "freedom")) + .withMetadata( + new ObjectMetaBuilder() + .withName("test_secret") + .withAnnotations( + ImmutableMap.of( + ANNOTATION_MOUNT_AS, + "file", + ANNOTATION_MOUNT_PATH, + "/home/user/.m2", + ANNOTATION_AUTOMOUNT, + "true")) + .withLabels(emptyMap()) + .build()) + .build(); + + when(secrets.get(any(LabelSelector.class))).thenReturn(singletonList(secret)); + // when + provisioner.provision(environment, runtimeIdentity, namespace); + // then + verify(fileSecretApplier).applySecret(eq(environment), eq(runtimeIdentity), eq(secret)); + verifyZeroInteractions(environmentVariableSecretApplier); + verifyZeroInteractions(gitCredentialStorageFileSecretApplier); + } + + @Test + public void shouldCallGitCredentialStorageFileSecretApplier() throws InfrastructureException { + // given + Secret secret = + new SecretBuilder() + .withData(ImmutableMap.of("credentials", "random")) + .withMetadata( + new ObjectMetaBuilder() + .withName("test_secret") + .withAnnotations( + ImmutableMap.of( + ANNOTATION_MOUNT_AS, + "file", + ANNOTATION_MOUNT_PATH, + "/home/user/.git", + GitCredentialStorageFileSecretApplier.ANNOTATION_GIT_CREDENTIALS, + "true", + ANNOTATION_AUTOMOUNT, + "true")) + .withLabels(emptyMap()) + .build()) + .build(); + + when(secrets.get(any(LabelSelector.class))).thenReturn(singletonList(secret)); + // when + provisioner.provision(environment, runtimeIdentity, namespace); + // then + verify(gitCredentialStorageFileSecretApplier) + .applySecret(eq(environment), eq(runtimeIdentity), eq(secret)); + verifyZeroInteractions(environmentVariableSecretApplier); + verifyZeroInteractions(fileSecretApplier); + } }