Skip to content

Commit

Permalink
Refactor global configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
nikita-tkachenko-datadog committed Dec 5, 2024
1 parent 5c75e54 commit 0404aa5
Show file tree
Hide file tree
Showing 81 changed files with 3,614 additions and 1,672 deletions.
8 changes: 7 additions & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,16 @@ To spin up a development environment for the *jenkins-datadog* plugin repository
- You can set Global Tag. For example `.*, owner:$1, release_env:$2, optional:Tag3`.

To test Configuration as Code update `docker/docker-compose.yaml`, uncommenting `CASC_JENKINS_CONFIG`.
The applied configuration is stored in `docker/controller-node/jenkins-casc.yaml` (note that it is placed inside the container at image build time).
The applied configuration is stored in `docker/controller-node/jenkins-casc.yaml` (note that it is placed inside the container at image build time).
You can find some examples of the plugin's configuration with CasC in `src/test/resources/org/datadog/jenkins/plugins/datadog`.

Jenkins controller container exposes port 5055 for remote debugging via JDWP.

#### Known errors

If docker-compose fails with a message that looks like `error mounting ".../datadog-plugin/target/datadog.hpi" to rootfs at "/var/jenkins_home/plugins/datadog.hpi"`,
make sure that you have properly set the `JENKINS_PLUGIN` env var and that you have built the plugin by running `mvn clean package -DskipTests`.

#### Manual Testing without an Agent

