Skip to content

Commit

Permalink
Manage (activate/deactivate/delete) a service resolves #20 (#23)
Browse files Browse the repository at this point in the history
* Make nodes from services
* Create new plugin to manage a service (activate/deactivate/delete)
* When copying a file, the movement of the temp file should be a debug-only message
  • Loading branch information
kdebisschop authored Feb 18, 2020
1 parent f4c9ad9 commit b4f8a57
Show file tree
Hide file tree
Showing 10 changed files with 379 additions and 76 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Features:

- Project can include multiple environments.
- API keys are not exposed in configuration.
- Nodes can be created from containers, services, or both.
- Can limit selected containers to one per service.
- Can exclude stopped containers.
- Can exclude global containers.
Expand Down Expand Up @@ -75,7 +76,7 @@ Environment ID most correspond to an existing Rancher environment. Stack name mu

### Add Service

Ads a service to an existing stack. Required inputs:
Adds a service to an existing stack. Required inputs:

- Environment ID (string)
- Stack Name (string)
Expand All @@ -89,6 +90,10 @@ Optional inputs:
- Service labels
- Secrets

### Manage Service

Activate, deactivate, or restart a service.

## Road Map

- 0.6.6 Make File Copier binary-safe.
Expand Down
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ ext.pluginClassNames='com.bioraft.rundeck.rancher.RancherNodeExecutorPlugin,' +
'com.bioraft.rundeck.rancher.RancherResourceModelSourceFactory,' +
'com.bioraft.rundeck.rancher.RancherUpgradeService,' +
'com.bioraft.rundeck.rancher.RancherNewStack,' +
'com.bioraft.rundeck.rancher.RancherAddService'
'com.bioraft.rundeck.rancher.RancherAddService,' +
'com.bioraft.rundeck.rancher.RancherManageService'
ext.pluginName = 'Rancher Node Plugins'
ext.pluginDescription = 'Interface with Rancher environments'

