/*
* Copyright 2016 ThoughtWorks, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.thoughtworks.go.server.service;
import com.google.common.collect.Sets;
import com.thoughtworks.go.config.*;
import com.thoughtworks.go.config.commands.EntityConfigUpdateCommand;
import com.thoughtworks.go.config.exceptions.GoConfigInvalidException;
import com.thoughtworks.go.config.update.AgentsEntityConfigUpdateCommand;
import com.thoughtworks.go.config.update.AgentsUpdateCommand;
import com.thoughtworks.go.config.update.ModifyEnvironmentCommand;
import com.thoughtworks.go.domain.AgentInstance;
import com.thoughtworks.go.domain.ConfigErrors;
import com.thoughtworks.go.i18n.Localizable;
import com.thoughtworks.go.i18n.LocalizedMessage;
import com.thoughtworks.go.listener.AgentChangeListener;
import com.thoughtworks.go.presentation.TriStateSelection;
import com.thoughtworks.go.server.domain.AgentInstances;
import com.thoughtworks.go.server.domain.Username;
import com.thoughtworks.go.server.service.result.HttpOperationResult;
import com.thoughtworks.go.server.service.result.LocalizedOperationResult;
import com.thoughtworks.go.serverhealth.HealthStateScope;
import com.thoughtworks.go.serverhealth.HealthStateType;
import com.thoughtworks.go.util.TriState;
import com.thoughtworks.go.validation.AgentConfigsUpdateValidator;
import com.thoughtworks.go.validation.ConfigUpdateValidator;
import com.thoughtworks.go.validation.DoNothingValidator;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static com.thoughtworks.go.util.ExceptionUtils.bomb;
import static com.thoughtworks.go.util.ExceptionUtils.bombIfNull;
import static java.util.Arrays.asList;
/**
* @understands how to convert persistant Agent configuration to useful objects and back
*/
@Service
public class AgentConfigService {
private GoConfigService goConfigService;
private static final Logger LOGGER = LoggerFactory.getLogger(AgentConfigService.class.getName());
@Autowired
public AgentConfigService(GoConfigService goConfigService) {
this.goConfigService = goConfigService;
}
public Agents agents() {
return goConfigService.agents();
}
public void register(AgentChangeListener agentChangeListener) {
goConfigService.register(agentChangeListener);
}
public void enableAgents(Username currentUser, AgentInstance... agentInstance) {
disableAgents(false, currentUser, agentInstance);
}
public void disableAgents(Username currentUser, AgentInstance... agentInstance) {
disableAgents(true, currentUser, agentInstance);
}
private void disableAgents(boolean disabled, Username currentUser, AgentInstance... instances) {
GoConfigDao.CompositeConfigCommand command = new GoConfigDao.CompositeConfigCommand();
ArrayList<String> uuids = new ArrayList<>();
for (AgentInstance agentInstance : instances) {
String uuid = agentInstance.getUuid();
uuids.add(uuid);
if (goConfigService.hasAgent(uuid)) {
command.addCommand(new UpdateAgentApprovalStatus(uuid, disabled));
} else {
AgentConfig agentConfig = agentInstance.agentConfig();
agentConfig.disable(disabled);
command.addCommand(new AddAgentCommand(agentConfig));
}
}
updateAgentWithoutValidations(command, currentUser);
}
protected static UpdateConfigCommand updateApprovalStatus(final String uuid, final Boolean isDenied) {
return new UpdateAgentApprovalStatus(uuid, isDenied);
}
public void deleteAgents(Username currentUser, AgentInstance... agentInstances) {
GoConfigDao.CompositeConfigCommand commandForDeletingAgents = commandForDeletingAgents(agentInstances);
ArrayList<String> uuids = new ArrayList<>();
for (AgentInstance agentInstance : agentInstances) {
uuids.add(agentInstance.getUuid());
}
updateAgentWithoutValidations(commandForDeletingAgents, currentUser);
}
protected GoConfigDao.CompositeConfigCommand commandForDeletingAgents(AgentInstance... agentInstances) {
GoConfigDao.CompositeConfigCommand command = new GoConfigDao.CompositeConfigCommand();
for (AgentInstance agentInstance : agentInstances) {
command.addCommand(deleteAgentCommand(agentInstance.getUuid()));
}
return command;
}
public static DeleteAgent deleteAgentCommand(String uuid) {
return new DeleteAgent(uuid);
}
private void updateAgents(final UpdateConfigCommand command, final ConfigUpdateValidator validator, Username currentUser) {
AgentsUpdateCommand updateCommand = new AgentsUpdateCommand(command, validator);
goConfigService.updateConfig(updateCommand, currentUser);
}
public void updateAgent(UpdateConfigCommand command, String uuid, Username currentUser) {
updateAgents(command, new AgentConfigsUpdateValidator(asList(uuid)), currentUser);
}
public AgentConfig updateAgent(UpdateConfigCommand command, String uuid, HttpOperationResult result, Username currentUser) {
AgentConfigsUpdateValidator validator = new AgentConfigsUpdateValidator(asList(uuid));
try {
updateAgents(command, validator, currentUser);
result.ok(String.format("Updated agent with uuid %s.", uuid));
} catch (Exception e) {
if (e instanceof GoConfigInvalidException) {
result.unprocessibleEntity("Updating agent failed:", e.getMessage(), HealthStateType.general(HealthStateScope.GLOBAL));
GoConfigInvalidException goConfigInvalidException = (GoConfigInvalidException) e;
return goConfigInvalidException.getCruiseConfig().agents().getAgentByUuid(uuid);
} else {
result.internalServerError("Updating agent failed: " + e.getMessage(), HealthStateType.general(HealthStateScope.GLOBAL));
return null;
}
}
return goConfigService.agents().getAgentByUuid(uuid);
}
private void updateAgentWithoutValidations(UpdateConfigCommand command, Username currentUser) {
updateAgents(command, new DoNothingValidator(), currentUser);
}
/**
* @understands how to delete agent
*/
private static class DeleteAgent implements UpdateConfigCommand {
private final String uuid;
public DeleteAgent(String uuid) {
this.uuid = uuid;
}
public CruiseConfig update(CruiseConfig cruiseConfig) {
AgentConfig agentConfig = cruiseConfig.agents().getAgentByUuid(uuid);
if (agentConfig.isNull()) {
bomb("Unable to delete agent; Agent [" + uuid + "] not found.");
}
cruiseConfig.getEnvironments().removeAgentFromAllEnvironments(uuid);
cruiseConfig.agents().remove(agentConfig);
return cruiseConfig;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
DeleteAgent that = (DeleteAgent) o;
if (uuid != null ? !uuid.equals(that.uuid) : that.uuid != null) {
return false;
}
return true;
}
@Override
public int hashCode() {
return uuid != null ? uuid.hashCode() : 0;
}
@Override
public String toString() {
return "DeleteAgent{" +
"uuid='" + uuid + '\'' +
'}';
}
}
public void updateAgentIpByUuid(String uuid, String ipAddress, Username userName) {
updateAgents(new UpdateAgentIp(uuid, ipAddress, userName), new AgentConfigsUpdateValidator(asList(uuid)), userName);
}
private static class UpdateAgentIp implements UpdateConfigCommand, UserAware {
private final String uuid;
private final String ipAddress;
private final String userName;
private UpdateAgentIp(String uuid, String ipAddress, Username userName) {
this.uuid = uuid;
this.ipAddress = ipAddress;
this.userName = userName.getUsername().toString();
}
public CruiseConfig update(CruiseConfig cruiseConfig) throws Exception {
AgentConfig agentConfig = cruiseConfig.agents().getAgentByUuid(uuid);
bombIfNull(agentConfig, "Unable to set agent ipAddress; Agent [" + uuid + "] not found.");
agentConfig.setIpAddress(ipAddress);
return cruiseConfig;
}
public ConfigModifyingUser user() {
return new ConfigModifyingUser(userName);
}
}
public void bulkUpdateAgentAttributes(AgentInstances agentInstances, final Username username, final LocalizedOperationResult result, final List<String> uuids, final List<String> resourcesToAdd, final List<String> resourcesToRemove, final List<String> environmentsToAdd, final List<String> environmentsToRemove, final TriState enable) {
Localizable.CurryableLocalizable successMessage = LocalizedMessage.string("BULK_AGENT_UPDATE_SUCESSFUL", StringUtils.join(uuids, ", "));
EntityConfigUpdateCommand<Agents> agentsEntityConfigUpdateCommand = new AgentsEntityConfigUpdateCommand(agentInstances, username, result, uuids, environmentsToAdd, environmentsToRemove, enable, resourcesToAdd, resourcesToRemove, goConfigService);
try {
goConfigService.updateConfig(agentsEntityConfigUpdateCommand, username);
if(result.isSuccessful()){
result.setMessage(successMessage);
}
} catch (Exception e) {
LOGGER.error("There was an error bulk updating agents", e);
if(e instanceof GoConfigInvalidException && !result.hasMessage()) {
result.unprocessableEntity(LocalizedMessage.string("ENTITY_CONFIG_VALIDATION_FAILED", Agents.class.getAnnotation(ConfigTag.class).value(), uuids, e.getMessage()));
}
else if(!result.hasMessage()) {
result.internalServerError(LocalizedMessage.string("INTERNAL_SERVER_ERROR"));
}
}
}
public AgentConfig updateAgentAttributes(final String uuid, Username username, String hostname, String resources, String environments, TriState enable, AgentInstances agentInstances, HttpOperationResult result) {
final GoConfigDao.CompositeConfigCommand command = new GoConfigDao.CompositeConfigCommand();
if (!goConfigService.hasAgent(uuid) && enable.isTrue()) {
AgentInstance agentInstance = agentInstances.findAgent(uuid);
AgentConfig agentConfig = agentInstance.agentConfig();
command.addCommand(new AddAgentCommand(agentConfig));
}
if (enable.isTrue()) {
command.addCommand(new UpdateAgentApprovalStatus(uuid, false));
}
if (enable.isFalse()) {
command.addCommand(new UpdateAgentApprovalStatus(uuid, true));
}
if (hostname != null) {
command.addCommand(new UpdateAgentHostname(uuid, hostname, username.getUsername().toString()));
}
if (resources != null) {
command.addCommand(new UpdateResourcesCommand(uuid, new Resources(resources)));
}
if (environments != null) {
Set<String> existingEnvironments = goConfigService.getCurrentConfig().getEnvironments().environmentsForAgent(uuid);
Set<String> newEnvironments = new HashSet<>(asList(environments.split(",")));
Set<String> environmentsToRemove = Sets.difference(existingEnvironments, newEnvironments);
Set<String> environmentsToAdd = Sets.difference(newEnvironments, existingEnvironments);
for (String environmentToRemove : environmentsToRemove) {
command.addCommand(new ModifyEnvironmentCommand(uuid, environmentToRemove, TriStateSelection.Action.remove));
}
for (String environmentToAdd : environmentsToAdd) {
command.addCommand(new ModifyEnvironmentCommand(uuid, environmentToAdd, TriStateSelection.Action.add));
}
}
return updateAgent(command, uuid, result, username);
}
public void saveOrUpdateAgent(AgentInstance agentInstance, Username currentUser) {
AgentConfig agentConfig = agentInstance.agentConfig();
if (goConfigService.hasAgent(agentConfig.getUuid())) {
this.updateAgentApprovalStatus(agentConfig.getUuid(), agentConfig.isDisabled(), currentUser);
} else {
this.addAgent(agentConfig, currentUser);
}
}
@Deprecated
public void approvePendingAgent(AgentInstance agentInstance) {
agentInstance.enable();
if (goConfigService.hasAgent(agentInstance.getUuid())) {
LOGGER.warn("Registered agent with the same uuid [" + agentInstance + "] already approved.");
} else {
updateAgent(new AddAgentCommand(agentInstance.agentConfig()), agentInstance.getUuid(), new HttpOperationResult(), Username.ANONYMOUS);
}
}
/**
* @understands how to add an agent to the config file
*/
public static class AddAgentCommand implements UpdateConfigCommand {
private final AgentConfig agentConfig;
public AddAgentCommand(AgentConfig agentConfig) {
this.agentConfig = agentConfig;
}
public CruiseConfig update(CruiseConfig cruiseConfig) throws Exception {
cruiseConfig.agents().add(agentConfig);
return cruiseConfig;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
AddAgentCommand that = (AddAgentCommand) o;
if (agentConfig != null ? !agentConfig.equals(that.agentConfig) : that.agentConfig != null) {
return false;
}
return true;
}
@Override
public int hashCode() {
return agentConfig != null ? agentConfig.hashCode() : 0;
}
@Override
public String toString() {
return "AddAgentcommand{" +
"agentConfig=" + agentConfig +
'}';
}
}
@Deprecated
public void updateAgentResources(final String uuid, final Resources resources) {
updateAgent(new UpdateResourcesCommand(uuid, resources), uuid, Username.ANONYMOUS);
}
public void updateAgentApprovalStatus(final String uuid, final Boolean isDenied, Username currentUser) {
updateAgentWithoutValidations(new UpdateAgentApprovalStatus(uuid, isDenied), currentUser);
}
public void addAgent(AgentConfig agentConfig, Username currentUser) {
updateAgent(new AddAgentCommand(agentConfig), agentConfig.getUuid(), currentUser);
}
public void modifyResources(AgentInstance[] agentInstances, List<TriStateSelection> selections, Username currentUser) {
GoConfigDao.CompositeConfigCommand command = new GoConfigDao.CompositeConfigCommand();
ArrayList<String> uuids = new ArrayList<>();
for (AgentInstance agentInstance : agentInstances) {
String uuid = agentInstance.getUuid();
uuids.add(uuid);
if (goConfigService.hasAgent(uuid)) {
for (TriStateSelection selection : selections) {
command.addCommand(new ModifyResourcesCommand(uuid, new Resource(selection.getValue()), selection.getAction()));
}
}
}
AgentConfigsUpdateValidator validator = new AgentConfigsUpdateValidator(uuids);
updateAgents(command, validator, currentUser);
}
private List<ConfigErrors> getAllErrors(Validatable v) {
final List<ConfigErrors> allErrors = new ArrayList<>();
new GoConfigGraphWalker(v).walk(new ErrorCollectingHandler(allErrors) {
@Override
public void handleValidation(Validatable validatable, ValidationContext context) {
// do nothing here
}
});
return allErrors;
}
public Agents findAgents(List<String> uuids) {
return agents().filter(uuids);
}
/**
* @understands how to update the agent approval status
*/
public static class UpdateAgentApprovalStatus implements UpdateConfigCommand {
private final String uuid;
private final Boolean denied;
public UpdateAgentApprovalStatus(String uuid, Boolean denied) {
this.uuid = uuid;
this.denied = denied;
}
public CruiseConfig update(CruiseConfig cruiseConfig) {
AgentConfig agentConfig = cruiseConfig.agents().getAgentByUuid(uuid);
bombIfNull(agentConfig, "Unable to set agent approval status; Agent [" + uuid + "] not found.");
agentConfig.setDisabled(denied);
return cruiseConfig;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
UpdateAgentApprovalStatus that = (UpdateAgentApprovalStatus) o;
if (denied != null ? !denied.equals(that.denied) : that.denied != null) {
return false;
}
if (uuid != null ? !uuid.equals(that.uuid) : that.uuid != null) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = uuid != null ? uuid.hashCode() : 0;
result = 31 * result + (denied != null ? denied.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "UpdateAgentApprovalStatus{" +
"uuid='" + uuid + '\'' +
", denied=" + denied +
'}';
}
}
public static class UpdateResourcesCommand implements UpdateConfigCommand {
private final String uuid;
private final Resources resources;
public UpdateResourcesCommand(String uuid, Resources resources) {
this.uuid = uuid;
this.resources = resources;
}
public CruiseConfig update(CruiseConfig cruiseConfig) {
AgentConfig agentConfig = cruiseConfig.agents().getAgentByUuid(uuid);
bombIfNull(agentConfig, "Unable to set agent resources; Agent [" + uuid + "] not found.");
agentConfig.setResources(resources);
return cruiseConfig;
}
}
public static class ModifyResourcesCommand implements UpdateConfigCommand {
private final String uuid;
private final Resource resource;
private final TriStateSelection.Action action;
public ModifyResourcesCommand(String uuid, Resource resource, TriStateSelection.Action action) {
this.uuid = uuid;
this.resource = resource;
this.action = action;
}
public CruiseConfig update(CruiseConfig cruiseConfig) {
AgentConfig agentConfig = cruiseConfig.agents().getAgentByUuid(uuid);
bombIfNull(agentConfig, "Unable to set agent resources; Agent [" + uuid + "] not found.");
if (action.equals(TriStateSelection.Action.add)) {
agentConfig.addResource(resource);
} else if (action.equals(TriStateSelection.Action.remove)) {
agentConfig.removeResource(resource);
} else if (action.equals(TriStateSelection.Action.nochange)) {
//do nothing
} else {
bomb(String.format("unsupported action '%s'", action));
}
return cruiseConfig;
}
}
public static class UpdateAgentHostname implements UpdateConfigCommand, UserAware {
private final String uuid;
private final String hostname;
private final String userName;
public UpdateAgentHostname(String uuid, String hostname, String userName) {
this.uuid = uuid;
this.hostname = hostname;
this.userName = userName;
}
public CruiseConfig update(CruiseConfig cruiseConfig) throws Exception {
AgentConfig agentConfig = cruiseConfig.agents().getAgentByUuid(uuid);
bombIfNull(agentConfig, "Unable to set agent hostname; Agent [" + uuid + "] not found.");
agentConfig.setHostName(hostname);
return cruiseConfig;
}
public ConfigModifyingUser user() {
return new ConfigModifyingUser(userName);
}
}
}