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