/*******************************************************************************
* Copyright (c) 2012-2017 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.api.workspace.server;
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 com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import org.eclipse.che.api.agent.server.WsAgentHealthChecker;
import org.eclipse.che.api.core.BadRequestException;
import org.eclipse.che.api.core.ConflictException;
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.core.model.workspace.WorkspaceStatus;
import org.eclipse.che.api.core.rest.Service;
import org.eclipse.che.api.core.rest.annotations.GenerateLink;
import org.eclipse.che.api.machine.server.model.impl.CommandImpl;
import org.eclipse.che.api.machine.server.model.impl.MachineImpl;
import org.eclipse.che.api.machine.shared.dto.CommandDto;
import org.eclipse.che.api.machine.shared.dto.SnapshotDto;
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.shared.dto.EnvironmentDto;
import org.eclipse.che.api.workspace.shared.dto.EnvironmentRecipeDto;
import org.eclipse.che.api.workspace.shared.dto.ProjectConfigDto;
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.WsAgentHealthStateDto;
import org.eclipse.che.commons.env.EnvironmentContext;
import javax.inject.Inject;
import javax.inject.Named;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
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.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import java.util.List;
import java.util.Map;
import static com.google.common.base.MoreObjects.firstNonNull;
import static java.lang.String.format;
import static java.util.Collections.emptyMap;
import static java.util.stream.Collectors.toList;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.Response.Status.NOT_FOUND;
import static org.eclipse.che.api.workspace.server.DtoConverter.asDto;
import static org.eclipse.che.api.workspace.shared.Constants.CHE_WORKSPACE_AUTO_RESTORE;
import static org.eclipse.che.api.workspace.shared.Constants.CHE_WORKSPACE_AUTO_SNAPSHOT;
import static org.eclipse.che.api.workspace.shared.Constants.CHE_WORKSPACE_AUTO_START;
import static org.eclipse.che.api.workspace.shared.Constants.LINK_REL_CREATE_WORKSPACE;
import static org.eclipse.che.api.workspace.shared.Constants.LINK_REL_GET_BY_NAMESPACE;
import static org.eclipse.che.api.workspace.shared.Constants.LINK_REL_GET_WORKSPACES;
import static org.eclipse.che.dto.server.DtoFactory.newDto;
/**
* Defines Workspace REST API.
*
* @author Yevhenii Voevodin
* @author Igor Vinokur
*/
@Api(value = "/workspace", description = "Workspace REST API")
@Path("/workspace")
public class WorkspaceService extends Service {
private final WorkspaceManager workspaceManager;
private final WorkspaceValidator validator;
private final WsAgentHealthChecker agentHealthChecker;
private final WorkspaceServiceLinksInjector linksInjector;
private final String apiEndpoint;
private final boolean cheWorkspaceAutoSnapshot;
private final boolean cheWorkspaceAutoRestore;
private final boolean cheWorkspaceAutoStart;
@Context
private SecurityContext securityContext;
@Inject
public WorkspaceService(@Named("che.api") String apiEndpoint,
WorkspaceManager workspaceManager,
WorkspaceValidator validator,
WsAgentHealthChecker agentHealthChecker,
WorkspaceServiceLinksInjector workspaceServiceLinksInjector,
@Named(CHE_WORKSPACE_AUTO_SNAPSHOT) boolean cheWorkspaceAutoSnapshot,
@Named(CHE_WORKSPACE_AUTO_RESTORE) boolean cheWorkspaceAutoRestore,
@Named(CHE_WORKSPACE_AUTO_START) boolean cheWorkspaceAutoStart) {
this.apiEndpoint = apiEndpoint;
this.workspaceManager = workspaceManager;
this.validator = validator;
this.agentHealthChecker = agentHealthChecker;
this.linksInjector = workspaceServiceLinksInjector;
this.cheWorkspaceAutoSnapshot = cheWorkspaceAutoSnapshot;
this.cheWorkspaceAutoRestore = cheWorkspaceAutoRestore;
this.cheWorkspaceAutoStart = cheWorkspaceAutoStart;
}
@POST
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@GenerateLink(rel = LINK_REL_CREATE_WORKSPACE)
@ApiOperation(value = "Create a new workspace based on the configuration",
notes = "This operation can be performed only by authorized user," +
"this user will be the owner of the created workspace",
response = WorkspaceConfigDto.class)
@ApiResponses({@ApiResponse(code = 201, message = "The workspace successfully created"),
@ApiResponse(code = 400, message = "Missed required parameters, parameters are not valid"),
@ApiResponse(code = 403, message = "The user does not have access to create a new workspace"),
@ApiResponse(code = 409, message = "Conflict error occurred during the workspace creation" +
"(e.g. The workspace with such name already exists)"),
@ApiResponse(code = 500, message = "Internal server error occurred")})
public Response create(@ApiParam(value = "The configuration to create the new workspace", required = true)
WorkspaceConfigDto config,
@ApiParam(value = "Workspace attribute defined in 'attrName:attrValue' format. " +
"The first ':' is considered as attribute name and value separator",
examples = @Example({@ExampleProperty("stackId:stack123"),
@ExampleProperty("attrName:value-with:colon")}))
@QueryParam("attribute")
List<String> attrsList,
@ApiParam("If true then the workspace will be immediately " +
"started after it is successfully created")
@QueryParam("start-after-create")
@DefaultValue("false")
Boolean startAfterCreate,
@ApiParam("Namespace where workspace should be created")
@QueryParam("namespace")
String namespace) throws ConflictException,
ServerException,
BadRequestException,
ForbiddenException,
NotFoundException {
requiredNotNull(config, "Workspace configuration");
final Map<String, String> attributes = parseAttrs(attrsList);
validator.validateAttributes(attributes);
validator.validateConfig(config);
relativizeRecipeLinks(config);
if (namespace == null) {
namespace = EnvironmentContext.getCurrent().getSubject().getUserName();
}
final WorkspaceImpl workspace = workspaceManager.createWorkspace(config,
namespace,
attributes);
if (startAfterCreate) {
workspaceManager.startWorkspace(workspace.getId(), null, false);
}
return Response.status(201)
.entity(linksInjector.injectLinks(asDto(workspace), getServiceContext()))
.build();
}
@GET
@Path("/{key:.*}")
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Get the workspace by the composite key",
notes = "Composite key can be just workspace ID or in the " +
"namespace:workspace_name form, where namespace is optional (e.g :workspace_name is valid key too." +
"namespace/workspace_name form, where namespace can contain '/' character.")
@ApiResponses({@ApiResponse(code = 200, message = "The response contains requested workspace entity"),
@ApiResponse(code = 404, message = "The workspace with specified id does not exist"),
@ApiResponse(code = 403, message = "The user is not workspace owner"),
@ApiResponse(code = 500, message = "Internal server error occurred")})
public WorkspaceDto getByKey(@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,
ForbiddenException,
BadRequestException {
validateKey(key);
final WorkspaceImpl workspace = workspaceManager.getWorkspace(key);
return linksInjector.injectLinks(asDto(workspace), getServiceContext());
}
@GET
@Produces(APPLICATION_JSON)
@GenerateLink(rel = LINK_REL_GET_WORKSPACES)
@ApiOperation(value = "Get workspaces which user can read",
notes = "This operation can be performed only by authorized user",
response = WorkspaceDto.class,
responseContainer = "List")
@ApiResponses({@ApiResponse(code = 200, message = "The workspaces successfully fetched"),
@ApiResponse(code = 500, message = "Internal server error occurred during workspaces fetching")})
public List<WorkspaceDto> getWorkspaces(@ApiParam("The number of the items to skip")
@DefaultValue("0")
@QueryParam("skipCount")
Integer skipCount,
@ApiParam("The limit of the items in the response, default is 30")
@DefaultValue("30")
@QueryParam("maxItems")
Integer maxItems,
@ApiParam("Workspace status")
@QueryParam("status")
String status) throws ServerException, BadRequestException {
//TODO add maxItems & skipCount to manager
return workspaceManager.getWorkspaces(EnvironmentContext.getCurrent().getSubject().getUserId(), false)
.stream()
.filter(ws -> status == null || status.equalsIgnoreCase(ws.getStatus().toString()))
.map(workspace -> linksInjector.injectLinks(asDto(workspace), getServiceContext()))
.collect(toList());
}
@GET
@Path("/namespace/{namespace:.*}")
@Produces(APPLICATION_JSON)
@GenerateLink(rel = LINK_REL_GET_BY_NAMESPACE)
@ApiOperation(value = "Get workspaces by given namespace",
notes = "This operation can be performed only by authorized user",
response = WorkspaceDto.class,
responseContainer = "List")
@ApiResponses({@ApiResponse(code = 200, message = "The workspaces successfully fetched"),
@ApiResponse(code = 500, message = "Internal server error occurred during workspaces fetching")})
public List<WorkspaceDto> getByNamespace(@ApiParam("Workspace status")
@QueryParam("status")
String status,
@ApiParam("The namespace")
@PathParam("namespace")
String namespace) throws ServerException, BadRequestException {
return workspaceManager.getByNamespace(namespace, false)
.stream()
.filter(ws -> status == null || status.equalsIgnoreCase(ws.getStatus().toString()))
.map(workspace -> linksInjector.injectLinks(asDto(workspace), getServiceContext()))
.collect(toList());
}
@PUT
@Path("/{id}")
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Update the workspace by replacing all the existing data with update",
notes = "This operation can be performed only by the workspace owner")
@ApiResponses({@ApiResponse(code = 200, message = "The workspace successfully updated"),
@ApiResponse(code = 400, message = "Missed required parameters, parameters are not valid"),
@ApiResponse(code = 403, message = "The user does not have access to update the workspace"),
@ApiResponse(code = 409, message = "Conflict error occurred during workspace update" +
"(e.g. Workspace with such name already exists)"),
@ApiResponse(code = 500, message = "Internal server error occurred")})
public WorkspaceDto update(@ApiParam("The workspace id")
@PathParam("id")
String id,
@ApiParam(value = "The workspace update", required = true)
WorkspaceDto update) throws BadRequestException,
ServerException,
ForbiddenException,
NotFoundException,
ConflictException {
requiredNotNull(update, "Workspace configuration");
validator.validateWorkspace(update);
relativizeRecipeLinks(update.getConfig());
return linksInjector.injectLinks(asDto(workspaceManager.updateWorkspace(id, update)), getServiceContext());
}
@DELETE
@Path("/{id}")
@ApiOperation(value = "Removes the workspace",
notes = "This operation can be performed only by the workspace owner")
@ApiResponses({@ApiResponse(code = 204, message = "The workspace successfully removed"),
@ApiResponse(code = 403, message = "The user does not have access to remove the workspace"),
@ApiResponse(code = 404, message = "The workspace doesn't exist"),
@ApiResponse(code = 409, message = "The workspace is not stopped(has runtime)"),
@ApiResponse(code = 500, message = "Internal server error occurred")})
public void delete(@ApiParam("The workspace id") @PathParam("id") String id) throws BadRequestException,
ServerException,
NotFoundException,
ConflictException,
ForbiddenException {
workspaceManager.removeSnapshots(id);
workspaceManager.removeWorkspace(id);
}
@POST
@Path("/{id}/runtime")
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Start the workspace by the id",
notes = "This operation can be performed only by the workspace owner." +
"The workspace starts asynchronously")
@ApiResponses({@ApiResponse(code = 200, message = "The workspace is starting"),
@ApiResponse(code = 404, message = "The workspace with specified id doesn't exist"),
@ApiResponse(code = 403, message = "The user is not workspace owner." +
"The operation is not allowed for the user"),
@ApiResponse(code = 409, message = "Any conflict occurs during the workspace start"),
@ApiResponse(code = 500, message = "Internal server error occurred")})
public WorkspaceDto startById(@ApiParam("The workspace id")
@PathParam("id")
String workspaceId,
@ApiParam("The name of the workspace environment that should be used for start")
@QueryParam("environment")
String envName,
@ApiParam("Restore workspace from snapshot")
@QueryParam("restore")
Boolean restore) throws ServerException,
BadRequestException,
NotFoundException,
ForbiddenException,
ConflictException {
return linksInjector.injectLinks(asDto(workspaceManager.startWorkspace(workspaceId, envName, restore)),
getServiceContext());
}
@POST
@Path("/runtime")
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Start the temporary workspace from the given configuration",
notes = "This operation can be performed only by the authorized user or temp user." +
"The workspace starts synchronously")
@ApiResponses({@ApiResponse(code = 200, message = "The workspace is starting"),
@ApiResponse(code = 400, message = "The update config is not valid"),
@ApiResponse(code = 404, message = "The workspace with specified id doesn't exist"),
@ApiResponse(code = 403, message = "The user is not workspace owner" +
"The operation is not allowed for the user"),
@ApiResponse(code = 409, message = "Any conflict occurs during the workspace start" +
"(e.g. workspace with such name already exists"),
@ApiResponse(code = 500, message = "Internal server error occurred")})
public WorkspaceDto startFromConfig(@ApiParam(value = "The configuration to start the workspace from", required = true)
WorkspaceConfigDto config,
@ApiParam("Weather this workspace is temporary or not")
@QueryParam("temporary")
Boolean isTemporary,
@ApiParam("Namespace where workspace should be created")
@QueryParam("namespace")
String namespace) throws BadRequestException,
ForbiddenException,
NotFoundException,
ServerException,
ConflictException {
requiredNotNull(config, "Workspace configuration");
validator.validateConfig(config);
relativizeRecipeLinks(config);
if (namespace == null) {
namespace = EnvironmentContext.getCurrent().getSubject().getUserName();
}
return linksInjector.injectLinks(asDto(workspaceManager.startWorkspace(config,
namespace,
firstNonNull(isTemporary, false))), getServiceContext());
}
@DELETE
@Path("/{id}/runtime")
@ApiOperation(value = "Stop the workspace",
notes = "This operation can be performed only by the workspace owner." +
"The workspace stops asynchronously")
@ApiResponses({@ApiResponse(code = 204, message = "The workspace is stopping"),
@ApiResponse(code = 404, message = "The workspace with specified id doesn't exist"),
@ApiResponse(code = 403, message = "The user is not workspace owner"),
@ApiResponse(code = 500, message = "Internal server error occurred")})
public void stop(@ApiParam("The workspace id") @PathParam("id") String id,
@ApiParam("Whether to snapshot workspace before stopping it")
@QueryParam("create-snapshot") Boolean createSnapshot) throws ForbiddenException,
NotFoundException,
ServerException,
ConflictException {
workspaceManager.stopWorkspace(id, createSnapshot);
}
@POST
@Path("/{id}/snapshot")
@ApiOperation(value = "Create a snapshot from the workspace",
notes = "This operation can be performed only by the workspace owner.")
@ApiResponses({@ApiResponse(code = 200, message = "The snapshot successfully created"),
@ApiResponse(code = 404, message = "The workspace with specified id doesn't exist."),
@ApiResponse(code = 403, message = "The user is not workspace owner. " +
"The operation is not allowed for the user"),
@ApiResponse(code = 409, message = "Any conflict occurs during the snapshot creation"),
@ApiResponse(code = 500, message = "Internal server error occurred")})
@Deprecated
public void createSnapshot(@ApiParam("The workspace id") @PathParam("id") String workspaceId) throws BadRequestException,
ForbiddenException,
NotFoundException,
ServerException,
ConflictException {
workspaceManager.createSnapshot(workspaceId);
}
@GET
@Path("/{id}/snapshot")
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Get the snapshot by the id",
notes = "This operation can be performed only by the workspace owner",
response = SnapshotDto.class,
responseContainer = "List")
@ApiResponses({@ApiResponse(code = 200, message = "Snapshots successfully fetched"),
@ApiResponse(code = 404, message = "The workspace with specified id doesn't exist." +
"The snapshot doesn't exist for the workspace"),
@ApiResponse(code = 403, message = "The user is not workspace owner"),
@ApiResponse(code = 500, message = "Internal server error occurred")})
@Deprecated
public List<SnapshotDto> getSnapshot(@ApiParam("The id of the workspace") @PathParam("id") String workspaceId)
throws ServerException,
BadRequestException,
NotFoundException,
ForbiddenException {
return workspaceManager.getSnapshot(workspaceId)
.stream()
.map(DtoConverter::asDto)
.map(snapshotDto -> linksInjector.injectLinks(snapshotDto, getServiceContext()))
.collect(toList());
}
@POST
@Path("/{id}/command")
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Update the workspace by adding a new command to it",
notes = "This operation can be performed only by the workspace owner")
@ApiResponses({@ApiResponse(code = 200, message = "The workspace successfully updated"),
@ApiResponse(code = 400, message = "Missed required parameters, parameters are not valid"),
@ApiResponse(code = 403, message = "The user does not have access to update the workspace"),
@ApiResponse(code = 404, message = "The workspace not found"),
@ApiResponse(code = 409, message = "The command with such name already exists"),
@ApiResponse(code = 500, message = "Internal server error occurred")})
public WorkspaceDto addCommand(@ApiParam("The workspace id")
@PathParam("id")
String id,
@ApiParam(value = "The new workspace command", required = true)
CommandDto newCommand) throws ServerException,
BadRequestException,
NotFoundException,
ConflictException,
ForbiddenException {
requiredNotNull(newCommand, "Command");
final WorkspaceImpl workspace = workspaceManager.getWorkspace(id);
workspace.getConfig().getCommands().add(new CommandImpl(newCommand));
validator.validateConfig(workspace.getConfig());
return linksInjector.injectLinks(asDto(workspaceManager.updateWorkspace(workspace.getId(), workspace)), getServiceContext());
}
@PUT
@Path("/{id}/command/{name}")
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Update the workspace command by replacing the command with a new one",
notes = "This operation can be performed only by the workspace owner")
@ApiResponses({@ApiResponse(code = 200, message = "The command successfully updated"),
@ApiResponse(code = 400, message = "Missed required parameters, parameters are not valid"),
@ApiResponse(code = 403, message = "The user does not have access to update the workspace"),
@ApiResponse(code = 404, message = "The workspace or the command not found"),
@ApiResponse(code = 409, message = "The Command with such name already exists"),
@ApiResponse(code = 500, message = "Internal server error occurred")})
public WorkspaceDto updateCommand(@ApiParam("The workspace id")
@PathParam("id") String id,
@ApiParam("The name of the command")
@PathParam("name")
String cmdName,
@ApiParam(value = "The command update", required = true)
CommandDto update) throws ServerException,
BadRequestException,
NotFoundException,
ConflictException,
ForbiddenException {
requiredNotNull(update, "Command update");
final WorkspaceImpl workspace = workspaceManager.getWorkspace(id);
final List<CommandImpl> commands = workspace.getConfig().getCommands();
if (!commands.removeIf(cmd -> cmd.getName().equals(cmdName))) {
throw new NotFoundException(format("Workspace '%s' doesn't contain command '%s'", id, cmdName));
}
commands.add(new CommandImpl(update));
validator.validateConfig(workspace.getConfig());
return linksInjector.injectLinks(asDto(workspaceManager.updateWorkspace(workspace.getId(), workspace)), getServiceContext());
}
@DELETE
@Path("/{id}/command/{name}")
@ApiOperation(value = "Remove the command from the workspace",
notes = "This operation can be performed only by the workspace owner")
@ApiResponses({@ApiResponse(code = 204, message = "The command successfully removed"),
@ApiResponse(code = 403, message = "The user does not have access delete the command"),
@ApiResponse(code = 404, message = "The workspace or the command not found"),
@ApiResponse(code = 500, message = "Internal server error occurred")})
public void deleteCommand(@ApiParam("The id of the workspace")
@PathParam("id")
String id,
@ApiParam("The name of the command to remove")
@PathParam("name")
String commandName) throws ServerException,
BadRequestException,
NotFoundException,
ConflictException,
ForbiddenException {
final WorkspaceImpl workspace = workspaceManager.getWorkspace(id);
if (workspace.getConfig().getCommands().removeIf(command -> command.getName().equals(commandName))) {
workspaceManager.updateWorkspace(id, workspace);
} else {
throw new NotFoundException(format("Command with name '%s' was not found in workspace '%s'",
commandName,
workspace.getConfig().getName()));
}
}
@POST
@Path("/{id}/environment")
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Add a new environment to the workspace",
notes = "This operation can be performed only by the workspace owner")
@ApiResponses({@ApiResponse(code = 200, message = "The workspace successfully updated"),
@ApiResponse(code = 400, message = "Missed required parameters, parameters are not valid"),
@ApiResponse(code = 403, message = "The user does not have access to add the environment"),
@ApiResponse(code = 404, message = "The workspace not found"),
@ApiResponse(code = 409, message = "Environment with such name already exists"),
@ApiResponse(code = 500, message = "Internal server error occurred")})
public WorkspaceDto addEnvironment(@ApiParam("The workspace id")
@PathParam("id")
String id,
@ApiParam(value = "The new environment", required = true)
EnvironmentDto newEnvironment,
@ApiParam(value = "The name of the environment", required = true)
@QueryParam("name")
String envName) throws ServerException,
BadRequestException,
NotFoundException,
ConflictException,
ForbiddenException {
requiredNotNull(newEnvironment, "New environment");
requiredNotNull(envName, "New environment name");
relativizeRecipeLinks(newEnvironment);
final WorkspaceImpl workspace = workspaceManager.getWorkspace(id);
workspace.getConfig().getEnvironments().put(envName, new EnvironmentImpl(newEnvironment));
validator.validateConfig(workspace.getConfig());
return linksInjector.injectLinks(asDto(workspaceManager.updateWorkspace(id, workspace)), getServiceContext());
}
@PUT
@Path("/{id}/environment/{name}")
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Update the workspace environment by replacing it with a new one",
notes = "This operation can be performed only by the workspace owner")
@ApiResponses({@ApiResponse(code = 200, message = "The environment successfully updated"),
@ApiResponse(code = 400, message = "Missed required parameters, parameters are not valid"),
@ApiResponse(code = 403, message = "The user does not have access to update the environment"),
@ApiResponse(code = 404, message = "The workspace or the environment not found"),
@ApiResponse(code = 500, message = "Internal server error occurred")})
public WorkspaceDto updateEnvironment(@ApiParam("The workspace id")
@PathParam("id")
String id,
@ApiParam("The name of the environment")
@PathParam("name")
String envName,
@ApiParam(value = "The environment update", required = true)
EnvironmentDto update) throws ServerException,
BadRequestException,
NotFoundException,
ConflictException,
ForbiddenException {
requiredNotNull(update, "Environment description");
relativizeRecipeLinks(update);
final WorkspaceImpl workspace = workspaceManager.getWorkspace(id);
EnvironmentImpl previous = workspace.getConfig().getEnvironments().put(envName, new EnvironmentImpl(update));
if (previous == null) {
throw new NotFoundException(format("Workspace '%s' doesn't contain environment '%s'", id, envName));
}
validator.validateConfig(workspace.getConfig());
return linksInjector.injectLinks(asDto(workspaceManager.updateWorkspace(id, workspace)), getServiceContext());
}
@DELETE
@Path("/{id}/environment/{name}")
@ApiOperation(value = "Remove the environment from the workspace",
notes = "This operation can be performed only by the workspace owner")
@ApiResponses({@ApiResponse(code = 204, message = "The environment successfully removed"),
@ApiResponse(code = 403, message = "The user does not have access remove the environment"),
@ApiResponse(code = 404, message = "The workspace not found"),
@ApiResponse(code = 500, message = "Internal server error occurred")})
public void deleteEnvironment(@ApiParam("The workspace id")
@PathParam("id")
String id,
@ApiParam("The name of the environment")
@PathParam("name")
String envName) throws ServerException,
BadRequestException,
NotFoundException,
ConflictException,
ForbiddenException {
final WorkspaceImpl workspace = workspaceManager.getWorkspace(id);
if (workspace.getConfig().getEnvironments().remove(envName) != null) {
workspaceManager.updateWorkspace(id, workspace);
}
}
@POST
@Path("/{id}/project")
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Adds a new project to the workspace",
notes = "This operation can be performed only by the workspace owner")
@ApiResponses({@ApiResponse(code = 200, message = "The project successfully added to the workspace"),
@ApiResponse(code = 400, message = "Missed required parameters, parameters are not valid"),
@ApiResponse(code = 403, message = "The user does not have access to add the project"),
@ApiResponse(code = 404, message = "The workspace not found"),
@ApiResponse(code = 409, message = "Any conflict error occurs"),
@ApiResponse(code = 500, message = "Internal server error occurred")})
public WorkspaceDto addProject(@ApiParam("The workspace id")
@PathParam("id")
String id,
@ApiParam(value = "The new project", required = true)
ProjectConfigDto newProject) throws ServerException,
BadRequestException,
NotFoundException,
ConflictException,
ForbiddenException {
requiredNotNull(newProject, "New project config");
final WorkspaceImpl workspace = workspaceManager.getWorkspace(id);
workspace.getConfig().getProjects().add(new ProjectConfigImpl(newProject));
validator.validateConfig(workspace.getConfig());
return linksInjector.injectLinks(asDto(workspaceManager.updateWorkspace(id, workspace)), getServiceContext());
}
@PUT
@Path("/{id}/project/{path:.*}")
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Update the workspace project by replacing it with a new one",
notes = "This operation can be performed only by the workspace owner")
@ApiResponses({@ApiResponse(code = 200, message = "The project successfully updated"),
@ApiResponse(code = 400, message = "Missed required parameters, parameters are not valid"),
@ApiResponse(code = 403, message = "The user does not have access to update the project"),
@ApiResponse(code = 404, message = "The workspace or the project not found"),
@ApiResponse(code = 500, message = "Internal server error occurred")})
public WorkspaceDto updateProject(@ApiParam("The workspace id")
@PathParam("id")
String id,
@ApiParam("The path to the project")
@PathParam("path")
String path,
@ApiParam(value = "The project update", required = true)
ProjectConfigDto update) throws ServerException,
BadRequestException,
NotFoundException,
ConflictException,
ForbiddenException {
requiredNotNull(update, "Project config");
final WorkspaceImpl workspace = workspaceManager.getWorkspace(id);
final List<ProjectConfigImpl> projects = workspace.getConfig().getProjects();
final String normalizedPath = path.startsWith("/") ? path : '/' + path;
if (!projects.removeIf(project -> project.getPath().equals(normalizedPath))) {
throw new NotFoundException(format("Workspace '%s' doesn't contain project with path '%s'",
id,
normalizedPath));
}
projects.add(new ProjectConfigImpl(update));
validator.validateConfig(workspace.getConfig());
return linksInjector.injectLinks(asDto(workspaceManager.updateWorkspace(id, workspace)), getServiceContext());
}
@DELETE
@Path("/{id}/project/{path:.*}")
@ApiOperation(value = "Remove the project from the workspace",
notes = "This operation can be performed only by the workspace owner")
@ApiResponses({@ApiResponse(code = 204, message = "The project successfully removed"),
@ApiResponse(code = 403, message = "The user does not have access remove the project"),
@ApiResponse(code = 404, message = "The workspace not found"),
@ApiResponse(code = 500, message = "Internal server error occurred")})
public void deleteProject(@ApiParam("The workspace id")
@PathParam("id")
String id,
@ApiParam("The name of the project to remove")
@PathParam("path")
String path) throws ServerException,
BadRequestException,
NotFoundException,
ConflictException,
ForbiddenException {
final WorkspaceImpl workspace = workspaceManager.getWorkspace(id);
final String normalizedPath = path.startsWith("/") ? path : '/' + path;
if (workspace.getConfig().getProjects().removeIf(project -> project.getPath().equals(normalizedPath))) {
workspaceManager.updateWorkspace(id, workspace);
}
}
@GET
@Path("/{id}/check")
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Get state of the workspace agent by the workspace id")
@ApiResponses({@ApiResponse(code = 200, message = "The response contains requested workspace entity"),
@ApiResponse(code = 404, message = "The workspace with specified id does not exist"),
@ApiResponse(code = 500, message = "Internal server error occurred")})
public WsAgentHealthStateDto checkAgentHealth(@ApiParam(value = "Workspace id")
@PathParam("id") String id) throws NotFoundException, ServerException {
final WorkspaceImpl workspace = workspaceManager.getWorkspace(id);
if (WorkspaceStatus.RUNNING != workspace.getStatus()) {
return newDto(WsAgentHealthStateDto.class).withWorkspaceStatus(workspace.getStatus());
}
final MachineImpl devMachine = workspace.getRuntime().getDevMachine();
if (devMachine == null) {
return newDto(WsAgentHealthStateDto.class)
.withWorkspaceStatus(workspace.getStatus())
.withCode(NOT_FOUND.getStatusCode())
.withReason("Workspace Agent isn't available if Dev machine isn't RUNNING");
}
final WsAgentHealthStateDto check = agentHealthChecker.check(devMachine);
check.setWorkspaceStatus(workspace.getStatus());
return check;
}
@GET
@Path("/settings")
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Get workspace server configuration values")
@ApiResponses({@ApiResponse(code = 200, message = "The response contains server settings")})
public Map<String, String> getSettings() {
return ImmutableMap.of(CHE_WORKSPACE_AUTO_SNAPSHOT, Boolean.toString(cheWorkspaceAutoSnapshot), //
CHE_WORKSPACE_AUTO_RESTORE, Boolean.toString(cheWorkspaceAutoRestore), //
CHE_WORKSPACE_AUTO_START, Boolean.toString(cheWorkspaceAutoStart));
}
private static Map<String, String> parseAttrs(List<String> attributes) throws BadRequestException {
if (attributes == null) {
return emptyMap();
}
final Map<String, String> res = Maps.newHashMapWithExpectedSize(attributes.size());
for (String attribute : attributes) {
final int colonIdx = attribute.indexOf(':');
if (colonIdx == -1) {
throw new BadRequestException("Attribute '" + attribute + "' is not valid, " +
"it should contain name and value separated with colon. " +
"For example: attributeName:attributeValue");
}
res.put(attribute.substring(0, colonIdx), attribute.substring(colonIdx + 1));
}
return res;
}
/**
* Checks object reference is not {@code null}
*
* @param object
* object reference to check
* @param subject
* used as subject of exception message "{subject} required"
* @throws BadRequestException
* when object reference is {@code null}
*/
private void requiredNotNull(Object object, String subject) throws BadRequestException {
if (object == null) {
throw new BadRequestException(subject + " required");
}
}
/*
* 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) {
config.getEnvironments().values().forEach(this::relativizeRecipeLinks);
}
private void relativizeRecipeLinks(EnvironmentDto environment) {
EnvironmentRecipeDto recipe = environment.getRecipe();
if (recipe.getType().equals("dockerfile")) {
String location = recipe.getLocation();
if (location != null && location.startsWith(apiEndpoint)) {
recipe.setLocation(location.substring(apiEndpoint.length()));
}
}
}
}