Expand Down
13 changes: 11 additions & 2 deletions src/main/java/com/bioraft/rundeck/rancher/RancherFileCopier.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@
import com.dtolabs.rundeck.core.execution.script.ScriptfileUtils;
import com.dtolabs.rundeck.core.execution.service.FileCopier;
import com.dtolabs.rundeck.core.execution.service.FileCopierException;
import com.dtolabs.rundeck.core.execution.service.NodeExecutorResultImpl;
import com.dtolabs.rundeck.core.execution.workflow.steps.FailureReason;
import com.dtolabs.rundeck.core.execution.workflow.steps.StepFailureReason;
import com.dtolabs.rundeck.core.plugins.Plugin;
import com.dtolabs.rundeck.core.plugins.configuration.*;
import com.dtolabs.rundeck.core.storage.ResourceMeta;
Expand Down Expand Up @@ -111,6 +113,12 @@ private String copyFile(final ExecutionContext context, final File scriptfile, f
String remotefile;

Map<String, String> nodeAttributes = node.getAttributes();

if (nodeAttributes.get("type").equals("service")) {
String message = "File copier is not currently supported for services";
throw new FileCopierException(message, FileCopyFailureReason.UnsupportedNodeType);
}

String accessKey;
String secretKey;
try {
Expand All @@ -133,7 +141,7 @@ private String copyFile(final ExecutionContext context, final File scriptfile, f
}
// write to a local temp file or use the input file
final File localTempfile = (null != scriptfile) ? scriptfile
: BaseFileCopier.writeTempFile(context, scriptfile, input, script);
: BaseFileCopier.writeTempFile(context, null, input, script);

// Copy the file over
ExecutionLogger logger = context.getExecutionLogger();
Expand Down Expand Up @@ -223,7 +231,8 @@ public enum FileCopyFailureReason implements FailureReason {
InterruptedException,
ConnectionFailure,
AuthenticationFailure,
UnsupportedOperatingSystem
UnsupportedOperatingSystem,
UnsupportedNodeType,
}

private static class StreamGobbler implements Runnable {
Expand Down
172 changes: 172 additions & 0 deletions src/main/java/com/bioraft/rundeck/rancher/RancherManageService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
* Copyright 2019 BioRAFT, Inc. (https://bioraft.com)
*
* 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.bioraft.rundeck.rancher;

import com.dtolabs.rundeck.core.Constants;
import com.dtolabs.rundeck.core.common.INodeEntry;
import com.dtolabs.rundeck.core.execution.ExecutionContext;
import com.dtolabs.rundeck.core.execution.workflow.steps.node.NodeStepException;
import com.dtolabs.rundeck.core.plugins.Plugin;
import com.dtolabs.rundeck.plugins.PluginLogger;
import com.dtolabs.rundeck.plugins.ServiceNameConstants;
import com.dtolabs.rundeck.plugins.descriptions.PluginDescription;
import com.dtolabs.rundeck.plugins.descriptions.PluginProperty;
import com.dtolabs.rundeck.plugins.descriptions.SelectValues;
import com.dtolabs.rundeck.plugins.step.NodeStepPlugin;
import com.dtolabs.rundeck.plugins.step.PluginStepContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;
import okhttp3.Request.Builder;

import java.io.IOException;
import java.util.Map;

import static com.bioraft.rundeck.rancher.RancherShared.ErrorCause;
import static com.bioraft.rundeck.rancher.RancherShared.loadStoragePathData;

/**
* Workflow Node Step Plug-in to upgrade a service associated with a node.
*
* @author Karl DeBisschop <[email protected]>
* @since 2019-12-20
*/
@Plugin(name = RancherShared.RANCHER_SERVICE_CONTROLLER, service = ServiceNameConstants.WorkflowNodeStep)
@PluginDescription(title = "Rancher - Manage Service", description = "Start/Stop/Restart the service associated with the selected node.")
public class RancherManageService implements NodeStepPlugin {

@PluginProperty(title = "Action", description = "What action is desired", required = true, defaultValue = "true")
@SelectValues(values = {"activate", "deactivate", "restart"})
private String action;

private String nodeName;

OkHttpClient client;

public RancherManageService() {
client = new OkHttpClient();
}

public RancherManageService(OkHttpClient client) {
this.client = client;
}

@Override
public void executeNodeStep(PluginStepContext ctx, Map<String, Object> cfg, INodeEntry node)
throws NodeStepException {

this.nodeName = node.getNodename();
ExecutionContext executionContext = ctx.getExecutionContext();
PluginLogger logger = ctx.getLogger();

Map<String, String> attributes = node.getAttributes();

String accessKey;
String secretKey;
try {
accessKey = loadStoragePathData(executionContext, attributes.get(RancherShared.CONFIG_ACCESSKEY_PATH));
secretKey = loadStoragePathData(executionContext, attributes.get(RancherShared.CONFIG_SECRETKEY_PATH));
} catch (IOException e) {
throw new NodeStepException("Could not get secret storage path", e, ErrorCause.IOException, this.nodeName);
}

JsonNode service;
if (attributes.get("type").equals("container")) {
service = apiGet(accessKey, secretKey, attributes.get("services")).path("data").path(0);
} else {
service = apiGet(accessKey, secretKey, attributes.get("self"));
}
String serviceState = service.path("state").asText();
String body = "";
switch (action) {
case "activate":
if (serviceState.equals("active")) {
String message = "Service state is already active";
throw new NodeStepException(message, ErrorCause.ServiceNotRunning, node.getNodename());
}
break;
case "deactivate":
case "restart":
if (!serviceState.equals("active")) {
String message = "Service state must be running, was " + serviceState;
throw new NodeStepException(message, ErrorCause.ServiceNotRunning, node.getNodename());
}
break;
}
String url = service.path("actions").path(action).asText();
if (url.length() == 0) {
throw new NodeStepException("No upgrade URL found", ErrorCause.MissingUpgradeURL, node.getNodename());
}

JsonNode newService = apiPost(accessKey, secretKey, url, body);

logger.log(Constants.INFO_LEVEL, "Upgraded " + nodeName);
}

/**
* Gets the web socket end point and connection token for an execute request.
*
* @param accessKey Rancher access key
* @param secretKey Rancher secret key
* @param url Rancher API url
* @return JSON from Rancher API request body.
* @throws NodeStepException when there API request fails
*/
private JsonNode apiGet(String accessKey, String secretKey, String url) throws NodeStepException {
try {
Builder builder = new Builder().url(url);
builder.addHeader("Authorization", Credentials.basic(accessKey, secretKey));
Response response = client.newCall(builder.build()).execute();
// Since URL comes from the Rancher server itself, assume there are no redirects.
if (response.code() >= 300) {
throw new IOException("API get failed" + response.message());
}
ObjectMapper mapper = new ObjectMapper();
assert response.body() != null;
return mapper.readTree(response.body().string());
} catch (IOException e) {
throw new NodeStepException(e.getMessage(), e, ErrorCause.NoServiceObject, nodeName);
}
}

/**
* Gets the web socket end point and connection token for an execute request.
*
* @param accessKey Rancher access key
* @param secretKey Rancher secret key
* @param url Rancher API url
* @param body Document contents to POST to Rancher.
* @return JSON from Rancher API request body.
* @throws NodeStepException when there API request fails
*/
private JsonNode apiPost(String accessKey, String secretKey, String url, String body) throws NodeStepException {
RequestBody postBody = RequestBody.create(MediaType.parse("application/json"), body);
try {
Builder builder = new Builder().url(url).post(postBody);
builder.addHeader("Authorization", Credentials.basic(accessKey, secretKey));
Response response = client.newCall(builder.build()).execute();
// Since URL comes from the Rancher server itself, assume there are no redirects.
if (response.code() >= 300) {
throw new IOException("API post failed" + response.message());
}
ObjectMapper mapper = new ObjectMapper();
assert response.body() != null;
return mapper.readTree(response.body().string());
} catch (IOException e) {
throw new NodeStepException(e.getMessage(), e, ErrorCause.UpgradeFailure, nodeName);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ public Description getDescription() {
public NodeExecutorResult executeCommand(final ExecutionContext context, final String[] command,
final INodeEntry node) {
Map<String, String> nodeAttributes = node.getAttributes();

if (nodeAttributes.get("type").equals("service")) {
String message = "Node executor is not currently supported for services";
return NodeExecutorResultImpl.createFailure(StepFailureReason.PluginFailed, message, node);
}

try {
accessKey = this.loadStoragePathData(context, nodeAttributes.get(CONFIG_ACCESSKEY_PATH));
secretKey = this.loadStoragePathData(context, nodeAttributes.get(CONFIG_SECRETKEY_PATH));
Expand Down
Loading

0 comments on commit b4f8a57

Please sign in to comment.