From fc531ea0153dfbb211c3fb2ed996a93708cb5a23 Mon Sep 17 00:00:00 2001 From: Max Shaposhnik Date: Tue, 8 Oct 2019 14:10:15 +0300 Subject: [PATCH] Devfile validation via message entity provider --- .../che/api/deploy/WsMasterModule.java | 2 + .../che/api/core/rest/CoreRestModule.java | 1 + .../rest/WebApplicationExceptionMapper.java | 89 +++++++++++ .../WorkspacePermissionsFilterTest.java | 2 +- .../server/WorkspaceEntityProvider.java | 138 +++++++++++++++++ .../workspace/server/WorkspaceService.java | 34 +---- .../server/devfile/DevfileEntityProvider.java | 140 ++++++++++++++++++ .../server/devfile/DevfileModule.java | 1 + .../server/WorkspaceEntityProviderTest.java | 59 ++++++++ .../server/WorkspaceServiceTest.java | 53 +------ .../devfile/DevfileEntityProviderTest.java | 81 ++++++++++ .../src/test/resources/devfile/devfile.json | 84 +++++++++++ 12 files changed, 604 insertions(+), 80 deletions(-) create mode 100644 core/che-core-api-core/src/main/java/org/eclipse/che/api/core/rest/WebApplicationExceptionMapper.java create mode 100644 wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceEntityProvider.java create mode 100644 wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/devfile/DevfileEntityProvider.java create mode 100644 wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceEntityProviderTest.java create mode 100644 wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/devfile/DevfileEntityProviderTest.java create mode 100644 wsmaster/che-core-api-workspace/src/test/resources/devfile/devfile.json diff --git a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java index cbe7fa9c3d..515cc29265 100644 --- a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java +++ b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java @@ -42,6 +42,7 @@ import org.eclipse.che.api.user.server.jpa.JpaPreferenceDao; import org.eclipse.che.api.user.server.jpa.JpaUserDao; import org.eclipse.che.api.user.server.spi.PreferenceDao; import org.eclipse.che.api.user.server.spi.UserDao; +import org.eclipse.che.api.workspace.server.WorkspaceEntityProvider; import org.eclipse.che.api.workspace.server.WorkspaceLockService; import org.eclipse.che.api.workspace.server.WorkspaceStatusCache; import org.eclipse.che.api.workspace.server.devfile.DevfileModule; @@ -148,6 +149,7 @@ public class WsMasterModule extends AbstractModule { install(new DevfileModule()); + bind(WorkspaceEntityProvider.class); bind(org.eclipse.che.api.workspace.server.TemporaryWorkspaceRemover.class); bind(org.eclipse.che.api.workspace.server.WorkspaceService.class); install(new FactoryModuleBuilder().build(ServersCheckerFactory.class)); diff --git a/core/che-core-api-core/src/main/java/org/eclipse/che/api/core/rest/CoreRestModule.java b/core/che-core-api-core/src/main/java/org/eclipse/che/api/core/rest/CoreRestModule.java index 134d6c70fe..9af3634e2d 100644 --- a/core/che-core-api-core/src/main/java/org/eclipse/che/api/core/rest/CoreRestModule.java +++ b/core/che-core-api-core/src/main/java/org/eclipse/che/api/core/rest/CoreRestModule.java @@ -25,5 +25,6 @@ public class CoreRestModule extends AbstractModule { bind(RuntimeExceptionMapper.class); bind(ApiInfo.class).toProvider(ApiInfoProvider.class); Multibinder.newSetBinder(binder(), Class.class, Names.named("che.json.ignored_classes")); + bind(WebApplicationExceptionMapper.class); } } diff --git a/core/che-core-api-core/src/main/java/org/eclipse/che/api/core/rest/WebApplicationExceptionMapper.java b/core/che-core-api-core/src/main/java/org/eclipse/che/api/core/rest/WebApplicationExceptionMapper.java new file mode 100644 index 0000000000..80b1ab81e4 --- /dev/null +++ b/core/che-core-api-core/src/main/java/org/eclipse/che/api/core/rest/WebApplicationExceptionMapper.java @@ -0,0 +1,89 @@ +/* + * 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.core.rest; + +import static org.eclipse.che.dto.server.DtoFactory.newDto; + +import javax.inject.Singleton; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.ForbiddenException; +import javax.ws.rs.NotAcceptableException; +import javax.ws.rs.NotAllowedException; +import javax.ws.rs.NotAuthorizedException; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.NotSupportedException; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import org.eclipse.che.api.core.rest.shared.dto.ServiceError; +import org.eclipse.che.dto.server.DtoFactory; + +/** + * Mapper for the {@link WebApplicationException} exceptions. + * + * @author Max Shaposhnyk + */ +@Provider +@Singleton +public class WebApplicationExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(WebApplicationException exception) { + + ServiceError error = newDto(ServiceError.class).withMessage(exception.getMessage()); + + if (exception instanceof BadRequestException) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(DtoFactory.getInstance().toJson(error)) + .type(MediaType.APPLICATION_JSON) + .build(); + } else if (exception instanceof ForbiddenException) { + return Response.status(Response.Status.FORBIDDEN) + .entity(DtoFactory.getInstance().toJson(error)) + .type(MediaType.APPLICATION_JSON) + .build(); + } else if (exception instanceof NotFoundException) { + return Response.status(Response.Status.NOT_FOUND) + .entity(DtoFactory.getInstance().toJson(error)) + .type(MediaType.APPLICATION_JSON) + .build(); + } else if (exception instanceof NotAuthorizedException) { + return Response.status(Response.Status.UNAUTHORIZED) + .entity(DtoFactory.getInstance().toJson(error)) + .type(MediaType.APPLICATION_JSON) + .build(); + } else if (exception instanceof NotAcceptableException) { + return Response.status(Status.NOT_ACCEPTABLE) + .entity(DtoFactory.getInstance().toJson(error)) + .type(MediaType.APPLICATION_JSON) + .build(); + } else if (exception instanceof NotAllowedException) { + return Response.status(Status.METHOD_NOT_ALLOWED) + .entity(DtoFactory.getInstance().toJson(error)) + .type(MediaType.APPLICATION_JSON) + .build(); + } else if (exception instanceof NotSupportedException) { + return Response.status(Status.UNSUPPORTED_MEDIA_TYPE) + .entity(DtoFactory.getInstance().toJson(error)) + .type(MediaType.APPLICATION_JSON) + .build(); + } else { + return Response.serverError() + .entity(DtoFactory.getInstance().toJson(error)) + .type(MediaType.APPLICATION_JSON) + .build(); + } + } +} diff --git a/multiuser/permission/che-multiuser-permission-workspace/src/test/java/org/eclipse/che/multiuser/permission/workspace/server/filters/WorkspacePermissionsFilterTest.java b/multiuser/permission/che-multiuser-permission-workspace/src/test/java/org/eclipse/che/multiuser/permission/workspace/server/filters/WorkspacePermissionsFilterTest.java index 726a8aa5d8..6d82197ac4 100644 --- a/multiuser/permission/che-multiuser-permission-workspace/src/test/java/org/eclipse/che/multiuser/permission/workspace/server/filters/WorkspacePermissionsFilterTest.java +++ b/multiuser/permission/che-multiuser-permission-workspace/src/test/java/org/eclipse/che/multiuser/permission/workspace/server/filters/WorkspacePermissionsFilterTest.java @@ -158,7 +158,7 @@ public class WorkspacePermissionsFilterTest { .post(SECURE_PATH + "/workspace/devfile?namespace=userok"); assertEquals(response.getStatusCode(), 204); - verify(workspaceService).create(anyString(), any(), any(), eq("userok"), any()); + verify(workspaceService).create(any(DevfileDto.class), any(), any(), eq("userok"), any()); verify(permissionsFilter).checkAccountPermissions("userok", AccountOperation.CREATE_WORKSPACE); verifyZeroInteractions(subject); } diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceEntityProvider.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceEntityProvider.java new file mode 100644 index 0000000000..15480e5462 --- /dev/null +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceEntityProvider.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.workspace.server; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.eclipse.che.api.workspace.server.DtoConverter.asDto; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.Consumes; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; +import org.eclipse.che.api.workspace.server.devfile.DevfileManager; +import org.eclipse.che.api.workspace.server.devfile.exception.DevfileFormatException; +import org.eclipse.che.api.workspace.server.dto.DtoServerImpls.WorkspaceDtoImpl; +import org.eclipse.che.api.workspace.shared.dto.WorkspaceDto; +import org.eclipse.che.api.workspace.shared.dto.devfile.DevfileDto; +import org.eclipse.che.dto.server.DtoFactory; + +/** + * Entity provider for {@link WorkspaceDto}. Performs schema validation of devfile part of the + * workspace before actual {@link DevfileDto} creation. + * + * @author Max Shaposhnyk + */ +@Singleton +@Provider +@Produces({APPLICATION_JSON}) +@Consumes({APPLICATION_JSON}) +public class WorkspaceEntityProvider + implements MessageBodyReader, MessageBodyWriter { + + private DevfileManager devfileManager; + private ObjectMapper mapper = new ObjectMapper(); + + @Inject + public WorkspaceEntityProvider(DevfileManager devfileManager) { + this.devfileManager = devfileManager; + SimpleModule module = new SimpleModule(); + module.addDeserializer(DevfileDto.class, new DevfileDtoDeserializer()); + mapper.registerModule(module); + } + + @Override + public boolean isReadable( + Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return type == WorkspaceDto.class; + } + + @Override + public WorkspaceDto readFrom( + Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, + InputStream entityStream) + throws IOException, WebApplicationException { + return mapper + .readerFor(WorkspaceDtoImpl.class) + .without(DeserializationFeature.WRAP_EXCEPTIONS) + .readValue(entityStream); + } + + @Override + public boolean isWriteable( + Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return WorkspaceDto.class.isAssignableFrom(type); + } + + @Override + public long getSize( + WorkspaceDto workspaceDto, + Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType) { + return -1; + } + + @Override + public void writeTo( + WorkspaceDto workspaceDto, + Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, + OutputStream entityStream) + throws IOException, WebApplicationException { + httpHeaders.putSingle(HttpHeaders.CACHE_CONTROL, "public, no-cache, no-store, no-transform"); + try (Writer w = new OutputStreamWriter(entityStream, StandardCharsets.UTF_8)) { + w.write(DtoFactory.getInstance().toJson(workspaceDto)); + w.flush(); + } + } + + class DevfileDtoDeserializer extends JsonDeserializer { + @Override + public DevfileDto deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + try { + return asDto(devfileManager.parseJson(p.readValueAsTree().toString())); + } catch (DevfileFormatException e) { + throw new BadRequestException(e.getMessage()); + } + } + } +} 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 ed19cdc286..7375bafbc5 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,14 +17,12 @@ import static java.util.Collections.emptyMap; import static java.util.stream.Collectors.toList; import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; -import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; 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_DEVFILE_REGISTRY_URL_PROPERTY; import static org.eclipse.che.api.workspace.shared.Constants.CHE_WORKSPACE_PLUGIN_REGISTRY_URL_PROPERTY; -import com.google.common.annotations.Beta; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap.Builder; @@ -66,16 +64,13 @@ import org.eclipse.che.api.core.ValidationException; import org.eclipse.che.api.core.model.workspace.Workspace; import org.eclipse.che.api.core.model.workspace.config.ServerConfig; import org.eclipse.che.api.core.rest.Service; -import org.eclipse.che.api.workspace.server.devfile.DevfileManager; import org.eclipse.che.api.workspace.server.devfile.FileContentProvider; import org.eclipse.che.api.workspace.server.devfile.URLFetcher; import org.eclipse.che.api.workspace.server.devfile.URLFileContentProvider; -import org.eclipse.che.api.workspace.server.devfile.exception.DevfileException; import org.eclipse.che.api.workspace.server.model.impl.CommandImpl; import org.eclipse.che.api.workspace.server.model.impl.EnvironmentImpl; import org.eclipse.che.api.workspace.server.model.impl.ProjectConfigImpl; import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl; -import org.eclipse.che.api.workspace.server.model.impl.devfile.DevfileImpl; import org.eclipse.che.api.workspace.server.token.MachineAccessForbidden; import org.eclipse.che.api.workspace.server.token.MachineTokenException; import org.eclipse.che.api.workspace.server.token.MachineTokenProvider; @@ -89,6 +84,7 @@ import org.eclipse.che.api.workspace.shared.dto.RuntimeDto; import org.eclipse.che.api.workspace.shared.dto.ServerDto; import org.eclipse.che.api.workspace.shared.dto.WorkspaceConfigDto; import org.eclipse.che.api.workspace.shared.dto.WorkspaceDto; +import org.eclipse.che.api.workspace.shared.dto.devfile.DevfileDto; import org.eclipse.che.commons.annotation.Nullable; import org.eclipse.che.commons.env.EnvironmentContext; @@ -110,7 +106,6 @@ public class WorkspaceService extends Service { private final String apiEndpoint; private final boolean cheWorkspaceAutoStart; private final FileContentProvider devfileContentProvider; - private final DevfileManager devfileManager; @Inject public WorkspaceService( @@ -121,8 +116,7 @@ public class WorkspaceService extends Service { WorkspaceLinksGenerator linksGenerator, @Named(CHE_WORKSPACE_PLUGIN_REGISTRY_URL_PROPERTY) @Nullable String pluginRegistryUrl, @Named(CHE_WORKSPACE_DEVFILE_REGISTRY_URL_PROPERTY) @Nullable String devfileRegistryUrl, - URLFetcher urlFetcher, - DevfileManager devfileManager) { + URLFetcher urlFetcher) { this.apiEndpoint = apiEndpoint; this.cheWorkspaceAutoStart = cheWorkspaceAutoStart; this.workspaceManager = workspaceManager; @@ -131,7 +125,6 @@ public class WorkspaceService extends Service { this.pluginRegistryUrl = pluginRegistryUrl; this.devfileRegistryUrl = devfileRegistryUrl; this.devfileContentProvider = new URLFileContentProvider(null, urlFetcher); - this.devfileManager = devfileManager; } @POST @@ -196,16 +189,12 @@ public class WorkspaceService extends Service { return Response.status(201).entity(asDtoWithLinksAndToken(workspace)).build(); } - @Beta @Path("/devfile") @POST @Consumes({APPLICATION_JSON, "text/yaml", "text/x-yaml"}) @Produces(APPLICATION_JSON) @ApiOperation( value = "Creates a new workspace based on the Devfile.", - notes = - "This method is in beta phase. It's strongly recommended to use `POST /devfile` instead" - + " to get a workspace from Devfile. Workspaces created with this method are not stable yet.", consumes = "application/json, text/yaml, text/x-yaml", produces = APPLICATION_JSON, nickname = "createFromDevfile", @@ -222,7 +211,8 @@ public class WorkspaceService extends Service { @ApiResponse(code = 500, message = "Internal server error occurred") }) public Response create( - @ApiParam(value = "The devfile of the workspace to create", required = true) String devfile, + @ApiParam(value = "The devfile of the workspace to create", required = true) + DevfileDto devfile, @ApiParam( value = "Workspace attribute defined in 'attrName:attrValue' format. " @@ -242,29 +232,15 @@ public class WorkspaceService extends Service { throws ConflictException, BadRequestException, ForbiddenException, NotFoundException, ServerException { requiredNotNull(devfile, "Devfile"); - - DevfileImpl devfileModel; - try { - if (APPLICATION_JSON_TYPE.isCompatible(contentType)) { - devfileModel = devfileManager.parseJson(devfile); - } else { - devfileModel = devfileManager.parseYaml(devfile); - } - } catch (DevfileException e) { - throw new BadRequestException(e.getMessage()); - } - final Map attributes = parseAttrs(attrsList); - if (namespace == null) { namespace = EnvironmentContext.getCurrent().getSubject().getUserName(); } - WorkspaceImpl workspace; try { workspace = workspaceManager.createWorkspace( - devfileModel, + devfile, namespace, attributes, // create a new cache for each request so that we don't have to care about lifetime diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/devfile/DevfileEntityProvider.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/devfile/DevfileEntityProvider.java new file mode 100644 index 0000000000..282c376b8c --- /dev/null +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/devfile/DevfileEntityProvider.java @@ -0,0 +1,140 @@ +/* + * 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.devfile; + +import static com.google.common.base.Strings.isNullOrEmpty; +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.eclipse.che.api.workspace.server.DtoConverter.asDto; + +import com.google.common.io.CharStreams; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.Consumes; +import javax.ws.rs.NotSupportedException; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; +import org.eclipse.che.api.workspace.server.devfile.exception.DevfileFormatException; +import org.eclipse.che.api.workspace.shared.dto.devfile.DevfileDto; +import org.eclipse.che.dto.server.DtoFactory; + +/** + * Parses {@link DevfileDto} either from Json or yaml content, and performs schema validation before + * the actual DTO created. + * + * @author Max Shaposhnyk + */ +@Singleton +@Provider +@Produces({APPLICATION_JSON}) +@Consumes({APPLICATION_JSON, "text/yaml", "text/x-yaml"}) +public class DevfileEntityProvider + implements MessageBodyReader, MessageBodyWriter { + + private DevfileManager devfileManager; + + @Inject + public DevfileEntityProvider(DevfileManager devfileManager) { + this.devfileManager = devfileManager; + } + + @Override + public boolean isReadable( + Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return type == DevfileDto.class; + } + + @Override + public DevfileDto readFrom( + Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, + InputStream entityStream) + throws IOException, WebApplicationException { + + try { + if (mediaType.isCompatible(MediaType.APPLICATION_JSON_TYPE)) { + return asDto( + devfileManager.parseJson( + CharStreams.toString( + new InputStreamReader(entityStream, getCharsetOrUtf8(mediaType))))); + } else if (mediaType.isCompatible(MediaType.valueOf("text/yaml")) + || mediaType.isCompatible(MediaType.valueOf("text/x-yaml"))) { + return asDto( + devfileManager.parseYaml( + CharStreams.toString( + new InputStreamReader(entityStream, getCharsetOrUtf8(mediaType))))); + } + } catch (DevfileFormatException e) { + throw new BadRequestException(e.getMessage()); + } + throw new NotSupportedException("Unknown media type " + mediaType.toString()); + } + + @Override + public boolean isWriteable( + Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return DevfileDto.class.isAssignableFrom(type); + } + + @Override + public long getSize( + DevfileDto devfileDto, + Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType) { + return -1; + } + + @Override + public void writeTo( + DevfileDto devfileDto, + Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, + OutputStream entityStream) + throws IOException, WebApplicationException { + httpHeaders.putSingle(HttpHeaders.CACHE_CONTROL, "public, no-cache, no-store, no-transform"); + try (Writer w = new OutputStreamWriter(entityStream, StandardCharsets.UTF_8)) { + w.write(DtoFactory.getInstance().toJson(devfileDto)); + w.flush(); + } + } + + private String getCharsetOrUtf8(MediaType mediaType) { + String charset = mediaType == null ? null : mediaType.getParameters().get("charset"); + if (isNullOrEmpty(charset)) { + charset = "UTF-8"; + } + return charset; + } +} diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/devfile/DevfileModule.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/devfile/DevfileModule.java index 2184f9d5bf..ab49aff0bc 100644 --- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/devfile/DevfileModule.java +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/devfile/DevfileModule.java @@ -25,6 +25,7 @@ public class DevfileModule extends AbstractModule { @Override protected void configure() { bind(DevfileService.class); + bind(DevfileEntityProvider.class); DevfileBindings.onWorkspaceApplierBinder( binder(), diff --git a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceEntityProviderTest.java b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceEntityProviderTest.java new file mode 100644 index 0000000000..66dfdee584 --- /dev/null +++ b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceEntityProviderTest.java @@ -0,0 +1,59 @@ +/* + * 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 org.eclipse.che.dto.server.DtoFactory.newDto; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedHashMap; +import org.eclipse.che.api.workspace.server.devfile.DevfileManager; +import org.eclipse.che.api.workspace.server.model.impl.devfile.DevfileImpl; +import org.eclipse.che.api.workspace.shared.dto.WorkspaceDto; +import org.eclipse.che.api.workspace.shared.dto.devfile.DevfileDto; +import org.eclipse.che.dto.server.DtoFactory; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +@Listeners(MockitoTestNGListener.class) +public class WorkspaceEntityProviderTest { + + @Mock private DevfileManager devfileManager; + + @InjectMocks private WorkspaceEntityProvider workspaceEntityProvider; + + @Test + public void shouldBuildDtoFromValidJson() throws Exception { + + when(devfileManager.parseJson(anyString())).thenReturn(new DevfileImpl()); + + WorkspaceDto actual = newDto(WorkspaceDto.class).withDevfile(newDto(DevfileDto.class)); + + workspaceEntityProvider.readFrom( + WorkspaceDto.class, + WorkspaceDto.class, + null, + MediaType.APPLICATION_JSON_TYPE, + new MultivaluedHashMap<>(), + new ByteArrayInputStream( + DtoFactory.getInstance().toJson(actual).getBytes(StandardCharsets.UTF_8))); + + verify(devfileManager).parseJson(anyString()); + } +} diff --git a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceServiceTest.java b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceServiceTest.java index 7b2b8e5bd5..93502b1af4 100644 --- a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceServiceTest.java +++ b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceServiceTest.java @@ -51,6 +51,7 @@ import java.util.Map; import org.eclipse.che.account.shared.model.Account; import org.eclipse.che.account.spi.AccountImpl; import org.eclipse.che.api.core.Page; +import org.eclipse.che.api.core.ValidationException; import org.eclipse.che.api.core.model.workspace.WorkspaceConfig; import org.eclipse.che.api.core.model.workspace.WorkspaceStatus; import org.eclipse.che.api.core.model.workspace.config.ProjectConfig; @@ -62,9 +63,7 @@ import org.eclipse.che.api.core.model.workspace.runtime.ServerStatus; import org.eclipse.che.api.core.rest.ApiExceptionMapper; import org.eclipse.che.api.core.rest.CheJsonProvider; import org.eclipse.che.api.core.rest.shared.dto.ServiceError; -import org.eclipse.che.api.workspace.server.devfile.DevfileManager; import org.eclipse.che.api.workspace.server.devfile.URLFetcher; -import org.eclipse.che.api.workspace.server.devfile.exception.DevfileFormatException; import org.eclipse.che.api.workspace.server.model.impl.CommandImpl; import org.eclipse.che.api.workspace.server.model.impl.EnvironmentImpl; import org.eclipse.che.api.workspace.server.model.impl.MachineConfigImpl; @@ -74,7 +73,6 @@ import org.eclipse.che.api.workspace.server.model.impl.RuntimeImpl; import org.eclipse.che.api.workspace.server.model.impl.ServerImpl; 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.server.model.impl.devfile.DevfileImpl; import org.eclipse.che.api.workspace.server.token.MachineTokenProvider; import org.eclipse.che.api.workspace.shared.Constants; import org.eclipse.che.api.workspace.shared.dto.CommandDto; @@ -134,7 +132,6 @@ public class WorkspaceServiceTest { @Mock private WorkspaceManager wsManager; @Mock private MachineTokenProvider machineTokenProvider; @Mock private WorkspaceLinksGenerator linksGenerator; - @Mock private DevfileManager devfileManager; @Mock private URLFetcher urlFetcher; private WorkspaceService service; @@ -150,8 +147,7 @@ public class WorkspaceServiceTest { linksGenerator, CHE_WORKSPACE_PLUGIN_REGISTRY_ULR, CHE_WORKSPACE_DEVFILE_REGISTRY_ULR, - urlFetcher, - devfileManager); + urlFetcher); } @Test @@ -193,8 +189,6 @@ public class WorkspaceServiceTest { final DevfileDto devfileDto = createDevfileDto(); final WorkspaceImpl workspace = createWorkspace(devfileDto); - when(devfileManager.parseJson(any())).thenReturn(new DevfileImpl()); - when(wsManager.createWorkspace(any(Devfile.class), anyString(), any(), any())) .thenReturn(workspace); @@ -226,52 +220,13 @@ public class WorkspaceServiceTest { any()); } - @Test - public void shouldAcceptYamlDevfileWhenCreatingWorkspace() throws Exception { - final DevfileDto devfileDto = createDevfileDto(); - final WorkspaceImpl workspace = createWorkspace(devfileDto); - - when(devfileManager.parseYaml(any())).thenReturn(new DevfileImpl()); - - when(wsManager.createWorkspace(any(Devfile.class), anyString(), any(), any())) - .thenReturn(workspace); - - final Response response = - given() - .auth() - .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD) - .contentType("text/yaml") - .when() - .post( - SECURE_PATH - + "/workspace/devfile" - + "?namespace=test" - + "&attribute=factoryId:factory123" - + "&attribute=custom:custom:value"); - - assertEquals(response.getStatusCode(), 201); - assertEquals( - new WorkspaceImpl(unwrapDto(response, WorkspaceDto.class), TEST_ACCOUNT), workspace); - verify(wsManager) - .createWorkspace( - any(Devfile.class), - eq("test"), - eq( - ImmutableMap.of( - "factoryId", "factory123", - "custom", "custom:value")), - any()); - } - @Test public void shouldReturnBadRequestOnInvalidDevfile() throws Exception { final DevfileDto devfileDto = createDevfileDto(); final WorkspaceImpl workspace = createWorkspace(devfileDto); - when(devfileManager.parseJson(any())).thenThrow(new DevfileFormatException("boom")); - when(wsManager.createWorkspace(any(Devfile.class), anyString(), any(), any())) - .thenReturn(workspace); + .thenThrow(new ValidationException("boom")); final Response response = given() @@ -290,8 +245,6 @@ public class WorkspaceServiceTest { assertEquals(response.getStatusCode(), 400); String error = unwrapError(response); assertEquals(error, "boom"); - - verify(wsManager, never()).createWorkspace(any(Devfile.class), any(), any(), any()); } @Test diff --git a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/devfile/DevfileEntityProviderTest.java b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/devfile/DevfileEntityProviderTest.java new file mode 100644 index 0000000000..d4fde146f3 --- /dev/null +++ b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/devfile/DevfileEntityProviderTest.java @@ -0,0 +1,81 @@ +/* + * 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.devfile; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import javax.ws.rs.NotSupportedException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedHashMap; +import org.eclipse.che.api.workspace.server.model.impl.devfile.DevfileImpl; +import org.eclipse.che.api.workspace.shared.dto.devfile.DevfileDto; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +@Listeners(MockitoTestNGListener.class) +public class DevfileEntityProviderTest { + + @Mock private DevfileManager devfileManager; + + @InjectMocks private DevfileEntityProvider devfileEntityProvider; + + @Test + public void shouldBuildDtoFromValidYaml() throws Exception { + + when(devfileManager.parseYaml(anyString())).thenReturn(new DevfileImpl()); + + devfileEntityProvider.readFrom( + DevfileDto.class, + DevfileDto.class, + null, + MediaType.valueOf("text/x-yaml"), + new MultivaluedHashMap<>(), + getClass().getClassLoader().getResourceAsStream("devfile/devfile.yaml")); + + verify(devfileManager).parseYaml(anyString()); + } + + @Test + public void shouldBuildDtoFromValidJson() throws Exception { + + when(devfileManager.parseJson(anyString())).thenReturn(new DevfileImpl()); + + devfileEntityProvider.readFrom( + DevfileDto.class, + DevfileDto.class, + null, + MediaType.APPLICATION_JSON_TYPE, + new MultivaluedHashMap<>(), + getClass().getClassLoader().getResourceAsStream("devfile/devfile.json")); + + verify(devfileManager).parseJson(anyString()); + } + + @Test( + expectedExceptions = NotSupportedException.class, + expectedExceptionsMessageRegExp = "Unknown media type text/plain") + public void shouldThrowErrorOnInvalidMediaType() throws Exception { + + devfileEntityProvider.readFrom( + DevfileDto.class, + DevfileDto.class, + null, + MediaType.TEXT_PLAIN_TYPE, + new MultivaluedHashMap<>(), + getClass().getClassLoader().getResourceAsStream("devfile/devfile.json")); + } +} diff --git a/wsmaster/che-core-api-workspace/src/test/resources/devfile/devfile.json b/wsmaster/che-core-api-workspace/src/test/resources/devfile/devfile.json new file mode 100644 index 0000000000..da300e6a32 --- /dev/null +++ b/wsmaster/che-core-api-workspace/src/test/resources/devfile/devfile.json @@ -0,0 +1,84 @@ +{ + "apiVersion": "1.0.0", + "metadata": { + "name": "petclinic-dev-environment", + "generateName": "petclinic-" + }, + "projects": [ + { + "name": "petclinic", + "source": { + "type": "git", + "location": "git@github.com:spring-projects/spring-petclinic.git" + } + } + ], + "components": [ + { + "alias": "mvn-stack", + "type": "chePlugin", + "id": "eclipse/chemaven-jdk8/1.0.0" + }, + { + "type": "cheEditor", + "id": "eclipse/che-theia/0.0.3" + }, + { + "alias": "jdt.ls", + "type": "chePlugin", + "id": "org.eclipse.chetheia-jdtls:0.0.3", + "preferences": { + "java.home": "/home/user/jdk11", + "java.jdt.ls.vmargs": "-noverify -Xmx1G -XX:+UseG1GC -XX:+UseStringDeduplication", + "java.jtg.memory": 12345, + "java.boolean": true + } + }, + { + "type": "openshift", + "reference": "petclinic.yaml", + "selector": { + "app.kubernetes.io/name": "mysql", + "app.kubernetes.io/component": "database", + "app.kubernetes.io/part-of": "petclinic" + } + } + ], + "commands": [ + { + "name": "build", + "actions": [ + { + "type": "exec", + "component": "mvn-stack", + "command": "mvn package", + "workdir": "/projects/spring-petclinic" + } + ] + }, + { + "name": "run", + "attributes": { + "runType": "sequential" + }, + "actions": [ + { + "type": "exec", + "component": "mvn-stack", + "command": "mvn spring-boot:run", + "workdir": "/projects/spring-petclinic" + } + ] + }, + { + "name": "other", + "actions": [ + { + "type": "exec", + "component": "jdt.ls", + "command": "run.sh" + } + ] + } + ] +}