Devfile local features implenentation with schema validation and automated model build.

6.19.x
Max Shaposhnik 2018-12-06 16:19:21 +02:00 committed by GitHub
parent 1fe397f874
commit 3a14bacda1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1922 additions and 29 deletions

View File

@ -157,6 +157,10 @@
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-core</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-devfile</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-factory</artifactId>
@ -305,6 +309,10 @@
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-machine-authentication</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-permission-devfile</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-permission-factory</artifactId>

View File

@ -31,6 +31,8 @@ import org.eclipse.che.api.core.notification.RemoteSubscriptionStorage;
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 @@ public class WsMasterModule extends AbstractModule {
bind(org.eclipse.che.api.user.server.PreferencesService.class);
bind(org.eclipse.che.security.oauth.OAuthAuthenticationService.class);
bind(DevfileSchemaValidator.class);
bind(DevfileService.class);
MapBinder<String, String> stacks =
MapBinder.newMapBinder(
binder(), String.class, String.class, Names.named(StackLoader.CHE_PREDEFINED_STACKS));
@ -371,6 +376,7 @@ public class WsMasterModule extends AbstractModule {
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);

View File

@ -21,6 +21,12 @@ import java.util.Map;
*/
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

View File

@ -13,6 +13,7 @@ package org.eclipse.che.workspace.infrastructure.kubernetes.wsplugins;
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 @@ public class KubernetesPluginsToolingApplier implements ChePluginsApplier {
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;
}

View File

@ -17,6 +17,7 @@ import static java.util.Collections.emptyList;
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 class KubernetesPluginsToolingApplierTest {
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");
}

View File

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>che-multiuser-permission</artifactId>
<groupId>org.eclipse.che.multiuser</groupId>
<version>6.16.0-SNAPSHOT</version>
</parent>
<artifactId>che-multiuser-permission-devfile</artifactId>
<name>Che Multiuser :: Devfile Permissions</name>
<dependencies>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
</dependency>
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>javax.ws.rs-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-core</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-devfile</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-workspace</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-commons-test</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-api-permission</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-permission-workspace</artifactId>
</dependency>
<dependency>
<groupId>org.everrest</groupId>
<artifactId>everrest-core</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.restassured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-dto</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-factory-shared</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.everrest</groupId>
<artifactId>everrest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-testng</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<!-- compiler inlines constants, so it is impossible to find reference on dependency -->
<execution>
<id>analyze</id>
<configuration>
<ignoredDependencies>
<ignoreDependency>org.eclipse.che.multiuser:che-multiuser-api-permission</ignoreDependency>
<ignoreDependency>org.eclipse.che.core:che-core-api-devfile</ignoreDependency>
</ignoredDependencies>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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
-->
<configuration>
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%-41(%date[%.15thread]) %-45([%-5level] [%.30logger{30} %L]) - %msg%n</pattern>
</encoder>
</appender>
<root level="ERROR">
<appender-ref ref="stdout"/>
</root>
</configuration>

View File

@ -25,6 +25,7 @@
<name>Che Multiuser :: Permissions Parent</name>
<modules>
<module>che-multiuser-permission-user</module>
<module>che-multiuser-permission-devfile</module>
<module>che-multiuser-permission-workspace</module>
<module>che-multiuser-permission-workspace-activity</module>
<module>che-multiuser-permission-factory</module>

10
pom.xml
View File

@ -325,6 +325,11 @@
<version>${che.version}</version>
<classifier>sources</classifier>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-devfile</artifactId>
<version>${che.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-dto</artifactId>
@ -1062,6 +1067,11 @@
<version>${che.version}</version>
<classifier>sources</classifier>
</dependency>
<dependency>
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-permission-devfile</artifactId>
<version>${che.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-permission-factory</artifactId>

View File

@ -0,0 +1,181 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>che-master-parent</artifactId>
<groupId>org.eclipse.che.core</groupId>
<version>6.16.0-SNAPSHOT</version>
</parent>
<artifactId>che-core-api-devfile</artifactId>
<packaging>jar</packaging>
<name>Che Core :: API :: Devfile</name>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
<dependency>
<groupId>com.github.java-json-tools</groupId>
<artifactId>jackson-coreutils</artifactId>
</dependency>
<dependency>
<groupId>com.github.java-json-tools</groupId>
<artifactId>json-schema-core</artifactId>
</dependency>
<dependency>
<groupId>com.github.java-json-tools</groupId>
<artifactId>json-schema-validator</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-core</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-model</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-workspace</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-workspace-shared</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-commons-lang</artifactId>
</dependency>
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>javax.ws.rs-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.jayway.restassured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-account</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-dto</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-commons-json</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.everrest</groupId>
<artifactId>everrest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.everrest</groupId>
<artifactId>everrest-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-testng</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<id>test</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${basedir}/target/java-gen</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jsonschema2pojo</groupId>
<artifactId>jsonschema2pojo-maven-plugin</artifactId>
<version>0.5.1</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<configuration>
<sourceDirectory>${basedir}/src/main/resources/schema</sourceDirectory>
<targetPackage>org.eclipse.che.api.devfile.model</targetPackage>
<includeAdditionalProperties>false</includeAdditionalProperties>
<includeHashcodeAndEquals>false</includeHashcodeAndEquals>
<includeToString>false</includeToString>
<initializeCollections>true</initializeCollections>
<generateBuilders>true</generateBuilders>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -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:
*
* <pre>
* eclipse/maven-jdk8:1.0.0=mvn-stack,eclipse/theia:0.0.3=theia-ide,eclipse/theia-jdtls:0.0.3=jdt.ls
* </pre>
*/
public static final String ALIASES_WORKSPACE_ATTRIBUTE_NAME = "toolsAliases";
}

View File

@ -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<Project> projects = new ArrayList<>();
wsConfig
.getProjects()
.forEach(projectConfig -> projects.add(projectConfigToDevProject(projectConfig)));
devFile.setProjects(projects);
// Manage commands
Map<String, String> toolsIdToName = parseTools(wsConfig);
List<Command> commands = new ArrayList<>();
wsConfig
.getCommands()
.forEach(command -> commands.add(commandImplToDevCommand(command, toolsIdToName)));
devFile.setCommands(commands);
// Manage tools
List<Tool> tools = new ArrayList<>();
for (Map.Entry<String, String> 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<ProjectConfigImpl> projects = new ArrayList<>();
devFile.getProjects().forEach(project -> projects.add(devProjectToProjectConfig(project)));
config.setProjects(projects);
// Manage tools
Map<String, String> 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<CommandImpl> commands = new ArrayList<>();
devFile
.getCommands()
.forEach(command -> commands.addAll(devCommandToCommandImpls(devFile, command)));
config.setCommands(commands);
return config;
}
private List<CommandImpl> devCommandToCommandImpls(Devfile devFile, Command devCommand) {
List<CommandImpl> 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<Tool> 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<String, String> 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<String, String> 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()));
}
}
}

