From 6b4bd00c7f1b4a71225c498b56d9364aaf497790 Mon Sep 17 00:00:00 2001 From: Vlad Zhukovskyi Date: Tue, 3 Sep 2019 19:18:18 +0300 Subject: [PATCH] Provision git configuration into each container Signed-off-by: Vlad Zhukovskyi --- infrastructures/kubernetes/pom.xml | 4 + .../KubernetesEnvironmentProvisioner.java | 7 +- .../provision/GitUserProfileProvisioner.java | 159 ++++++++++++++++++ .../KubernetesEnvironmentProvisionerTest.java | 9 +- .../GitUserProfileProvisionerTest.java | 131 +++++++++++++++ 5 files changed, 307 insertions(+), 3 deletions(-) create mode 100644 infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/GitUserProfileProvisioner.java create mode 100644 infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/GitUserProfileProvisionerTest.java diff --git a/infrastructures/kubernetes/pom.xml b/infrastructures/kubernetes/pom.xml index 095e79f0a0a..32d0c49fb88 100644 --- a/infrastructures/kubernetes/pom.xml +++ b/infrastructures/kubernetes/pom.xml @@ -127,6 +127,10 @@ org.eclipse.che.core che-core-api-system + + org.eclipse.che.core + che-core-api-user + org.eclipse.che.core che-core-api-workspace diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesEnvironmentProvisioner.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesEnvironmentProvisioner.java index bfe7f4be8e4..df94c6f39b3 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesEnvironmentProvisioner.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesEnvironmentProvisioner.java @@ -21,6 +21,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.WorkspaceVolumesStrategy; import org.eclipse.che.workspace.infrastructure.kubernetes.provision.CertificateProvisioner; +import org.eclipse.che.workspace.infrastructure.kubernetes.provision.GitUserProfileProvisioner; import org.eclipse.che.workspace.infrastructure.kubernetes.provision.ImagePullSecretProvisioner; import org.eclipse.che.workspace.infrastructure.kubernetes.provision.IngressTlsProvisioner; import org.eclipse.che.workspace.infrastructure.kubernetes.provision.InstallerServersPortProvisioner; @@ -73,6 +74,7 @@ class KubernetesEnvironmentProvisionerImpl private final ServiceAccountProvisioner serviceAccountProvisioner; private final CertificateProvisioner certificateProvisioner; private final VcsSshKeysProvisioner vcsSshKeysProvisioner; + private final GitUserProfileProvisioner gitUserProfileProvisioner; @Inject public KubernetesEnvironmentProvisionerImpl( @@ -92,7 +94,8 @@ public KubernetesEnvironmentProvisionerImpl( ProxySettingsProvisioner proxySettingsProvisioner, ServiceAccountProvisioner serviceAccountProvisioner, CertificateProvisioner certificateProvisioner, - VcsSshKeysProvisioner vcsSshKeysProvisioner) { + VcsSshKeysProvisioner vcsSshKeysProvisioner, + GitUserProfileProvisioner gitUserProfileProvisioner) { this.pvcEnabled = pvcEnabled; this.volumesStrategy = volumesStrategy; this.uniqueNamesProvisioner = uniqueNamesProvisioner; @@ -110,6 +113,7 @@ public KubernetesEnvironmentProvisionerImpl( this.serviceAccountProvisioner = serviceAccountProvisioner; this.certificateProvisioner = certificateProvisioner; this.vcsSshKeysProvisioner = vcsSshKeysProvisioner; + this.gitUserProfileProvisioner = gitUserProfileProvisioner; } @Traced @@ -149,6 +153,7 @@ public void provision(KubernetesEnvironment k8sEnv, RuntimeIdentity identity) serviceAccountProvisioner.provision(k8sEnv, identity); certificateProvisioner.provision(k8sEnv, identity); vcsSshKeysProvisioner.provision(k8sEnv, identity); + gitUserProfileProvisioner.provision(k8sEnv, identity); LOG.debug("Provisioning Kubernetes environment done for workspace '{}'", workspaceId); } } diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/GitUserProfileProvisioner.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/GitUserProfileProvisioner.java new file mode 100644 index 00000000000..90a31d287f6 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/GitUserProfileProvisioner.java @@ -0,0 +1,159 @@ +/* + * 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; + +import static com.google.common.base.Strings.isNullOrEmpty; +import static java.util.Collections.singletonMap; +import static java.util.Optional.empty; +import static java.util.Optional.of; +import static java.util.Optional.ofNullable; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.ConfigMapVolumeSourceBuilder; +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.PodSpec; +import io.fabric8.kubernetes.api.model.VolumeBuilder; +import io.fabric8.kubernetes.api.model.VolumeMount; +import io.fabric8.kubernetes.api.model.VolumeMountBuilder; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javax.inject.Inject; +import javax.inject.Singleton; +import org.eclipse.che.api.core.ServerException; +import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; +import org.eclipse.che.api.user.server.PreferenceManager; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.commons.env.EnvironmentContext; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; + +@Singleton +public class GitUserProfileProvisioner implements ConfigurationProvisioner { + + private final String GIT_CONFIG_MAP_NAME_SUFFIX = "-gitconfig"; + + private static final String GIT_BASE_CONFIG_PATH = "/etc/"; + private 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"; + private static final String GIT_USER_EMAIL_PROPERTY = "git.user.email"; + private static final String CONFIG_MAP_VOLUME_NAME = "gitconfigvolume"; + + private PreferenceManager preferenceManager; + + @Inject + public GitUserProfileProvisioner(PreferenceManager preferenceManager) { + this.preferenceManager = preferenceManager; + } + + @Override + public void provision(KubernetesEnvironment k8sEnv, RuntimeIdentity identity) + throws InfrastructureException { + getPreferenceValue(PREFERENCES_KEY_FILTER) + .ifPresent( + preferenceJsonValue -> { + Map theiaPreferences = getMapFromJsonObject(preferenceJsonValue); + + getGlobalGitConfigFileContent( + theiaPreferences.get(GIT_USER_NAME_PROPERTY), + theiaPreferences.get(GIT_USER_EMAIL_PROPERTY)) + .ifPresent( + gitConfigFileContent -> { + String gitConfigMapName = + identity.getWorkspaceId() + GIT_CONFIG_MAP_NAME_SUFFIX; + + doProvisionGlobalGitConfig(gitConfigMapName, gitConfigFileContent, k8sEnv); + }); + }); + } + + private Map getMapFromJsonObject(String json) { + Type stringMapType = new TypeToken>() {}.getType(); + + return new Gson().fromJson(json, stringMapType); + } + + private Optional getPreferenceValue(String keyFilter) throws InfrastructureException { + try { + String userId = EnvironmentContext.getCurrent().getSubject().getUserId(); + Map preferencesMap = preferenceManager.find(userId, keyFilter); + + return ofNullable(preferencesMap.get(keyFilter)); + } catch (ServerException e) { + throw new InfrastructureException(e); + } + } + + private Optional getGlobalGitConfigFileContent(String userName, String userEmail) { + if (isNullOrEmpty(userName) && isNullOrEmpty(userEmail)) { + return empty(); + } + + StringBuilder config = new StringBuilder(); + config.append("[user]").append('\n'); + + if (userName != null) { + config.append('\t').append("name = ").append(userName).append('\n'); + } + + if (userEmail != null) { + config.append('\t').append("email = ").append(userEmail).append('\n'); + } + + return of(config.toString()); + } + + private void doProvisionGlobalGitConfig( + String gitConfigMapName, String gitConfig, KubernetesEnvironment k8sEnv) { + Map gitConfigData = singletonMap(GIT_CONFIG, gitConfig); + ConfigMap configMap = + new ConfigMapBuilder() + .withNewMetadata() + .withName(gitConfigMapName) + .endMetadata() + .withData(gitConfigData) + .build(); + + k8sEnv.getConfigMaps().put(configMap.getMetadata().getName(), configMap); + k8sEnv.getPodsData().values().forEach(p -> mountConfigFile(p.getSpec(), gitConfigMapName)); + } + + private void mountConfigFile(PodSpec podSpec, String gitConfigMapName) { + podSpec + .getVolumes() + .add( + new VolumeBuilder() + .withName(CONFIG_MAP_VOLUME_NAME) + .withConfigMap( + new ConfigMapVolumeSourceBuilder().withName(gitConfigMapName).build()) + .build()); + + List containers = podSpec.getContainers(); + containers.forEach( + container -> { + VolumeMount volumeMount = + new VolumeMountBuilder() + .withName(CONFIG_MAP_VOLUME_NAME) + .withMountPath(GIT_CONFIG_PATH) + .withSubPath(GIT_CONFIG) + .withReadOnly(false) + .withNewReadOnly(false) + .build(); + container.getVolumeMounts().add(volumeMount); + }); + } +} diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesEnvironmentProvisionerTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesEnvironmentProvisionerTest.java index c957e215a2f..8b1a7c2778d 100644 --- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesEnvironmentProvisionerTest.java +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesEnvironmentProvisionerTest.java @@ -19,6 +19,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.WorkspaceVolumesStrategy; import org.eclipse.che.workspace.infrastructure.kubernetes.provision.CertificateProvisioner; +import org.eclipse.che.workspace.infrastructure.kubernetes.provision.GitUserProfileProvisioner; import org.eclipse.che.workspace.infrastructure.kubernetes.provision.ImagePullSecretProvisioner; import org.eclipse.che.workspace.infrastructure.kubernetes.provision.IngressTlsProvisioner; import org.eclipse.che.workspace.infrastructure.kubernetes.provision.InstallerServersPortProvisioner; @@ -66,6 +67,7 @@ public class KubernetesEnvironmentProvisionerTest { @Mock private ServiceAccountProvisioner serviceAccountProvisioner; @Mock private CertificateProvisioner certificateProvisioner; @Mock private VcsSshKeysProvisioner vcsSshKeysProvisioner; + @Mock private GitUserProfileProvisioner gitUserProfileProvisioner; private KubernetesEnvironmentProvisioner k8sInfraProvisioner; @@ -91,7 +93,8 @@ public void setUp() { proxySettingsProvisioner, serviceAccountProvisioner, certificateProvisioner, - vcsSshKeysProvisioner); + vcsSshKeysProvisioner, + gitUserProfileProvisioner); provisionOrder = inOrder( installerServersPortProvisioner, @@ -108,7 +111,8 @@ public void setUp() { imagePullSecretProvisioner, proxySettingsProvisioner, serviceAccountProvisioner, - certificateProvisioner); + certificateProvisioner, + gitUserProfileProvisioner); } @Test @@ -136,6 +140,7 @@ public void performsOrderedProvisioning() throws Exception { provisionOrder.verify(proxySettingsProvisioner).provision(eq(k8sEnv), eq(runtimeIdentity)); provisionOrder.verify(serviceAccountProvisioner).provision(eq(k8sEnv), eq(runtimeIdentity)); provisionOrder.verify(certificateProvisioner).provision(eq(k8sEnv), eq(runtimeIdentity)); + provisionOrder.verify(gitUserProfileProvisioner).provision(eq(k8sEnv), eq(runtimeIdentity)); provisionOrder.verifyNoMoreInteractions(); } } diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/GitUserProfileProvisionerTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/GitUserProfileProvisionerTest.java new file mode 100644 index 00000000000..c8805d3e128 --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/GitUserProfileProvisionerTest.java @@ -0,0 +1,131 @@ +/* + * 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; + +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +import io.fabric8.kubernetes.api.model.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; +import org.eclipse.che.api.user.server.PreferenceManager; +import org.eclipse.che.commons.env.EnvironmentContext; +import org.eclipse.che.commons.subject.Subject; +import org.eclipse.che.commons.subject.SubjectImpl; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; +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 GitUserProfileProvisionerTest { + + private KubernetesEnvironment k8sEnv; + + @Mock private RuntimeIdentity runtimeIdentity; + + @Mock private Pod pod; + + @Mock private PodSpec podSpec; + + @Mock private Container container; + + @Mock private PreferenceManager preferenceManager; + + private GitUserProfileProvisioner gitUserProfileProvisioner; + + @BeforeMethod + public void setup() { + k8sEnv = KubernetesEnvironment.builder().build(); + ObjectMeta podMeta = new ObjectMetaBuilder().withName("wksp").build(); + when(pod.getMetadata()).thenReturn(podMeta); + when(pod.getSpec()).thenReturn(podSpec); + k8sEnv.addPod(pod); + gitUserProfileProvisioner = new GitUserProfileProvisioner(preferenceManager); + + Subject subject = new SubjectImpl(null, "id", null, false); + EnvironmentContext environmentContext = new EnvironmentContext(); + environmentContext.setSubject(subject); + EnvironmentContext.setCurrent(environmentContext); + } + + @Test + public void testShouldDoNothingWhenGitUserNameAndEmailIsNotConfigured() throws Exception { + when(preferenceManager.find(eq("id"), eq("theia-user-preferences"))).thenReturn(emptyMap()); + gitUserProfileProvisioner.provision(k8sEnv, runtimeIdentity); + + verifyZeroInteractions(runtimeIdentity); + } + + @Test + public void testShouldDoNothingWhenGitPreferencesAreEmpty() throws Exception { + Map preferences = singletonMap("theia-user-preferences", "{}"); + when(preferenceManager.find(eq("id"), eq("theia-user-preferences"))).thenReturn(preferences); + + gitUserProfileProvisioner.provision(k8sEnv, runtimeIdentity); + + verifyZeroInteractions(runtimeIdentity); + } + + @Test + public void testShouldCheckIfPodHasMountAndK8HasConfigMapForGitConfig() throws Exception { + String json = "{\"git.user.name\":\"user\",\"git.user.email\":\"email\"}"; + Map preferences = singletonMap("theia-user-preferences", json); + when(preferenceManager.find(eq("id"), eq("theia-user-preferences"))).thenReturn(preferences); + when(runtimeIdentity.getWorkspaceId()).thenReturn("wksp"); + + ObjectMeta podMeta = new ObjectMetaBuilder().withName("wksp").build(); + when(pod.getMetadata()).thenReturn(podMeta); + when(pod.getSpec()).thenReturn(podSpec); + when(podSpec.getContainers()).thenReturn(singletonList(container)); + + List volumeMounts = new ArrayList<>(); + + when(container.getVolumeMounts()).thenReturn(volumeMounts); + k8sEnv.addPod(pod); + + gitUserProfileProvisioner.provision(k8sEnv, runtimeIdentity); + + assertEquals(volumeMounts.size(), 1); + + VolumeMount mount = volumeMounts.get(0); + + assertEquals(mount.getMountPath(), "/etc/gitconfig"); + assertEquals(mount.getName(), "gitconfigvolume"); + assertFalse(mount.getReadOnly()); + assertEquals(mount.getSubPath(), "gitconfig"); + + assertEquals(k8sEnv.getConfigMaps().size(), 1); + assertTrue(k8sEnv.getConfigMaps().containsKey("wksp-gitconfig")); + + ConfigMap configMap = k8sEnv.getConfigMaps().get("wksp-gitconfig"); + + assertEquals(configMap.getData().size(), 1); + assertTrue(configMap.getData().containsKey("gitconfig")); + + String gitconfig = configMap.getData().get("gitconfig"); + String expectedGitconfig = "[user]\n\tname = user\n\temail = email\n"; + + assertEquals(gitconfig, expectedGitconfig); + } +}