Alternatively, you can manually test the plugin by running the command `mvn hpi:run`, which will spin up a local development environment without the agent. This allows you to test using the HTTP client without needing docker. See the [jenkins documentation](https://jenkinsci.github.io/maven-hpi-plugin/run-mojo.html) for more details and options.
Expand Down
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,13 @@
<artifactId>json</artifactId>
<version>20240303</version>
</dependency>
<dependency>
<groupId>com.github.stefanbirkner</groupId>
<artifactId>system-lambda</artifactId>
<version>1.2.1</version>
<scope>test</scope>
</dependency>

<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
<dependency>
<groupId>org.mockito</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,6 @@ of this software and associated documentation files (the "Software"), to deal

public interface DatadogClient {

enum ClientType {
HTTP,
DSD;

ClientType() { }
}

enum Status {
OK(0),
WARNING(1),
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,17 @@
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.model.TopLevelItem;
import hudson.util.Secret;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nonnull;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.datadog.jenkins.plugins.datadog.DatadogClient;
import org.datadog.jenkins.plugins.datadog.DatadogGlobalConfiguration;
import org.datadog.jenkins.plugins.datadog.DatadogUtilities;
import org.datadog.jenkins.plugins.datadog.model.DatadogPluginAction;
import org.datadog.jenkins.plugins.datadog.configuration.DatadogClientConfiguration;

public class DatadogTracerConfigurator {

Expand Down Expand Up @@ -125,20 +120,9 @@ private static Map<String, String> getCommonEnvVariables(DatadogGlobalConfigurat
variables.put("DD_ENV", "ci");
variables.put("DD_SERVICE", tracerConfig.getServiceName());

DatadogClient.ClientType clientType = DatadogClient.ClientType.valueOf(datadogConfig.getReportWith());
switch (clientType) {
case HTTP:
variables.put("DD_CIVISIBILITY_AGENTLESS_ENABLED", "true");
variables.put("DD_SITE", getSite(datadogConfig.getTargetApiURL()));
variables.put("DD_API_KEY", Secret.toString(datadogConfig.getUsedApiKey()));
break;
case DSD:
variables.put("DD_AGENT_HOST", datadogConfig.getTargetHost());
variables.put("DD_TRACE_AGENT_PORT", getAgentPort(datadogConfig.getTargetTraceCollectionPort()));
break;
default:
throw new IllegalArgumentException("Unexpected client type: " + clientType);
}
DatadogClientConfiguration clientConfiguration = datadogConfig.getDatadogClientConfiguration();
Map<String, String> clientEnvironmentVariables = clientConfiguration.toEnvironmentVariables();
variables.putAll(clientEnvironmentVariables);

Map<String, String> additionalVariables = tracerConfig.getAdditionalVariables();
if (additionalVariables != null) {
Expand All @@ -148,31 +132,6 @@ private static Map<String, String> getCommonEnvVariables(DatadogGlobalConfigurat
return variables;
}

private static String getSite(String apiUrl) {
// what users configure for Pipelines looks like "https://api.datadoghq.com/api/"
// while what the tracer needs "datadoghq.com"
try {
URI uri = new URL(apiUrl).toURI();
String host = uri.getHost();
if (host == null) {
throw new IllegalArgumentException("Cannot find host in Datadog API URL: " + uri);
}

String[] parts = host.split("\\.");
return (parts.length >= 2 ? parts[parts.length - 2] + "." : "") + parts[parts.length - 1];

} catch (MalformedURLException | URISyntaxException e) {
throw new IllegalArgumentException("Cannot parse Datadog API URL", e);
}
}

private static String getAgentPort(Integer traceCollectionPort) {
if (traceCollectionPort == null) {
throw new IllegalArgumentException("Traces collection port is not set");
} else {
return traceCollectionPort.toString();
}
}

private static final class ConfigureTracerAction extends DatadogPluginAction {
private final String nodeHostname;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,24 @@ of this software and associated documentation files (the "Software"), to deal
import com.timgroup.statsd.NonBlockingStatsDClient;
import com.timgroup.statsd.ServiceCheck;
import com.timgroup.statsd.StatsDClient;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.logging.Logger;
import javax.annotation.concurrent.GuardedBy;
import org.datadog.jenkins.plugins.datadog.DatadogClient;
import org.datadog.jenkins.plugins.datadog.DatadogEvent;
import org.datadog.jenkins.plugins.datadog.DatadogGlobalConfiguration;
Expand All @@ -44,18 +62,6 @@ of this software and associated documentation files (the "Software"), to deal
import org.json.JSONArray;
import org.json.JSONObject;

import javax.annotation.concurrent.GuardedBy;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.Function;
import java.util.logging.Logger;

/**
* This class is used to collect all methods that has to do with transmitting
* data to Datadog.
Expand Down Expand Up @@ -91,40 +97,13 @@ public DatadogAgentClient(String hostname, Integer port, Integer logCollectionPo
}

public DatadogAgentClient(String hostname, Integer port, Integer logCollectionPort, Integer traceCollectionPort, long evpProxyTimeoutMillis) {
validate(hostname, port, logCollectionPort, traceCollectionPort);

this.hostname = hostname;
this.port = port;
this.logCollectionPort = logCollectionPort;
this.traceCollectionPort = traceCollectionPort;
this.client = new HttpClient(evpProxyTimeoutMillis);
}

private static void validate(String hostname, Integer port, Integer logCollectionPort, Integer traceCollectionPort) {
if (hostname == null || hostname.isEmpty()) {
throw new IllegalArgumentException("Datadog Target URL is not set properly");
}
if (port == null) {
throw new IllegalArgumentException("Datadog Target Port is not set properly");
}
if (DatadogUtilities.getDatadogGlobalDescriptor().isCollectBuildLogs() && logCollectionPort == null) {
logger.warning("Datadog Log Collection Port is not set properly");
}

if (DatadogUtilities.getDatadogGlobalDescriptor().getEnableCiVisibility() && traceCollectionPort == null) {
logger.warning("Datadog Trace Collection Port is not set properly");
}
}

public static ConnectivityResult checkConnectivity(final String host, final int port) {
try(Socket ignored = new Socket(host, port)) {
return ConnectivityResult.SUCCESS;
} catch (Exception ex) {
DatadogUtilities.severe(logger, ex, "Failed to create socket to host: " + host + ", port: " +port + ". Error: " + ex);
return new ConnectivityResult(true, ex.toString());
}
}

public static class ConnectivityResult {
private final boolean error;
private final String errorMessage;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,16 @@ of this software and associated documentation files (the "Software"), to deal
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.logging.Logger;
import net.sf.json.JSON;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import net.sf.json.JSONSerializer;
import org.datadog.jenkins.plugins.datadog.DatadogClient;
import org.datadog.jenkins.plugins.datadog.DatadogEvent;
import org.datadog.jenkins.plugins.datadog.DatadogGlobalConfiguration;
Expand Down Expand Up @@ -80,100 +83,13 @@ public class DatadogApiClient implements DatadogClient {
private final HttpClient httpClient;

public DatadogApiClient(String url, String logIntakeUrl, String webhookIntakeUrl, Secret apiKey) {
validate(url, logIntakeUrl, webhookIntakeUrl, apiKey);
this.url = url;
this.apiKey = apiKey;
this.logIntakeUrl = logIntakeUrl;
this.webhookIntakeUrl = webhookIntakeUrl;
this.httpClient = new HttpClient(HTTP_TIMEOUT_MS);
}

private static void validate(String url, String logIntakeUrl, String webhookIntakeUrl, Secret apiKey) throws IllegalArgumentException {
if (url == null || url.isEmpty()) {
throw new IllegalArgumentException("Datadog Target URL is not set properly");
}
if (apiKey == null || Secret.toString(apiKey).isEmpty()){
throw new IllegalArgumentException("Datadog API Key is not set properly");
}

if (!validateDefaultIntakeConnection(url, apiKey)) {
throw new IllegalArgumentException("Connection broken, please double check both your API URL and Key");
}

if (DatadogUtilities.getDatadogGlobalDescriptor().isCollectBuildLogs() ) {
if (logIntakeUrl == null || logIntakeUrl.isEmpty()) {
throw new IllegalArgumentException("Datadog Log Intake URL is not set properly");
}
if (!validateLogIntakeConnection(logIntakeUrl, apiKey)) {
throw new IllegalArgumentException("Connection broken, please double check both your Log Intake URL and Key");
}
}

if (DatadogUtilities.getDatadogGlobalDescriptor().getEnableCiVisibility() ) {
if (webhookIntakeUrl == null || webhookIntakeUrl.isEmpty()) {
throw new IllegalArgumentException("Datadog Webhook Intake URL is not set properly");
}
if (!validateWebhookIntakeConnection(webhookIntakeUrl, apiKey)) {
throw new IllegalArgumentException("Connection broken, please double check both your Webhook Intake URL and Key");
}
}
}

public static boolean validateDefaultIntakeConnection(String validatedUrl, Secret apiKey) {
String urlParameters = "?api_key=" + Secret.toString(apiKey);
String url = validatedUrl + VALIDATE + urlParameters;
try {
JSONObject json = (JSONObject) new HttpClient(HTTP_TIMEOUT_MS).get(url, Collections.emptyMap(), JSONSerializer::toJSON);
return json.getBoolean("valid");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
DatadogUtilities.severe(logger, e, "Failed to validate webhook connection");
return false;
} catch (Exception e) {
DatadogUtilities.severe(logger, e, "Failed to validate webhook connection");
return false;
}
}

@SuppressFBWarnings("DLS_DEAD_LOCAL_STORE")
public static boolean validateWebhookIntakeConnection(String webhookIntakeUrl, Secret apiKey) {
Map<String, String> headers = new HashMap<>();
headers.put("DD-API-KEY", Secret.toString(apiKey));

byte[] body = "{}".getBytes(StandardCharsets.UTF_8);
try {
JSON jsonResponse = new HttpClient(HTTP_TIMEOUT_MS).post(webhookIntakeUrl, headers, "application/json", body, JSONSerializer::toJSON);
// consider test successful if JSON was parsed without errors
return true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
DatadogUtilities.severe(logger, e, "Failed to validate webhook connection");
return false;
} catch (Exception e) {
DatadogUtilities.severe(logger, e, "Failed to validate webhook connection");
return false;
}
}

public static boolean validateLogIntakeConnection(String logsIntakeUrl, Secret apiKey) {
HttpClient httpClient = new HttpClient(HTTP_TIMEOUT_MS);

Map<String, String> headers = new HashMap<>();
headers.put("DD-API-KEY", Secret.toString(apiKey));

String payload = "{\"message\":\"[datadog-plugin] Check connection\", " +
"\"ddsource\":\"Jenkins\", \"service\":\"Jenkins\", " +
"\"hostname\":\"" + DatadogUtilities.getHostname(null) + "\"}";
byte[] body = payload.getBytes(StandardCharsets.UTF_8);
try {
httpClient.post(logsIntakeUrl, headers, "application/json", body, Function.identity());
return true;
} catch (Exception e) {
DatadogUtilities.severe(logger, e, "Failed to post logs");
return false;
}
}

public boolean event(DatadogEvent event) {
logger.fine("Sending event");
try {
Expand Down
Loading

0 comments on commit 0404aa5

Please sign in to comment.