Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Devfile validation via message entity provider #14740

Merged
merged 14 commits into from
Oct 8, 2019
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
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;
Expand Down Expand Up @@ -148,6 +149,7 @@ protected void configure() {

install(new DevfileModule());

bind(WorkspaceEntityProvider.class);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say we should have something like WorkspaceModule like DevfileModule. Up2you to decide.

bind(org.eclipse.che.api.workspace.server.TemporaryWorkspaceRemover.class);
bind(org.eclipse.che.api.workspace.server.WorkspaceService.class);
install(new FactoryModuleBuilder().build(ServersCheckerFactory.class));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ protected void configure() {
bind(RuntimeExceptionMapper.class);
bind(ApiInfo.class).toProvider(ApiInfoProvider.class);
Multibinder.newSetBinder(binder(), Class.class, Names.named("che.json.ignored_classes"));
bind(WebApplicationExceptionMapper.class);
}
}
Original file line number Diff line number Diff line change
@@ -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<WebApplicationException> {

@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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ public void shouldCheckAccountPermissionsAccessOnWorkspaceCreationFromDevfile()
.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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<WorkspaceDto>, MessageBodyWriter<WorkspaceDto> {

private DevfileManager devfileManager;
private ObjectMapper mapper = new ObjectMapper();
skabashnyuk marked this conversation as resolved.
Show resolved Hide resolved

@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<WorkspaceDto> type,
Type genericType,
Annotation[] annotations,
MediaType mediaType,
MultivaluedMap<String, String> 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<String, Object> 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<DevfileDto> {
@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());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,12 @@
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;
Expand Down Expand Up @@ -66,16 +64,13 @@
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;
Expand All @@ -89,6 +84,7 @@
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;

Expand All @@ -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(
Expand All @@ -121,8 +116,7 @@ public WorkspaceService(
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;
Expand All @@ -131,7 +125,6 @@ public WorkspaceService(
this.pluginRegistryUrl = pluginRegistryUrl;
this.devfileRegistryUrl = devfileRegistryUrl;
this.devfileContentProvider = new URLFileContentProvider(null, urlFetcher);
this.devfileManager = devfileManager;
}

@POST
Expand Down Expand Up @@ -196,16 +189,12 @@ public Response create(
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",
Expand All @@ -222,7 +211,8 @@ public Response create(
@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. "
Expand All @@ -242,29 +232,15 @@ public Response create(
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<String, String> 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
Expand Down
Loading