/*
* 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.controller;
import com.thoughtworks.go.config.AgentConfig;
import com.thoughtworks.go.config.GoConfigDao;
import com.thoughtworks.go.config.exceptions.GoConfigInvalidException;
import com.thoughtworks.go.config.update.UpdateEnvironmentsCommand;
import com.thoughtworks.go.config.update.UpdateResourceCommand;
import com.thoughtworks.go.domain.AllConfigErrors;
import com.thoughtworks.go.domain.ConfigErrors;
import com.thoughtworks.go.domain.materials.tfs.TFSJarDetector;
import com.thoughtworks.go.plugin.infra.commons.PluginsZip;
import com.thoughtworks.go.security.Registration;
import com.thoughtworks.go.security.RegistrationJSONizer;
import com.thoughtworks.go.server.service.*;
import com.thoughtworks.go.server.service.result.HttpOperationResult;
import com.thoughtworks.go.util.SystemEnvironment;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import static com.thoughtworks.go.util.FileDigester.md5DigestOfStream;
import static org.apache.commons.lang.StringUtils.isBlank;
import static org.apache.commons.lang.StringUtils.isNotBlank;
@Controller
public class AgentRegistrationController {
private static final Logger LOG = LoggerFactory.getLogger(AgentRegistrationController.class);
private final AgentService agentService;
private final GoConfigService goConfigService;
private final SystemEnvironment systemEnvironment;
private PluginsZip pluginsZip;
private final AgentConfigService agentConfigService;
private volatile String agentChecksum;
private volatile String agentLauncherChecksum;
private volatile String tfsSdkChecksum;
@Autowired
public AgentRegistrationController(AgentService agentService, GoConfigService goConfigService, SystemEnvironment systemEnvironment, PluginsZip pluginsZip, AgentConfigService agentConfigService) {
this.agentService = agentService;
this.goConfigService = goConfigService;
this.systemEnvironment = systemEnvironment;
this.pluginsZip = pluginsZip;
this.agentConfigService = agentConfigService;
}
@RequestMapping(value = "/admin/latest-agent.status", method = {RequestMethod.HEAD, RequestMethod.GET})
public void checkAgentStatus(HttpServletResponse response) throws IOException {
populateAgentChecksum();
populateLauncherChecksum();
populateTFSSDKChecksum();
response.setHeader(SystemEnvironment.AGENT_CONTENT_MD5_HEADER, agentChecksum);
response.setHeader(SystemEnvironment.AGENT_LAUNCHER_CONTENT_MD5_HEADER, agentLauncherChecksum);
response.setHeader(SystemEnvironment.AGENT_PLUGINS_ZIP_MD5_HEADER, pluginsZip.md5());
response.setHeader(SystemEnvironment.AGENT_TFS_SDK_MD5_HEADER, tfsSdkChecksum);
setOtherHeaders(response);
}
@RequestMapping(value = "/admin/agent", method = RequestMethod.HEAD)
public void checkAgentVersion(HttpServletResponse response) throws IOException {
populateAgentChecksum();
response.setHeader("Content-MD5", agentChecksum);
setOtherHeaders(response);
}
@RequestMapping(value = "/admin/agent-launcher.jar", method = RequestMethod.HEAD)
public void checkAgentLauncherVersion(HttpServletResponse response) throws IOException {
populateLauncherChecksum();
response.setHeader("Content-MD5", agentLauncherChecksum);
setOtherHeaders(response);
}
@RequestMapping(value = "/admin/tfs-impl.jar", method = RequestMethod.HEAD)
public void checkTfsImplVersion(HttpServletResponse response) throws IOException {
populateTFSSDKChecksum();
response.setHeader("Content-MD5", tfsSdkChecksum);
setOtherHeaders(response);
}
@RequestMapping(value = "/admin/tfs-impl.jar", method = RequestMethod.GET)
public void downloadTfsImplJar(HttpServletResponse response) throws IOException {
checkTfsImplVersion(response);
sendFile(new TFSImplSrc(), response);
}
@RequestMapping(value = "/admin/agent-plugins.zip", method = RequestMethod.HEAD)
public void checkAgentPluginsZipStatus(HttpServletResponse response) throws IOException {
response.setHeader("Content-MD5", pluginsZip.md5());
setOtherHeaders(response);
}
private void populateLauncherChecksum() throws IOException {
synchronized (this) {
if (agentLauncherChecksum == null) {
agentLauncherChecksum = getChecksumFor(new AgentLauncherSrc());
}
}
}
private void populateAgentChecksum() throws IOException {
synchronized (this) {
if (agentChecksum == null) {
agentChecksum = getChecksumFor(new AgentJarSrc());
}
}
}
private void populateTFSSDKChecksum() throws IOException {
synchronized (this) {
if (tfsSdkChecksum == null) {
tfsSdkChecksum = getChecksumFor(new TFSImplSrc());
}
}
}
private String getChecksumFor(final InputStreamSrc src) throws IOException {
InputStream inputStream = null;
String checksum = null;
try {
inputStream = src.invoke();
checksum = md5DigestOfStream(inputStream);
} finally {
IOUtils.closeQuietly(inputStream);
}
assert (checksum != null);
return checksum;
}
private void setOtherHeaders(HttpServletResponse response) {
response.setHeader("Cruise-Server-Ssl-Port", Integer.toString(systemEnvironment.getSslServerPort()));
}
@RequestMapping(value = "/admin/agent", method = RequestMethod.GET)
public void downloadAgent(HttpServletResponse response) throws IOException {
checkAgentVersion(response);
sendFile(new AgentJarSrc(), response);
}
@RequestMapping(value = "/admin/agent-launcher.jar", method = RequestMethod.GET)
public void downloadAgentLauncher(HttpServletResponse response) throws IOException {
checkAgentLauncherVersion(response);
sendFile(new AgentLauncherSrc(), response);
}
@RequestMapping(value = "/admin/agent-plugins.zip", method = RequestMethod.GET)
public void downloadPluginsZip(HttpServletResponse response) throws IOException {
checkAgentPluginsZipStatus(response);
sendFile(new AgentPluginsZipSrc(), response);
}
@RequestMapping(value = "/admin/agent", method = RequestMethod.POST)
public ModelAndView agentRequest(@RequestParam("hostname") String hostname,
@RequestParam("uuid") String uuid,
@RequestParam("location") String location,
@RequestParam("usablespace") String usablespaceAsString,
@RequestParam("operatingSystem") String operatingSystem,
@RequestParam("agentAutoRegisterKey") String agentAutoRegisterKey,
@RequestParam("agentAutoRegisterResources") String agentAutoRegisterResources,
@RequestParam("agentAutoRegisterEnvironments") String agentAutoRegisterEnvironments,
@RequestParam("agentAutoRegisterHostname") String agentAutoRegisterHostname,
@RequestParam("elasticAgentId") String elasticAgentId,
@RequestParam("elasticPluginId") String elasticPluginId,
@RequestParam(value = "supportsBuildCommandProtocol", required = false, defaultValue = "false") boolean supportsBuildCommandProtocol,
HttpServletRequest request) throws IOException {
final String ipAddress = request.getRemoteAddr();
if (LOG.isDebugEnabled()) {
LOG.debug("Processing registration request from agent [{}/{}]", hostname, ipAddress);
}
Registration keyEntry;
String preferredHostname = hostname;
try {
if (goConfigService.serverConfig().shouldAutoRegisterAgentWith(agentAutoRegisterKey)) {
preferredHostname = getPreferredHostname(agentAutoRegisterHostname, hostname);
LOG.info("[Agent Auto Registration] Auto registering agent with uuid {} ", uuid);
} else {
if (elasticAgentAutoregistrationInfoPresent(elasticAgentId, elasticPluginId)) {
throw new RuntimeException(String.format("Elastic agent registration requires an auto-register agent key to be setup on the server. Agent-id: [%s], Plugin-id: [%s]", elasticAgentId, elasticPluginId));
}
}
AgentConfig agentConfig = new AgentConfig(uuid, preferredHostname, ipAddress);
if (partialElasticAgentAutoregistrationInfo(elasticAgentId, elasticPluginId)) {
throw new RuntimeException("Elastic agents must submit both elasticAgentId and elasticPluginId");
}
if (elasticAgentAutoregistrationInfoPresent(elasticAgentId, elasticPluginId)) {
agentConfig.setElasticAgentId(elasticAgentId);
agentConfig.setElasticPluginId(elasticPluginId);
}
if (goConfigService.serverConfig().shouldAutoRegisterAgentWith(agentAutoRegisterKey)) {
LOG.info(String.format("[Agent Auto Registration] Auto registering agent with uuid %s ", uuid));
GoConfigDao.CompositeConfigCommand compositeConfigCommand = new GoConfigDao.CompositeConfigCommand(
new AgentConfigService.AddAgentCommand(agentConfig),
new UpdateResourceCommand(uuid, agentAutoRegisterResources),
new UpdateEnvironmentsCommand(uuid, agentAutoRegisterEnvironments)
);
HttpOperationResult result = new HttpOperationResult();
agentConfig = agentConfigService.updateAgent(compositeConfigCommand, uuid, result, agentService.agentUsername(uuid, ipAddress, preferredHostname));
if (!result.isSuccess()) {
List<ConfigErrors> errors = com.thoughtworks.go.config.ErrorCollector.getAllErrors(agentConfig);
ConfigErrors e = new ConfigErrors();
e.add("resultMessage", result.detailedMessage());
errors.add(e);
throw new GoConfigInvalidException(null, new AllConfigErrors(errors).asString());
}
}
boolean registeredAlready = goConfigService.hasAgent(uuid);
long usablespace = Long.parseLong(usablespaceAsString);
AgentRuntimeInfo agentRuntimeInfo = AgentRuntimeInfo.fromServer(agentConfig, registeredAlready, location, usablespace, operatingSystem, supportsBuildCommandProtocol);
if (elasticAgentAutoregistrationInfoPresent(elasticAgentId, elasticPluginId)) {
agentRuntimeInfo = ElasticAgentRuntimeInfo.fromServer(agentRuntimeInfo, elasticAgentId, elasticPluginId);
}
keyEntry = agentService.requestRegistration(agentService.agentUsername(uuid, ipAddress, preferredHostname), agentRuntimeInfo);
} catch (Exception e) {
keyEntry = Registration.createNullPrivateKeyEntry();
LOG.error("Error occured during agent registration process: ", e);
}
return render(keyEntry);
}
private ModelAndView render(final Registration registration) {
return new ModelAndView(new View() {
public String getContentType() {
return "application/json";
}
public void render(Map model, HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType(getContentType());
if (!registration.isValid()) {
response.setStatus(HttpServletResponse.SC_ACCEPTED);
}
response.getWriter().print(RegistrationJSONizer.toJson(registration));
}
});
}
private void sendFile(InputStreamSrc input, HttpServletResponse response) throws IOException {
response.setContentType("application/octet-stream");
try (InputStream in = input.invoke()) {
IOUtils.copy(in, response.getOutputStream());
}
}
private boolean partialElasticAgentAutoregistrationInfo(String elasticAgentId, String elasticPluginId) {
return (isBlank(elasticAgentId) && isNotBlank(elasticPluginId)) || (isNotBlank(elasticAgentId) && isBlank(elasticPluginId));
}
private boolean elasticAgentAutoregistrationInfoPresent(String elasticAgentId, String elasticPluginId) {
return isNotBlank(elasticAgentId) && isNotBlank(elasticPluginId);
}
private String getPreferredHostname(String agentAutoRegisterHostname, String hostname) {
return isNotBlank(agentAutoRegisterHostname) ? agentAutoRegisterHostname : hostname;
}
public interface InputStreamSrc {
InputStream invoke() throws IOException;
}
private class AgentJarSrc implements InputStreamSrc {
public InputStream invoke() throws IOException {
return JarDetector.create(systemEnvironment, "agent.jar");
}
}
private class AgentLauncherSrc implements InputStreamSrc {
public InputStream invoke() throws IOException {
return JarDetector.create(systemEnvironment, "agent-launcher.jar");
}
}
private class AgentPluginsZipSrc implements InputStreamSrc {
public InputStream invoke() throws FileNotFoundException {
return new FileInputStream(systemEnvironment.get(SystemEnvironment.ALL_PLUGINS_ZIP_PATH));
}
}
private class TFSImplSrc implements InputStreamSrc {
@Override
public InputStream invoke() throws IOException {
return TFSJarDetector.create(systemEnvironment).getJarURL().openStream();
}
}
}