/*******************************************************************************
* 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 com.google.inject.Inject;
import org.eclipse.che.account.api.AccountManager;
import org.eclipse.che.account.shared.model.Account;
import org.eclipse.che.api.agent.server.exception.AgentException;
import org.eclipse.che.api.core.ApiException;
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.model.machine.MachineConfig;
import org.eclipse.che.api.core.model.workspace.Workspace;
import org.eclipse.che.api.core.model.workspace.WorkspaceConfig;
import org.eclipse.che.api.core.model.workspace.WorkspaceStatus;
import org.eclipse.che.api.core.notification.EventService;
import org.eclipse.che.api.environment.server.exception.EnvironmentException;
import org.eclipse.che.api.machine.server.exception.SnapshotException;
import org.eclipse.che.api.machine.server.exception.SourceNotFoundException;
import org.eclipse.che.api.machine.server.model.impl.SnapshotImpl;
import org.eclipse.che.api.machine.server.spi.Instance;
import org.eclipse.che.api.machine.server.spi.SnapshotDao;
import org.eclipse.che.api.workspace.server.event.WorkspaceCreatedEvent;
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.spi.WorkspaceDao;
import org.eclipse.che.api.workspace.shared.dto.event.WorkspaceStatusEvent.EventType;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.commons.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Throwables.getCausalChain;
import static java.lang.Boolean.parseBoolean;
import static java.lang.String.format;
import static java.lang.System.currentTimeMillis;
import static java.util.Collections.emptyMap;
import static java.util.Objects.requireNonNull;
import static org.eclipse.che.api.core.model.workspace.WorkspaceStatus.RUNNING;
import static org.eclipse.che.api.workspace.shared.Constants.AUTO_CREATE_SNAPSHOT;
import static org.eclipse.che.api.workspace.shared.Constants.AUTO_RESTORE_FROM_SNAPSHOT;
import static org.eclipse.che.api.workspace.shared.Constants.WORKSPACE_STOPPED_BY;
/**
* Facade for Workspace related operations.
*
* @author gazarenkov
* @author Alexander Garagatyi
* @author Yevhenii Voevodin
* @author Igor Vinokur
*/
@Singleton
public class WorkspaceManager {
private static final Logger LOG = LoggerFactory.getLogger(WorkspaceManager.class);
/** This attribute describes time when workspace was created. */
public static final String CREATED_ATTRIBUTE_NAME = "created";
/** This attribute describes time when workspace was last updated or started/stopped/recovered. */
public static final String UPDATED_ATTRIBUTE_NAME = "updated";
/** Describes time when workspace was snapshotted. */
public static final String SNAPSHOTTED_AT_ATTRIBUTE_NAME = "snapshotted_at";
private final WorkspaceDao workspaceDao;
private final SnapshotDao snapshotDao;
private final WorkspaceRuntimes runtimes;
private final AccountManager accountManager;
private final WorkspaceSharedPool sharedPool;
private final EventService eventService;
private final boolean defaultAutoSnapshot;
private final boolean defaultAutoRestore;
@Inject
public WorkspaceManager(WorkspaceDao workspaceDao,
WorkspaceRuntimes workspaceRegistry,
EventService eventService,
AccountManager accountManager,
@Named("che.workspace.auto_snapshot") boolean defaultAutoSnapshot,
@Named("che.workspace.auto_restore") boolean defaultAutoRestore,
SnapshotDao snapshotDao,
WorkspaceSharedPool sharedPool) {
this.workspaceDao = workspaceDao;
this.snapshotDao = snapshotDao;
this.runtimes = workspaceRegistry;
this.accountManager = accountManager;
this.eventService = eventService;
this.defaultAutoSnapshot = defaultAutoSnapshot;
this.defaultAutoRestore = defaultAutoRestore;
this.sharedPool = sharedPool;
}
/**
* Creates a new {@link WorkspaceImpl} instance based on the given configuration.
*
* @param config
* the workspace config to create the new workspace instance
* @param namespace
* workspace name is unique in this namespace
* @return new workspace instance
* @throws NullPointerException
* when either {@code config} or {@code owner} is null
* @throws NotFoundException
* when account with given id was not found
* @throws ConflictException
* when any conflict occurs (e.g Workspace with such name already exists for {@code owner})
* @throws ServerException
* when any other error occurs
*/
public WorkspaceImpl createWorkspace(WorkspaceConfig config,
String namespace) throws ServerException,
ConflictException,
NotFoundException {
requireNonNull(config, "Required non-null config");
requireNonNull(namespace, "Required non-null namespace");
WorkspaceImpl workspace = doCreateWorkspace(config,
accountManager.getByName(namespace),
emptyMap(),
false);
workspace.setStatus(WorkspaceStatus.STOPPED);
return workspace;
}
/**
* Creates a new {@link Workspace} instance based on
* the given configuration and the instance attributes.
*
* @param config
* the workspace config to create the new workspace instance
* @param namespace
* workspace name is unique in this namespace
* @param attributes
* workspace instance attributes
* @return new workspace instance
* @throws NullPointerException
* when either {@code config} or {@code owner} is null
* @throws NotFoundException
* when account with given id was not found
* @throws ConflictException
* when any conflict occurs (e.g Workspace with such name already exists for {@code owner})
* @throws ServerException
* when any other error occurs
*/
public WorkspaceImpl createWorkspace(WorkspaceConfig config,
String namespace,
Map<String, String> attributes) throws ServerException,
NotFoundException,
ConflictException {
requireNonNull(config, "Required non-null config");
requireNonNull(namespace, "Required non-null namespace");
requireNonNull(attributes, "Required non-null attributes");
WorkspaceImpl workspace = doCreateWorkspace(config,
accountManager.getByName(namespace),
attributes,
false);
workspace.setStatus(WorkspaceStatus.STOPPED);
return workspace;
}
/**
* Gets workspace by composite key.
*
* <p> Key rules:
* <ul>
* <li>@Deprecated : If it contains <b>:</b> character then that key is combination of namespace and workspace name
* <li>@Deprecated : <b></>:workspace_name</b> is valid abstract key and current user name will be used as namespace
* <li>If it doesn't contain <b>/</b> character then that key is id(e.g. workspace123456)
* <li>If it contains <b>/</b> character then that key is combination of namespace and workspace name
* </ul>
*
* Note that namespace can contain <b>/</b> character.
*
* @param key
* composite key(e.g. workspace 'id' or 'namespace/name')
* @return the workspace instance
* @throws NullPointerException
* when {@code key} is null
* @throws NotFoundException
* when workspace doesn't exist
* @throws ServerException
* when any server error occurs
*/
public WorkspaceImpl getWorkspace(String key) throws NotFoundException, ServerException {
requireNonNull(key, "Required non-null workspace key");
WorkspaceImpl workspace = getByKey(key);
runtimes.injectRuntime(workspace);
addExtraAttributes(workspace);
return workspace;
}
/**
* Gets workspace by name and owner.
*
* <p>Returned instance status is either {@link WorkspaceStatus#STOPPED}
* or defined by its runtime(if exists).
*
* @param name
* the name of the workspace
* @param namespace
* the owner of the workspace
* @return the workspace instance
* @throws NotFoundException
* when workspace with such id doesn't exist
* @throws ServerException
* when any server error occurs
*/
public WorkspaceImpl getWorkspace(String name, String namespace) throws NotFoundException, ServerException {
requireNonNull(name, "Required non-null workspace name");
requireNonNull(namespace, "Required non-null workspace owner");
WorkspaceImpl workspace = workspaceDao.get(name, namespace);
runtimes.injectRuntime(workspace);
addExtraAttributes(workspace);
return workspace;
}
/**
* Gets list of workspaces which user can read. Runtimes are included
*
* @param user
* the id of the user
* @return the list of workspaces or empty list if user can't read any workspace
* @throws NullPointerException
* when {@code user} is null
* @throws ServerException
* when any server error occurs while getting workspaces with {@link WorkspaceDao#getWorkspaces(String)}
* @deprecated use #getWorkspaces(String user, boolean includeRuntimes) instead
*/
@Deprecated
public List<WorkspaceImpl> getWorkspaces(String user) throws ServerException {
return getWorkspaces(user, true);
}
/**
* Gets list of workspaces which user can read
*
* <p>Returned workspaces have either {@link WorkspaceStatus#STOPPED} status
* or status defined by their runtime instances(if those exist).
*
* @param user
* the id of the user
* @param includeRuntimes
* if <code>true</code>, will fetch runtime info for workspaces.
* If <code>false</code>, will not fetch runtime info.
* @return the list of workspaces or empty list if user can't read any workspace
* @throws NullPointerException
* when {@code user} is null
* @throws ServerException
* when any server error occurs while getting workspaces with {@link WorkspaceDao#getWorkspaces(String)}
*/
public List<WorkspaceImpl> getWorkspaces(String user, boolean includeRuntimes) throws ServerException {
requireNonNull(user, "Required non-null user id");
final List<WorkspaceImpl> workspaces = workspaceDao.getWorkspaces(user);
injectRuntimeAndAttributes(workspaces, !includeRuntimes);
return workspaces;
}
/**
* Gets list of workspaces which has given namespace. Runtimes are included
*
* @param namespace
* the namespace to find workspaces
* @return the list of workspaces or empty list if no matches
* @throws NullPointerException
* when {@code namespace} is null
* @throws ServerException
* when any server error occurs while getting workspaces with {@link WorkspaceDao#getByNamespace(String)}
* @deprecated use #getByNamespace(String user, boolean includeRuntimes) instead
*/
@Deprecated
public List<WorkspaceImpl> getByNamespace(String namespace) throws ServerException {
return getByNamespace(namespace, true);
}
/**
* Gets list of workspaces which has given namespace
*
* <p>Returned workspaces have either {@link WorkspaceStatus#STOPPED} status
* or status defined by their runtime instances(if those exist).
*
* @param namespace
* the namespace to find workspaces
* @param includeRuntimes
* if <code>true</code>, will fetch runtime info for workspaces.
* If <code>false</code>, will not fetch runtime info.
* @return the list of workspaces or empty list if no matches
* @throws NullPointerException
* when {@code namespace} is null
* @throws ServerException
* when any server error occurs while getting workspaces with {@link WorkspaceDao#getByNamespace(String)}
*/
public List<WorkspaceImpl> getByNamespace(String namespace, boolean includeRuntimes) throws ServerException {
requireNonNull(namespace, "Required non-null namespace");
final List<WorkspaceImpl> workspaces = workspaceDao.getByNamespace(namespace);
injectRuntimeAndAttributes(workspaces, !includeRuntimes);
return workspaces;
}
/**
* Updates an existing workspace with a new configuration.
*
* <p>Replace strategy is used for workspace update, it means
* that existing workspace data will be replaced with given {@code update}.
*
* @param update
* workspace update
* @return updated instance of the workspace
* @throws NullPointerException
* when either {@code workspaceId} or {@code update} is null
* @throws NotFoundException
* when workspace with given id doesn't exist
* @throws ConflictException
* when any conflict occurs (e.g Workspace with such name already exists in {@code namespace})
* @throws ServerException
* when any other error occurs
*/
public WorkspaceImpl updateWorkspace(String id, Workspace update) throws ConflictException,
ServerException,
NotFoundException {
requireNonNull(id, "Required non-null workspace id");
requireNonNull(update, "Required non-null workspace update");
WorkspaceImpl workspace = workspaceDao.get(id);
workspace.setConfig(new WorkspaceConfigImpl(update.getConfig()));
update.getAttributes().put(UPDATED_ATTRIBUTE_NAME, Long.toString(currentTimeMillis()));
workspace.setAttributes(update.getAttributes());
workspace.setTemporary(update.isTemporary());
WorkspaceImpl updated = workspaceDao.update(workspace);
runtimes.injectRuntime(updated);
return updated;
}
/**
* Removes workspace with specified identifier.
*
* <p>Does not remove the workspace if it has the runtime,
* throws {@link ConflictException} in this case.
* Won't throw any exception if workspace doesn't exist.
*
* @param workspaceId
* workspace id to remove workspace
* @throws ConflictException
* when workspace has runtime
* @throws ServerException
* when any server error occurs
* @throws NullPointerException
* when {@code workspaceId} is null
*/
public void removeWorkspace(String workspaceId) throws ConflictException, ServerException {
requireNonNull(workspaceId, "Required non-null workspace id");
if (runtimes.hasRuntime(workspaceId)) {
throw new ConflictException(format("The workspace '%s' is currently running and cannot be removed.",
workspaceId));
}
workspaceDao.remove(workspaceId);
LOG.info("Workspace '{}' removed by user '{}'", workspaceId, sessionUserNameOr("undefined"));
}
/**
* Asynchronously starts certain workspace with specified environment and account.
*
* @param workspaceId
* identifier of workspace which should be started
* @param envName
* name of environment or null, when default environment should be used
* @param restore
* if <code>true</code> workspace will be restored from snapshot if snapshot exists,
* otherwise (if snapshot does not exist) workspace will be started from default source.
* If <code>false</code> workspace will be started from default source,
* even if auto-restore is enabled and snapshot exists.
* If <code>null</code> workspace will be restored from snapshot
* only if workspace has `auto-restore` attribute set to <code>true</code>,
* or system wide parameter `auto-restore` is enabled and snapshot exists.
* <p>
* This parameter has the highest priority to define if it is needed to restore from snapshot or not.
* If it is not defined workspace `auto-restore` attribute will be checked, then if last is not defined
* system wide `auto-restore` parameter will be checked.
* @return starting workspace
* @throws NullPointerException
* when {@code workspaceId} is null
* @throws NotFoundException
* when workspace with given {@code workspaceId} doesn't exist
* @throws ServerException
* when any other error occurs during workspace start
*/
public WorkspaceImpl startWorkspace(String workspaceId,
@Nullable String envName,
@Nullable Boolean restore) throws NotFoundException,
ServerException,
ConflictException {
requireNonNull(workspaceId, "Required non-null workspace id");
final WorkspaceImpl workspace = workspaceDao.get(workspaceId);
final String restoreAttr = workspace.getAttributes().get(AUTO_RESTORE_FROM_SNAPSHOT);
final boolean autoRestore = restoreAttr == null ? defaultAutoRestore : parseBoolean(restoreAttr);
startAsync(workspace, envName, firstNonNull(restore, autoRestore) && !getSnapshot(workspaceId).isEmpty());
runtimes.injectRuntime(workspace);
addExtraAttributes(workspace);
return workspace;
}
/**
* Asynchronously starts workspace from the given configuration.
*
* @param config
* workspace configuration from which workspace is created and started
* @param namespace
* workspace name is unique in this namespace
* @return starting workspace
* @throws NullPointerException
* when {@code workspaceId} is null
* @throws NotFoundException
* when workspace with given {@code workspaceId} doesn't exist
* @throws ServerException
* when any other error occurs during workspace start
*/
public WorkspaceImpl startWorkspace(WorkspaceConfig config,
String namespace,
boolean isTemporary) throws ServerException,
NotFoundException,
ConflictException {
requireNonNull(config, "Required non-null configuration");
requireNonNull(namespace, "Required non-null namespace");
final WorkspaceImpl workspace = doCreateWorkspace(config,
accountManager.getByName(namespace),
emptyMap(),
isTemporary);
startAsync(workspace, workspace.getConfig().getDefaultEnv(), false);
runtimes.injectRuntime(workspace);
return workspace;
}
/**
* Starts machine in running workspace
*
* @param machineConfig
* configuration of machine to start
* @param workspaceId
* id of workspace in which machine should be started
* @throws NotFoundException
* if machine type from recipe is unsupported
* @throws NotFoundException
* if no instance provider implementation found for provided machine type
* @throws ConflictException
* if machine with given name already exists
* @throws ConflictException
* if workspace is not in RUNNING state
* @throws BadRequestException
* if machine name is invalid
* @throws ServerException
* if any other exception occurs during starting
*/
public void startMachine(MachineConfig machineConfig,
String workspaceId) throws ServerException,
ConflictException,
BadRequestException,
NotFoundException {
final WorkspaceImpl workspace = getWorkspace(workspaceId);
if (RUNNING != workspace.getStatus()) {
throw new ConflictException(format("Workspace '%s' is not running, new machine can't be started", workspaceId));
}
startAsync(machineConfig, workspaceId);
}
/**
* Asynchronously stops the workspace.
*
* @param workspaceId
* the id of the workspace to stop
* @throws ServerException
* when any server error occurs
* @throws NullPointerException
* when {@code workspaceId} is null
* @throws NotFoundException
* when workspace {@code workspaceId} doesn't have runtime
*/
public void stopWorkspace(String workspaceId) throws ServerException,
NotFoundException,
ConflictException {
stopWorkspace(workspaceId, null);
}
/**
* Asynchronously stops the workspace,
* creates a snapshot of it if {@code createSnapshot} is set to true.
*
* @param workspaceId
* the id of the workspace to stop
* @param createSnapshot
* true if create snapshot, false if don't,
* null if default behaviour should be used
* @throws ServerException
* when any server error occurs
* @throws NullPointerException
* when {@code workspaceId} is null
* @throws NotFoundException
* when workspace {@code workspaceId} doesn't have runtime
*/
public void stopWorkspace(String workspaceId, @Nullable Boolean createSnapshot) throws ConflictException,
NotFoundException,
ServerException {
requireNonNull(workspaceId, "Required non-null workspace id");
final WorkspaceImpl workspace = workspaceDao.get(workspaceId);
workspace.setStatus(runtimes.getStatus(workspaceId));
if (workspace.getStatus() != WorkspaceStatus.RUNNING && workspace.getStatus() != WorkspaceStatus.STARTING) {
throw new ConflictException(format("Could not stop the workspace '%s/%s' because its status is '%s'. " +
"Workspace must be either 'STARTING' or 'RUNNING'",
workspace.getNamespace(),
workspace.getConfig().getName(),
workspace.getStatus()));
}
stopAsync(workspace, createSnapshot);
}
/**
* Creates snapshot of runtime workspace.
*
* <p>Basically creates {@link SnapshotImpl snapshot} instance for each machine from
* runtime workspace's active environment.
*
* <p> If snapshot of workspace's dev machine was created successfully
* publishes {@link EventType#SNAPSHOT_CREATED} event, otherwise publishes {@link EventType#SNAPSHOT_CREATION_ERROR}
* with appropriate error message.
*
* <p> Note that:
* <br>Snapshots are created asynchronously
* <br>If snapshot creation for one machine failed, it wouldn't affect another snapshot creations
*
* @param workspaceId
* runtime workspace id
* @throws NullPointerException
* when {@code workspaceId} is null
* @throws NotFoundException
* when runtime workspace with given id does not exist
* @throws ServerException
* when any other error occurs
* @throws ConflictException
* when workspace is not running
*/
public void createSnapshot(String workspaceId) throws NotFoundException,
ServerException,
ConflictException {
requireNonNull(workspaceId, "Required non-null workspace id");
runtimes.snapshotAsync(workspaceId);
}
/**
* Returns list of machine snapshots which are related to workspace with given id.
*
* @param workspaceId
* workspace id to get snapshot
* @return list of machine snapshots related to given workspace
* @throws NullPointerException
* when {@code workspaceId} is null
* @throws NotFoundException
* when workspace with given id doesn't exists
* @throws ServerException
* when any other error occurs
*/
public List<SnapshotImpl> getSnapshot(String workspaceId) throws ServerException, NotFoundException {
requireNonNull(workspaceId, "Required non-null workspace id");
// check if workspace exists
workspaceDao.get(workspaceId);
return snapshotDao.findSnapshots(workspaceId);
}
/**
* Removes all snapshots of workspace machines,
* continues to remove snapshots even when removal of some of them fails.
*
* <p>Note that snapshots binaries are removed asynchronously
* while metadata removal is synchronous operation.
*
* @param workspaceId
* workspace id to remove machine snapshots
* @throws NotFoundException
* when workspace with given id doesn't exists
* @throws ServerException
* when any other error occurs
*/
public void removeSnapshots(String workspaceId) throws NotFoundException, ServerException {
List<SnapshotImpl> snapshots = getSnapshot(workspaceId);
List<SnapshotImpl> removed = new ArrayList<>(snapshots.size());
for (SnapshotImpl snapshot : snapshots) {
try {
snapshotDao.removeSnapshot(snapshot.getId());
removed.add(snapshot);
} catch (Exception x) {
LOG.error(format("Couldn't remove snapshot '%s' meta data, " +
"binaries won't be removed either", snapshot.getId()), x);
}
}
// binaries removal may take some time, do it asynchronously
sharedPool.execute(() -> runtimes.removeBinaries(removed));
}
/**
* Stops machine in running workspace.
*
* @param workspaceId
* ID of workspace that owns machine
* @param machineId
* ID of machine that should be stopped
* @throws NotFoundException
* if machine is not found in running workspace
* @throws ConflictException
* if workspace is not running
* @throws ConflictException
* if machine stop is forbidden (e.g. machine is dev-machine)
* @throws ServerException
* if other error occurs
*/
public void stopMachine(String workspaceId,
String machineId) throws NotFoundException,
ServerException,
ConflictException {
requireNonNull(workspaceId, "Required non-null workspace id");
requireNonNull(machineId, "Required non-null machine id");
final WorkspaceImpl workspace = workspaceDao.get(workspaceId);
workspace.setStatus(runtimes.getStatus(workspaceId));
checkWorkspaceIsRunning(workspace, format("stop machine with ID '%s' of", machineId));
runtimes.stopMachine(workspaceId, machineId);
}
/**
* Retrieves machine instance that allows to execute commands in a machine.
*
* @param workspaceId
* ID of workspace that owns machine
* @param machineId
* ID of requested machine
* @return instance of requested machine
* @throws NotFoundException
* if workspace is not running
* @throws NotFoundException
* if machine is not found in the workspace
*/
public Instance getMachineInstance(String workspaceId,
String machineId) throws NotFoundException,
ServerException {
requireNonNull(workspaceId, "Required non-null workspace id");
requireNonNull(machineId, "Required non-null machine id");
workspaceDao.get(workspaceId);
return runtimes.getMachine(workspaceId, machineId);
}
/**
* Shuts down workspace service and waits for it to finish, so currently
* starting and running workspaces are stopped and it becomes unavailable to start new workspaces.
*
* @throws InterruptedException
* if it's interrupted while waiting for running workspaces to stop
* @throws IllegalStateException
* if component shutdown is already called
*/
public void shutdown() throws InterruptedException {
if (!runtimes.refuseWorkspacesStart()) {
throw new IllegalStateException("Workspace service shutdown has been already called");
}
stopRunningWorkspacesNormally();
runtimes.shutdown();
sharedPool.shutdown();
}
/**
* Returns set of workspace ids that are not {@link WorkspaceStatus#STOPPED}.
*/
public Set<String> getRunningWorkspacesIds() {
return runtimes.getRuntimesIds();
}
/**
* Stops all the running and starting workspaces - snapshotting them before if needed.
* Workspace stop operations executed asynchronously while the method waits
* for async task to finish.
*/
private void stopRunningWorkspacesNormally() throws InterruptedException {
if (runtimes.isAnyRunning()) {
// getting all the running or starting workspaces
ArrayList<WorkspaceImpl> runningOrStarting = new ArrayList<>();
for (String workspaceId : runtimes.getRuntimesIds()) {
try {
WorkspaceImpl workspace = workspaceDao.get(workspaceId);
workspace.setStatus(runtimes.getStatus(workspaceId));
if (workspace.getStatus() == WorkspaceStatus.RUNNING || workspace.getStatus() == WorkspaceStatus.STARTING) {
runningOrStarting.add(workspace);
}
} catch (NotFoundException | ServerException x) {
if (runtimes.hasRuntime(workspaceId)) {
LOG.error("Couldn't get the workspace '{}' while it's running, the occurred error: '{}'",
workspaceId,
x.getMessage());
}
}
}
// stopping them asynchronously
CountDownLatch stopLatch = new CountDownLatch(runningOrStarting.size());
for (WorkspaceImpl workspace : runningOrStarting) {
try {
stopAsync(workspace, null).whenComplete((res, ex) -> stopLatch.countDown());
} catch (Exception x) {
stopLatch.countDown();
if (runtimes.hasRuntime(workspace.getId())) {
LOG.warn("Couldn't stop the workspace '{}' normally, due to error: {}", workspace.getId(), x.getMessage());
}
}
}
// wait for stopping workspaces to complete
stopLatch.await();
}
}
/** Asynchronously starts given workspace. */
private void startAsync(WorkspaceImpl workspace,
String envName,
boolean recover) throws ConflictException,
NotFoundException,
ServerException {
if (envName != null && !workspace.getConfig().getEnvironments().containsKey(envName)) {
throw new NotFoundException(format("Workspace '%s/%s' doesn't contain environment '%s'",
workspace.getNamespace(),
workspace.getConfig().getName(),
envName));
}
workspace.getAttributes().put(UPDATED_ATTRIBUTE_NAME, Long.toString(currentTimeMillis()));
workspaceDao.update(workspace);
final String env = firstNonNull(envName, workspace.getConfig().getDefaultEnv());
runtimes.startAsync(workspace, env, recover)
.whenComplete((runtime, ex) -> {
if (ex == null) {
LOG.info("Workspace '{}/{}' with id '{}' started by user '{}'",
workspace.getNamespace(),
workspace.getConfig().getName(),
workspace.getId(),
sessionUserNameOr("undefined"));
} else {
if (workspace.isTemporary()) {
removeWorkspaceQuietly(workspace);
}
for (Throwable cause : getCausalChain(ex)) {
if (cause instanceof SourceNotFoundException) {
return;
}
}
try {
throw ex;
} catch (EnvironmentException | AgentException e) {
// it's okay, e.g. recipe is invalid | start interrupted | agent start failed
LOG.info("Workspace '{}/{}' can't be started because: {}",
workspace.getNamespace(),
workspace.getConfig().getName(),
e.getMessage());
} catch (Throwable thr) {
LOG.error(thr.getMessage(), thr);
}
}
});
}
private CompletableFuture<Void> stopAsync(WorkspaceImpl workspace,
@Nullable Boolean createSnapshot) throws ConflictException,
NotFoundException,
ServerException {
if (!workspace.isTemporary()) {
workspace.getAttributes().put(UPDATED_ATTRIBUTE_NAME, Long.toString(currentTimeMillis()));
workspaceDao.update(workspace);
}
return sharedPool.runAsync(() -> {
final String stoppedBy = sessionUserNameOr(workspace.getAttributes().get(WORKSPACE_STOPPED_BY));
LOG.info("Workspace '{}/{}' with id '{}' is being stopped by user '{}'",
workspace.getNamespace(),
workspace.getConfig().getName(),
workspace.getId(),
firstNonNull(stoppedBy, "undefined"));
final boolean snapshotBeforeStop;
if (workspace.isTemporary() || workspace.getStatus() == WorkspaceStatus.STARTING) {
snapshotBeforeStop = false;
} else if (createSnapshot != null) {
snapshotBeforeStop = createSnapshot;
} else if (workspace.getAttributes().containsKey(AUTO_CREATE_SNAPSHOT)) {
snapshotBeforeStop = parseBoolean(workspace.getAttributes().get(AUTO_CREATE_SNAPSHOT));
} else {
snapshotBeforeStop = defaultAutoSnapshot;
}
if (snapshotBeforeStop) {
try {
runtimes.snapshot(workspace.getId());
} catch (ConflictException | NotFoundException | ServerException x) {
LOG.warn("Could not create a snapshot of the workspace '{}/{}' " +
"with workspace id '{}'. The workspace will be stopped",
workspace.getNamespace(),
workspace.getConfig().getName(),
workspace.getId());
}
}
try {
runtimes.stop(workspace.getId());
LOG.info("Workspace '{}/{}' with id '{}' stopped by user '{}'",
workspace.getNamespace(),
workspace.getConfig().getName(),
workspace.getId(),
firstNonNull(stoppedBy, "undefined"));
} catch (Exception ex) {
LOG.error(ex.getLocalizedMessage(), ex);
} finally {
if (workspace.isTemporary()) {
removeWorkspaceQuietly(workspace);
}
}
});
}
private void startAsync(MachineConfig machineConfig, String workspaceId) {
sharedPool.execute(() -> {
try {
runtimes.startMachine(workspaceId, machineConfig);
} catch (AgentException e) {
// Agent start failed. User should fix that. No need to disturb an admin
LOG.warn("Error occurs on start of additional machine in workspace %s. Error: %s",
workspaceId, e.getLocalizedMessage());
} catch (ApiException | EnvironmentException e) {
LOG.error(e.getLocalizedMessage(), e);
}
});
}
private void checkWorkspaceIsRunning(WorkspaceImpl workspace, String operation) throws ConflictException {
if (workspace.getStatus() != RUNNING) {
throw new ConflictException(format("Could not %s the workspace '%s/%s' because its status is '%s'.",
operation,
workspace.getNamespace(),
workspace.getConfig().getName(),
workspace.getStatus()));
}
}
private void removeWorkspaceQuietly(Workspace workspace) {
try {
workspaceDao.remove(workspace.getId());
} catch (ServerException x) {
LOG.error("Unable to remove temporary workspace '{}'", workspace.getId());
}
}
private String sessionUserNameOr(String nameIfNoUser) {
final Subject subject = EnvironmentContext.getCurrent().getSubject();
if (!subject.isAnonymous()) {
return subject.getUserName();
}
return nameIfNoUser;
}
private WorkspaceImpl doCreateWorkspace(WorkspaceConfig config,
Account account,
Map<String, String> attributes,
boolean isTemporary) throws NotFoundException,
ServerException,
ConflictException {
final WorkspaceImpl workspace = WorkspaceImpl.builder()
.generateId()
.setConfig(config)
.setAccount(account)
.setAttributes(attributes)
.setTemporary(isTemporary)
.build();
workspace.getAttributes().put(CREATED_ATTRIBUTE_NAME, Long.toString(currentTimeMillis()));
workspaceDao.create(workspace);
LOG.info("Workspace '{}/{}' with id '{}' created by user '{}'",
account.getName(),
workspace.getConfig().getName(),
workspace.getId(),
sessionUserNameOr("undefined"));
eventService.publish(new WorkspaceCreatedEvent(workspace));
return workspace;
}
private WorkspaceImpl getByKey(String key) throws NotFoundException, ServerException {
int lastColonIndex = key.indexOf(":");
int lastSlashIndex = key.lastIndexOf("/");
if (lastSlashIndex == -1 && lastColonIndex == -1) {
// key is id
return workspaceDao.get(key);
}
final String namespace;
final String wsName;
if (lastColonIndex == 0) {
// no namespace, use current user namespace
namespace = EnvironmentContext.getCurrent().getSubject().getUserName();
wsName = key.substring(1);
} else if (lastColonIndex > 0) {
wsName = key.substring(lastColonIndex + 1);
namespace = key.substring(0, lastColonIndex);
} else {
namespace = key.substring(0, lastSlashIndex);
wsName = key.substring(lastSlashIndex + 1);
}
return workspaceDao.get(wsName, namespace);
}
/** Adds runtime data (whole or status only) and extra attributes to each of the given workspaces. */
private void injectRuntimeAndAttributes(List<WorkspaceImpl> workspaces, boolean statusOnly) throws SnapshotException {
if (statusOnly) {
for (WorkspaceImpl workspace : workspaces) {
workspace.setStatus(runtimes.getStatus(workspace.getId()));
addExtraAttributes(workspace);
}
} else {
for (WorkspaceImpl workspace : workspaces) {
runtimes.injectRuntime(workspace);
addExtraAttributes(workspace);
}
}
}
/** Adds attributes that are not originally stored in workspace but should be published. */
private void addExtraAttributes(WorkspaceImpl workspace) throws SnapshotException {
// snapshotted_at
List<SnapshotImpl> snapshots = snapshotDao.findSnapshots(workspace.getId());
if (!snapshots.isEmpty()) {
workspace.getAttributes().put(SNAPSHOTTED_AT_ATTRIBUTE_NAME, Long.toString(snapshots.get(0).getCreationDate()));
}
}
}