Skip to content

Commit

Permalink
Merge pull request #195 from marvinthepa/feature/JENKINS-46085-contai…
Browse files Browse the repository at this point in the history
…ner-log-pipeline-step

JENKINS-46085 containerLog step to get the logs of a container running in the slave pod
  • Loading branch information
carlossg authored Aug 17, 2017
2 parents 64a6717 + 43cb9e4 commit 7847e3e
Show file tree
Hide file tree
Showing 15 changed files with 593 additions and 167 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright (C) 2017 Original Authors
*
* 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 org.csanchez.jenkins.plugins.kubernetes.pipeline;

import com.google.common.collect.ImmutableSet;
import hudson.Extension;
import hudson.FilePath;
import hudson.model.Node;
import hudson.model.TaskListener;
import org.jenkinsci.plugins.workflow.steps.Step;
import org.jenkinsci.plugins.workflow.steps.StepContext;
import org.jenkinsci.plugins.workflow.steps.StepDescriptor;
import org.jenkinsci.plugins.workflow.steps.StepExecution;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;

import java.io.Serializable;
import java.util.Set;

public class ContainerLogStep extends Step implements Serializable {
private static final long serialVersionUID = 5588861066775717487L;

private final String name;
private boolean returnLog = false;
private int tailingLines = 0;
private int sinceSeconds = 0;
private int limitBytes = 0;

@DataBoundConstructor
public ContainerLogStep(String name) {
this.name = name;
}

public String getName() {
return name;
}

@DataBoundSetter
public void setReturnLog(boolean returnLog) {
this.returnLog = returnLog;
}

public boolean isReturnLog() {
return returnLog;
}

@Override
public StepExecution start(StepContext context) throws Exception {
return new ContainerLogStepExecution(this, context);
}

public int getTailingLines() {
return tailingLines;
}

@DataBoundSetter
public void setTailingLines(int tailingLines) {
this.tailingLines = tailingLines;
}

public int getSinceSeconds() {
return sinceSeconds;
}

@DataBoundSetter
public void setSinceSeconds(int sinceSeconds) {
this.sinceSeconds = sinceSeconds;
}

public int getLimitBytes() {
return limitBytes;
}

@DataBoundSetter
public void setLimitBytes(int limitBytes) {
this.limitBytes = limitBytes;
}

@Extension
public static class DescriptorImpl extends StepDescriptor {

@Override
public String getFunctionName() {
return "containerLog";
}

@Override
public String getDisplayName() {
return "Get container log from Kubernetes";
}

@Override
public boolean takesImplicitBlockArgument() {
return false;
}

@Override
public boolean isAdvanced() {
return false;
}

@Override
public Set<? extends Class<?>> getRequiredContext() {
return ImmutableSet.of(Node.class, FilePath.class, TaskListener.class);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright (C) 2017 Original Authors
*
* 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 org.csanchez.jenkins.plugins.kubernetes.pipeline;

import hudson.model.TaskListener;
import hudson.util.LogTaskListener;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.dsl.*;
import org.jenkinsci.plugins.workflow.steps.StepContext;
import org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution;

import java.io.*;
import java.util.logging.Level;
import java.util.logging.Logger;

import static org.csanchez.jenkins.plugins.kubernetes.pipeline.Resources.closeQuietly;

public class ContainerLogStepExecution extends SynchronousNonBlockingStepExecution<String> {
private static final long serialVersionUID = 5588861066775717487L;
private static final transient Logger LOGGER = Logger.getLogger(ContainerLogStepExecution.class.getName());

private final ContainerLogStep step;
private transient KubernetesClient client;

ContainerLogStepExecution(ContainerLogStep step, StepContext context) {
super(context);
this.step = step;
}

private PrintStream logger() {
TaskListener l = null;
StepContext context = getContext();
try {
l = context.get(TaskListener.class);
} catch (Exception x) {
LOGGER.log(Level.WARNING, "Failed to find TaskListener in context");
} finally {
if (l == null) {
l = new LogTaskListener(LOGGER, Level.FINE);
}
}
return l.getLogger();
}

@Override
protected String run() throws Exception {
boolean returnLog = step.isReturnLog();
String containerName = step.getName();
int tailingLines = step.getTailingLines();
int sinceSeconds = step.getSinceSeconds();
int limitBytes = step.getLimitBytes();

try {
LOGGER.log(Level.FINE, "Starting containerLog step.");

KubernetesNodeContext nodeContext = new KubernetesNodeContext(getContext());
client = nodeContext.connectToCloud();

String podName = nodeContext.getPodName();
ContainerResource<String, LogWatch, InputStream, PipedOutputStream, OutputStream, PipedInputStream,
String, ExecWatch> container = client.pods()
.inNamespace(nodeContext.getNamespace())
.withName(podName)
.inContainer(containerName);

TimeTailPrettyLoggable<String, LogWatch> limited = limitBytes > 0 ? container.limitBytes(limitBytes) : container;

TailPrettyLoggable<String, LogWatch> since = sinceSeconds > 0 ? limited.sinceSeconds(sinceSeconds) : limited;

PrettyLoggable<String, LogWatch> tailed = tailingLines > 0 ? since.tailingLines(tailingLines) : since;
String log = tailed.getLog();

if (returnLog) {
return log;
} else {
logger().println("> start log of container '" + containerName + "' in pod '" + podName + "'");
logger().print(log);
if (log.length() > 0 && log.charAt(log.length() - 1) != '\n') {
logger().println();
}
logger().println("> end log of container '" + containerName + "' in pod '" + podName + "'");
}

return "";
} catch (InterruptedException e) {
logger().println("Interrupted while getting logs of container");
LOGGER.log(Level.FINE, "interrupted while getting logs of container {1}", containerName);
return "";
} catch (Exception e) {
String message = "Failed to get logs for container";
logger().println(message);
LOGGER.log(Level.WARNING, message, e);
return "";
} finally {
closeQuietly(getContext(), client);
}
}

@Override
public void stop(Throwable cause) throws Exception {
LOGGER.log(Level.FINE, "Stopping container log step.");
try {
super.stop(cause);
} finally {
closeQuietly(getContext(), client);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,26 @@
package org.csanchez.jenkins.plugins.kubernetes.pipeline;

import java.io.Closeable;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.csanchez.jenkins.plugins.kubernetes.KubernetesCloud;
import org.csanchez.jenkins.plugins.kubernetes.KubernetesSlave;
import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl;
import org.jenkinsci.plugins.workflow.steps.BodyExecutionCallback;
import org.jenkinsci.plugins.workflow.steps.BodyInvoker;
import org.jenkinsci.plugins.workflow.steps.StepContext;

import hudson.AbortException;
import hudson.FilePath;
import hudson.LauncherDecorator;
import hudson.model.Node;
import hudson.model.TaskListener;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.KubernetesClient;

import javax.annotation.Nonnull;

import static org.csanchez.jenkins.plugins.kubernetes.pipeline.Resources.closeQuietly;

public class ContainerStepExecution extends AbstractStepExecutionImpl {

private static final long serialVersionUID = 7634132798345235774L;

private static final transient Logger LOGGER = Logger.getLogger(ContainerStepExecution.class.getName());
private static final transient String HOSTNAME_FILE = "/etc/hostname";

private final ContainerStep step;

Expand All @@ -40,24 +35,12 @@ public class ContainerStepExecution extends AbstractStepExecutionImpl {
@Override
public boolean start() throws Exception {
LOGGER.log(Level.FINE, "Starting container step.");
FilePath workspace = getContext().get(FilePath.class);
String podName = workspace.child(HOSTNAME_FILE).readToString().trim();
String namespace = workspace.child(Config.KUBERNETES_NAMESPACE_PATH).readToString().trim();

String containerName = step.getName();

Node node = getContext().get(Node.class);
if (! (node instanceof KubernetesSlave)) {
throw new AbortException(String.format("Node is not a Kubernetes node: %s", node.getNodeName()));
}
KubernetesSlave slave = (KubernetesSlave) node;
KubernetesCloud cloud = (KubernetesCloud) slave.getCloud();
if (cloud == null) {
throw new AbortException(String.format("Cloud does not exist: %s", slave.getCloudName()));
}
client = cloud.connect();
KubernetesNodeContext nodeContext = new KubernetesNodeContext(getContext());
client = nodeContext.connectToCloud();

decorator = new ContainerExecDecorator(client, podName, containerName, namespace);
decorator = new ContainerExecDecorator(client, nodeContext.getPodName(), containerName, nodeContext.getNamespace());
getContext().newBodyInvoker()
.withContext(BodyInvoker
.mergeLauncherDecorators(getContext().get(LauncherDecorator.class), decorator))
Expand All @@ -67,13 +50,9 @@ public boolean start() throws Exception {
}

@Override
public void stop(Throwable cause) throws Exception {
public void stop(@Nonnull Throwable cause) throws Exception {
LOGGER.log(Level.FINE, "Stopping container step.");
closeQuietly(client, decorator);
}

private void closeQuietly(Closeable... closeables) {
closeQuietly(getContext(), closeables);
closeQuietly(getContext(), client, decorator);
}

private static class ContainerExecCallback extends BodyExecutionCallback.TailCall {
Expand All @@ -91,19 +70,4 @@ public void finished(StepContext context) {
}
}

private static void closeQuietly(StepContext context, Closeable... closeables) {
for (Closeable c : closeables) {
if (c != null) {
try {
c.close();
} catch (IOException e) {
try {
context.get(TaskListener.class).error("Error while closing: [" + c + "]");
} catch (IOException | InterruptedException e1) {
LOGGER.log(Level.WARNING, "Error writing to task listener", e);
}
}
}
}
}
}
Loading

0 comments on commit 7847e3e

Please sign in to comment.