diff --git a/assembly/assembly-wsmaster-war/pom.xml b/assembly/assembly-wsmaster-war/pom.xml index 82cd801270a..19518209d21 100644 --- a/assembly/assembly-wsmaster-war/pom.xml +++ b/assembly/assembly-wsmaster-war/pom.xml @@ -157,6 +157,10 @@ org.eclipse.che.core che-core-api-core + + org.eclipse.che.core + che-core-api-devfile + org.eclipse.che.core che-core-api-factory @@ -305,6 +309,10 @@ org.eclipse.che.multiuser che-multiuser-machine-authentication + + org.eclipse.che.multiuser + che-multiuser-permission-devfile + org.eclipse.che.multiuser che-multiuser-permission-factory diff --git a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java index 6e8e766552d..f5f2c8cec33 100644 --- a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java +++ b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java @@ -31,6 +31,8 @@ import org.eclipse.che.api.core.rest.CheJsonProvider; import org.eclipse.che.api.core.rest.MessageBodyAdapter; import org.eclipse.che.api.core.rest.MessageBodyAdapterInterceptor; +import org.eclipse.che.api.devfile.server.DevfileSchemaValidator; +import org.eclipse.che.api.devfile.server.DevfileService; import org.eclipse.che.api.factory.server.FactoryAcceptValidator; import org.eclipse.che.api.factory.server.FactoryCreateValidator; import org.eclipse.che.api.factory.server.FactoryEditValidator; @@ -154,6 +156,9 @@ protected void configure() { bind(org.eclipse.che.api.user.server.PreferencesService.class); bind(org.eclipse.che.security.oauth.OAuthAuthenticationService.class); + bind(DevfileSchemaValidator.class); + bind(DevfileService.class); + MapBinder stacks = MapBinder.newMapBinder( binder(), String.class, String.class, Names.named(StackLoader.CHE_PREDEFINED_STACKS)); @@ -371,6 +376,7 @@ private void configureMultiUserMode( bind(org.eclipse.che.multiuser.permission.logger.LoggerServicePermissionsFilter.class); bind(org.eclipse.che.multiuser.permission.factory.FactoryPermissionsFilter.class); + bind(org.eclipse.che.multiuser.permission.devfile.DevfilePermissionsFilter.class); bind( org.eclipse.che.multiuser.permission.installer.InstallerRegistryServicePermissionsFilter .class); diff --git a/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/config/Command.java b/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/config/Command.java index 76c9d8d3041..9cf13a7ad01 100644 --- a/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/config/Command.java +++ b/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/config/Command.java @@ -21,6 +21,12 @@ */ public interface Command { + /** + * {@link Command} attribute which indicates the working directory where the given command must be + * run + */ + String WORKING_DIRECTORY_ATTRIBUTE = "workingDir"; + /** * Returns command name (i.e. 'start tomcat') The name should be unique per user in one workspace, * which means that user may create only one command with the same name in the same workspace diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/KubernetesPluginsToolingApplier.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/KubernetesPluginsToolingApplier.java index 716418602c1..154d72d8754 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/KubernetesPluginsToolingApplier.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/KubernetesPluginsToolingApplier.java @@ -13,6 +13,7 @@ import static java.lang.String.format; import static java.util.Collections.emptyList; +import static org.eclipse.che.api.core.model.workspace.config.Command.WORKING_DIRECTORY_ATTRIBUTE; import static org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.SecureServerExposerFactoryProvider.SECURE_EXPOSER_IMPL_PROPERTY; import com.google.common.annotations.Beta; @@ -212,7 +213,7 @@ private CommandImpl asCommand(String machineName, Command command) { command.getName(), command.getCommand().stream().collect(Collectors.joining(" ")), "custom"); - cmd.getAttributes().put("workDir", command.getWorkingDir()); + cmd.getAttributes().put(WORKING_DIRECTORY_ATTRIBUTE, command.getWorkingDir()); cmd.getAttributes().put("machineName", machineName); return cmd; } diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/KubernetesPluginsToolingApplierTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/KubernetesPluginsToolingApplierTest.java index 0739ad8eb6f..5fdecc4d973 100644 --- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/KubernetesPluginsToolingApplierTest.java +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/KubernetesPluginsToolingApplierTest.java @@ -17,6 +17,7 @@ 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.Command.WORKING_DIRECTORY_ATTRIBUTE; import static org.eclipse.che.api.core.model.workspace.config.MachineConfig.MEMORY_LIMIT_ATTRIBUTE; import static org.eclipse.che.commons.lang.NameGenerator.generate; import static org.eclipse.che.workspace.infrastructure.kubernetes.Constants.CHE_ORIGINAL_NAME_LABEL; @@ -136,7 +137,8 @@ public void shouldProvisionPluginsCommandsToEnvironment() throws Exception { envCommand.getCommandLine(), pluginCommand.getCommand().stream().collect(Collectors.joining(" "))); assertEquals(envCommand.getType(), "custom"); - assertEquals(envCommand.getAttributes().get("workDir"), pluginCommand.getWorkingDir()); + assertEquals( + envCommand.getAttributes().get(WORKING_DIRECTORY_ATTRIBUTE), pluginCommand.getWorkingDir()); assertEquals(envCommand.getAttributes().get("machineName"), POD_NAME + "/plugin-container"); } diff --git a/multiuser/permission/che-multiuser-permission-devfile/pom.xml b/multiuser/permission/che-multiuser-permission-devfile/pom.xml new file mode 100644 index 00000000000..446c0ffbc04 --- /dev/null +++ b/multiuser/permission/che-multiuser-permission-devfile/pom.xml @@ -0,0 +1,122 @@ + + + + 4.0.0 + + che-multiuser-permission + org.eclipse.che.multiuser + 6.16.0-SNAPSHOT + + che-multiuser-permission-devfile + Che Multiuser :: Devfile Permissions + + + javax.inject + javax.inject + + + javax.ws.rs + javax.ws.rs-api + + + org.eclipse.che.core + che-core-api-core + + + org.eclipse.che.core + che-core-api-devfile + + + org.eclipse.che.core + che-core-api-workspace + + + org.eclipse.che.core + che-core-commons-test + + + org.eclipse.che.multiuser + che-multiuser-api-permission + + + org.eclipse.che.multiuser + che-multiuser-permission-workspace + + + org.everrest + everrest-core + + + ch.qos.logback + logback-classic + test + + + com.jayway.restassured + rest-assured + test + + + org.eclipse.che.core + che-core-api-dto + test + + + org.eclipse.che.core + che-core-api-factory-shared + test + + + org.everrest + everrest-assured + test + + + org.mockito + mockito-core + test + + + org.mockito + mockito-testng + test + + + org.testng + testng + test + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + + analyze + + + org.eclipse.che.multiuser:che-multiuser-api-permission + org.eclipse.che.core:che-core-api-devfile + + + + + + + + diff --git a/multiuser/permission/che-multiuser-permission-devfile/src/main/java/org/eclipse/che/multiuser/permission/devfile/DevfilePermissionsFilter.java b/multiuser/permission/che-multiuser-permission-devfile/src/main/java/org/eclipse/che/multiuser/permission/devfile/DevfilePermissionsFilter.java new file mode 100644 index 00000000000..3600f4f0be8 --- /dev/null +++ b/multiuser/permission/che-multiuser-permission-devfile/src/main/java/org/eclipse/che/multiuser/permission/devfile/DevfilePermissionsFilter.java @@ -0,0 +1,73 @@ +/* + * 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.multiuser.permission.devfile; + +import javax.inject.Inject; +import javax.ws.rs.Path; +import org.eclipse.che.api.core.ForbiddenException; +import org.eclipse.che.api.core.NotFoundException; +import org.eclipse.che.api.core.ServerException; +import org.eclipse.che.api.devfile.server.DevfileService; +import org.eclipse.che.api.workspace.server.WorkspaceManager; +import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl; +import org.eclipse.che.commons.env.EnvironmentContext; +import org.eclipse.che.commons.subject.Subject; +import org.eclipse.che.everrest.CheMethodInvokerFilter; +import org.eclipse.che.multiuser.permission.workspace.server.WorkspaceDomain; +import org.everrest.core.Filter; +import org.everrest.core.resource.GenericResourceMethod; + +/** Restricts access to methods of {@link DevfileService} by user's permissions. */ +@Filter +@Path("/devfile{path:(/.*)?}") +public class DevfilePermissionsFilter extends CheMethodInvokerFilter { + + private final WorkspaceManager workspaceManager; + + @Inject + public DevfilePermissionsFilter(WorkspaceManager workspaceManager) { + this.workspaceManager = workspaceManager; + } + + @Override + protected void filter(GenericResourceMethod genericResourceMethod, Object[] arguments) + throws ForbiddenException, NotFoundException, ServerException { + final String methodName = genericResourceMethod.getMethod().getName(); + switch (methodName) { + // public methods + case "getSchema": + case "createFromYaml": + return; + case "createFromWorkspace": + { + // check user has reading rights + checkPermissionsWithCompositeKey((String) arguments[0]); + return; + } + default: + throw new ForbiddenException("The user does not have permission to perform this operation"); + } + } + + private void checkPermissionsWithCompositeKey(String key) + throws ForbiddenException, NotFoundException, ServerException { + final Subject currentSubject = EnvironmentContext.getCurrent().getSubject(); + if (!key.contains(":") && !key.contains("/")) { + // key is id + currentSubject.checkPermission(WorkspaceDomain.DOMAIN_ID, key, WorkspaceDomain.READ); + } else { + final WorkspaceImpl workspace = workspaceManager.getWorkspace(key); + currentSubject.checkPermission( + WorkspaceDomain.DOMAIN_ID, workspace.getId(), WorkspaceDomain.READ); + } + } +} diff --git a/multiuser/permission/che-multiuser-permission-devfile/src/test/java/org/eclipse/che/multiuser/permissions/devfile/DevfilePermissionsFilterTest.java b/multiuser/permission/che-multiuser-permission-devfile/src/test/java/org/eclipse/che/multiuser/permissions/devfile/DevfilePermissionsFilterTest.java new file mode 100644 index 00000000000..09048af0e01 --- /dev/null +++ b/multiuser/permission/che-multiuser-permission-devfile/src/test/java/org/eclipse/che/multiuser/permissions/devfile/DevfilePermissionsFilterTest.java @@ -0,0 +1,116 @@ +/* + * 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.multiuser.permissions.devfile; + +import static com.jayway.restassured.RestAssured.given; +import static org.eclipse.che.multiuser.permission.workspace.server.WorkspaceDomain.DOMAIN_ID; +import static org.eclipse.che.multiuser.permission.workspace.server.WorkspaceDomain.READ; +import static org.everrest.assured.JettyHttpServer.ADMIN_USER_NAME; +import static org.everrest.assured.JettyHttpServer.ADMIN_USER_PASSWORD; +import static org.everrest.assured.JettyHttpServer.SECURE_PATH; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; + +import com.jayway.restassured.response.Response; +import org.eclipse.che.api.core.ForbiddenException; +import org.eclipse.che.api.devfile.server.DevfileService; +import org.eclipse.che.api.workspace.server.WorkspaceManager; +import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl; +import org.eclipse.che.commons.env.EnvironmentContext; +import org.eclipse.che.commons.subject.Subject; +import org.eclipse.che.multiuser.permission.devfile.DevfilePermissionsFilter; +import org.everrest.assured.EverrestJetty; +import org.everrest.core.Filter; +import org.everrest.core.GenericContainerRequest; +import org.everrest.core.RequestFilter; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +@Listeners(value = {EverrestJetty.class, MockitoTestNGListener.class}) +public class DevfilePermissionsFilterTest { + + @SuppressWarnings("unused") + private static final EnvironmentFilter FILTER = new EnvironmentFilter(); + + @Mock private static Subject subject; + @Mock private WorkspaceManager workspaceManager; + @Mock private DevfileService service; + + @SuppressWarnings("unused") + @InjectMocks + private DevfilePermissionsFilter permissionsFilter; + + @Test + public void shouldCheckPermissionsOnExportingWorkspaceById() throws Exception { + final String wsId = "workspace123"; + final Response response = + given() + .auth() + .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD) + .when() + .get(SECURE_PATH + "/devfile/" + wsId); + + assertEquals(response.getStatusCode(), 204); + verify(subject).checkPermission(DOMAIN_ID, wsId, READ); + verify(service).createFromWorkspace((eq(wsId))); + } + + @Test + public void shouldCheckPermissionsOnExportingWorkspaceByKey() throws Exception { + final String key = "namespace/ws_name"; + final String wsId = "workspace123"; + WorkspaceImpl workspace = new WorkspaceImpl(); + workspace.setId(wsId); + when(workspaceManager.getWorkspace(eq(key))).thenReturn(workspace); + final Response response = + given() + .auth() + .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD) + .when() + .get(SECURE_PATH + "/devfile/" + key); + + assertEquals(response.getStatusCode(), 204); + verify(subject).checkPermission(DOMAIN_ID, wsId, READ); + verify(service).createFromWorkspace((eq(key))); + } + + @Test + public void shouldReturnForbiddenWhenUserDoesHavePermissionsToExportWorkspaceToDevfile() + throws Exception { + doThrow(new ForbiddenException("User in not authorized")) + .when(subject) + .checkPermission(anyString(), anyString(), anyString()); + + final Response response = + given() + .auth() + .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD) + .when() + .get(SECURE_PATH + "/devfile/workspace123"); + + assertEquals(response.getStatusCode(), 403); + } + + @Filter + public static class EnvironmentFilter implements RequestFilter { + public void doFilter(GenericContainerRequest request) { + EnvironmentContext.getCurrent().setSubject(subject); + } + } +} diff --git a/multiuser/permission/che-multiuser-permission-devfile/src/test/resources/logback-test.xml b/multiuser/permission/che-multiuser-permission-devfile/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..e7bf50602e3 --- /dev/null +++ b/multiuser/permission/che-multiuser-permission-devfile/src/test/resources/logback-test.xml @@ -0,0 +1,26 @@ + + + + + + + %-41(%date[%.15thread]) %-45([%-5level] [%.30logger{30} %L]) - %msg%n + + + + + + + diff --git a/multiuser/permission/pom.xml b/multiuser/permission/pom.xml index 9bb03c525b7..d2d3a8181c0 100644 --- a/multiuser/permission/pom.xml +++ b/multiuser/permission/pom.xml @@ -25,6 +25,7 @@ Che Multiuser :: Permissions Parent che-multiuser-permission-user + che-multiuser-permission-devfile che-multiuser-permission-workspace che-multiuser-permission-workspace-activity che-multiuser-permission-factory diff --git a/pom.xml b/pom.xml index 653b5c52b47..41fb6ec597b 100644 --- a/pom.xml +++ b/pom.xml @@ -325,6 +325,11 @@ ${che.version} sources + + org.eclipse.che.core + che-core-api-devfile + ${che.version} + org.eclipse.che.core che-core-api-dto @@ -1062,6 +1067,11 @@ ${che.version} sources + + org.eclipse.che.multiuser + che-multiuser-permission-devfile + ${che.version} + org.eclipse.che.multiuser che-multiuser-permission-factory diff --git a/wsmaster/che-core-api-devfile/pom.xml b/wsmaster/che-core-api-devfile/pom.xml new file mode 100644 index 00000000000..cea2e310c91 --- /dev/null +++ b/wsmaster/che-core-api-devfile/pom.xml @@ -0,0 +1,181 @@ + + + + 4.0.0 + + che-master-parent + org.eclipse.che.core + 6.16.0-SNAPSHOT + + che-core-api-devfile + jar + Che Core :: API :: Devfile + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + com.github.java-json-tools + jackson-coreutils + + + com.github.java-json-tools + json-schema-core + + + com.github.java-json-tools + json-schema-validator + + + com.google.guava + guava + + + io.swagger + swagger-annotations + + + javax.inject + javax.inject + + + org.eclipse.che.core + che-core-api-core + + + org.eclipse.che.core + che-core-api-model + + + org.eclipse.che.core + che-core-api-workspace + + + org.eclipse.che.core + che-core-api-workspace-shared + + + org.eclipse.che.core + che-core-commons-lang + + + javax.ws.rs + javax.ws.rs-api + provided + + + com.jayway.restassured + rest-assured + test + + + org.eclipse.che.core + che-core-api-account + test + + + org.eclipse.che.core + che-core-api-dto + test + + + org.eclipse.che.core + che-core-commons-json + test + + + org.everrest + everrest-assured + test + + + org.everrest + everrest-core + test + + + org.mockito + mockito-core + test + + + org.mockito + mockito-testng + test + + + org.testng + testng + test + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 1.4 + + + test + generate-sources + + add-source + + + + ${basedir}/target/java-gen + + + + + + + org.jsonschema2pojo + jsonschema2pojo-maven-plugin + 0.5.1 + + + + generate + + + + + ${basedir}/src/main/resources/schema + org.eclipse.che.api.devfile.model + false + false + false + true + true + + + + + diff --git a/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/Constants.java b/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/Constants.java new file mode 100644 index 00000000000..58e83df0deb --- /dev/null +++ b/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/Constants.java @@ -0,0 +1,29 @@ +/* + * 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.api.devfile.server; + +public class Constants { + + public static final String SCHEMA_LOCATION = "schema/devfile.json"; + + public static final String CURRENT_SPEC_VERSION = "0.0.1"; + + /** + * Workspace attribute which contains comma-separated list of mappings of tool id to its name + * Example value: + * + *
+   * eclipse/maven-jdk8:1.0.0=mvn-stack,eclipse/theia:0.0.3=theia-ide,eclipse/theia-jdtls:0.0.3=jdt.ls
+   * 
+ */ + public static final String ALIASES_WORKSPACE_ATTRIBUTE_NAME = "toolsAliases"; +} diff --git a/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/DevfileConverter.java b/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/DevfileConverter.java new file mode 100644 index 00000000000..a8627a99082 --- /dev/null +++ b/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/DevfileConverter.java @@ -0,0 +1,221 @@ +/* + * 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.api.devfile.server; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Strings.isNullOrEmpty; +import static java.lang.String.format; +import static org.eclipse.che.api.core.model.workspace.config.Command.WORKING_DIRECTORY_ATTRIBUTE; +import static org.eclipse.che.api.devfile.server.Constants.ALIASES_WORKSPACE_ATTRIBUTE_NAME; +import static org.eclipse.che.api.devfile.server.Constants.CURRENT_SPEC_VERSION; +import static org.eclipse.che.api.workspace.shared.Constants.WORKSPACE_TOOLING_EDITOR_ATTRIBUTE; +import static org.eclipse.che.api.workspace.shared.Constants.WORKSPACE_TOOLING_PLUGINS_ATTRIBUTE; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.StringJoiner; +import java.util.stream.Collectors; +import org.eclipse.che.api.devfile.model.Action; +import org.eclipse.che.api.devfile.model.Command; +import org.eclipse.che.api.devfile.model.Devfile; +import org.eclipse.che.api.devfile.model.Project; +import org.eclipse.che.api.devfile.model.Source; +import org.eclipse.che.api.devfile.model.Tool; +import org.eclipse.che.api.workspace.server.model.impl.CommandImpl; +import org.eclipse.che.api.workspace.server.model.impl.ProjectConfigImpl; +import org.eclipse.che.api.workspace.server.model.impl.SourceStorageImpl; +import org.eclipse.che.api.workspace.server.model.impl.WorkspaceConfigImpl; + +/** Helps to convert devfile into workspace config and back. */ +public class DevfileConverter { + + public Devfile workspaceToDevFile(WorkspaceConfigImpl wsConfig) throws WorkspaceExportException { + + if (!isNullOrEmpty(wsConfig.getDefaultEnv()) || !wsConfig.getEnvironments().isEmpty()) { + throw new WorkspaceExportException( + format( + "Workspace %s cannot be converted to devfile since it is contains environments (which have no equivalent in devfile model)", + wsConfig.getName())); + } + + Devfile devFile = + new Devfile().withSpecVersion(CURRENT_SPEC_VERSION).withName(wsConfig.getName()); + + // Manage projects + List projects = new ArrayList<>(); + wsConfig + .getProjects() + .forEach(projectConfig -> projects.add(projectConfigToDevProject(projectConfig))); + devFile.setProjects(projects); + + // Manage commands + Map toolsIdToName = parseTools(wsConfig); + List commands = new ArrayList<>(); + wsConfig + .getCommands() + .forEach(command -> commands.add(commandImplToDevCommand(command, toolsIdToName))); + devFile.setCommands(commands); + + // Manage tools + List tools = new ArrayList<>(); + for (Map.Entry entry : wsConfig.getAttributes().entrySet()) { + if (entry.getKey().equals(WORKSPACE_TOOLING_EDITOR_ATTRIBUTE)) { + String editorId = entry.getValue(); + Tool editorTool = + new Tool() + .withType("cheEditor") + .withId(editorId) + .withName(toolsIdToName.getOrDefault(editorId, editorId)); + tools.add(editorTool); + } else if (entry.getKey().equals(WORKSPACE_TOOLING_PLUGINS_ATTRIBUTE)) { + for (String pluginId : entry.getValue().split(",")) { + Tool pluginTool = + new Tool() + .withId(pluginId) + .withType("chePlugin") + .withName(toolsIdToName.getOrDefault(pluginId, pluginId)); + tools.add(pluginTool); + } + } + } + devFile.setTools(tools); + return devFile; + } + + public WorkspaceConfigImpl devFileToWorkspaceConfig(Devfile devFile) + throws DevfileFormatException { + validateCurrentVersion(devFile); + WorkspaceConfigImpl config = new WorkspaceConfigImpl(); + + config.setName(devFile.getName()); + + // Manage projects + List projects = new ArrayList<>(); + devFile.getProjects().forEach(project -> projects.add(devProjectToProjectConfig(project))); + config.setProjects(projects); + + // Manage tools + Map attributes = new HashMap<>(); + StringJoiner pluginsStringJoiner = new StringJoiner(","); + StringJoiner toolIdToNameMappingStringJoiner = new StringJoiner(","); + for (Tool tool : devFile.getTools()) { + switch (tool.getType()) { + case "cheEditor": + attributes.put(WORKSPACE_TOOLING_EDITOR_ATTRIBUTE, tool.getId()); + break; + case "chePlugin": + pluginsStringJoiner.add(tool.getId()); + break; + default: + throw new DevfileFormatException( + format("Unsupported tool %s type provided: %s", tool.getName(), tool.getType())); + } + toolIdToNameMappingStringJoiner.add(tool.getId() + "=" + tool.getName()); + } + if (pluginsStringJoiner.length() > 0) { + attributes.put(WORKSPACE_TOOLING_PLUGINS_ATTRIBUTE, pluginsStringJoiner.toString()); + } + if (toolIdToNameMappingStringJoiner.length() > 0) { + attributes.put(ALIASES_WORKSPACE_ATTRIBUTE_NAME, toolIdToNameMappingStringJoiner.toString()); + } + config.setAttributes(attributes); + + // Manage commands + List commands = new ArrayList<>(); + devFile + .getCommands() + .forEach(command -> commands.addAll(devCommandToCommandImpls(devFile, command))); + config.setCommands(commands); + return config; + } + + private List devCommandToCommandImpls(Devfile devFile, Command devCommand) { + List commands = new ArrayList<>(); + for (Action devAction : devCommand.getActions()) { + CommandImpl command = new CommandImpl(); + command.setName(devCommand.getName() + ":" + devAction.getTool()); + command.setType(devAction.getType()); + command.setCommandLine(devAction.getCommand()); + if (devAction.getWorkdir() != null) { + command.getAttributes().put(WORKING_DIRECTORY_ATTRIBUTE, devAction.getWorkdir()); + } + Optional toolOfCommand = + devFile + .getTools() + .stream() + .filter(tool -> tool.getName().equals(devAction.getTool())) + .findFirst(); + if (toolOfCommand.isPresent() && !isNullOrEmpty(toolOfCommand.get().getId())) { + command.getAttributes().put("pluginId", toolOfCommand.get().getId()); + } + if (devCommand.getAttributes() != null) { + command.getAttributes().putAll(devCommand.getAttributes()); + } + commands.add(command); + } + return commands; + } + + private Command commandImplToDevCommand(CommandImpl command, Map toolsIdToName) { + Command devCommand = new Command().withName(command.getName()); + Action action = new Action().withCommand(command.getCommandLine()).withType(command.getType()); + String workingDir = command.getAttributes().get(WORKING_DIRECTORY_ATTRIBUTE); + if (!isNullOrEmpty(workingDir)) { + action.setWorkdir(workingDir); + } + action.setTool(toolsIdToName.getOrDefault(command.getAttributes().get("pluginId"), "")); + devCommand.getActions().add(action); + devCommand.setAttributes(command.getAttributes()); + // Remove internal attributes + devCommand.getAttributes().remove(WORKING_DIRECTORY_ATTRIBUTE); + devCommand.getAttributes().remove("pluginId"); + return devCommand; + } + + private Project projectConfigToDevProject(ProjectConfigImpl projectConfig) { + Source source = + new Source() + .withType(projectConfig.getSource().getType()) + .withLocation(projectConfig.getSource().getLocation()); + return new Project().withName(projectConfig.getName()).withSource(source); + } + + private ProjectConfigImpl devProjectToProjectConfig(Project devProject) { + ProjectConfigImpl projectConfig = new ProjectConfigImpl(); + projectConfig.setName(devProject.getName()); + projectConfig.setPath("/" + projectConfig.getName()); + SourceStorageImpl sourceStorage = new SourceStorageImpl(); + sourceStorage.setType(devProject.getSource().getType()); + sourceStorage.setLocation(devProject.getSource().getLocation()); + projectConfig.setSource(sourceStorage); + return projectConfig; + } + + private Map parseTools(WorkspaceConfigImpl wsConfig) { + String aliasesString = + firstNonNull(wsConfig.getAttributes().get(ALIASES_WORKSPACE_ATTRIBUTE_NAME), ""); + return Arrays.stream(aliasesString.split(",")) + .map(s -> s.split("=", 2)) + .collect(Collectors.toMap(arr -> arr[0], arr -> arr[1])); + } + + private static void validateCurrentVersion(Devfile devFile) throws DevfileFormatException { + if (!CURRENT_SPEC_VERSION.equals(devFile.getSpecVersion())) { + throw new DevfileFormatException( + format("Provided Devfile has unsupported version %s", devFile.getSpecVersion())); + } + } +} diff --git a/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/DevfileFormatException.java b/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/DevfileFormatException.java new file mode 100644 index 00000000000..1fc4c6fbd0c --- /dev/null +++ b/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/DevfileFormatException.java @@ -0,0 +1,20 @@ +/* + * 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.api.devfile.server; + +/** Thrown when devfile schema or integrity validation is failed. */ +public class DevfileFormatException extends Exception { + + public DevfileFormatException(String formatError) { + super(formatError); + } +} diff --git a/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/DevfileSchemaProvider.java b/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/DevfileSchemaProvider.java new file mode 100644 index 00000000000..22e29114e01 --- /dev/null +++ b/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/DevfileSchemaProvider.java @@ -0,0 +1,46 @@ +/* + * 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.api.devfile.server; + +import static org.eclipse.che.api.devfile.server.Constants.SCHEMA_LOCATION; +import static org.eclipse.che.commons.lang.IoUtil.getResource; +import static org.eclipse.che.commons.lang.IoUtil.readAndCloseQuietly; + +import com.fasterxml.jackson.databind.JsonNode; +import com.github.fge.jackson.JsonLoader; +import java.io.IOException; +import java.lang.ref.SoftReference; +import javax.inject.Singleton; + +/** Loads a schema content and stores it in soft reference. */ +@Singleton +public class DevfileSchemaProvider { + + private SoftReference schemaRef = new SoftReference<>(null); + + public String getSchemaContent() throws IOException { + String schema = schemaRef.get(); + if (schema == null) { + schema = loadFile(); + schemaRef = new SoftReference<>(schema); + } + return schema; + } + + public JsonNode getJsoneNode() throws IOException { + return JsonLoader.fromString(getSchemaContent()); + } + + private String loadFile() throws IOException { + return readAndCloseQuietly(getResource(SCHEMA_LOCATION)); + } +} diff --git a/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/DevfileSchemaValidator.java b/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/DevfileSchemaValidator.java new file mode 100644 index 00000000000..00b827c3e46 --- /dev/null +++ b/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/DevfileSchemaValidator.java @@ -0,0 +1,65 @@ +/* + * 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.api.devfile.server; + +import static java.lang.String.format; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.github.fge.jsonschema.core.exceptions.ProcessingException; +import com.github.fge.jsonschema.core.report.LogLevel; +import com.github.fge.jsonschema.core.report.ProcessingReport; +import com.github.fge.jsonschema.main.JsonSchemaFactory; +import com.github.fge.jsonschema.main.JsonValidator; +import java.io.IOException; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import javax.inject.Inject; +import javax.inject.Singleton; + +/** Validates YAML devfile content against given JSON schema. */ +@Singleton +public class DevfileSchemaValidator { + + private JsonValidator validator; + private ObjectMapper yamlReader; + private DevfileSchemaProvider schemaProvider; + + @Inject + DevfileSchemaValidator(DevfileSchemaProvider schemaProvider) { + this.schemaProvider = schemaProvider; + this.validator = JsonSchemaFactory.byDefault().getValidator(); + this.yamlReader = new ObjectMapper(new YAMLFactory()); + } + + JsonNode validateBySchema(String yamlContent, boolean verbose) throws DevfileFormatException { + ProcessingReport report; + JsonNode data; + try { + data = yamlReader.readTree(yamlContent); + report = validator.validate(schemaProvider.getJsoneNode(), data); + } catch (IOException | ProcessingException e) { + throw new DevfileFormatException("Unable to validate Devfile. Error: " + e.getMessage()); + } + if (!report.isSuccess()) { + String error = + StreamSupport.stream(report.spliterator(), false) + .filter(m -> m.getLogLevel() == LogLevel.ERROR || m.getLogLevel() == LogLevel.FATAL) + .map(message -> verbose ? message.asJson().toString() : message.getMessage()) + .collect(Collectors.joining(", ", "[", "]")); + throw new DevfileFormatException( + format("Devfile schema validation failed. Errors: %s", error)); + } + return data; + } +} diff --git a/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/DevfileService.java b/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/DevfileService.java new file mode 100644 index 00000000000..705ce65bdcb --- /dev/null +++ b/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/DevfileService.java @@ -0,0 +1,213 @@ +/* + * 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.api.devfile.server; + +import static java.util.Collections.emptyMap; +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.eclipse.che.api.workspace.server.DtoConverter.asDto; +import static org.eclipse.che.api.workspace.server.WorkspaceKeyValidator.validateKey; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import io.swagger.annotations.Example; +import io.swagger.annotations.ExampleProperty; +import java.io.IOException; +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; +import org.eclipse.che.api.core.BadRequestException; +import org.eclipse.che.api.core.ConflictException; +import org.eclipse.che.api.core.NotFoundException; +import org.eclipse.che.api.core.ServerException; +import org.eclipse.che.api.core.ValidationException; +import org.eclipse.che.api.core.rest.Service; +import org.eclipse.che.api.devfile.model.Devfile; +import org.eclipse.che.api.workspace.server.WorkspaceLinksGenerator; +import org.eclipse.che.api.workspace.server.WorkspaceManager; +import org.eclipse.che.api.workspace.server.model.impl.WorkspaceConfigImpl; +import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl; +import org.eclipse.che.api.workspace.shared.dto.WorkspaceDto; +import org.eclipse.che.commons.env.EnvironmentContext; + +@Api(value = "/devfile", description = "Devfile REST API") +@Path("/devfile") +public class DevfileService extends Service { + + private WorkspaceLinksGenerator linksGenerator; + private DevfileSchemaValidator schemaValidator; + private DevfileSchemaProvider schemaCachedProvider; + private WorkspaceManager workspaceManager; + private ObjectMapper objectMapper; + private DevfileConverter devfileConverter; + + @Inject + public DevfileService( + WorkspaceLinksGenerator linksGenerator, + DevfileSchemaValidator schemaValidator, + DevfileSchemaProvider schemaCachedProvider, + WorkspaceManager workspaceManager) { + this.linksGenerator = linksGenerator; + this.schemaValidator = schemaValidator; + this.schemaCachedProvider = schemaCachedProvider; + this.workspaceManager = workspaceManager; + this.objectMapper = new ObjectMapper(new YAMLFactory()); + this.devfileConverter = new DevfileConverter(); + } + + /** + * Retrieves the json schema. + * + * @return json schema + */ + @GET + @Produces(APPLICATION_JSON) + @ApiOperation(value = "Retrieves current version of devfile JSON schema") + @ApiResponses({ + @ApiResponse(code = 200, message = "The schema successfully retrieved"), + @ApiResponse(code = 500, message = "Internal server error occurred") + }) + public Response getSchema() throws ServerException { + try { + return Response.ok(schemaCachedProvider.getSchemaContent()).build(); + } catch (IOException e) { + throw new ServerException(e); + } + } + + /** + * Creates workspace from provided devfile + * + * @param data devfile content + * @param verbose return more explained validation error messages if any + * @return created workspace configuration + */ + @POST + @Consumes({"text/yaml", "text/x-yaml", "application/yaml", "application/json"}) + @Produces(APPLICATION_JSON) + @ApiOperation( + value = "Create a new workspace based on provided devfile", + notes = + "This operation can be performed only by authorized user," + + "this user will be the owner of the created workspace", + response = WorkspaceDto.class) + @ApiResponses({ + @ApiResponse(code = 200, message = "The workspace successfully created"), + @ApiResponse( + code = 400, + message = + "Provided devfile syntactically incorrect, doesn't match with actual schema or has integrity violations"), + @ApiResponse(code = 403, message = "The user does not have access to create a new workspace"), + @ApiResponse(code = 500, message = "Internal server error occurred") + }) + public Response createFromYaml( + String data, + @ApiParam(value = "Provide extended validation messages") + @DefaultValue("false") + @QueryParam("verbose") + boolean verbose) + throws ServerException, ConflictException, NotFoundException, ValidationException, + BadRequestException { + + Devfile devFile; + WorkspaceConfigImpl workspaceConfig; + try { + JsonNode parsed = schemaValidator.validateBySchema(data, verbose); + devFile = objectMapper.treeToValue(parsed, Devfile.class); + workspaceConfig = devfileConverter.devFileToWorkspaceConfig(devFile); + } catch (IOException e) { + throw new ServerException(e.getMessage()); + } catch (DevfileFormatException e) { + throw new BadRequestException(e.getMessage()); + } + + final String namespace = EnvironmentContext.getCurrent().getSubject().getUserName(); + WorkspaceImpl workspace = + workspaceManager.createWorkspace(findAvailableName(workspaceConfig), namespace, emptyMap()); + return Response.status(201) + .entity(asDto(workspace).withLinks(linksGenerator.genLinks(workspace, getServiceContext()))) + .build(); + } + + /** + * Generates the devfile based on an existing workspace. Key is workspace id or + * namespace/workspace_name + * + * @see WorkspaceManager#getByKey(String) + */ + @GET + @Path("/{key:.*}") + @Produces("text/yml") + @ApiOperation( + value = "Generates the devfile from given workspace", + notes = + "This operation can be performed only by authorized user," + + "this user must be the owner of the exported workspace") + @ApiResponses({ + @ApiResponse(code = 200, message = "The workspace successfully exported"), + @ApiResponse(code = 403, message = "The user does not have access to create a new workspace"), + @ApiResponse(code = 500, message = "Internal server error occurred") + }) + public Response createFromWorkspace( + @ApiParam( + value = "Composite key", + examples = + @Example({ + @ExampleProperty("workspace12345678"), + @ExampleProperty("namespace/workspace_name"), + @ExampleProperty("namespace_part_1/namespace_part_2/workspace_name") + })) + @PathParam("key") + String key) + throws NotFoundException, ServerException, BadRequestException, ConflictException { + validateKey(key); + WorkspaceImpl workspace = workspaceManager.getWorkspace(key); + try { + Devfile workspaceDevFile = devfileConverter.workspaceToDevFile(workspace.getConfig()); + // Write object as YAML + return Response.ok().entity(objectMapper.writeValueAsString(workspaceDevFile)).build(); + } catch (JsonProcessingException e) { + throw new ServerException(e.getMessage(), e); + } catch (WorkspaceExportException e) { + throw new ConflictException(e.getMessage()); + } + } + + private WorkspaceConfigImpl findAvailableName(WorkspaceConfigImpl config) throws ServerException { + String nameCandidate = config.getName(); + String namespace = EnvironmentContext.getCurrent().getSubject().getUserName(); + int counter = 0; + while (true) { + try { + workspaceManager.getWorkspace(nameCandidate, namespace); + nameCandidate = config.getName() + "_" + ++counter; + } catch (NotFoundException nf) { + config.setName(nameCandidate); + break; + } + } + return config; + } +} diff --git a/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/WorkspaceExportException.java b/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/WorkspaceExportException.java new file mode 100644 index 00000000000..e030c1d3da2 --- /dev/null +++ b/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/WorkspaceExportException.java @@ -0,0 +1,20 @@ +/* + * 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.api.devfile.server; + +/** Thrown when workspace can not be exported into devfile by some reason. */ +public class WorkspaceExportException extends Exception { + + public WorkspaceExportException(String error) { + super(error); + } +} diff --git a/wsmaster/che-core-api-devfile/src/main/resources/schema/devfile.json b/wsmaster/che-core-api-devfile/src/main/resources/schema/devfile.json new file mode 100644 index 00000000000..9a3aeaf77c3 --- /dev/null +++ b/wsmaster/che-core-api-devfile/src/main/resources/schema/devfile.json @@ -0,0 +1,180 @@ +{ + "definitions": { + "attributes" : { + "id": "propertyList", + "type": "object", + "javaType": "java.util.Map" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "The Root Schema of DevFile object", + "required": [ + "specVersion", + "name", + "projects", + "tools", + "commands" + ], + "properties": { + "specVersion": { + "type": "string", + "title": "Devfile Schema Specification Version", + "examples": [ + "0.0.1" + ] + }, + "name": { + "type": "string", + "title": "Devfile Name", + "examples": [ + "petclinic-dev-environment" + ] + }, + "projects": { + "type": "array", + "title": "The Projects Schema", + "description" : "Description of the project sources location and type", + "items": { + "type": "object", + "required": [ + "name", + "source" + ], + "properties": { + "name": { + "type": "string", + "examples": [ + "petclinic" + ] + }, + "source": { + "type": "object", + "title": "The Project Source Schema", + "required": [ + "type", + "location" + ], + "properties": { + "type": { + "type": "string", + "description": "Project-s source type.", + "examples": [ + "git", + "github", + "zip" + ] + }, + "location": { + "type": "string", + "description": "Project-s source location address. Should be URL for git and github located projects, and file:// for zip.", + "examples": [ + "git@github.com:spring-projects/spring-petclinic.git" + ] + } + } + } + } + } + }, + "tools": { + "type": "array", + "title": "The Tools Schema", + "items": { + "type": "object", + "required": [ + "name", + "type", + "id" + ], + "properties": { + "name": { + "type": "string", + "examples": [ + "mvn-stack" + ] + }, + "type": { + "description": "Describes type or tool, e.g. whether it is and plugin or editor or other type", + "type": "string", + "examples": [ + "chePlugin", + "cheEditor" + ] + }, + "id": { + "type": "string", + "description": "Describes the tool FQN", + "examples": [ + "eclipse/maven-jdk8:1.0.0" + ] + } + } + } + }, + "commands": { + "type": "array", + "title": "The Commands Schema", + "items": { + "type": "object", + "required": [ + "name", + "actions" + ], + "properties": { + "name": { + "type": "string", + "examples": [ + "build" + ] + }, + "attributes": { + "$ref": "#/definitions/attributes" + }, + "actions": { + "type": "array", + "title": "The Command Actions Schema", + "items": { + "type": "object", + "required": [ + "type", + "tool", + "command" + ], + "properties": { + "type": { + "description": "Describes action type", + "type": "string", + "examples": [ + "exec" + ] + }, + "tool": { + "type": "string", + "description": "Describes tool to which given action relates", + "examples": [ + "mvn-stack" + ] + }, + "command": { + "type": "string", + "description": "The actual action command-line string", + "examples": [ + "mvn package" + ] + }, + "workdir": { + "type": "string", + "description": "Working directory where the schema should be executed", + "examples": [ + "/projects/spring-petclinic" + ] + } + } + } + } + } + } + } + } +} diff --git a/wsmaster/che-core-api-devfile/src/test/java/org/eclipse/che/api/devfile/server/DevfileConverterTest.java b/wsmaster/che-core-api-devfile/src/test/java/org/eclipse/che/api/devfile/server/DevfileConverterTest.java new file mode 100644 index 00000000000..e9a5f62760e --- /dev/null +++ b/wsmaster/che-core-api-devfile/src/test/java/org/eclipse/che/api/devfile/server/DevfileConverterTest.java @@ -0,0 +1,138 @@ +/* + * 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.api.devfile.server; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import org.eclipse.che.api.devfile.model.Action; +import org.eclipse.che.api.devfile.model.Command; +import org.eclipse.che.api.devfile.model.Devfile; +import org.eclipse.che.api.devfile.model.Project; +import org.eclipse.che.api.devfile.model.Tool; +import org.eclipse.che.api.workspace.server.model.impl.EnvironmentImpl; +import org.eclipse.che.api.workspace.server.model.impl.WorkspaceConfigImpl; +import org.eclipse.che.commons.json.JsonHelper; +import org.testng.annotations.Test; +import org.testng.reporters.Files; + +public class DevfileConverterTest { + + private ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); + private DevfileConverter devfileConverter = new DevfileConverter(); + + @Test + public void shouldBuildWorkspaceConfigFromYamlDevFile() throws Exception { + + String yamlContent = + Files.readFile(getClass().getClassLoader().getResourceAsStream("devfile.yaml")); + + Devfile devFile = objectMapper.readValue(yamlContent, Devfile.class); + + WorkspaceConfigImpl wsConfigImpl = devfileConverter.devFileToWorkspaceConfig(devFile); + + String jsonContent = + Files.readFile(getClass().getClassLoader().getResourceAsStream("workspace_config.json")); + + assertEquals(wsConfigImpl, JsonHelper.fromJson(jsonContent, WorkspaceConfigImpl.class, null)); + } + + @Test + public void shouldBuildYamlDevFileFromWorkspaceConfig() throws Exception { + + String jsonContent = + Files.readFile(getClass().getClassLoader().getResourceAsStream("workspace_config.json")); + WorkspaceConfigImpl workspaceConfig = + JsonHelper.fromJson(jsonContent, WorkspaceConfigImpl.class, null); + Devfile devFile = devfileConverter.workspaceToDevFile(workspaceConfig); + + String yamlContent = + Files.readFile(getClass().getClassLoader().getResourceAsStream("devfile.yaml")); + + Devfile expectedDevFile = objectMapper.readValue(yamlContent, Devfile.class); + + // Recursively compare + assertEquals(devFile.getSpecVersion(), expectedDevFile.getSpecVersion()); + assertEquals(devFile.getName(), expectedDevFile.getName()); + assertEquals(devFile.getProjects().size(), expectedDevFile.getProjects().size()); + for (Project project : devFile.getProjects()) { + Project expectedProject = + expectedDevFile + .getProjects() + .stream() + .filter(project1 -> project1.getName().equals(project.getName())) + .findFirst() + .get(); + assertEquals(project.getSource().getType(), expectedProject.getSource().getType()); + assertEquals(project.getSource().getLocation(), expectedProject.getSource().getLocation()); + } + + assertEquals(devFile.getCommands().size(), expectedDevFile.getCommands().size()); + for (Command command : devFile.getCommands()) { + Command expectedCommand = + expectedDevFile + .getCommands() + .stream() + .filter(command1 -> command1.getName().equals(command.getName().split(":")[0])) + .findFirst() + .get(); + for (Action action : command.getActions()) { + Action expectedAction = + expectedCommand + .getActions() + .stream() + .filter(action1 -> action1.getTool().equals(action.getTool())) + .findFirst() + .get(); + assertEquals(action.getCommand(), expectedAction.getCommand()); + assertEquals(action.getType(), expectedAction.getType()); + assertEquals(action.getWorkdir(), expectedAction.getWorkdir()); + } + if (command.getAttributes() != null && expectedCommand.getAttributes() != null) { + assertTrue( + command + .getAttributes() + .entrySet() + .containsAll(expectedCommand.getAttributes().entrySet())); + } + } + + assertEquals(devFile.getTools().size(), expectedDevFile.getTools().size()); + for (Tool tool : devFile.getTools()) { + Tool expectedTool = + expectedDevFile + .getTools() + .stream() + .filter(tool1 -> tool1.getName().equals(tool.getName())) + .findFirst() + .get(); + assertEquals(tool.getId(), expectedTool.getId()); + assertEquals(tool.getType(), expectedTool.getType()); + } + } + + @Test( + expectedExceptions = WorkspaceExportException.class, + expectedExceptionsMessageRegExp = + "Workspace .* cannot be converted to devfile since it is contains environments \\(which have no equivalent in devfile model\\)") + public void shouldThrowExceptionWhenWorkspaceHasEnvironments() throws Exception { + String jsonContent = + Files.readFile(getClass().getClassLoader().getResourceAsStream("workspace_config.json")); + WorkspaceConfigImpl workspaceConfig = + JsonHelper.fromJson(jsonContent, WorkspaceConfigImpl.class, null); + workspaceConfig.getEnvironments().put("env1", new EnvironmentImpl()); + + devfileConverter.workspaceToDevFile(workspaceConfig); + } +} diff --git a/wsmaster/che-core-api-devfile/src/test/java/org/eclipse/che/api/devfile/server/DevfileSchemaValidatorTest.java b/wsmaster/che-core-api-devfile/src/test/java/org/eclipse/che/api/devfile/server/DevfileSchemaValidatorTest.java new file mode 100644 index 00000000000..b5088af5371 --- /dev/null +++ b/wsmaster/che-core-api-devfile/src/test/java/org/eclipse/che/api/devfile/server/DevfileSchemaValidatorTest.java @@ -0,0 +1,45 @@ +/* + * 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.api.devfile.server; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import org.testng.reporters.Files; + +public class DevfileSchemaValidatorTest { + + private DevfileSchemaValidator schemaValidator; + + @BeforeClass + public void setUp() throws Exception { + schemaValidator = new DevfileSchemaValidator(new DevfileSchemaProvider()); + } + + @Test + public void shouldValidateCorrectYamlBySchema() throws Exception { + String devFileYamlContent = + Files.readFile(getClass().getClassLoader().getResourceAsStream("devfile.yaml")); + // when + schemaValidator.validateBySchema(devFileYamlContent, false); + } + + @Test( + expectedExceptions = DevfileFormatException.class, + expectedExceptionsMessageRegExp = + "Devfile schema validation failed. Errors: \\[object has missing required properties \\(\\[\"name\"\\]\\)\\]$") + public void shouldValidateIncorrectYamlBySchema() throws Exception { + String devFileYamlContent = + Files.readFile(getClass().getClassLoader().getResourceAsStream("devfile_bad.yaml")); + // when + schemaValidator.validateBySchema(devFileYamlContent, false); + } +} diff --git a/wsmaster/che-core-api-devfile/src/test/java/org/eclipse/che/api/devfile/server/DevfileServiceTest.java b/wsmaster/che-core-api-devfile/src/test/java/org/eclipse/che/api/devfile/server/DevfileServiceTest.java new file mode 100644 index 00000000000..2198bbff768 --- /dev/null +++ b/wsmaster/che-core-api-devfile/src/test/java/org/eclipse/che/api/devfile/server/DevfileServiceTest.java @@ -0,0 +1,174 @@ +/* + * 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.api.devfile.server; + +import static com.jayway.restassured.RestAssured.given; +import static org.everrest.assured.JettyHttpServer.ADMIN_USER_NAME; +import static org.everrest.assured.JettyHttpServer.ADMIN_USER_PASSWORD; +import static org.everrest.assured.JettyHttpServer.SECURE_PATH; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.jayway.restassured.http.ContentType; +import com.jayway.restassured.response.Response; +import java.io.IOException; +import org.eclipse.che.account.spi.AccountImpl; +import org.eclipse.che.api.core.NotFoundException; +import org.eclipse.che.api.core.model.workspace.WorkspaceConfig; +import org.eclipse.che.api.core.model.workspace.WorkspaceStatus; +import org.eclipse.che.api.devfile.model.Devfile; +import org.eclipse.che.api.workspace.server.WorkspaceLinksGenerator; +import org.eclipse.che.api.workspace.server.WorkspaceManager; +import org.eclipse.che.api.workspace.server.model.impl.WorkspaceConfigImpl; +import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl; +import org.eclipse.che.commons.env.EnvironmentContext; +import org.eclipse.che.commons.json.JsonHelper; +import org.eclipse.che.commons.json.JsonParseException; +import org.eclipse.che.commons.subject.Subject; +import org.eclipse.che.commons.subject.SubjectImpl; +import org.everrest.assured.EverrestJetty; +import org.everrest.core.Filter; +import org.everrest.core.GenericContainerRequest; +import org.everrest.core.RequestFilter; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import org.testng.reporters.Files; + +@Listeners({EverrestJetty.class, MockitoTestNGListener.class}) +public class DevfileServiceTest { + + @Mock private WorkspaceLinksGenerator linksGenerator; + + @Mock private WorkspaceManager workspaceManager; + @Mock private EnvironmentContext environmentContext; + private DevfileSchemaProvider schemaProvider = new DevfileSchemaProvider(); + private DevfileSchemaValidator validator; + + @SuppressWarnings("unused") + private static final EnvironmentFilter FILTER = new EnvironmentFilter(); + + private static final Subject SUBJECT = new SubjectImpl("user", "user123", "token", false); + + @SuppressWarnings("unused") + private DevfileService devFileService; + + @BeforeMethod + public void initService() throws IOException { + this.validator = spy(new DevfileSchemaValidator(schemaProvider)); + this.devFileService = + new DevfileService(linksGenerator, validator, schemaProvider, workspaceManager); + } + + @Test + public void shouldRetrieveSchema() throws Exception { + final Response response = + given() + .auth() + .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD) + .when() + .get(SECURE_PATH + "/devfile"); + + assertEquals(response.getStatusCode(), 200); + assertEquals(response.getBody().asString(), schemaProvider.getSchemaContent()); + } + + @Test + public void shouldAcceptDevFileAndFindAvailableName() throws Exception { + ArgumentCaptor captor = ArgumentCaptor.forClass(WorkspaceConfigImpl.class); + EnvironmentContext.setCurrent(environmentContext); + WorkspaceImpl ws = mock(WorkspaceImpl.class); + when(workspaceManager.createWorkspace(any(), eq(SUBJECT.getUserName()), anyMap())) + .thenReturn(createWorkspace(WorkspaceStatus.STOPPED)); + String yamlContent = + Files.readFile(getClass().getClassLoader().getResourceAsStream("devfile.yaml")); + when(workspaceManager.getWorkspace(anyString(), anyString())) + .thenAnswer( + invocation -> { + String wsname = invocation.getArgument(0); + if (wsname.equals("petclinic-dev-environment") + || wsname.equals("petclinic-dev-environment_1")) { + return ws; + } + throw new NotFoundException("ws not found"); + }); + + final Response response = + given() + .auth() + .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD) + .contentType(ContentType.JSON) + .body(yamlContent) + .when() + .post(SECURE_PATH + "/devfile"); + + assertEquals(response.getStatusCode(), 201); + verify(validator).validateBySchema(eq(yamlContent), eq(false)); + verify(workspaceManager).createWorkspace(captor.capture(), eq(SUBJECT.getUserName()), anyMap()); + assertEquals("petclinic-dev-environment_2", captor.getValue().getName()); + } + + @Test + public void shouldCreateDevFileFromWorkspace() throws Exception { + ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); + when(workspaceManager.getWorkspace(anyString())) + .thenReturn(createWorkspace(WorkspaceStatus.STOPPED)); + + final Response response = + given() + .auth() + .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD) + .when() + .get(SECURE_PATH + "/devfile/ws123456"); + + assertEquals(response.getStatusCode(), 200); + Devfile devFile = objectMapper.readValue(response.getBody().asString(), Devfile.class); + assertNotNull(devFile); + } + + private WorkspaceImpl createWorkspace(WorkspaceStatus status) + throws IOException, JsonParseException { + return WorkspaceImpl.builder() + .setConfig(createConfig()) + .generateId() + .setAccount(new AccountImpl("anyId", SUBJECT.getUserName(), "test")) + .setStatus(status) + .build(); + } + + private WorkspaceConfig createConfig() throws IOException, JsonParseException { + String jsonContent = + Files.readFile(getClass().getClassLoader().getResourceAsStream("workspace_config.json")); + return JsonHelper.fromJson(jsonContent, WorkspaceConfigImpl.class, null); + } + + @Filter + public static class EnvironmentFilter implements RequestFilter { + @Override + public void doFilter(GenericContainerRequest request) { + EnvironmentContext.getCurrent().setSubject(SUBJECT); + } + } +} diff --git a/wsmaster/che-core-api-devfile/src/test/resources/devfile.yaml b/wsmaster/che-core-api-devfile/src/test/resources/devfile.yaml new file mode 100644 index 00000000000..02bd815b982 --- /dev/null +++ b/wsmaster/che-core-api-devfile/src/test/resources/devfile.yaml @@ -0,0 +1,50 @@ +# +# 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 +# + +--- +specVersion: 0.0.1 +name: petclinic-dev-environment +projects: + - name: petclinic + source: + type: git + location: 'git@github.com:spring-projects/spring-petclinic.git' +tools: + - name: mvn-stack + type: chePlugin + id: eclipse/maven-jdk8:1.0.0 + - name: theia-ide + type: cheEditor + id: eclipse/theia:0.0.3 + - name: jdt.ls + type: chePlugin + id: eclipse/theia-jdtls:0.0.3 +commands: + - name: build + actions: + - type: exec + tool: mvn-stack + command: mvn package + workdir: /projects/spring-petclinic + - name: run + attributes: + runType: sequential + actions: + - type: exec + tool: mvn-stack + command: mvn spring-boot:run + workdir: /projects/spring-petclinic + - name: other + actions: + - type: exec + tool: jdt.ls + command: run.sh diff --git a/wsmaster/che-core-api-devfile/src/test/resources/devfile_bad.yaml b/wsmaster/che-core-api-devfile/src/test/resources/devfile_bad.yaml new file mode 100644 index 00000000000..fca1187910a --- /dev/null +++ b/wsmaster/che-core-api-devfile/src/test/resources/devfile_bad.yaml @@ -0,0 +1,30 @@ +# +# 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 +# + +--- +specVersion: 0.0.1 +name: petclinic-dev-environment +projects: + - name: petclinic + source: + type: git + location: 'git@github.com:spring-projects/spring-petclinic.git' +tools: + - type: chePlugin + id: eclipse/maven-jdk8:1.0.0 +commands: + - name: build + actions: + - type: exec + tool: mvn-stack + command: mvn package + workdir: /projects/spring-petclinic diff --git a/wsmaster/che-core-api-devfile/src/test/resources/logback-test.xml b/wsmaster/che-core-api-devfile/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..e8944d1d9c4 --- /dev/null +++ b/wsmaster/che-core-api-devfile/src/test/resources/logback-test.xml @@ -0,0 +1,35 @@ + + + + + + + %-41(%date[%.15thread]) %-45([%-5level] [%.30logger{30} %L]) - %msg%n + + + + + target/log/codenvy-factory-commons.log + + %-41(%date[%.15thread]) %-45([%-5level] [%.30logger{30} %L]) - %msg%n + + + + + + + + + diff --git a/wsmaster/che-core-api-devfile/src/test/resources/workspace_config.json b/wsmaster/che-core-api-devfile/src/test/resources/workspace_config.json new file mode 100644 index 00000000000..c6a85608c54 --- /dev/null +++ b/wsmaster/che-core-api-devfile/src/test/resources/workspace_config.json @@ -0,0 +1,51 @@ +{ + "projects": [ + { + "source": { + "location": "git@github.com:spring-projects/spring-petclinic.git", + "type": "git", + "parameters": {} + }, + "mixins": [], + "name": "petclinic", + "path": "/petclinic", + "attributes": {} + } + ], + "commands": [ + { + "commandLine": "mvn package", + "name": "build:mvn-stack", + "type": "exec", + "attributes": { + "pluginId": "eclipse/maven-jdk8:1.0.0", + "workingDir": "/projects/spring-petclinic" + } + }, + { + "commandLine": "mvn spring-boot:run", + "name": "run:mvn-stack", + "type": "exec", + "attributes": { + "pluginId": "eclipse/maven-jdk8:1.0.0", + "runType": "sequential", + "workingDir": "/projects/spring-petclinic" + } + }, + { + "commandLine": "run.sh", + "name": "other:jdt.ls", + "type": "exec", + "attributes": { + "pluginId": "eclipse/theia-jdtls:0.0.3" + } + } + ], + "environments": {}, + "name": "petclinic-dev-environment", + "attributes": { + "toolsAliases": "eclipse/maven-jdk8:1.0.0=mvn-stack,eclipse/theia:0.0.3=theia-ide,eclipse/theia-jdtls:0.0.3=jdt.ls", + "editor": "eclipse/theia:0.0.3", + "plugins": "eclipse/maven-jdk8:1.0.0,eclipse/theia-jdtls:0.0.3" + } +} diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceKeyValidator.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceKeyValidator.java new file mode 100644 index 00000000000..bbfb97a2109 --- /dev/null +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceKeyValidator.java @@ -0,0 +1,49 @@ +/* + * 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.api.workspace.server; + +import static java.lang.String.format; + +import org.eclipse.che.api.core.BadRequestException; + +/** Helper class to validate workspace composite keys. */ +public class WorkspaceKeyValidator { + + /** + * Checks that key consists either from workspaceId or username:workspace_name string. + * + * @param key key string to validate + * @throws BadRequestException if validation is failed + */ + public static void validateKey(String key) throws BadRequestException { + String[] parts = key.split(":", -1); // -1 is to prevent skipping trailing part + switch (parts.length) { + case 1: + { + return; // consider it's id + } + case 2: + { + if (parts[1].isEmpty()) { + throw new BadRequestException( + "Wrong composite key format - workspace name required to be set."); + } + break; + } + default: + { + throw new BadRequestException( + format("Wrong composite key %s. Format should be 'username:workspace_name'. ", key)); + } + } + } +} diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceService.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceService.java index 22e466acfc2..2266ff63a8d 100644 --- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceService.java +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceService.java @@ -17,6 +17,7 @@ import static java.util.stream.Collectors.toList; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; import static org.eclipse.che.api.workspace.server.DtoConverter.asDto; +import static org.eclipse.che.api.workspace.server.WorkspaceKeyValidator.validateKey; import static org.eclipse.che.api.workspace.shared.Constants.CHE_WORKSPACE_AUTO_START; import static org.eclipse.che.api.workspace.shared.Constants.CHE_WORKSPACE_PLUGIN_REGISTRY_URL_PROPERTY; @@ -733,33 +734,6 @@ private void requiredNotNull(Object object, String subject) throws BadRequestExc } } - /* - * Validate composite key. - * - */ - private void validateKey(String key) throws BadRequestException { - String[] parts = key.split(":", -1); // -1 is to prevent skipping trailing part - switch (parts.length) { - case 1: - { - return; // consider it's id - } - case 2: - { - if (parts[1].isEmpty()) { - throw new BadRequestException( - "Wrong composite key format - workspace name required to be set."); - } - break; - } - default: - { - throw new BadRequestException( - format("Wrong composite key %s. Format should be 'username:workspace_name'. ", key)); - } - } - } - private void relativizeRecipeLinks(WorkspaceConfigDto config) { if (config != null) { Map environments = config.getEnvironments(); diff --git a/wsmaster/pom.xml b/wsmaster/pom.xml index d5983b2acff..cf33afbf46f 100644 --- a/wsmaster/pom.xml +++ b/wsmaster/pom.xml @@ -28,6 +28,7 @@ che-core-api-installer che-core-api-auth-shared che-core-api-auth + che-core-api-devfile che-core-api-project-templates-shared che-core-api-project-templates che-core-api-workspace-shared