diff --git a/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/devfile/Component.java b/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/devfile/Component.java index e354a8750b3..ac5c90bfee2 100644 --- a/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/devfile/Component.java +++ b/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/devfile/Component.java @@ -149,4 +149,7 @@ public interface Component { * type. */ List getEndpoints(); + + /** Indicates whether namespace secrets should be mount into containers of this component. */ + Boolean getAutomountWorkspaceSecrets(); } diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInternalRuntime.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInternalRuntime.java index 83a771e836d..a4d9faf3073 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInternalRuntime.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInternalRuntime.java @@ -92,7 +92,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.log.PodLogToEventPublisher; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.WorkspaceVolumesStrategy; import org.eclipse.che.workspace.infrastructure.kubernetes.provision.PreviewUrlCommandProvisioner; -import org.eclipse.che.workspace.infrastructure.kubernetes.provision.SecretAsContainerResourceProvisioner; +import org.eclipse.che.workspace.infrastructure.kubernetes.provision.secret.SecretAsContainerResourceProvisioner; import org.eclipse.che.workspace.infrastructure.kubernetes.server.KubernetesServerResolver; import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.IngressPathTransformInverter; import org.eclipse.che.workspace.infrastructure.kubernetes.util.KubernetesSharedPool; @@ -213,7 +213,8 @@ protected void internalStart(Map startOptions) throws Infrastruc // from previous provisioners into infrastructure specific objects kubernetesEnvironmentProvisioner.provision(context.getEnvironment(), context.getIdentity()); - secretAsContainerResourceProvisioner.provision(context.getEnvironment(), namespace); + secretAsContainerResourceProvisioner.provision( + context.getEnvironment(), context.getIdentity(), namespace); LOG.debug("Provisioning of workspace '{}' completed.", workspaceId); volumesStrategy.prepare( diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/SecretAsContainerResourceProvisioner.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/SecretAsContainerResourceProvisioner.java deleted file mode 100644 index 53e12b4533d..00000000000 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/SecretAsContainerResourceProvisioner.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - * 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.MoreObjects.firstNonNull; -import static java.lang.String.format; -import static java.util.stream.Collectors.toMap; - -import com.google.common.annotations.Beta; -import io.fabric8.kubernetes.api.model.Container; -import io.fabric8.kubernetes.api.model.EnvVarBuilder; -import io.fabric8.kubernetes.api.model.EnvVarSourceBuilder; -import io.fabric8.kubernetes.api.model.LabelSelector; -import io.fabric8.kubernetes.api.model.LabelSelectorBuilder; -import io.fabric8.kubernetes.api.model.Secret; -import io.fabric8.kubernetes.api.model.SecretKeySelectorBuilder; -import io.fabric8.kubernetes.api.model.SecretVolumeSourceBuilder; -import io.fabric8.kubernetes.api.model.Volume; -import io.fabric8.kubernetes.api.model.VolumeBuilder; -import io.fabric8.kubernetes.api.model.VolumeMountBuilder; -import java.util.Arrays; -import java.util.Map; -import java.util.Map.Entry; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; -import org.eclipse.che.api.workspace.server.spi.InfrastructureException; -import org.eclipse.che.commons.lang.NameGenerator; -import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; -import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment.PodData; -import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment.PodRole; -import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespace; - -/** - * Finds secrets with specific labels in namespace, and mount their values as file or environment - * variable into all (or specified by "org.eclipse.che/target-container" annotation) workspace - * containers. Secrets with annotation "org.eclipse.che/mount-as=env" are mount as env variables, - * env name is read from "org.eclipse.che/env-name" annotation. Secrets which don't have - * "org.eclipse.che/mount-as=env" or having "org.eclipse.che/mount-as=file" are mounted as file in - * the folder specified by "org.eclipse.che/mount-path" annotation. Refer to che-docs for concrete - * examples. - */ -@Beta -@Singleton -public class SecretAsContainerResourceProvisioner { - - private static final String ANNOTATION_PREFIX = "che.eclipse.org"; - static final String ANNOTATION_MOUNT_AS = ANNOTATION_PREFIX + "/" + "mount-as"; - static final String ANNOTATION_TARGET_CONTAINER = ANNOTATION_PREFIX + "/" + "target-container"; - static final String ANNOTATION_ENV_NAME = ANNOTATION_PREFIX + "/" + "env-name"; - static final String ANNOTATION_ENV_NAME_TEMPLATE = ANNOTATION_PREFIX + "/%s_" + "env-name"; - static final String ANNOTATION_MOUNT_PATH = ANNOTATION_PREFIX + "/" + "mount-path"; - - private final Map secretLabels; - - @Inject - public SecretAsContainerResourceProvisioner( - @Named("che.workspace.provision.secret.labels") String[] labels) { - this.secretLabels = - Arrays.stream(labels) - .map(item -> item.split("=", 2)) - .collect(toMap(p -> p[0], p -> p.length == 1 ? "" : p[1])); - } - - public void provision(E env, KubernetesNamespace namespace) throws InfrastructureException { - LabelSelector selector = new LabelSelectorBuilder().withMatchLabels(secretLabels).build(); - for (Secret secret : namespace.secrets().get(selector)) { - String targetContainerName = - secret.getMetadata().getAnnotations().get(ANNOTATION_TARGET_CONTAINER); - String mountType = secret.getMetadata().getAnnotations().get(ANNOTATION_MOUNT_AS); - if ("env".equalsIgnoreCase(mountType)) { - mountAsEnv(env, secret, targetContainerName); - } else if ("file".equalsIgnoreCase(mountType)) { - mountAsFile(env, secret, targetContainerName); - } else { - throw new InfrastructureException( - format( - "Unable to mount secret '%s': it has missing or unknown type of the mount. Please make sure that '%s' annotation has value either 'env' or 'file'.", - secret.getMetadata().getName(), ANNOTATION_MOUNT_AS)); - } - } - } - - private void mountAsEnv(E env, Secret secret, String targetContainerName) - throws InfrastructureException { - for (PodData podData : env.getPodsData().values()) { - if (!podData.getRole().equals(PodRole.DEPLOYMENT)) { - continue; - } - for (Container container : podData.getSpec().getContainers()) { - if (targetContainerName != null && !container.getName().equals(targetContainerName)) { - continue; - } - for (Entry secretDataEntry : secret.getData().entrySet()) { - final String mountEnvName = envName(secret, secretDataEntry.getKey()); - container - .getEnv() - .add( - new EnvVarBuilder() - .withName(mountEnvName) - .withValueFrom( - new EnvVarSourceBuilder() - .withSecretKeyRef( - new SecretKeySelectorBuilder() - .withName(secret.getMetadata().getName()) - .withKey(secretDataEntry.getKey()) - .build()) - .build()) - .build()); - } - } - } - } - - private void mountAsFile(E env, Secret secret, String targetContainerName) - throws InfrastructureException { - final String mountPath = secret.getMetadata().getAnnotations().get(ANNOTATION_MOUNT_PATH); - if (mountPath == null) { - throw new InfrastructureException( - format( - "Unable to mount secret '%s': It is configured to be mounted as a file but the mount path was not specified. Please define the '%s' annotation on the secret to specify it.", - secret.getMetadata().getName(), ANNOTATION_MOUNT_PATH)); - } - - Volume volumeFromSecret = - new VolumeBuilder() - .withName(secret.getMetadata().getName()) - .withSecret( - new SecretVolumeSourceBuilder() - .withNewSecretName(secret.getMetadata().getName()) - .build()) - .build(); - - for (PodData podData : env.getPodsData().values()) { - if (!podData.getRole().equals(PodRole.DEPLOYMENT)) { - continue; - } - if (podData - .getSpec() - .getVolumes() - .stream() - .anyMatch(v -> v.getName().equals(volumeFromSecret.getName()))) { - volumeFromSecret.setName(volumeFromSecret.getName() + "_" + NameGenerator.generate("", 6)); - } - - podData.getSpec().getVolumes().add(volumeFromSecret); - - for (Container container : podData.getSpec().getContainers()) { - if (targetContainerName != null && !container.getName().equals(targetContainerName)) { - continue; - } - secret - .getData() - .keySet() - .forEach( - secretFile -> - container - .getVolumeMounts() - .add( - new VolumeMountBuilder() - .withName(volumeFromSecret.getName()) - .withMountPath(mountPath + "/" + secretFile) - .withSubPath(secretFile) - .withReadOnly(true) - .build())); - } - } - } - - private String envName(Secret secret, String key) throws InfrastructureException { - String mountEnvName; - if (secret.getData().size() == 1) { - try { - mountEnvName = - firstNonNull( - secret.getMetadata().getAnnotations().get(ANNOTATION_ENV_NAME), - secret - .getMetadata() - .getAnnotations() - .get(format(ANNOTATION_ENV_NAME_TEMPLATE, key))); - } catch (NullPointerException e) { - throw new InfrastructureException( - format( - "Unable to mount secret '%s': It is configured to be mount as a environment variable, but its was not specified. Please define the '%s' annotation on the secret to specify it.", - secret.getMetadata().getName(), ANNOTATION_ENV_NAME)); - } - } else { - mountEnvName = - secret.getMetadata().getAnnotations().get(format(ANNOTATION_ENV_NAME_TEMPLATE, key)); - if (mountEnvName == null) { - throw new InfrastructureException( - format( - "Unable to mount key '%s' of secret '%s': It is configured to be mount as a environment variable, but its was not specified. Please define the '%s' annotation on the secret to specify it.", - key, secret.getMetadata().getName(), format(ANNOTATION_ENV_NAME_TEMPLATE, key))); - } - } - return mountEnvName; - } -} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/EnvironmentVariableSecretApplier.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/EnvironmentVariableSecretApplier.java new file mode 100644 index 00000000000..72f50ef7725 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/EnvironmentVariableSecretApplier.java @@ -0,0 +1,150 @@ +/* + * 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.Container; +import io.fabric8.kubernetes.api.model.EnvVarBuilder; +import io.fabric8.kubernetes.api.model.EnvVarSourceBuilder; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretKeySelectorBuilder; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; +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.model.impl.devfile.ComponentImpl; +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.environment.KubernetesEnvironment.PodData; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment.PodRole; +import org.eclipse.che.workspace.infrastructure.kubernetes.util.RuntimeEventsPublisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Mounts Kubernetes secret with specific annotations as an environment variable(s) in workspace + * containers. Allows per-component control of secret applying in devfile. + */ +@Beta +@Singleton +public class EnvironmentVariableSecretApplier + extends KubernetesSecretApplier { + + @Inject private RuntimeEventsPublisher runtimeEventsPublisher; + + private static final Logger LOG = LoggerFactory.getLogger(EnvironmentVariableSecretApplier.class); + + static final String ANNOTATION_ENV_NAME = ANNOTATION_PREFIX + "/" + "env-name"; + static final String ANNOTATION_ENV_NAME_TEMPLATE = ANNOTATION_PREFIX + "/%s_" + "env-name"; + + /** + * Applies secret as environment variable into workspace containers, respecting automount + * attribute and optional devfile automount property override. + * + * @param env kubernetes environment with workspace containers configuration + * @param runtimeIdentity identity of current runtime + * @param secret source secret to apply + * @throws InfrastructureException on misconfigured secrets or other apply error + */ + @Override + public void applySecret(KubernetesEnvironment env, RuntimeIdentity runtimeIdentity, Secret secret) + throws InfrastructureException { + boolean secretAutomount = + Boolean.parseBoolean(secret.getMetadata().getAnnotations().get(ANNOTATION_AUTOMOUNT)); + for (PodData podData : env.getPodsData().values()) { + if (!podData.getRole().equals(PodRole.DEPLOYMENT)) { + continue; + } + for (Container container : podData.getSpec().getContainers()) { + Optional component = getComponent(env, container.getName()); + // skip components that explicitly disable automount + if (component.isPresent() && isComponentAutomountFalse(component.get())) { + continue; + } + // if automount disabled globally and not overridden in component + if (!secretAutomount + && (!component.isPresent() || !isComponentAutomountTrue(component.get()))) { + continue; + } + for (Entry secretDataEntry : secret.getData().entrySet()) { + final String mountEnvName = envName(secret, secretDataEntry.getKey(), runtimeIdentity); + container + .getEnv() + .add( + new EnvVarBuilder() + .withName(mountEnvName) + .withValueFrom( + new EnvVarSourceBuilder() + .withSecretKeyRef( + new SecretKeySelectorBuilder() + .withName(secret.getMetadata().getName()) + .withKey(secretDataEntry.getKey()) + .build()) + .build()) + .build()); + } + } + } + } + + private String envName(Secret secret, String key, RuntimeIdentity runtimeIdentity) + throws InfrastructureException { + String mountEnvName; + if (secret.getData().size() == 1) { + List providedNames = + Stream.of( + secret.getMetadata().getAnnotations().get(ANNOTATION_ENV_NAME), + secret + .getMetadata() + .getAnnotations() + .get(format(ANNOTATION_ENV_NAME_TEMPLATE, key))) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + if (providedNames.isEmpty()) { + throw new InfrastructureException( + format( + "Unable to mount secret '%s': It is configured to be mount as a environment variable, but its name was not specified. Please define the '%s' annotation on the secret to specify it.", + secret.getMetadata().getName(), ANNOTATION_ENV_NAME)); + } + + if (providedNames.size() > 1) { + String msg = + String.format( + "Secret '%s' defines multiple environment variable name annotations, but contains only one data entry. That may cause inconsistent behavior and needs to be corrected.", + secret.getMetadata().getName()); + LOG.warn(msg); + runtimeEventsPublisher.sendRuntimeLogEvent( + msg, ZonedDateTime.now().toString(), runtimeIdentity); + } + mountEnvName = providedNames.get(0); + } else { + mountEnvName = + secret.getMetadata().getAnnotations().get(format(ANNOTATION_ENV_NAME_TEMPLATE, key)); + if (mountEnvName == null) { + throw new InfrastructureException( + format( + "Unable to mount key '%s' of secret '%s': It is configured to be mount as a environment variable, but its name was not specified. Please define the '%s' annotation on the secret to specify it.", + key, secret.getMetadata().getName(), format(ANNOTATION_ENV_NAME_TEMPLATE, key))); + } + } + return mountEnvName; + } +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/FileSecretApplier.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/FileSecretApplier.java new file mode 100644 index 00000000000..4dbc2ad781c --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/FileSecretApplier.java @@ -0,0 +1,141 @@ +/* + * 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 com.google.common.base.Strings.isNullOrEmpty; +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.Container; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretVolumeSourceBuilder; +import io.fabric8.kubernetes.api.model.Volume; +import io.fabric8.kubernetes.api.model.VolumeBuilder; +import io.fabric8.kubernetes.api.model.VolumeMountBuilder; +import java.nio.file.Paths; +import java.util.Optional; +import javax.inject.Singleton; +import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; +import org.eclipse.che.api.workspace.server.model.impl.devfile.ComponentImpl; +import org.eclipse.che.api.workspace.server.model.impl.devfile.VolumeImpl; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.commons.lang.NameGenerator; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment.PodData; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment.PodRole; + +/** + * Mounts Kubernetes secret with specific annotations as an file in workspace containers. Via + * devfile, allows per-component control of secret applying and path overrides using specific + * property. + */ +@Beta +@Singleton +public class FileSecretApplier extends KubernetesSecretApplier { + + static final String ANNOTATION_MOUNT_PATH = ANNOTATION_PREFIX + "/" + "mount-path"; + + /** + * Applies secret as file into workspace containers, respecting automount attribute and optional + * devfile automount property and/or mount path override. + * + * @param env kubernetes environment with workspace containers configuration + * @param runtimeIdentity identity of current runtime + * @param secret source secret to apply + * @throws InfrastructureException on misconfigured secrets or other apply error + */ + @Override + public void applySecret(KubernetesEnvironment env, RuntimeIdentity runtimeIdentity, Secret secret) + throws InfrastructureException { + final String secretMountPath = secret.getMetadata().getAnnotations().get(ANNOTATION_MOUNT_PATH); + boolean secretAutomount = + Boolean.parseBoolean(secret.getMetadata().getAnnotations().get(ANNOTATION_AUTOMOUNT)); + if (secretMountPath == null) { + throw new InfrastructureException( + format( + "Unable to mount secret '%s': It is configured to be mounted as a file but the mount path was not specified. Please define the '%s' annotation on the secret to specify it.", + secret.getMetadata().getName(), ANNOTATION_MOUNT_PATH)); + } + + Volume volumeFromSecret = + new VolumeBuilder() + .withName(secret.getMetadata().getName()) + .withSecret( + new SecretVolumeSourceBuilder() + .withNewSecretName(secret.getMetadata().getName()) + .build()) + .build(); + + for (PodData podData : env.getPodsData().values()) { + if (!podData.getRole().equals(PodRole.DEPLOYMENT)) { + continue; + } + if (podData + .getSpec() + .getVolumes() + .stream() + .anyMatch(v -> v.getName().equals(volumeFromSecret.getName()))) { + volumeFromSecret.setName(volumeFromSecret.getName() + "_" + NameGenerator.generate("", 6)); + } + + podData.getSpec().getVolumes().add(volumeFromSecret); + + for (Container container : podData.getSpec().getContainers()) { + Optional component = getComponent(env, container.getName()); + // skip components that explicitly disable automount + if (component.isPresent() && isComponentAutomountFalse(component.get())) { + continue; + } + // if automount disabled globally and not overridden in component + if (!secretAutomount + && (!component.isPresent() || !isComponentAutomountTrue(component.get()))) { + continue; + } + // find path override if any + Optional overridePathOptional = Optional.empty(); + if (component.isPresent()) { + overridePathOptional = + getOverridenComponentPath(component.get(), secret.getMetadata().getName()); + } + final String componentMountPath = overridePathOptional.orElse(secretMountPath); + container + .getVolumeMounts() + .removeIf(vm -> Paths.get(vm.getMountPath()).equals(Paths.get(componentMountPath))); + + secret + .getData() + .keySet() + .forEach( + secretFile -> + container + .getVolumeMounts() + .add( + new VolumeMountBuilder() + .withName(volumeFromSecret.getName()) + .withMountPath(componentMountPath + "/" + secretFile) + .withSubPath(secretFile) + .withReadOnly(true) + .build())); + } + } + } + + private Optional getOverridenComponentPath(ComponentImpl component, String secretName) { + Optional matchedVolume = + component.getVolumes().stream().filter(v -> v.getName().equals(secretName)).findFirst(); + if (matchedVolume.isPresent() && !isNullOrEmpty(matchedVolume.get().getContainerPath())) { + return Optional.of(matchedVolume.get().getContainerPath()); + } + return Optional.empty(); + } +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/KubernetesSecretApplier.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/KubernetesSecretApplier.java new file mode 100644 index 00000000000..7e341fb7e1c --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/KubernetesSecretApplier.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 org.eclipse.che.api.core.model.workspace.config.MachineConfig.DEVFILE_COMPONENT_ALIAS_ATTRIBUTE; +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.Secret; +import java.util.Optional; +import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; +import org.eclipse.che.api.workspace.server.model.impl.devfile.ComponentImpl; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.api.workspace.server.spi.environment.InternalMachineConfig; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; + +/** + * Base class for secret appliers. Contains common functionality to find devfile components by name + * and check override automount properties. + */ +@Beta +public abstract class KubernetesSecretApplier { + + static final String ANNOTATION_AUTOMOUNT = ANNOTATION_PREFIX + "/" + "automount-workspace-secret"; + + /** + * Applies particular secret to workspace containers. + * + * @param env environment to retrieve components from + * @param runtimeIdentity identity of current runtime + * @param secret secret to apply + * @throws InfrastructureException when secret applying error + */ + public abstract void applySecret(E env, RuntimeIdentity runtimeIdentity, Secret secret) + throws InfrastructureException; + + /** + * Tries to retrieve devfile component by given container name. + * + * @param env kubernetes environment of the workspace + * @param containerName name of container to find it's parent component + * @return matched component + */ + final Optional getComponent(E env, String containerName) { + InternalMachineConfig internalMachineConfig = env.getMachines().get(containerName); + if (internalMachineConfig != null) { + String componentName = + internalMachineConfig.getAttributes().get(DEVFILE_COMPONENT_ALIAS_ATTRIBUTE); + if (componentName != null) { + return env.getDevfile() + .getComponents() + .stream() + .filter(c -> c.getAlias().equals(componentName)) + .findFirst(); + } + } + return Optional.empty(); + } + + /** + * @param component source component + * @return {@code true} when {@code automountWorkspaceSecret} property explicitly set to {@code + * false},or {@code false} otherwise. + */ + final boolean isComponentAutomountFalse(ComponentImpl component) { + return component.getAutomountWorkspaceSecrets() != null + && !component.getAutomountWorkspaceSecrets(); + } + + /** + * @param component source component + * @return {@code true} when {@code automountWorkspaceSecret} property explicitly set to {@code + * true},or {@code false} otherwise. + */ + final boolean isComponentAutomountTrue(ComponentImpl component) { + return component.getAutomountWorkspaceSecrets() != null + && component.getAutomountWorkspaceSecrets(); + } +} 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 new file mode 100644 index 00000000000..95af2c87685 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/SecretAsContainerResourceProvisioner.java @@ -0,0 +1,85 @@ +/* + * 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 java.util.stream.Collectors.toMap; + +import com.google.common.annotations.Beta; +import io.fabric8.kubernetes.api.model.LabelSelector; +import io.fabric8.kubernetes.api.model.LabelSelectorBuilder; +import io.fabric8.kubernetes.api.model.Secret; +import java.util.Arrays; +import java.util.Map; +import javax.inject.Inject; +import javax.inject.Named; +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.KubernetesNamespace; + +/** + * Finds secrets with specific labels in namespace, and mount their values as file or environment + * variable into workspace containers. Secrets annotated with "che.eclipse.org/mount-as=env" are + * mount as env variables, env name is read from "che.eclipse.org/env-name" annotation. Secrets + * which having "che.eclipse.org/mount-as=file" are mounted as file in the folder specified by + * "che.eclipse.org/mount-path" annotation. Refer to che docs for concrete examples. + */ +@Beta +@Singleton +public class SecretAsContainerResourceProvisioner { + + static final String ANNOTATION_PREFIX = "che.eclipse.org"; + static final String ANNOTATION_MOUNT_AS = ANNOTATION_PREFIX + "/" + "mount-as"; + private final FileSecretApplier fileSecretApplier; + private final EnvironmentVariableSecretApplier environmentVariableSecretApplier; + + private final Map secretLabels; + + @Inject + public SecretAsContainerResourceProvisioner( + FileSecretApplier fileSecretApplier, + EnvironmentVariableSecretApplier environmentVariableSecretApplier, + @Named("che.workspace.provision.secret.labels") String[] labels) { + this.fileSecretApplier = fileSecretApplier; + this.environmentVariableSecretApplier = environmentVariableSecretApplier; + this.secretLabels = + Arrays.stream(labels) + .map(item -> item.split("=", 2)) + .collect(toMap(p -> p[0], p -> p.length == 1 ? "" : p[1])); + } + + public void provision(E env, RuntimeIdentity runtimeIdentity, KubernetesNamespace namespace) + throws InfrastructureException { + LabelSelector selector = new LabelSelectorBuilder().withMatchLabels(secretLabels).build(); + for (Secret secret : namespace.secrets().get(selector)) { + if (secret.getMetadata().getAnnotations() == null) { + throw new InfrastructureException( + format( + "Unable to mount secret '%s': it has missing required annotations. Please check documentation for secret format guide.", + secret.getMetadata().getName())); + } + String mountType = secret.getMetadata().getAnnotations().get(ANNOTATION_MOUNT_AS); + if ("env".equalsIgnoreCase(mountType)) { + environmentVariableSecretApplier.applySecret(env, runtimeIdentity, secret); + } else if ("file".equalsIgnoreCase(mountType)) { + fileSecretApplier.applySecret(env, runtimeIdentity, secret); + } else { + throw new InfrastructureException( + format( + "Unable to mount secret '%s': it has missing or unknown type of the mount. Please make sure that '%s' annotation has value either 'env' or 'file'.", + secret.getMetadata().getName(), ANNOTATION_MOUNT_AS)); + } + } + } +} diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInternalRuntimeTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInternalRuntimeTest.java index 635d506d1fa..02e7d657746 100644 --- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInternalRuntimeTest.java +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInternalRuntimeTest.java @@ -132,7 +132,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.log.PodLogHandler; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.WorkspaceVolumesStrategy; import org.eclipse.che.workspace.infrastructure.kubernetes.provision.KubernetesPreviewUrlCommandProvisioner; -import org.eclipse.che.workspace.infrastructure.kubernetes.provision.SecretAsContainerResourceProvisioner; +import org.eclipse.che.workspace.infrastructure.kubernetes.provision.secret.SecretAsContainerResourceProvisioner; import org.eclipse.che.workspace.infrastructure.kubernetes.server.KubernetesServerResolver; import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.IngressPathTransformInverter; import org.eclipse.che.workspace.infrastructure.kubernetes.util.KubernetesSharedPool; diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/SecretAsContainerResourceProvisionerTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/EnvironmentVariableSecretApplierTest.java similarity index 56% rename from infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/SecretAsContainerResourceProvisionerTest.java rename to infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/EnvironmentVariableSecretApplierTest.java index 220ecf9de34..f971fce0bbe 100644 --- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/SecretAsContainerResourceProvisionerTest.java +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/EnvironmentVariableSecretApplierTest.java @@ -9,25 +9,23 @@ * Contributors: * Red Hat, Inc. - initial API and implementation */ -package org.eclipse.che.workspace.infrastructure.kubernetes.provision; +package org.eclipse.che.workspace.infrastructure.kubernetes.provision.secret; -import static java.lang.String.format; 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.SecretAsContainerResourceProvisioner.ANNOTATION_ENV_NAME; -import static org.eclipse.che.workspace.infrastructure.kubernetes.provision.SecretAsContainerResourceProvisioner.ANNOTATION_ENV_NAME_TEMPLATE; -import static org.eclipse.che.workspace.infrastructure.kubernetes.provision.SecretAsContainerResourceProvisioner.ANNOTATION_MOUNT_AS; -import static org.eclipse.che.workspace.infrastructure.kubernetes.provision.SecretAsContainerResourceProvisioner.ANNOTATION_MOUNT_PATH; -import static org.eclipse.che.workspace.infrastructure.kubernetes.provision.SecretAsContainerResourceProvisioner.ANNOTATION_TARGET_CONTAINER; +import static org.eclipse.che.api.core.model.workspace.config.MachineConfig.DEVFILE_COMPONENT_ALIAS_ATTRIBUTE; +import static org.eclipse.che.workspace.infrastructure.kubernetes.provision.secret.EnvironmentVariableSecretApplier.ANNOTATION_ENV_NAME; +import static org.eclipse.che.workspace.infrastructure.kubernetes.provision.secret.EnvironmentVariableSecretApplier.ANNOTATION_ENV_NAME_TEMPLATE; +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.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; 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 com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -37,16 +35,16 @@ import io.fabric8.kubernetes.api.model.LabelSelector; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.PodSpec; -import io.fabric8.kubernetes.api.model.PodSpecBuilder; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.SecretBuilder; -import io.fabric8.kubernetes.api.model.Volume; -import io.fabric8.kubernetes.api.model.VolumeMount; +import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; +import org.eclipse.che.api.workspace.server.model.impl.devfile.ComponentImpl; +import org.eclipse.che.api.workspace.server.model.impl.devfile.DevfileImpl; import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.api.workspace.server.spi.environment.InternalMachineConfig; import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment.PodData; import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment.PodRole; -import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespace; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesSecrets; import org.mockito.Mock; import org.mockito.testng.MockitoTestNGListener; @@ -55,26 +53,23 @@ import org.testng.annotations.Test; @Listeners(MockitoTestNGListener.class) -public class SecretAsContainerResourceProvisionerTest { - - private SecretAsContainerResourceProvisioner provisioner = - new SecretAsContainerResourceProvisioner<>(new String[] {"app:che"}); +public class EnvironmentVariableSecretApplierTest { @Mock private KubernetesEnvironment environment; - @Mock private KubernetesNamespace namespace; - @Mock private KubernetesSecrets secrets; @Mock private PodData podData; @Mock private PodSpec podSpec; + @Mock private RuntimeIdentity runtimeIdentity; + + EnvironmentVariableSecretApplier secretApplier = new EnvironmentVariableSecretApplier(); + @BeforeMethod public void setUp() throws Exception { - when(namespace.secrets()).thenReturn(secrets); when(environment.getPodsData()).thenReturn(singletonMap("pod1", podData)); - when(podData.getRole()).thenReturn(PodRole.DEPLOYMENT); when(podData.getSpec()).thenReturn(podSpec); } @@ -98,20 +93,16 @@ public void shouldProvisionSingleEnvVariable() throws Exception { "MY_FOO", ANNOTATION_MOUNT_AS, "env", - ANNOTATION_TARGET_CONTAINER, - "maven")) + ANNOTATION_AUTOMOUNT, + "true")) .withLabels(emptyMap()) .build()) .build(); when(secrets.get(any(LabelSelector.class))).thenReturn(singletonList(secret)); - provisioner.provision(environment, namespace); - - // nothing to do with unmatched container - verify(container_unmatch).getName(); - verifyNoMoreInteractions(container_unmatch); + secretApplier.applySecret(environment, runtimeIdentity, secret); - // matched container has env set + // container has env set assertEquals(container_match.getEnv().size(), 1); EnvVar var = container_match.getEnv().get(0); assertEquals(var.getName(), "MY_FOO"); @@ -122,9 +113,8 @@ public void shouldProvisionSingleEnvVariable() throws Exception { @Test public void shouldProvisionMultiEnvVariable() throws Exception { Container container_match = new ContainerBuilder().withName("maven").build(); - Container container_unmatch = spy(new ContainerBuilder().withName("other").build()); - when(podSpec.getContainers()).thenReturn(ImmutableList.of(container_match, container_unmatch)); + when(podSpec.getContainers()).thenReturn(ImmutableList.of(container_match)); Secret secret = new SecretBuilder() @@ -134,26 +124,22 @@ public void shouldProvisionMultiEnvVariable() throws Exception { .withName("test_secret") .withAnnotations( ImmutableMap.of( - format(ANNOTATION_ENV_NAME_TEMPLATE, "foo"), + String.format(ANNOTATION_ENV_NAME_TEMPLATE, "foo"), "MY_FOO", - format(ANNOTATION_ENV_NAME_TEMPLATE, "bar"), + String.format(ANNOTATION_ENV_NAME_TEMPLATE, "bar"), "MY_BAR", ANNOTATION_MOUNT_AS, "env", - ANNOTATION_TARGET_CONTAINER, - "maven")) + ANNOTATION_AUTOMOUNT, + "true")) .withLabels(emptyMap()) .build()) .build(); when(secrets.get(any(LabelSelector.class))).thenReturn(singletonList(secret)); - provisioner.provision(environment, namespace); - - // nothing to do with unmatched container - verify(container_unmatch).getName(); - verifyNoMoreInteractions(container_unmatch); + secretApplier.applySecret(environment, runtimeIdentity, secret); - // matched container has env set + // container has env set assertEquals(container_match.getEnv().size(), 2); EnvVar var = container_match.getEnv().get(0); assertEquals(var.getName(), "MY_FOO"); @@ -167,7 +153,7 @@ public void shouldProvisionMultiEnvVariable() throws Exception { } @Test - public void shouldProvisionAllContainersIfNotSpecifyOne() throws Exception { + public void shouldProvisionAllContainersIfAutomountEnabled() throws Exception { Container container_match1 = new ContainerBuilder().withName("maven").build(); Container container_match2 = new ContainerBuilder().withName("other").build(); @@ -180,13 +166,16 @@ public void shouldProvisionAllContainersIfNotSpecifyOne() throws Exception { new ObjectMetaBuilder() .withName("test_secret") .withAnnotations( - ImmutableMap.of(ANNOTATION_ENV_NAME, "MY_FOO", ANNOTATION_MOUNT_AS, "env")) + 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)); - provisioner.provision(environment, namespace); + secretApplier.applySecret(environment, runtimeIdentity, secret); // both containers has env set assertEquals(container_match1.getEnv().size(), 1); @@ -203,152 +192,126 @@ public void shouldProvisionAllContainersIfNotSpecifyOne() throws Exception { } @Test - public void shouldProvisionAsFiles() throws Exception { - Container container_match = new ContainerBuilder().withName("maven").build(); - Container container_unmatch = new ContainerBuilder().withName("other").build(); - - PodSpec localSpec = - new PodSpecBuilder() - .withContainers(ImmutableList.of(container_match, container_unmatch)) - .build(); + public void shouldProvisionContainersWithAutomountOverrideTrue() throws Exception { + Container container_match1 = new ContainerBuilder().withName("maven").build(); + Container container_match2 = new ContainerBuilder().withName("other").build(); + DevfileImpl mock_defvile = mock(DevfileImpl.class); + ComponentImpl component = new ComponentImpl(); + component.setAlias("maven"); + component.setAutomountWorkspaceSecrets(true); - when(podData.getSpec()).thenReturn(localSpec); + when(podSpec.getContainers()).thenReturn(ImmutableList.of(container_match1, container_match2)); + InternalMachineConfig internalMachineConfig = new InternalMachineConfig(); + internalMachineConfig.getAttributes().put(DEVFILE_COMPONENT_ALIAS_ATTRIBUTE, "maven"); + when(environment.getMachines()).thenReturn(ImmutableMap.of("maven", internalMachineConfig)); + when(environment.getDevfile()).thenReturn(mock_defvile); + when(mock_defvile.getComponents()).thenReturn(singletonList(component)); Secret secret = new SecretBuilder() - .withData(ImmutableMap.of("settings.xml", "random", "another.xml", "freedom")) + .withData(singletonMap("foo", "random")) .withMetadata( new ObjectMetaBuilder() .withName("test_secret") .withAnnotations( ImmutableMap.of( - ANNOTATION_MOUNT_AS, - "file", - ANNOTATION_MOUNT_PATH, - "/home/user/.m2", - ANNOTATION_TARGET_CONTAINER, - "maven")) + ANNOTATION_ENV_NAME, "MY_FOO", + ANNOTATION_MOUNT_AS, "env", + ANNOTATION_AUTOMOUNT, "false")) .withLabels(emptyMap()) .build()) .build(); when(secrets.get(any(LabelSelector.class))).thenReturn(singletonList(secret)); - provisioner.provision(environment, namespace); - - // pod has volume created - assertEquals(environment.getPodsData().get("pod1").getSpec().getVolumes().size(), 1); - Volume volume = environment.getPodsData().get("pod1").getSpec().getVolumes().get(0); - assertEquals(volume.getName(), "test_secret"); - assertEquals(volume.getSecret().getSecretName(), "test_secret"); - - // matched container has mounts set - assertEquals( - environment - .getPodsData() - .get("pod1") - .getSpec() - .getContainers() - .get(0) - .getVolumeMounts() - .size(), - 2); - VolumeMount mount1 = - environment - .getPodsData() - .get("pod1") - .getSpec() - .getContainers() - .get(0) - .getVolumeMounts() - .get(0); - assertEquals(mount1.getName(), "test_secret"); - assertEquals(mount1.getMountPath(), "/home/user/.m2/" + mount1.getSubPath()); - assertFalse(mount1.getSubPath().isEmpty()); - assertTrue(mount1.getReadOnly()); - - VolumeMount mount2 = - environment - .getPodsData() - .get("pod1") - .getSpec() - .getContainers() - .get(0) - .getVolumeMounts() - .get(1); - assertEquals(mount2.getName(), "test_secret"); - assertEquals(mount2.getMountPath(), "/home/user/.m2/" + mount2.getSubPath()); - assertFalse(mount2.getSubPath().isEmpty()); - assertTrue(mount2.getReadOnly()); - - // unmatched container has no mounts - assertEquals( - environment - .getPodsData() - .get("pod1") - .getSpec() - .getContainers() - .get(1) - .getVolumeMounts() - .size(), - 0); - - if ("settings.xml".equals(mount1.getSubPath())) { - assertEquals(mount1.getSubPath(), "settings.xml"); - assertEquals(mount2.getSubPath(), "another.xml"); - } else { - assertEquals(mount1.getSubPath(), "another.xml"); - assertEquals(mount2.getSubPath(), "settings.xml"); - } + secretApplier.applySecret(environment, runtimeIdentity, secret); + + // only first container has env set + assertEquals(container_match1.getEnv().size(), 1); + EnvVar var = container_match1.getEnv().get(0); + assertEquals(var.getName(), "MY_FOO"); + assertEquals(var.getValueFrom().getSecretKeyRef().getName(), "test_secret"); + assertEquals(var.getValueFrom().getSecretKeyRef().getKey(), "foo"); + + assertEquals(container_match2.getEnv().size(), 0); } - @Test( - expectedExceptions = InfrastructureException.class, - expectedExceptionsMessageRegExp = - "Unable to mount secret 'test_secret': It is configured to be mounted as a file but the mount path was not specified. Please define the 'che.eclipse.org/mount-path' annotation on the secret to specify it.") - public void shouldThrowExceptionWhenNoMountPathSpecifiedForFiles() throws Exception { - Container container_match = new ContainerBuilder().withName("maven").build(); + @Test + public void shouldNotProvisionContainersWithAutomountOverrideFalse() throws Exception { + Container container_match1 = new ContainerBuilder().withName("maven").build(); + Container container_match2 = new ContainerBuilder().withName("other").build(); + DevfileImpl mock_defvile = mock(DevfileImpl.class); + ComponentImpl component = new ComponentImpl(); + component.setAlias("maven"); + component.setAutomountWorkspaceSecrets(false); - PodSpec localSpec = - new PodSpecBuilder().withContainers(ImmutableList.of(container_match)).build(); + when(podSpec.getContainers()).thenReturn(ImmutableList.of(container_match1, container_match2)); + InternalMachineConfig internalMachineConfig = new InternalMachineConfig(); + internalMachineConfig.getAttributes().put(DEVFILE_COMPONENT_ALIAS_ATTRIBUTE, "maven"); + when(environment.getMachines()).thenReturn(ImmutableMap.of("maven", internalMachineConfig)); + when(environment.getDevfile()).thenReturn(mock_defvile); + when(mock_defvile.getComponents()).thenReturn(singletonList(component)); - when(podData.getSpec()).thenReturn(localSpec); Secret secret = new SecretBuilder() - .withData(ImmutableMap.of("settings.xml", "random", "another.xml", "freedom")) + .withData(singletonMap("foo", "random")) .withMetadata( new ObjectMetaBuilder() .withName("test_secret") - .withAnnotations(singletonMap(ANNOTATION_MOUNT_AS, "file")) + .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)); - provisioner.provision(environment, namespace); + + secretApplier.applySecret(environment, runtimeIdentity, secret); + + // only second container has env set + assertEquals(container_match1.getEnv().size(), 0); + + assertEquals(container_match2.getEnv().size(), 1); + EnvVar var2 = container_match2.getEnv().get(0); + assertEquals(var2.getName(), "MY_FOO"); + assertEquals(var2.getValueFrom().getSecretKeyRef().getName(), "test_secret"); + assertEquals(var2.getValueFrom().getSecretKeyRef().getKey(), "foo"); } - @Test( - expectedExceptions = InfrastructureException.class, - 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 { + @Test + public void shouldNotProvisionAllContainersifAutomountDisabled() throws Exception { + Container container_match1 = spy(new ContainerBuilder().withName("maven").build()); + Container container_match2 = spy(new ContainerBuilder().withName("other").build()); + + when(podSpec.getContainers()).thenReturn(ImmutableList.of(container_match1, container_match2)); + Secret secret = new SecretBuilder() - .withData(ImmutableMap.of("settings.xml", "random", "another.xml", "freedom")) + .withData(singletonMap("foo", "random")) .withMetadata( new ObjectMetaBuilder() .withName("test_secret") - .withAnnotations(emptyMap()) + .withAnnotations( + ImmutableMap.of( + ANNOTATION_ENV_NAME, "MY_FOO", + ANNOTATION_MOUNT_AS, "env", + ANNOTATION_AUTOMOUNT, "false")) .withLabels(emptyMap()) .build()) .build(); - when(secrets.get(any(LabelSelector.class))).thenReturn(singletonList(secret)); - provisioner.provision(environment, namespace); + + secretApplier.applySecret(environment, runtimeIdentity, secret); + + verify(container_match1).getName(); + verify(container_match2).getName(); + // both containers no actions + verifyNoMoreInteractions(container_match1, container_match2); } @Test( expectedExceptions = InfrastructureException.class, expectedExceptionsMessageRegExp = - "Unable to mount secret 'test_secret': It is configured to be mount as a environment variable, but its was not specified. Please define the 'che.eclipse.org/env-name' annotation on the secret to specify it.") + "Unable to mount secret 'test_secret': It is configured to be mount as a environment variable, but its name was not specified. Please define the 'che.eclipse.org/env-name' annotation on the secret to specify it.") public void shouldThrowExceptionWhenNoEnvNameSpecifiedSingleValue() throws Exception { Container container_match = new ContainerBuilder().withName("maven").build(); @@ -360,19 +323,20 @@ public void shouldThrowExceptionWhenNoEnvNameSpecifiedSingleValue() throws Excep .withMetadata( new ObjectMetaBuilder() .withName("test_secret") - .withAnnotations(ImmutableMap.of(ANNOTATION_MOUNT_AS, "env")) + .withAnnotations( + ImmutableMap.of(ANNOTATION_MOUNT_AS, "env", ANNOTATION_AUTOMOUNT, "true")) .withLabels(emptyMap()) .build()) .build(); when(secrets.get(any(LabelSelector.class))).thenReturn(singletonList(secret)); - provisioner.provision(environment, namespace); + secretApplier.applySecret(environment, runtimeIdentity, secret); } @Test( expectedExceptions = InfrastructureException.class, expectedExceptionsMessageRegExp = - "Unable to mount key 'foo' of secret 'test_secret': It is configured to be mount as a environment variable, but its was not specified. Please define the 'che.eclipse.org/foo_env-name' annotation on the secret to specify it.") + "Unable to mount key 'foo' of secret 'test_secret': It is configured to be mount as a environment variable, but its name was not specified. Please define the 'che.eclipse.org/foo_env-name' annotation on the secret to specify it.") public void shouldThrowExceptionWhenNoEnvNameSpecifiedMultiValue() throws Exception { Container container_match = new ContainerBuilder().withName("maven").build(); @@ -384,12 +348,13 @@ public void shouldThrowExceptionWhenNoEnvNameSpecifiedMultiValue() throws Except .withMetadata( new ObjectMetaBuilder() .withName("test_secret") - .withAnnotations(ImmutableMap.of(ANNOTATION_MOUNT_AS, "env")) + .withAnnotations( + ImmutableMap.of(ANNOTATION_MOUNT_AS, "env", ANNOTATION_AUTOMOUNT, "true")) .withLabels(emptyMap()) .build()) .build(); when(secrets.get(any(LabelSelector.class))).thenReturn(singletonList(secret)); - provisioner.provision(environment, namespace); + secretApplier.applySecret(environment, runtimeIdentity, secret); } } diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/FileSecretApplierTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/FileSecretApplierTest.java new file mode 100644 index 00000000000..884cc45d9d4 --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/FileSecretApplierTest.java @@ -0,0 +1,473 @@ +/* + * 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.singletonList; +import static java.util.Collections.singletonMap; +import static org.eclipse.che.api.core.model.workspace.config.MachineConfig.DEVFILE_COMPONENT_ALIAS_ATTRIBUTE; +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.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +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 com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.ContainerBuilder; +import io.fabric8.kubernetes.api.model.LabelSelector; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.PodSpec; +import io.fabric8.kubernetes.api.model.PodSpecBuilder; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.api.model.Volume; +import io.fabric8.kubernetes.api.model.VolumeMount; +import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; +import org.eclipse.che.api.workspace.server.model.impl.devfile.ComponentImpl; +import org.eclipse.che.api.workspace.server.model.impl.devfile.DevfileImpl; +import org.eclipse.che.api.workspace.server.model.impl.devfile.VolumeImpl; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.api.workspace.server.spi.environment.InternalMachineConfig; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment.PodData; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment.PodRole; +import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesSecrets; +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 FileSecretApplierTest { + + @Mock private KubernetesEnvironment environment; + + @Mock private KubernetesSecrets secrets; + + @Mock private PodData podData; + + @Mock private PodSpec podSpec; + + @Mock private RuntimeIdentity runtimeIdentity; + + FileSecretApplier secretApplier = new FileSecretApplier(); + + @BeforeMethod + public void setUp() throws Exception { + when(environment.getPodsData()).thenReturn(singletonMap("pod1", podData)); + when(podData.getRole()).thenReturn(PodRole.DEPLOYMENT); + when(podData.getSpec()).thenReturn(podSpec); + } + + @Test + public void shouldProvisionAsFiles() throws Exception { + Container container_match1 = new ContainerBuilder().withName("maven").build(); + Container container_match2 = new ContainerBuilder().withName("other").build(); + + PodSpec localSpec = + new PodSpecBuilder() + .withContainers(ImmutableList.of(container_match1, container_match2)) + .build(); + + when(podData.getSpec()).thenReturn(localSpec); + + 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(); + + secretApplier.applySecret(environment, runtimeIdentity, secret); + + // pod has volume created + assertEquals(environment.getPodsData().get("pod1").getSpec().getVolumes().size(), 1); + Volume volume = environment.getPodsData().get("pod1").getSpec().getVolumes().get(0); + assertEquals(volume.getName(), "test_secret"); + assertEquals(volume.getSecret().getSecretName(), "test_secret"); + + // both containers has mounts set + assertEquals( + environment + .getPodsData() + .get("pod1") + .getSpec() + .getContainers() + .get(0) + .getVolumeMounts() + .size(), + 2); + VolumeMount mount1 = + environment + .getPodsData() + .get("pod1") + .getSpec() + .getContainers() + .get(0) + .getVolumeMounts() + .get(0); + assertEquals(mount1.getName(), "test_secret"); + assertEquals(mount1.getMountPath(), "/home/user/.m2/" + mount1.getSubPath()); + assertFalse(mount1.getSubPath().isEmpty()); + assertTrue(mount1.getReadOnly()); + + VolumeMount mount2 = + environment + .getPodsData() + .get("pod1") + .getSpec() + .getContainers() + .get(0) + .getVolumeMounts() + .get(1); + assertEquals(mount2.getName(), "test_secret"); + assertEquals(mount2.getMountPath(), "/home/user/.m2/" + mount2.getSubPath()); + assertFalse(mount2.getSubPath().isEmpty()); + assertTrue(mount2.getReadOnly()); + + assertEquals( + environment + .getPodsData() + .get("pod1") + .getSpec() + .getContainers() + .get(1) + .getVolumeMounts() + .size(), + 2); + + if ("settings.xml".equals(mount1.getSubPath())) { + assertEquals(mount1.getSubPath(), "settings.xml"); + assertEquals(mount2.getSubPath(), "another.xml"); + } else { + assertEquals(mount1.getSubPath(), "another.xml"); + assertEquals(mount2.getSubPath(), "settings.xml"); + } + } + + @Test + public void shouldProvisionAsFilesWithPathOverride() throws Exception { + Container container = new ContainerBuilder().withName("maven").build(); + + DevfileImpl mock_defvile = mock(DevfileImpl.class); + ComponentImpl component = new ComponentImpl(); + component.setAlias("maven"); + component.getVolumes().add(new VolumeImpl("test_secret", "/path/to/override")); + + InternalMachineConfig internalMachineConfig = new InternalMachineConfig(); + internalMachineConfig.getAttributes().put(DEVFILE_COMPONENT_ALIAS_ATTRIBUTE, "maven"); + when(environment.getMachines()).thenReturn(ImmutableMap.of("maven", internalMachineConfig)); + when(environment.getDevfile()).thenReturn(mock_defvile); + when(mock_defvile.getComponents()).thenReturn(singletonList(component)); + + PodSpec localSpec = new PodSpecBuilder().withContainers(ImmutableList.of(container)).build(); + + when(podData.getSpec()).thenReturn(localSpec); + + 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(); + + secretApplier.applySecret(environment, runtimeIdentity, secret); + + // pod has volume created + assertEquals(environment.getPodsData().get("pod1").getSpec().getVolumes().size(), 1); + Volume volume = environment.getPodsData().get("pod1").getSpec().getVolumes().get(0); + assertEquals(volume.getName(), "test_secret"); + assertEquals(volume.getSecret().getSecretName(), "test_secret"); + + // both containers has mounts set + assertEquals( + environment + .getPodsData() + .get("pod1") + .getSpec() + .getContainers() + .get(0) + .getVolumeMounts() + .size(), + 2); + VolumeMount mount1 = + environment + .getPodsData() + .get("pod1") + .getSpec() + .getContainers() + .get(0) + .getVolumeMounts() + .get(0); + assertEquals(mount1.getName(), "test_secret"); + assertEquals(mount1.getMountPath(), "/path/to/override/settings.xml"); + assertTrue(mount1.getReadOnly()); + } + + @Test + public void shouldProvisionContainersWithAutomountOverrideTrue() throws Exception { + Container container_match1 = new ContainerBuilder().withName("maven").build(); + Container container_match2 = new ContainerBuilder().withName("other").build(); + DevfileImpl mock_defvile = mock(DevfileImpl.class); + ComponentImpl component = new ComponentImpl(); + component.setAlias("maven"); + component.setAutomountWorkspaceSecrets(true); + + InternalMachineConfig internalMachineConfig = new InternalMachineConfig(); + internalMachineConfig.getAttributes().put(DEVFILE_COMPONENT_ALIAS_ATTRIBUTE, "maven"); + when(environment.getMachines()).thenReturn(ImmutableMap.of("maven", internalMachineConfig)); + when(environment.getDevfile()).thenReturn(mock_defvile); + when(mock_defvile.getComponents()).thenReturn(singletonList(component)); + + PodSpec localSpec = + new PodSpecBuilder() + .withContainers(ImmutableList.of(container_match1, container_match2)) + .build(); + + when(podData.getSpec()).thenReturn(localSpec); + + Secret secret = + new SecretBuilder() + .withData(singletonMap("foo", "random")) + .withMetadata( + new ObjectMetaBuilder() + .withName("test_secret") + .withAnnotations( + ImmutableMap.of( + ANNOTATION_MOUNT_AS, + "file", + ANNOTATION_MOUNT_PATH, + "/home/user/.m2", + ANNOTATION_AUTOMOUNT, + "false")) + .withLabels(emptyMap()) + .build()) + .build(); + + secretApplier.applySecret(environment, runtimeIdentity, secret); + + // pod has volume created + assertEquals(environment.getPodsData().get("pod1").getSpec().getVolumes().size(), 1); + Volume volume = environment.getPodsData().get("pod1").getSpec().getVolumes().get(0); + assertEquals(volume.getName(), "test_secret"); + assertEquals(volume.getSecret().getSecretName(), "test_secret"); + + // first container has mount set + assertEquals( + environment + .getPodsData() + .get("pod1") + .getSpec() + .getContainers() + .get(0) + .getVolumeMounts() + .size(), + 1); + VolumeMount mount1 = + environment + .getPodsData() + .get("pod1") + .getSpec() + .getContainers() + .get(0) + .getVolumeMounts() + .get(0); + assertEquals(mount1.getName(), "test_secret"); + assertEquals(mount1.getMountPath(), "/home/user/.m2/foo"); + assertTrue(mount1.getReadOnly()); + + // second container has no mounts + assertEquals( + environment + .getPodsData() + .get("pod1") + .getSpec() + .getContainers() + .get(1) + .getVolumeMounts() + .size(), + 0); + } + + @Test + public void shouldNotProvisionContainersWithAutomountOverrideFalse() throws Exception { + Container container_match1 = new ContainerBuilder().withName("maven").build(); + Container container_match2 = new ContainerBuilder().withName("other").build(); + DevfileImpl mock_defvile = mock(DevfileImpl.class); + ComponentImpl component = new ComponentImpl(); + component.setAlias("maven"); + component.setAutomountWorkspaceSecrets(false); + + InternalMachineConfig internalMachineConfig = new InternalMachineConfig(); + internalMachineConfig.getAttributes().put(DEVFILE_COMPONENT_ALIAS_ATTRIBUTE, "maven"); + when(environment.getMachines()).thenReturn(ImmutableMap.of("maven", internalMachineConfig)); + when(environment.getDevfile()).thenReturn(mock_defvile); + when(mock_defvile.getComponents()).thenReturn(singletonList(component)); + + PodSpec localSpec = + new PodSpecBuilder() + .withContainers(ImmutableList.of(container_match1, container_match2)) + .build(); + + when(podData.getSpec()).thenReturn(localSpec); + + Secret secret = + new SecretBuilder() + .withData(singletonMap("foo", "random")) + .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(); + + secretApplier.applySecret(environment, runtimeIdentity, secret); + + // only second container has mounts + assertEquals(environment.getPodsData().get("pod1").getSpec().getVolumes().size(), 1); + Volume volume = environment.getPodsData().get("pod1").getSpec().getVolumes().get(0); + assertEquals(volume.getName(), "test_secret"); + assertEquals(volume.getSecret().getSecretName(), "test_secret"); + + assertEquals( + environment + .getPodsData() + .get("pod1") + .getSpec() + .getContainers() + .get(0) + .getVolumeMounts() + .size(), + 0); + + assertEquals( + environment + .getPodsData() + .get("pod1") + .getSpec() + .getContainers() + .get(1) + .getVolumeMounts() + .size(), + 1); + VolumeMount mount2 = + environment + .getPodsData() + .get("pod1") + .getSpec() + .getContainers() + .get(1) + .getVolumeMounts() + .get(0); + assertEquals(mount2.getName(), "test_secret"); + assertEquals(mount2.getMountPath(), "/home/user/.m2/foo"); + assertTrue(mount2.getReadOnly()); + } + + @Test + public void shouldNotProvisionAllContainersifAutomountDisabled() throws Exception { + Container container_match1 = spy(new ContainerBuilder().withName("maven").build()); + Container container_match2 = spy(new ContainerBuilder().withName("other").build()); + + when(podSpec.getContainers()).thenReturn(ImmutableList.of(container_match1, container_match2)); + + Secret secret = + new SecretBuilder() + .withData(singletonMap("foo", "random")) + .withMetadata( + new ObjectMetaBuilder() + .withName("test_secret") + .withAnnotations( + ImmutableMap.of( + ANNOTATION_MOUNT_AS, + "file", + ANNOTATION_MOUNT_PATH, + "/home/user/.m2", + ANNOTATION_AUTOMOUNT, + "false")) + .withLabels(emptyMap()) + .build()) + .build(); + + secretApplier.applySecret(environment, runtimeIdentity, secret); + + verify(container_match1).getName(); + verify(container_match2).getName(); + // both containers no actions + verifyNoMoreInteractions(container_match1, container_match2); + } + + @Test( + expectedExceptions = InfrastructureException.class, + expectedExceptionsMessageRegExp = + "Unable to mount secret 'test_secret': It is configured to be mounted as a file but the mount path was not specified. Please define the 'che.eclipse.org/mount-path' annotation on the secret to specify it.") + public void shouldThrowExceptionWhenNoMountPathSpecifiedForFiles() throws Exception { + Container container_match = new ContainerBuilder().withName("maven").build(); + + PodSpec localSpec = + new PodSpecBuilder().withContainers(ImmutableList.of(container_match)).build(); + + when(podData.getSpec()).thenReturn(localSpec); + Secret secret = + new SecretBuilder() + .withData(ImmutableMap.of("settings.xml", "random", "another.xml", "freedom")) + .withMetadata( + new ObjectMetaBuilder() + .withName("test_secret") + .withAnnotations(singletonMap(ANNOTATION_MOUNT_AS, "file")) + .withLabels(emptyMap()) + .build()) + .build(); + when(secrets.get(any(LabelSelector.class))).thenReturn(singletonList(secret)); + 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 new file mode 100644 index 00000000000..08b702557b0 --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/secret/SecretAsContainerResourceProvisionerTest.java @@ -0,0 +1,77 @@ +/* + * 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.singletonList; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableMap; +import io.fabric8.kubernetes.api.model.LabelSelector; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +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.KubernetesNamespace; +import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesSecrets; +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 SecretAsContainerResourceProvisionerTest { + + @Mock EnvironmentVariableSecretApplier environmentVariableSecretApplier; + @Mock FileSecretApplier fileSecretApplier; + + private SecretAsContainerResourceProvisioner provisioner; + + @Mock private KubernetesEnvironment environment; + + @Mock private KubernetesNamespace namespace; + + @Mock private KubernetesSecrets secrets; + + @Mock private RuntimeIdentity runtimeIdentity; + + @BeforeMethod + public void setUp() throws Exception { + when(namespace.secrets()).thenReturn(secrets); + provisioner = + new SecretAsContainerResourceProvisioner<>( + fileSecretApplier, environmentVariableSecretApplier, new String[] {"app:che"}); + } + + @Test( + expectedExceptions = InfrastructureException.class, + 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 { + Secret secret = + new SecretBuilder() + .withData(ImmutableMap.of("settings.xml", "random", "another.xml", "freedom")) + .withMetadata( + new ObjectMetaBuilder() + .withName("test_secret") + .withAnnotations(emptyMap()) + .withLabels(emptyMap()) + .build()) + .build(); + when(secrets.get(any(LabelSelector.class))).thenReturn(singletonList(secret)); + provisioner.provision(environment, runtimeIdentity, namespace); + } +} diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInternalRuntime.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInternalRuntime.java index de256f0e991..c2f57b90f0b 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInternalRuntime.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInternalRuntime.java @@ -37,7 +37,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.cache.KubernetesMachineCache; import org.eclipse.che.workspace.infrastructure.kubernetes.cache.KubernetesRuntimeStateCache; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.WorkspaceVolumesStrategy; -import org.eclipse.che.workspace.infrastructure.kubernetes.provision.SecretAsContainerResourceProvisioner; +import org.eclipse.che.workspace.infrastructure.kubernetes.provision.secret.SecretAsContainerResourceProvisioner; import org.eclipse.che.workspace.infrastructure.kubernetes.util.KubernetesSharedPool; import org.eclipse.che.workspace.infrastructure.kubernetes.util.RuntimeEventsPublisher; import org.eclipse.che.workspace.infrastructure.kubernetes.util.UnrecoverablePodEventListenerFactory; diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInternalRuntimeTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInternalRuntimeTest.java index dc32446e226..2b7840782f6 100644 --- a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInternalRuntimeTest.java +++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInternalRuntimeTest.java @@ -75,7 +75,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesSecrets; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesServices; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.WorkspaceVolumesStrategy; -import org.eclipse.che.workspace.infrastructure.kubernetes.provision.SecretAsContainerResourceProvisioner; +import org.eclipse.che.workspace.infrastructure.kubernetes.provision.secret.SecretAsContainerResourceProvisioner; import org.eclipse.che.workspace.infrastructure.kubernetes.util.KubernetesSharedPool; import org.eclipse.che.workspace.infrastructure.kubernetes.util.RuntimeEventsPublisher; import org.eclipse.che.workspace.infrastructure.kubernetes.util.UnrecoverablePodEventListenerFactory; diff --git a/wsmaster/che-core-api-workspace-shared/src/main/java/org/eclipse/che/api/workspace/shared/dto/devfile/ComponentDto.java b/wsmaster/che-core-api-workspace-shared/src/main/java/org/eclipse/che/api/workspace/shared/dto/devfile/ComponentDto.java index 1b6844ecce9..7d8536494af 100644 --- a/wsmaster/che-core-api-workspace-shared/src/main/java/org/eclipse/che/api/workspace/shared/dto/devfile/ComponentDto.java +++ b/wsmaster/che-core-api-workspace-shared/src/main/java/org/eclipse/che/api/workspace/shared/dto/devfile/ComponentDto.java @@ -126,6 +126,13 @@ public interface ComponentDto extends Component { ComponentDto withMountSources(Boolean mountSources); + @Override + Boolean getAutomountWorkspaceSecrets(); + + void setAutomountWorkspaceSecrets(Boolean automountWorkspaceSecrets); + + ComponentDto withAutomountWorkspaceSecrets(Boolean automountWorkspaceSecrets); + @Override List getCommand(); diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/DtoConverter.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/DtoConverter.java index f579fcf9cfd..28dcb6d28e5 100644 --- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/DtoConverter.java +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/DtoConverter.java @@ -138,6 +138,7 @@ private static ComponentDto asDto(Component component) { return newDto(ComponentDto.class) .withType(component.getType()) .withAlias(component.getAlias()) + .withAutomountWorkspaceSecrets(component.getAutomountWorkspaceSecrets()) // chePlugin/cheEditor .withId(component.getId()) .withRegistryUrl(component.getRegistryUrl()) diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/devfile/validator/DevfileIntegrityValidator.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/devfile/validator/DevfileIntegrityValidator.java index 383745c9750..1e49d4b54f4 100644 --- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/devfile/validator/DevfileIntegrityValidator.java +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/devfile/validator/DevfileIntegrityValidator.java @@ -152,6 +152,14 @@ private Set validateComponents(Devfile devfile) throws DevfileFormatExce getIdentifiableComponentName(component), component.getType())); } + if (component.getAutomountWorkspaceSecrets() != null && component.getAlias() == null) { + throw new DevfileFormatException( + format( + "The 'automountWorkspaceSecrets' property cannot be used in component which doesn't have alias. " + + "Please add alias to component '%s' that would allow to distinguish its containers.", + getIdentifiableComponentName(component))); + } + switch (component.getType()) { case EDITOR_COMPONENT_TYPE: if (editorComponent != null) { diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/model/impl/devfile/ComponentImpl.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/model/impl/devfile/ComponentImpl.java index 6537df0d827..7c7ed4fcf78 100644 --- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/model/impl/devfile/ComponentImpl.java +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/model/impl/devfile/ComponentImpl.java @@ -105,6 +105,9 @@ public class ComponentImpl implements Component { @Column(name = "mount_sources") private Boolean mountSources; + @Column(name = "automount_secrets") + private Boolean automountWorkspaceSecrets; + @ElementCollection(fetch = FetchType.EAGER) @CollectionTable( name = "devfile_component_command", @@ -186,6 +189,7 @@ public ComponentImpl( String cpuLimit, String cpuRequest, Boolean mountSources, + Boolean automountWorkspaceSecrets, List command, List args, List volumes, @@ -213,6 +217,7 @@ public ComponentImpl( this.cpuLimit = cpuLimit; this.cpuRequest = cpuRequest; this.mountSources = mountSources; + this.automountWorkspaceSecrets = automountWorkspaceSecrets; this.command = command; this.args = args; if (volumes != null) { @@ -244,6 +249,7 @@ public ComponentImpl(Component component) { component.getCpuLimit(), component.getCpuRequest(), component.getMountSources(), + component.getAutomountWorkspaceSecrets(), component.getCommand(), component.getArgs(), component.getVolumes(), @@ -394,6 +400,15 @@ public void setMountSources(Boolean mountSources) { this.mountSources = mountSources; } + @Override + public Boolean getAutomountWorkspaceSecrets() { + return automountWorkspaceSecrets; + } + + public void setAutomountWorkspaceSecrets(Boolean automountWorkspaceSecrets) { + this.automountWorkspaceSecrets = automountWorkspaceSecrets; + } + @Override public List getCommand() { if (command == null) { @@ -464,6 +479,7 @@ public boolean equals(Object o) { } ComponentImpl component = (ComponentImpl) o; return getMountSources() == component.getMountSources() + && getAutomountWorkspaceSecrets() == component.getAutomountWorkspaceSecrets() && Objects.equals(generatedId, component.generatedId) && Objects.equals(alias, component.alias) && Objects.equals(type, component.type) @@ -500,6 +516,7 @@ public int hashCode() { getSelector(), getEntrypoints(), getMountSources(), + getAutomountWorkspaceSecrets(), getCommand(), getArgs(), getVolumes(), @@ -542,6 +559,8 @@ public String toString() { + '\'' + ", mountSources=" + mountSources + + ", automountWorkspaceSecrets=" + + automountWorkspaceSecrets + ", command=" + command + ", args=" diff --git a/wsmaster/che-core-api-workspace/src/main/resources/schema/1.0.0/devfile.json b/wsmaster/che-core-api-workspace/src/main/resources/schema/1.0.0/devfile.json index 8dac07a20f6..8b5b7db049c 100644 --- a/wsmaster/che-core-api-workspace/src/main/resources/schema/1.0.0/devfile.json +++ b/wsmaster/che-core-api-workspace/src/main/resources/schema/1.0.0/devfile.json @@ -310,6 +310,7 @@ "registryUrl": {}, "memoryLimit": {}, "memoryRequest": {}, + "automountWorkspaceSecrets": {}, "cpuLimit": {}, "cpuRequest": {} } @@ -336,6 +337,7 @@ "volumes": {}, "memoryLimit": {}, "memoryRequest": {}, + "automountWorkspaceSecrets": {}, "cpuLimit": {}, "cpuRequest": {}, "reference": {}, @@ -402,6 +404,7 @@ "type": {}, "alias": {}, "mountSources": {}, + "automountWorkspaceSecrets": {}, "volumes": {}, "env": {}, "endpoints": {}, @@ -494,6 +497,7 @@ "env": {}, "cpuLimit": {}, "cpuRequest": {}, + "automountWorkspaceSecrets": {}, "volumes": {}, "endpoints": {}, "memoryLimit": { @@ -614,6 +618,10 @@ "1230m" ] }, + "automountWorkspaceSecrets": { + "type": "boolean", + "description": "Describes whether namespace secrets should be mount to the component." + }, "volumes": { "type": "array", "description": "Describes volumes which should be mount to component", diff --git a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/devfile/validator/DevfileSchemaValidatorTest.java b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/devfile/validator/DevfileSchemaValidatorTest.java index 1a3cf05d48e..4f28cf9e6a2 100644 --- a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/devfile/validator/DevfileSchemaValidatorTest.java +++ b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/devfile/validator/DevfileSchemaValidatorTest.java @@ -53,6 +53,7 @@ public Object[][] validDevfiles() { {"kubernetes_openshift_component/devfile_kubernetes_component.yaml"}, {"kubernetes_openshift_component/devfile_kubernetes_component_absolute_reference.yaml"}, {"component/devfile_without_any_component.yaml"}, + {"component/devfile_component_with_automount_secrets.yaml"}, { "kubernetes_openshift_component/devfile_kubernetes_component_reference_and_content_as_block.yaml" }, diff --git a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/spi/tck/WorkspaceDaoTest.java b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/spi/tck/WorkspaceDaoTest.java index 8d915f2700f..b05593fa6c6 100644 --- a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/spi/tck/WorkspaceDaoTest.java +++ b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/spi/tck/WorkspaceDaoTest.java @@ -634,6 +634,7 @@ public void shouldUpdateWorkspaceWithDevfile() throws Exception { "2", "1", false, + false, singletonList("command"), singletonList("arg"), singletonList(volume3), @@ -968,6 +969,7 @@ private static DevfileImpl createDevfile(String name) { "2", "130m", false, + false, singletonList("command"), singletonList("arg"), asList(volume1, volume2), @@ -998,6 +1000,7 @@ private static DevfileImpl createDevfile(String name) { "3", "180m", false, + false, singletonList("command"), singletonList("arg"), asList(volume1, volume2), @@ -1005,16 +1008,13 @@ private static DevfileImpl createDevfile(String name) { asList(endpoint1, endpoint2)); component2.setSelector(singletonMap("key2", "value2")); - DevfileImpl devfile = - new DevfileImpl( - "0.0.1", - asList(project1, project2), - asList(component1, component2), - asList(command1, command2), - singletonMap("attribute1", "value1"), - new MetadataImpl(name)); - - return devfile; + return new DevfileImpl( + "0.0.1", + asList(project1, project2), + asList(component1, component2), + asList(command1, command2), + singletonMap("attribute1", "value1"), + new MetadataImpl(name)); } private CascadeEventSubscriber mockCascadeEventSubscriber() { diff --git a/wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/component/devfile_component_with_automount_secrets.yaml b/wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/component/devfile_component_with_automount_secrets.yaml new file mode 100644 index 00000000000..f5ed4ba1585 --- /dev/null +++ b/wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/component/devfile_component_with_automount_secrets.yaml @@ -0,0 +1,26 @@ +# +# 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 +# + +--- +apiVersion: 1.0.0 +metadata: + name: petclinic-dev-environment +components: + - type: chePlugin + alias: maven + id: eclipse/chemaven-jdk8/1.0.0 + automountWorkspaceSecrets: false + - type: dockerimage + alias: maven2 + image: eclipse/chemaven-jdk8/1.0.0 + automountWorkspaceSecrets: true + memoryLimit: 256Mi diff --git a/wsmaster/che-core-sql-schema/src/main/resources/che-schema/7.16.0/1__add_devfile_component_automount_workspace_secrets.sql b/wsmaster/che-core-sql-schema/src/main/resources/che-schema/7.16.0/1__add_devfile_component_automount_workspace_secrets.sql new file mode 100644 index 00000000000..159717dfdbc --- /dev/null +++ b/wsmaster/che-core-sql-schema/src/main/resources/che-schema/7.16.0/1__add_devfile_component_automount_workspace_secrets.sql @@ -0,0 +1,13 @@ +-- +-- Copyright (c) 2012-2020 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 +-- + +ALTER TABLE devfile_component ADD COLUMN automount_secrets BOOLEAN; diff --git a/wsmaster/integration-tests/cascade-removal/src/test/java/org/eclipse/che/core/db/jpa/TestObjectsFactory.java b/wsmaster/integration-tests/cascade-removal/src/test/java/org/eclipse/che/core/db/jpa/TestObjectsFactory.java index 16c42dc8cc8..e57415e6f18 100644 --- a/wsmaster/integration-tests/cascade-removal/src/test/java/org/eclipse/che/core/db/jpa/TestObjectsFactory.java +++ b/wsmaster/integration-tests/cascade-removal/src/test/java/org/eclipse/che/core/db/jpa/TestObjectsFactory.java @@ -126,6 +126,7 @@ private static ComponentImpl createDevfileComponent(String name) { "200m", "100m", false, + false, singletonList("command"), singletonList("arg"), null,