View File

@ -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);
}
}

View File

@ -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<String> 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));
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,180 @@
{
"definitions": {
"attributes" : {
"id": "propertyList",
"type": "object",
"javaType": "java.util.Map<String, String>"
}
},
"$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"
]
}
}
}
}
}
}
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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<WorkspaceConfigImpl> 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);
}
}
}

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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
-->
<configuration>
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%-41(%date[%.15thread]) %-45([%-5level] [%.30logger{30} %L]) - %msg%n</pattern>
</encoder>
</appender>
<appender name="file" class="ch.qos.logback.core.FileAppender">
<File>target/log/codenvy-factory-commons.log</File>
<encoder>
<pattern>%-41(%date[%.15thread]) %-45([%-5level] [%.30logger{30} %L]) - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="stdout"/>
<appender-ref ref="file"/>
</root>
</configuration>

View File

@ -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"
}
}

View File

@ -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));
}
}
}
}

View File

@ -17,6 +17,7 @@ import static java.util.Collections.emptyMap;
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 @@ public class WorkspaceService extends Service {
}
}
/*
* 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<String, EnvironmentDto> environments = config.getEnvironments();

View File

@ -28,6 +28,7 @@
<module>che-core-api-installer</module>
<module>che-core-api-auth-shared</module>
<module>che-core-api-auth</module>
<module>che-core-api-devfile</module>
<module>che-core-api-project-templates-shared</module>
<module>che-core-api-project-templates</module>
<module>che-core-api-workspace-shared</module>