diff --git a/src/main/java/cd/go/contrib/elasticagent/AgentInstances.java b/src/main/java/cd/go/contrib/elasticagent/AgentInstances.java index d20431b6..ff1aced8 100644 --- a/src/main/java/cd/go/contrib/elasticagent/AgentInstances.java +++ b/src/main/java/cd/go/contrib/elasticagent/AgentInstances.java @@ -19,6 +19,8 @@ import cd.go.contrib.elasticagent.executors.ServerPingRequestExecutor; import cd.go.contrib.elasticagent.requests.CreateAgentRequest; +import java.util.Optional; +import java.util.function.Function; /** * Plugin implementors should implement these methods to interface to your cloud. @@ -36,7 +38,7 @@ public interface AgentInstances { * @param pluginRequest the plugin request object * @param consoleLogAppender */ - T create(CreateAgentRequest request, PluginSettings settings, PluginRequest pluginRequest, ConsoleLogAppender consoleLogAppender) throws Exception; + Optional requestCreateAgent(CreateAgentRequest request, PluginSettings settings, PluginRequest pluginRequest, ConsoleLogAppender consoleLogAppender) throws Exception; /** * This message is sent when the plugin needs to terminate the agent instance. @@ -84,5 +86,16 @@ public interface AgentInstances { * @param agentId the elastic agent id */ T find(String agentId); + + /** + * Atomically update the agent instance for the given agentId. + * computeFn is called with the current agent instance if it exists, + * or null if it doesn't exist. computeFn should return a new agent instance + * that represents its new state. + * @param agentId + * @param computeFn + * @return + */ + T updateAgent(String agentId, Function computeFn); } diff --git a/src/main/java/cd/go/contrib/elasticagent/KubernetesAgentInstances.java b/src/main/java/cd/go/contrib/elasticagent/KubernetesAgentInstances.java index b7d3dc64..b7ad4990 100644 --- a/src/main/java/cd/go/contrib/elasticagent/KubernetesAgentInstances.java +++ b/src/main/java/cd/go/contrib/elasticagent/KubernetesAgentInstances.java @@ -18,25 +18,25 @@ import cd.go.contrib.elasticagent.model.JobIdentifier; import cd.go.contrib.elasticagent.requests.CreateAgentRequest; +import cd.go.contrib.elasticagent.KubernetesInstance.AgentState; +import cd.go.contrib.elasticagent.utils.Util; import io.fabric8.kubernetes.api.model.Pod; -import io.fabric8.kubernetes.api.model.PodList; import io.fabric8.kubernetes.client.KubernetesClient; import java.net.SocketTimeoutException; import java.time.Duration; import java.time.Instant; -import java.util.ArrayList; -import java.util.Map; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Semaphore; +import java.util.function.Function; +import java.util.stream.Collectors; import static cd.go.contrib.elasticagent.KubernetesPlugin.LOG; import static java.text.MessageFormat.format; public class KubernetesAgentInstances implements AgentInstances { - private final ConcurrentHashMap instances = new ConcurrentHashMap<>(); + private final ConcurrentHashMap instances; public Clock clock = Clock.DEFAULT; - final Semaphore semaphore = new Semaphore(0, true); private KubernetesClientFactory factory; private KubernetesInstanceFactory kubernetesInstanceFactory; @@ -50,55 +50,127 @@ public KubernetesAgentInstances(KubernetesClientFactory factory) { } public KubernetesAgentInstances(KubernetesClientFactory factory, KubernetesInstanceFactory kubernetesInstanceFactory) { + this(factory, kubernetesInstanceFactory, Collections.emptyMap()); + } + + public KubernetesAgentInstances(KubernetesClientFactory factory, KubernetesInstanceFactory kubernetesInstanceFactory, Map initialInstances) { this.factory = factory; this.kubernetesInstanceFactory = kubernetesInstanceFactory; + this.instances = new ConcurrentHashMap<>(initialInstances); } @Override - public KubernetesInstance create(CreateAgentRequest request, PluginSettings settings, PluginRequest pluginRequest, ConsoleLogAppender consoleLogAppender) { - final Integer maxAllowedContainers = settings.getMaxPendingPods(); + public Optional requestCreateAgent(CreateAgentRequest request, PluginSettings settings, PluginRequest pluginRequest, ConsoleLogAppender consoleLogAppender) { + final Integer maxAllowedPods = settings.getMaxPendingPods(); synchronized (instances) { - refreshAll(settings); - doWithLockOnSemaphore(new SetupSemaphore(maxAllowedContainers, instances, semaphore)); - consoleLogAppender.accept("Waiting to create agent pod."); - if (semaphore.tryAcquire()) { - return createKubernetesInstance(request, settings, pluginRequest, consoleLogAppender); + if (instances.size() < maxAllowedPods) { + return requestCreateAgentHelper(request, settings, pluginRequest, consoleLogAppender); } else { - String message = format("[Create Agent Request] The number of pending kubernetes pods is currently at the maximum permissible limit ({0}). Total kubernetes pods ({1}). Not creating any more containers.", maxAllowedContainers, instances.size()); + String message = String.format("[Create Agent Request] The number of pending kubernetes pods is currently at the maximum permissible limit (%s). Total kubernetes pods (%s). Not creating any more pods.", + maxAllowedPods, + instances.size()); LOG.warn(message); consoleLogAppender.accept(message); - return null; + return Optional.empty(); } } } - private void doWithLockOnSemaphore(Runnable runnable) { - synchronized (semaphore) { - runnable.run(); + private List findPodsEligibleForReuse(CreateAgentRequest request) { + Long jobId = request.jobIdentifier().getJobId(); + String jobElasticConfigHash = KubernetesInstanceFactory.agentConfigHash( + request.clusterProfileProperties(), request.elasticProfileProperties()); + + List eligiblePods = new ArrayList<>(); + + for (KubernetesInstance instance : instances.values()) { + if (instance.getJobId().equals(jobId)) { + eligiblePods.add(instance); + continue; + } + + String podElasticConfigHash = instance.getPodAnnotations().get(KubernetesInstance.ELASTIC_CONFIG_HASH); + boolean sameElasticConfig = Objects.equals(podElasticConfigHash, jobElasticConfigHash); + boolean instanceIsIdle = instance.getAgentState().equals(KubernetesInstance.AgentState.Idle); + boolean podIsRunning = instance.getPodState().equals(PodState.Running); + boolean isReusable = sameElasticConfig && instanceIsIdle && podIsRunning; + + LOG.info( + "[reuse] Is pod {} reusable for job {}? {}. Job has {}={}; pod has {}={}, agentState={}, podState={}", + instance.getPodName(), + jobId, + isReusable, + KubernetesInstance.ELASTIC_CONFIG_HASH, + jobElasticConfigHash, + KubernetesInstance.ELASTIC_CONFIG_HASH, + podElasticConfigHash, + instance.getAgentState(), + instance.getPodState() + ); + + if (isReusable) { + eligiblePods.add(instance); + } } + + return eligiblePods; } - private KubernetesInstance createKubernetesInstance(CreateAgentRequest request, PluginSettings settings, PluginRequest pluginRequest, ConsoleLogAppender consoleLogAppender) { + + private Optional requestCreateAgentHelper( + CreateAgentRequest request, + PluginSettings settings, + PluginRequest pluginRequest, + ConsoleLogAppender consoleLogAppender) { JobIdentifier jobIdentifier = request.jobIdentifier(); - if (isAgentCreatedForJob(jobIdentifier.getJobId())) { - String message = format("[Create Agent Request] Request for creating an agent for Job Identifier [{0}] has already been scheduled. Skipping current request.", jobIdentifier); - LOG.warn(message); - consoleLogAppender.accept(message); - return null; + Long jobId = jobIdentifier.getJobId(); + + // Agent reuse disabled - create a new pod only if one hasn't already been created for this job ID. + if (!settings.getEnableAgentReuse()) { + // Already created a pod for this job ID. + if (isAgentCreatedForJob(jobId)) { + String message = format("[Create Agent Request] Request for creating an agent for Job Identifier [{0}] has already been scheduled. Skipping current request.", jobIdentifier); + LOG.warn(message); + consoleLogAppender.accept(message); + return Optional.empty(); + } + // No pod created yet for this job ID. Create one. + KubernetesClient client = factory.client(settings); + KubernetesInstance instance = kubernetesInstanceFactory.create(request, settings, client, pluginRequest); + consoleLogAppender.accept(String.format("Created pod: %s", instance.getPodName())); + instance = instance.toBuilder().agentState(AgentState.Building).build(); + register(instance); + consoleLogAppender.accept(String.format("Agent pod %s created. Waiting for it to register to the GoCD server.", instance.getPodName())); + return Optional.of(instance); } - KubernetesClient client = factory.client(settings); - KubernetesInstance instance = kubernetesInstanceFactory.create(request, settings, client, pluginRequest); - consoleLogAppender.accept(String.format("Creating pod: %s", instance.name())); - register(instance); - consoleLogAppender.accept(String.format("Agent pod %s created. Waiting for it to register to the GoCD server.", instance.name())); + // Agent reuse enabled - look for any extant pods that match this job, + // and create a new one only if there are none. + List reusablePods = findPodsEligibleForReuse(request); + LOG.info("[reuse] Found {} pods eligible for reuse for CreateAgentRequest for job {}: {}", + reusablePods.size(), + jobId, + reusablePods.stream().map(pod -> pod.getPodName()).collect(Collectors.toList())); - return instance; + if (reusablePods.isEmpty()) { + KubernetesClient client = factory.client(settings); + KubernetesInstance instance = kubernetesInstanceFactory.create(request, settings, client, pluginRequest); + consoleLogAppender.accept(String.format("Created pod: %s", instance.getPodName())); + instance = instance.toBuilder().agentState(AgentState.Building).build(); + register(instance); + consoleLogAppender.accept(String.format("Agent pod %s created. Waiting for it to register to the GoCD server.", instance.getPodName())); + return Optional.of(instance); + } else { + String message = String.format("[reuse] Not creating a new pod - found %s eligible for reuse.", reusablePods.size()); + consoleLogAppender.accept(message); + LOG.info(message); + return Optional.empty(); + } } private boolean isAgentCreatedForJob(Long jobId) { for (KubernetesInstance instance : instances.values()) { - if (instance.jobId().equals(jobId)) { + if (instance.getJobId().equals(jobId)) { return true; } } @@ -111,7 +183,7 @@ public void terminate(String agentId, PluginSettings settings) { KubernetesInstance instance = instances.get(agentId); if (instance != null) { KubernetesClient client = factory.client(settings); - instance.terminate(client); + client.pods().withName(instance.getPodName()).delete(); } else { LOG.warn(format("Requested to terminate an instance that does not exist {0}.", agentId)); } @@ -140,20 +212,30 @@ public Agents instancesCreatedAfterTimeout(PluginSettings settings, Agents agent continue; } - if (clock.now().isAfter(instance.createdAt().plus(settings.getAutoRegisterPeriod()))) { + if (clock.now().isAfter(instance.getCreatedAt().plus(settings.getAutoRegisterPeriod()))) { oldAgents.add(agent); } } return new Agents(oldAgents); } + public List listAgentPods(KubernetesClient client) { + if (client == null) { + throw new IllegalArgumentException("client is null"); + } + return client.pods() + .withLabel(Constants.KUBERNETES_POD_KIND_LABEL_KEY, Constants.KUBERNETES_POD_KIND_LABEL_VALUE) + .list() + .getItems(); + } + @Override public void refreshAll(PluginSettings properties) { LOG.debug("[Refresh Instances] Syncing k8s elastic agent pod information for cluster {}.", properties); - PodList list = null; + List pods = null; try { KubernetesClient client = factory.client(properties); - list = client.pods().list(); + pods = listAgentPods(client); } catch (Exception e) { LOG.error("Error occurred while trying to list kubernetes pods:", e); @@ -161,35 +243,46 @@ public void refreshAll(PluginSettings properties) { LOG.error("Error caused due to SocketTimeoutException. This generally happens due to stale kubernetes client. Clearing out existing kubernetes client and creating a new one!"); factory.clearOutExistingClient(); KubernetesClient client = factory.client(properties); - list = client.pods().list(); + pods = listAgentPods(client); } } - if (list == null) { + if (pods == null) { LOG.info("Did not find any running kubernetes pods."); return; } + Map oldInstances = Map.copyOf(instances); instances.clear(); - for (Pod pod : list.getItems()) { - Map podLabels = pod.getMetadata().getLabels(); - if (podLabels != null) { - if (Constants.KUBERNETES_POD_KIND_LABEL_VALUE.equals(podLabels.get(Constants.KUBERNETES_POD_KIND_LABEL_KEY))) { - register(kubernetesInstanceFactory.fromKubernetesPod(pod)); - } + + for (Pod pod : pods) { + String podName = pod.getMetadata().getName(); + // preserve pod's agent state + KubernetesInstance newInstance = kubernetesInstanceFactory.fromKubernetesPod(pod); + KubernetesInstance oldInstance = oldInstances.get(podName); + if (oldInstance != null) { + AgentState oldAgentState = oldInstances.get(podName).getAgentState(); + newInstance = newInstance.toBuilder().agentState(oldAgentState).build(); + LOG.debug("[reuse] Preserved AgentState {} upon refresh of pod {}", oldAgentState, podName); } + register(newInstance); } LOG.info(String.format("[refresh-pod-state] Pod information successfully synced. All(Running/Pending) pod count is %d.", instances.size())); } + @Override + public KubernetesInstance updateAgent(String agentId, Function updateFn) { + return instances.compute(agentId, (_agentId, instance) -> updateFn.apply(instance)); + } + @Override public KubernetesInstance find(String agentId) { return instances.get(agentId); } public void register(KubernetesInstance instance) { - instances.put(instance.name(), instance); + instances.put(instance.getPodName(), instance); } private KubernetesAgentInstances unregisteredAfterTimeout(PluginSettings settings, Agents knownAgents) throws Exception { diff --git a/src/main/java/cd/go/contrib/elasticagent/KubernetesInstance.java b/src/main/java/cd/go/contrib/elasticagent/KubernetesInstance.java index 3cdf6383..ef199f81 100644 --- a/src/main/java/cd/go/contrib/elasticagent/KubernetesInstance.java +++ b/src/main/java/cd/go/contrib/elasticagent/KubernetesInstance.java @@ -16,53 +16,172 @@ package cd.go.contrib.elasticagent; -import io.fabric8.kubernetes.client.KubernetesClient; - -import java.time.Instant; +import java.util.Collections; import java.util.Map; +import java.time.Instant; +/** + * KubernetesInstance represents an agent pod in Kubernetes. + * Its fields are immutable. + */ public class KubernetesInstance { + /** + * AgentState represents the possible agent states from the + * GoCD server perspective - whether it is currently running a job, + * ready to accept a new job, etc. + */ + public enum AgentState { + /** + * Unknown means the agent hasn't yet been registered with the plugin. + * For example, if the GoCD server restarted while a pod was building, + * the state will be Unknown until the pod finishes its job. + */ + Unknown, + /** + * Idle means the agent has just finished a job. + */ + Idle, + /** + * Building means the agent has been assigned a job. + */ + Building, + } + + /** + * ELASTIC_CONFIG_HASH is a pod annotation that contains a hash of the cluster profile + * configuration and elastic profile configuration that were used to create the pod. + */ + public static final String ELASTIC_CONFIG_HASH = "go.cd/elastic-config-hash"; + private final Instant createdAt; + + public Instant getCreatedAt() { + return this.createdAt; + } + + /** + * environment is populated from k8s pod metadata.labels.Elastic-Agent-Environment + */ private final String environment; - private final String name; - private final Map properties; - private final Long jobId; - private final PodState state; - public KubernetesInstance(Instant createdAt, String environment, String name, Map properties, Long jobId, PodState state) { - this.createdAt = createdAt; - this.environment = environment; - this.name = name; - this.properties = properties; - this.jobId = jobId; - this.state = state; + public String getEnvironment() { + return this.environment; } - public void terminate(KubernetesClient client) { - client.pods().withName(name).delete(); + private final String podName; + + public String getPodName() { + return this.podName; } - public String name() { - return name; + /** + * podAnnotations is populated from k8s pod metadata.annotations + */ + private final Map podAnnotations; + + public Map getPodAnnotations() { + return Map.copyOf(this.podAnnotations); } - public Instant createdAt() { - return createdAt; + /** + * jobId is populated from k8s pod metadata.labels.Elastic-Agent-Job-Id + */ + private final Long jobId; + public Long getJobId() { + return this.jobId; + } + + private final PodState podState; + public PodState getPodState() { + return this.podState; + } + + private final AgentState agentState; + public AgentState getAgentState() { + return this.agentState; } - public String environment() { - return environment; + KubernetesInstance( + Instant createdAt, + String environment, + String podName, + Map podAnnotations, + Long jobId, + PodState podState, + AgentState agentState) { + this.createdAt = createdAt; + this.environment = environment; + this.podName = podName; + this.podAnnotations = Map.copyOf(podAnnotations); + this.jobId = jobId; + this.podState = podState; + this.agentState = agentState; } - public Map getInstanceProperties() { - return properties; + public static class KubernetesInstanceBuilder { + private Instant createdAt = Instant.now(); + public KubernetesInstanceBuilder createdAt(Instant createdAt) { + this.createdAt = createdAt; + return this; + } + + private String environment; + public KubernetesInstanceBuilder environment(String environment) { + this.environment = environment; + return this; + } + + private String podName; + public KubernetesInstanceBuilder podName(String podName) { + this.podName = podName; + return this; + } + + private Map podAnnotations = Collections.emptyMap(); + public KubernetesInstanceBuilder podAnnotations(Map podAnnotations) { + if (podAnnotations == null) { + this.podAnnotations = Collections.emptyMap(); + } else { + this.podAnnotations = Map.copyOf(podAnnotations); + } + return this; + } + + private Long jobId; + public KubernetesInstanceBuilder jobId(Long jobId) { + this.jobId = jobId; + return this; + } + + private PodState podState = PodState.Pending; + public KubernetesInstanceBuilder podState(PodState podState) { + this.podState = podState; + return this; + } + + private AgentState agentState = AgentState.Unknown; + public KubernetesInstanceBuilder agentState(AgentState agentState) { + this.agentState = agentState; + return this; + } + + public KubernetesInstance build() { + return new KubernetesInstance(createdAt, environment, podName, podAnnotations, jobId, podState, agentState); + } } - public Long jobId() { - return jobId; + public static KubernetesInstanceBuilder builder() { + return new KubernetesInstanceBuilder(); } - public boolean isPending() { - return this.state.equals(PodState.Pending); + public KubernetesInstanceBuilder toBuilder() { + return new KubernetesInstanceBuilder() + .createdAt(createdAt) + .environment(environment) + .podName(podName) + .podAnnotations(podAnnotations) + .jobId(jobId) + .podState(podState) + .agentState(agentState); } } diff --git a/src/main/java/cd/go/contrib/elasticagent/KubernetesInstanceFactory.java b/src/main/java/cd/go/contrib/elasticagent/KubernetesInstanceFactory.java index 2a3cecc7..6e88c463 100644 --- a/src/main/java/cd/go/contrib/elasticagent/KubernetesInstanceFactory.java +++ b/src/main/java/cd/go/contrib/elasticagent/KubernetesInstanceFactory.java @@ -18,6 +18,7 @@ import cd.go.contrib.elasticagent.requests.CreateAgentRequest; import cd.go.contrib.elasticagent.utils.Size; +import cd.go.contrib.elasticagent.utils.Util; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; @@ -48,7 +49,7 @@ public class KubernetesInstanceFactory { public KubernetesInstance create(CreateAgentRequest request, PluginSettings settings, KubernetesClient client, PluginRequest pluginRequest) { - String podSpecType = request.properties().get(POD_SPEC_TYPE.getKey()); + String podSpecType = request.elasticProfileProperties().get(POD_SPEC_TYPE.getKey()); if (podSpecType != null) { switch (podSpecType) { case "properties": @@ -62,7 +63,7 @@ public KubernetesInstance create(CreateAgentRequest request, PluginSettings sett } } else { - if (Boolean.parseBoolean(request.properties().get(SPECIFIED_USING_POD_CONFIGURATION.getKey()))) { + if (Boolean.parseBoolean(request.elasticProfileProperties().get(SPECIFIED_USING_POD_CONFIGURATION.getKey()))) { return createUsingPodYaml(request, settings, client, pluginRequest); } else { return createUsingProperties(request, settings, client, pluginRequest); @@ -75,7 +76,7 @@ private KubernetesInstance createUsingProperties(CreateAgentRequest request, Plu Container container = new Container(); container.setName(containerName); - container.setImage(image(request.properties())); + container.setImage(image(request.elasticProfileProperties())); container.setImagePullPolicy("IfNotPresent"); container.setSecurityContext(new SecurityContextBuilder().withPrivileged(privileged(request)).build()); @@ -95,7 +96,7 @@ private KubernetesInstance createUsingProperties(CreateAgentRequest request, Plu } private Boolean privileged(CreateAgentRequest request) { - final String privilegedMode = request.properties().get(PRIVILEGED.getKey()); + final String privilegedMode = request.elasticProfileProperties().get(PRIVILEGED.getKey()); if (isBlank(privilegedMode)) { return false; } @@ -114,7 +115,7 @@ private ResourceRequirements getPodResources(CreateAgentRequest request) { ResourceRequirements resources = new ResourceRequirements(); HashMap limits = new HashMap<>(); - String maxMemory = request.properties().get("MaxMemory"); + String maxMemory = request.elasticProfileProperties().get("MaxMemory"); if (!isBlank(maxMemory)) { Size mem = Size.parse(maxMemory); LOG.debug(format("[Create Agent] Setting memory resource limit on k8s pod: {0}.", new Quantity(valueOf((long) mem.toMegabytes()), "M"))); @@ -122,7 +123,7 @@ private ResourceRequirements getPodResources(CreateAgentRequest request) { limits.put("memory", new Quantity(valueOf(memory))); } - String maxCPU = request.properties().get("MaxCPU"); + String maxCPU = request.elasticProfileProperties().get("MaxCPU"); if (!isBlank(maxCPU)) { LOG.debug(format("[Create Agent] Setting cpu resource limit on k8s pod: {0}.", new Quantity(maxCPU))); limits.put("cpu", new Quantity(maxCPU)); @@ -139,10 +140,32 @@ private static void setLabels(Pod pod, CreateAgentRequest request) { pod.getMetadata().setLabels(existingLabels); } + /** + * Compute a string that uniquely identifies the configuration that was used to launch an agent pod. + * @param clusterProfileProperties Cluster profile properties of the agent + * @param elasticProfileProperties Elastic profile properties of the agent + * @return The unique identifier string. + */ + public static String agentConfigHash(ClusterProfileProperties clusterProfileProperties, Map elasticProfileProperties) { + return Util.GSON.toJson(Map.of( + "cluster_profile_properties", Util.objectUUID(clusterProfileProperties), + "elastic_profile_properties", Util.objectUUID(elasticProfileProperties) + )); + } + + public static boolean isSameConfigHash(String hashA, String hashB) { + return hashA != null && hashA.equals(hashB); + } + private static void setAnnotations(Pod pod, CreateAgentRequest request) { Map existingAnnotations = (pod.getMetadata().getAnnotations() != null) ? pod.getMetadata().getAnnotations() : new HashMap<>(); - existingAnnotations.putAll(request.properties()); + existingAnnotations.putAll(request.elasticProfileProperties()); existingAnnotations.put(JOB_IDENTIFIER_LABEL_KEY, request.jobIdentifier().toJson()); + Map annotationsForAgentReuse = Map.of( + KubernetesInstance.ELASTIC_CONFIG_HASH, agentConfigHash(request.clusterProfileProperties(), request.elasticProfileProperties()) + ); + LOG.debug("[reuse] Annotating newly-created pod {} with {}", pod.getMetadata().getName(), annotationsForAgentReuse); + existingAnnotations.putAll(annotationsForAgentReuse); pod.getMetadata().setAnnotations(existingAnnotations); } @@ -153,7 +176,6 @@ private KubernetesInstance createKubernetesPod(KubernetesClient client, Pod elas } KubernetesInstance fromKubernetesPod(Pod elasticAgentPod) { - KubernetesInstance kubernetesInstance; try { ObjectMeta metadata = elasticAgentPod.getMetadata(); Instant createdAt = Instant.now(); @@ -162,18 +184,24 @@ KubernetesInstance fromKubernetesPod(Pod elasticAgentPod) { } String environment = metadata.getLabels().get(ENVIRONMENT_LABEL_KEY); Long jobId = Long.valueOf(metadata.getLabels().get(JOB_ID_LABEL_KEY)); - kubernetesInstance = new KubernetesInstance(createdAt, environment, metadata.getName(), metadata.getAnnotations(), jobId, PodState.fromPod(elasticAgentPod)); + return KubernetesInstance.builder() + .createdAt(createdAt) + .environment(environment) + .podName(metadata.getName()) + .podAnnotations(metadata.getAnnotations()) + .jobId(jobId) + .podState(PodState.fromPod(elasticAgentPod)) + .build(); } catch (DateTimeParseException e) { throw new RuntimeException(e); } - return kubernetesInstance; } private static List environmentFrom(CreateAgentRequest request, PluginSettings settings, String podName, PluginRequest pluginRequest) { ArrayList env = new ArrayList<>(); String goServerUrl = isBlank(settings.getGoServerUrl()) ? pluginRequest.getSeverInfo().getSecureSiteUrl() : settings.getGoServerUrl(); env.add(new EnvVar("GO_EA_SERVER_URL", goServerUrl, null)); - String environment = request.properties().get("Environment"); + String environment = request.elasticProfileProperties().get("Environment"); if (!isBlank(environment)) { env.addAll(parseEnvironments(environment)); } @@ -230,7 +258,7 @@ private static String image(Map properties) { private KubernetesInstance createUsingPodYaml(CreateAgentRequest request, PluginSettings settings, KubernetesClient client, PluginRequest pluginRequest) { ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); - String podYaml = request.properties().get(POD_CONFIGURATION.getKey()); + String podYaml = request.elasticProfileProperties().get(POD_CONFIGURATION.getKey()); String templatizedPodYaml = getTemplatizedPodSpec(podYaml); Pod elasticAgentPod = new Pod(); @@ -247,8 +275,8 @@ private KubernetesInstance createUsingPodYaml(CreateAgentRequest request, Plugin } private KubernetesInstance createUsingRemoteFile(CreateAgentRequest request, PluginSettings settings, KubernetesClient client, PluginRequest pluginRequest) { - String fileToDownload = request.properties().get(REMOTE_FILE.getKey()); - String fileType = request.properties().get(REMOTE_FILE_TYPE.getKey()); + String fileToDownload = request.elasticProfileProperties().get(REMOTE_FILE.getKey()); + String fileType = request.elasticProfileProperties().get(REMOTE_FILE_TYPE.getKey()); Pod elasticAgentPod = new Pod(); ObjectMapper mapper; diff --git a/src/main/java/cd/go/contrib/elasticagent/KubernetesPlugin.java b/src/main/java/cd/go/contrib/elasticagent/KubernetesPlugin.java index ddbc6555..f0ead19f 100644 --- a/src/main/java/cd/go/contrib/elasticagent/KubernetesPlugin.java +++ b/src/main/java/cd/go/contrib/elasticagent/KubernetesPlugin.java @@ -86,8 +86,6 @@ public GoPluginApiResponse handle(GoPluginApiRequest request) { return shouldAssignWorkRequest.executor(getAgentInstancesFor(clusterProfileProperties)).execute(); case REQUEST_SERVER_PING: ServerPingRequest serverPingRequest = ServerPingRequest.fromJSON(request.requestBody()); - List listOfClusterProfileProperties = serverPingRequest.allClusterProfileProperties(); - refreshInstancesForAllClusters(listOfClusterProfileProperties); return serverPingRequest.executor(clusterSpecificAgentInstances, pluginRequest).execute(); case REQUEST_JOB_COMPLETION: JobCompletionRequest jobCompletionRequest = JobCompletionRequest.fromJSON(request.requestBody()); @@ -108,6 +106,8 @@ public GoPluginApiResponse handle(GoPluginApiRequest request) { return new DefaultGoPluginApiResponse(200); case REQUEST_MIGRATE_CONFIGURATION: return MigrateConfigurationRequest.fromJSON(request.requestBody()).executor().execute(); + case REQUEST_PLUGIN_SETTINGS: + return DefaultGoPluginApiResponse.success("{}"); default: throw new UnhandledRequestTypeException(request.requestName()); } @@ -117,12 +117,6 @@ public GoPluginApiResponse handle(GoPluginApiRequest request) { } } - private void refreshInstancesForAllClusters(List listOfClusterProfileProperties) throws Exception { - for (ClusterProfileProperties clusterProfileProperties : listOfClusterProfileProperties) { - refreshInstancesForCluster(clusterProfileProperties); - } - } - private AgentInstances getAgentInstancesFor(ClusterProfileProperties clusterProfileProperties) throws Exception { KubernetesAgentInstances agentInstances = clusterSpecificAgentInstances.get(clusterProfileProperties.uuid()); diff --git a/src/main/java/cd/go/contrib/elasticagent/PluginSettings.java b/src/main/java/cd/go/contrib/elasticagent/PluginSettings.java index 28f4410a..2b142b70 100644 --- a/src/main/java/cd/go/contrib/elasticagent/PluginSettings.java +++ b/src/main/java/cd/go/contrib/elasticagent/PluginSettings.java @@ -22,7 +22,6 @@ import com.google.gson.annotations.SerializedName; import java.time.Duration; -import java.time.temporal.ChronoUnit; import static cd.go.contrib.elasticagent.utils.Util.IntTypeAdapter; import static cd.go.contrib.elasticagent.utils.Util.isBlank; @@ -56,6 +55,10 @@ public class PluginSettings { @SerializedName("namespace") private String namespace; + @Expose + @SerializedName("enable_agent_reuse") + private boolean enableAgentReuse; + private Duration autoRegisterPeriod; public PluginSettings() { @@ -76,7 +79,7 @@ public static PluginSettings fromJSON(String json) { public Duration getAutoRegisterPeriod() { if (this.autoRegisterPeriod == null) { - this.autoRegisterPeriod = Duration.of(getAutoRegisterTimeout(), ChronoUnit.MINUTES); + this.autoRegisterPeriod = Duration.ofMinutes(getAutoRegisterTimeout()); } return this.autoRegisterPeriod; } @@ -111,6 +114,14 @@ public String getNamespace() { return getOrDefault(this.namespace, "default"); } + public boolean getEnableAgentReuse() { + return enableAgentReuse; + } + + public void setEnableAgentReuse(boolean enableAgentReuse) { + this.enableAgentReuse = enableAgentReuse; + } + private T getOrDefault(T t, T defaultValue) { if (t instanceof String && isBlank(String.valueOf(t))) { return defaultValue; diff --git a/src/main/java/cd/go/contrib/elasticagent/Request.java b/src/main/java/cd/go/contrib/elasticagent/Request.java index 5aee61f4..1afbc9f7 100644 --- a/src/main/java/cd/go/contrib/elasticagent/Request.java +++ b/src/main/java/cd/go/contrib/elasticagent/Request.java @@ -39,7 +39,9 @@ public enum Request { REQUEST_GET_CAPABILITIES(Constants.ELASTIC_AGENT_REQUEST_PREFIX + ".get-capabilities"), REQUEST_ELASTIC_AGENT_STATUS_REPORT(Constants.ELASTIC_AGENT_REQUEST_PREFIX + ".agent-status-report"), - REQUEST_CLUSTER_STATUS_REPORT(Constants.ELASTIC_AGENT_REQUEST_PREFIX + ".cluster-status-report"); + REQUEST_CLUSTER_STATUS_REPORT(Constants.ELASTIC_AGENT_REQUEST_PREFIX + ".cluster-status-report"), + + REQUEST_PLUGIN_SETTINGS(Constants.GO_PLUGIN_SETTINGS_PREFIX + ".get-configuration"); private final String requestName; diff --git a/src/main/java/cd/go/contrib/elasticagent/SetupSemaphore.java b/src/main/java/cd/go/contrib/elasticagent/SetupSemaphore.java deleted file mode 100644 index 189eea57..00000000 --- a/src/main/java/cd/go/contrib/elasticagent/SetupSemaphore.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2022 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 cd.go.contrib.elasticagent; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.Semaphore; - -class SetupSemaphore implements Runnable { - private final Integer maxAllowedPendingPods; - private final Map instances; - private final Semaphore semaphore; - - public SetupSemaphore(Integer maxAllowedPendingPods, Map instances, Semaphore semaphore) { - this.maxAllowedPendingPods = maxAllowedPendingPods; - this.instances = instances; - this.semaphore = semaphore; - } - - @Override - public void run() { - List pendingInstances = getPendingInstances(instances); - int totalPendingPods = pendingInstances.size(); - int availablePermits = maxAllowedPendingPods - totalPendingPods; - - if (availablePermits <= 0) { - // no more capacity available. - semaphore.drainPermits(); - } else { - int semaphoreValueDifference = availablePermits - semaphore.availablePermits(); - if (semaphoreValueDifference > 0) { - semaphore.release(semaphoreValueDifference); - } else if (semaphoreValueDifference < 0) { - semaphore.tryAcquire(Math.abs(semaphoreValueDifference)); - } - } - } - - private List getPendingInstances(Map instances) { - ArrayList pendingInstances = new ArrayList<>(); - for (KubernetesInstance kubernetesInstance : instances.values()) { - if (kubernetesInstance.isPending()) { - pendingInstances.add(kubernetesInstance); - } - } - - return pendingInstances; - } -} diff --git a/src/main/java/cd/go/contrib/elasticagent/executors/CreateAgentRequestExecutor.java b/src/main/java/cd/go/contrib/elasticagent/executors/CreateAgentRequestExecutor.java index d7c543bc..4c5edfc6 100644 --- a/src/main/java/cd/go/contrib/elasticagent/executors/CreateAgentRequestExecutor.java +++ b/src/main/java/cd/go/contrib/elasticagent/executors/CreateAgentRequestExecutor.java @@ -44,7 +44,7 @@ public CreateAgentRequestExecutor(CreateAgentRequest request, AgentInstances { final String message = String.format("%s %s\n", MESSAGE_PREFIX_FORMATTER.format(LocalDateTime.ofInstant(Instant.now(), ZoneOffset.UTC)), text); pluginRequest.appendToConsoleLog(request.jobIdentifier(), message); @@ -52,7 +52,7 @@ public GoPluginApiResponse execute() throws Exception { LocalDateTime localNow = LocalDateTime.ofInstant(Instant.now(), ZoneOffset.UTC); consoleLogAppender.accept(format("Received request to create a pod for job {0} in cluster {1} at {2}", request.jobIdentifier(), request.clusterProfileProperties().getClusterUrl(), UTC_FORMAT.format(localNow))); try { - agentInstances.create(request, request.clusterProfileProperties(), pluginRequest, consoleLogAppender); + agentInstances.requestCreateAgent(request, request.clusterProfileProperties(), pluginRequest, consoleLogAppender); } catch (Exception e) { consoleLogAppender.accept(String.format("Failed to create agent pod: %s", e.getMessage())); throw e; diff --git a/src/main/java/cd/go/contrib/elasticagent/executors/GetClusterProfileMetadataExecutor.java b/src/main/java/cd/go/contrib/elasticagent/executors/GetClusterProfileMetadataExecutor.java index 0f640384..3065c40c 100644 --- a/src/main/java/cd/go/contrib/elasticagent/executors/GetClusterProfileMetadataExecutor.java +++ b/src/main/java/cd/go/contrib/elasticagent/executors/GetClusterProfileMetadataExecutor.java @@ -37,18 +37,18 @@ public class GetClusterProfileMetadataExecutor implements RequestExecutor { public static final Metadata NAMESPACE = new Metadata("namespace", false, false); public static final Metadata SECURITY_TOKEN = new Metadata("security_token", true, true); public static final Metadata CLUSTER_CA_CERT = new Metadata("kubernetes_cluster_ca_cert", false, true); - - public static final List FIELDS = new ArrayList<>(); - - static { - FIELDS.add(GO_SERVER_URL); - FIELDS.add(AUTO_REGISTER_TIMEOUT); - FIELDS.add(MAX_PENDING_PODS); - FIELDS.add(CLUSTER_URL); - FIELDS.add(NAMESPACE); - FIELDS.add(SECURITY_TOKEN); - FIELDS.add(CLUSTER_CA_CERT); - } + public static final Metadata ENABLE_AGENT_REUSE = new Metadata("enable_agent_reuse", false, false); + + public static final List FIELDS = List.of( + GO_SERVER_URL, + AUTO_REGISTER_TIMEOUT, + MAX_PENDING_PODS, + CLUSTER_URL, + NAMESPACE, + SECURITY_TOKEN, + CLUSTER_CA_CERT, + ENABLE_AGENT_REUSE + ); @Override public GoPluginApiResponse execute() throws Exception { diff --git a/src/main/java/cd/go/contrib/elasticagent/executors/GetProfileMetadataExecutor.java b/src/main/java/cd/go/contrib/elasticagent/executors/GetProfileMetadataExecutor.java index 7307e54c..88460539 100644 --- a/src/main/java/cd/go/contrib/elasticagent/executors/GetProfileMetadataExecutor.java +++ b/src/main/java/cd/go/contrib/elasticagent/executors/GetProfileMetadataExecutor.java @@ -38,20 +38,18 @@ public class GetProfileMetadataExecutor implements RequestExecutor { public static final Metadata REMOTE_FILE = new Metadata("RemoteFile", false, false); public static final Metadata REMOTE_FILE_TYPE = new Metadata("RemoteFileType", false, false); public static final Metadata PRIVILEGED = new Metadata("Privileged", false, false); - public static final List FIELDS = new ArrayList<>(); - - static { - FIELDS.add(IMAGE); - FIELDS.add(MAX_MEMORY); - FIELDS.add(MAX_CPU); - FIELDS.add(ENVIRONMENT); - FIELDS.add(POD_CONFIGURATION); - FIELDS.add(SPECIFIED_USING_POD_CONFIGURATION); - FIELDS.add(POD_SPEC_TYPE); - FIELDS.add(REMOTE_FILE); - FIELDS.add(REMOTE_FILE_TYPE); - FIELDS.add(PRIVILEGED); - } + public static final List FIELDS = List.of( + IMAGE, + MAX_MEMORY, + MAX_CPU, + ENVIRONMENT, + POD_CONFIGURATION, + SPECIFIED_USING_POD_CONFIGURATION, + POD_SPEC_TYPE, + REMOTE_FILE, + REMOTE_FILE_TYPE, + PRIVILEGED + ); @Override public GoPluginApiResponse execute() throws Exception { diff --git a/src/main/java/cd/go/contrib/elasticagent/executors/JobCompletionRequestExecutor.java b/src/main/java/cd/go/contrib/elasticagent/executors/JobCompletionRequestExecutor.java index d4b630aa..efd11dd9 100644 --- a/src/main/java/cd/go/contrib/elasticagent/executors/JobCompletionRequestExecutor.java +++ b/src/main/java/cd/go/contrib/elasticagent/executors/JobCompletionRequestExecutor.java @@ -21,11 +21,9 @@ import com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse; import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; -import java.util.Arrays; import java.util.List; import static cd.go.contrib.elasticagent.KubernetesPlugin.LOG; -import static java.text.MessageFormat.format; public class JobCompletionRequestExecutor implements RequestExecutor { private final JobCompletionRequest jobCompletionRequest; @@ -40,20 +38,38 @@ public JobCompletionRequestExecutor(JobCompletionRequest jobCompletionRequest, A @Override public GoPluginApiResponse execute() throws Exception { - ClusterProfileProperties clusterProfileProperties = jobCompletionRequest.clusterProfileProperties(); - String elasticAgentId = jobCompletionRequest.getElasticAgentId(); + ClusterProfileProperties clusterProfileProperties = jobCompletionRequest.clusterProfileProperties(); + if (!clusterProfileProperties.getEnableAgentReuse()) { + // Agent reuse disabled - immediately clean up the pod and agent, as it was only valid for this job. + Agent agent = new Agent(); + agent.setElasticAgentId(elasticAgentId); - Agent agent = new Agent(); - agent.setElasticAgentId(elasticAgentId); - - LOG.info(format("[Job Completion] Terminating elastic agent with id {0} on job completion {1}.", agent.elasticAgentId(), jobCompletionRequest.jobIdentifier())); - - List agents = Arrays.asList(agent); - pluginRequest.disableAgents(agents); - agentInstances.terminate(agent.elasticAgentId(), clusterProfileProperties); - pluginRequest.deleteAgents(agents); + LOG.info("[Job Completion] Terminating elastic agent with id {} on job completion {}.", elasticAgentId, jobCompletionRequest.jobIdentifier()); + List agents = List.of(agent); + pluginRequest.disableAgents(agents); + agentInstances.terminate(agent.elasticAgentId(), clusterProfileProperties); + pluginRequest.deleteAgents(agents); + } else { + // Agent reuse enabled - mark the pod/agent as idle and leave it for reuse by other jobs or eventual cleanup. + KubernetesInstance updated = agentInstances.updateAgent( + elasticAgentId, + instance -> instance.toBuilder().agentState(KubernetesInstance.AgentState.Idle).build()); + if (updated != null) { + LOG.info("[Job Completion] Received job completion for agent ID {}. It is now marked Idle.", elasticAgentId); + } else { + // This is unlikely to happen. This means the agent just + // completed a job, but is not present in the plugin's + // in-memory view of agents. If this agent continues running, + // it will eventually be found by the periodic call to refresh + // all agents, put in an Unknown state, and then terminated + // after a timeout. + // Alternatively, this could register the instance and put it + // in an idle state. + LOG.warn("[Job Completion] Received job completion for agent ID {}, which is not known to this plugin.", elasticAgentId); + } + } return DefaultGoPluginApiResponse.success(""); } } diff --git a/src/main/java/cd/go/contrib/elasticagent/executors/ServerPingRequestExecutor.java b/src/main/java/cd/go/contrib/elasticagent/executors/ServerPingRequestExecutor.java index 8e49426c..711bfb61 100644 --- a/src/main/java/cd/go/contrib/elasticagent/executors/ServerPingRequestExecutor.java +++ b/src/main/java/cd/go/contrib/elasticagent/executors/ServerPingRequestExecutor.java @@ -30,29 +30,47 @@ public class ServerPingRequestExecutor implements RequestExecutor { - private final ServerPingRequest serverPingRequest; + private final List allClusterProfileProperties; private final Map clusterSpecificAgentInstances; private final PluginRequest pluginRequest; - public ServerPingRequestExecutor(ServerPingRequest serverPingRequest, Map clusterSpecificAgentInstances, PluginRequest pluginRequest) { - this.serverPingRequest = serverPingRequest; + public ServerPingRequestExecutor(List allClusterProfileProperties, Map clusterSpecificAgentInstances, PluginRequest pluginRequest) { + this.allClusterProfileProperties = List.copyOf(allClusterProfileProperties); this.clusterSpecificAgentInstances = clusterSpecificAgentInstances; this.pluginRequest = pluginRequest; } @Override public GoPluginApiResponse execute() throws Exception { - List allClusterProfileProperties = serverPingRequest.allClusterProfileProperties(); + refreshAllClusterInstances(); for (ClusterProfileProperties clusterProfileProperties : allClusterProfileProperties) { performCleanupForACluster(clusterProfileProperties, clusterSpecificAgentInstances.get(clusterProfileProperties.uuid())); } - CheckForPossiblyMissingAgents(); + checkForPossiblyMissingAgents(); return DefaultGoPluginApiResponse.success(""); } - private void performCleanupForACluster(ClusterProfileProperties clusterProfileProperties, KubernetesAgentInstances kubernetesAgentInstances) throws Exception { + public KubernetesAgentInstances newKubernetesInstances() { + return new KubernetesAgentInstances(); + } + + public void refreshAllClusterInstances() { + for (ClusterProfileProperties clusterProfileProperties : allClusterProfileProperties) { + String clusterId = clusterProfileProperties.uuid(); + KubernetesAgentInstances instances = clusterSpecificAgentInstances.get(clusterId); + if (instances != null) { + instances.refreshAll(clusterProfileProperties); + } else { + instances = newKubernetesInstances(); + instances.refreshAll(clusterProfileProperties); + clusterSpecificAgentInstances.put(clusterId, instances); + } + } + } + + public void performCleanupForACluster(ClusterProfileProperties clusterProfileProperties, KubernetesAgentInstances kubernetesAgentInstances) throws Exception { Agents allAgents = pluginRequest.listAgents(); Agents agentsToDisable = kubernetesAgentInstances.instancesCreatedAfterTimeout(clusterProfileProperties, allAgents); disableIdleAgents(agentsToDisable); @@ -63,7 +81,7 @@ private void performCleanupForACluster(ClusterProfileProperties clusterProfilePr kubernetesAgentInstances.terminateUnregisteredInstances(clusterProfileProperties, allAgents); } - private void CheckForPossiblyMissingAgents() throws Exception { + public void checkForPossiblyMissingAgents() throws Exception { Collection allAgents = pluginRequest.listAgents().agents(); List missingAgents = allAgents.stream().filter(agent -> clusterSpecificAgentInstances.values().stream() @@ -84,11 +102,11 @@ private void disableIdleAgents(Agents agents) throws ServerRequestFailedExceptio } } - private void terminateDisabledAgents(Agents agents, ClusterProfileProperties clusterProfileProperties, KubernetesAgentInstances dockerContainers) throws Exception { + private void terminateDisabledAgents(Agents agents, ClusterProfileProperties clusterProfileProperties, KubernetesAgentInstances instances) throws Exception { Collection toBeDeleted = agents.findInstancesToTerminate(); for (Agent agent : toBeDeleted) { - dockerContainers.terminate(agent.elasticAgentId(), clusterProfileProperties); + instances.terminate(agent.elasticAgentId(), clusterProfileProperties); } pluginRequest.deleteAgents(toBeDeleted); diff --git a/src/main/java/cd/go/contrib/elasticagent/executors/ShouldAssignWorkRequestExecutor.java b/src/main/java/cd/go/contrib/elasticagent/executors/ShouldAssignWorkRequestExecutor.java index 42073965..eade984f 100644 --- a/src/main/java/cd/go/contrib/elasticagent/executors/ShouldAssignWorkRequestExecutor.java +++ b/src/main/java/cd/go/contrib/elasticagent/executors/ShouldAssignWorkRequestExecutor.java @@ -17,15 +17,12 @@ package cd.go.contrib.elasticagent.executors; -import cd.go.contrib.elasticagent.AgentInstances; -import cd.go.contrib.elasticagent.KubernetesInstance; -import cd.go.contrib.elasticagent.RequestExecutor; +import cd.go.contrib.elasticagent.*; import cd.go.contrib.elasticagent.requests.ShouldAssignWorkRequest; import com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse; import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; import static cd.go.contrib.elasticagent.KubernetesPlugin.LOG; -import static java.text.MessageFormat.format; public class ShouldAssignWorkRequestExecutor implements RequestExecutor { private final AgentInstances agentInstances; @@ -38,18 +35,51 @@ public ShouldAssignWorkRequestExecutor(ShouldAssignWorkRequest request, AgentIns @Override public GoPluginApiResponse execute() { - KubernetesInstance pod = agentInstances.find(request.agent().elasticAgentId()); + String agentId = request.agent().elasticAgentId(); + KubernetesInstance updated = agentInstances.updateAgent(agentId, instance -> { + // No such agent is known to this plugin. + if (instance == null) { + return null; + } - if (pod == null) { - return DefaultGoPluginApiResponse.success("false"); - } + Long jobId = request.jobIdentifier().getJobId(); - if (request.jobIdentifier().getJobId().equals(pod.jobId())) { - LOG.debug(format("[should-assign-work] Job with identifier {0} can be assigned to an agent {1}.", request.jobIdentifier(), pod.name())); - return DefaultGoPluginApiResponse.success("true"); - } + // Agent reuse disabled - only assign if the agent pod was created exactly for this job ID. + if (!request.clusterProfileProperties().getEnableAgentReuse()) { + // Job ID matches - assign work and mark the instance as building. + if (jobId.equals(instance.getJobId())) { + LOG.debug("[should-assign-work] Job with identifier {} can be assigned to pod {}.", + request.jobIdentifier(), + instance.getPodName()); + return instance.toBuilder().agentState(KubernetesInstance.AgentState.Building).build(); + } + // Job ID doesn't match - don't assign work. + return null; + } - LOG.debug(format("[should-assign-work] Job with identifier {0} can not be assigned to an agent {1}.", request.jobIdentifier(), pod.name())); - return DefaultGoPluginApiResponse.success("false"); + // Agent reuse enabled: assign work if the job and agent have the same elastic config hash. + String jobConfigHash = KubernetesInstanceFactory.agentConfigHash( + request.clusterProfileProperties(), + request.elasticProfileProperties()); + String podConfigHash = instance.getPodAnnotations().get(KubernetesInstance.ELASTIC_CONFIG_HASH); + boolean assignWork = KubernetesInstanceFactory.isSameConfigHash(jobConfigHash, podConfigHash); + + LOG.info("[agent-reuse] Should assign job {} to pod {}? {}. Job has config hash {}; pod has {}={}", + jobId, + instance.getPodName(), + assignWork, + jobConfigHash, + KubernetesInstance.ELASTIC_CONFIG_HASH, + podConfigHash); + + if (assignWork) { + return instance.toBuilder().agentState(KubernetesInstance.AgentState.Building).build(); + } + + LOG.info("[agent-reuse] No KubernetesInstance can handle request {}", request); + return null; + }); + + return DefaultGoPluginApiResponse.success(updated == null ? "false" : "true"); } } diff --git a/src/main/java/cd/go/contrib/elasticagent/requests/CreateAgentRequest.java b/src/main/java/cd/go/contrib/elasticagent/requests/CreateAgentRequest.java index 2b98013c..42345249 100644 --- a/src/main/java/cd/go/contrib/elasticagent/requests/CreateAgentRequest.java +++ b/src/main/java/cd/go/contrib/elasticagent/requests/CreateAgentRequest.java @@ -36,7 +36,7 @@ public class CreateAgentRequest { private String autoRegisterKey; @Expose @SerializedName("elastic_agent_profile_properties") - private Map properties; + private Map elasticProfileProperties; @Expose @SerializedName("environment") private String environment; @@ -50,19 +50,19 @@ public class CreateAgentRequest { public CreateAgentRequest() { } - private CreateAgentRequest(String autoRegisterKey, Map properties, String environment) { + private CreateAgentRequest(String autoRegisterKey, Map elasticProfileProperties, String environment) { this.autoRegisterKey = autoRegisterKey; - this.properties = properties; + this.elasticProfileProperties = elasticProfileProperties; this.environment = environment; } - public CreateAgentRequest(String autoRegisterKey, Map properties, String environment, JobIdentifier identifier) { - this(autoRegisterKey, properties, environment); + public CreateAgentRequest(String autoRegisterKey, Map elasticProfileProperties, String environment, JobIdentifier identifier) { + this(autoRegisterKey, elasticProfileProperties, environment); this.jobIdentifier = identifier; } - public CreateAgentRequest(String autoRegisterKey, Map properties, String environment, JobIdentifier identifier, ClusterProfileProperties clusterProfileProperties) { - this(autoRegisterKey, properties, environment, identifier); + public CreateAgentRequest(String autoRegisterKey, Map elasticProfileProperties, String environment, JobIdentifier identifier, ClusterProfileProperties clusterProfileProperties) { + this(autoRegisterKey, elasticProfileProperties, environment, identifier); this.clusterProfileProperties = clusterProfileProperties; } @@ -75,8 +75,8 @@ public String autoRegisterKey() { return autoRegisterKey; } - public Map properties() { - return properties; + public Map elasticProfileProperties() { + return elasticProfileProperties; } public String environment() { @@ -112,7 +112,7 @@ public Collection autoregisterPropertiesAsEnvironmentVars(String elastic public String toString() { return "CreateAgentRequest{" + "autoRegisterKey='" + autoRegisterKey + '\'' + - ", properties=" + properties + + ", properties=" + elasticProfileProperties + ", environment='" + environment + '\'' + ", jobIdentifier=" + jobIdentifier + ", clusterProfileProperties=" + clusterProfileProperties + diff --git a/src/main/java/cd/go/contrib/elasticagent/requests/ServerPingRequest.java b/src/main/java/cd/go/contrib/elasticagent/requests/ServerPingRequest.java index 4af8e142..d2195b57 100644 --- a/src/main/java/cd/go/contrib/elasticagent/requests/ServerPingRequest.java +++ b/src/main/java/cd/go/contrib/elasticagent/requests/ServerPingRequest.java @@ -61,7 +61,7 @@ public String toString() { } public ServerPingRequestExecutor executor(Map clusterSpecificAgentInstances, PluginRequest pluginRequest) { - return new ServerPingRequestExecutor(this, clusterSpecificAgentInstances, pluginRequest); + return new ServerPingRequestExecutor(this.allClusterProfileProperties(), clusterSpecificAgentInstances, pluginRequest); } @Override diff --git a/src/main/java/cd/go/contrib/elasticagent/requests/ShouldAssignWorkRequest.java b/src/main/java/cd/go/contrib/elasticagent/requests/ShouldAssignWorkRequest.java index d7179467..a583e0ff 100644 --- a/src/main/java/cd/go/contrib/elasticagent/requests/ShouldAssignWorkRequest.java +++ b/src/main/java/cd/go/contrib/elasticagent/requests/ShouldAssignWorkRequest.java @@ -40,7 +40,7 @@ public class ShouldAssignWorkRequest { @Expose @SerializedName("elastic_agent_profile_properties") - private Map properties; + private Map elasticProfileProperties; @Expose @SerializedName("job_identifier") @@ -53,15 +53,16 @@ public class ShouldAssignWorkRequest { public ShouldAssignWorkRequest() { } - public ShouldAssignWorkRequest(Agent agent, String environment, Map properties, JobIdentifier jobIdentifier) { + public ShouldAssignWorkRequest( + Agent agent, + String environment, + Map elasticProfileProperties, + JobIdentifier jobIdentifier, + ClusterProfileProperties clusterProfileProperties) { this.agent = agent; this.environment = environment; - this.properties = properties; + this.elasticProfileProperties = elasticProfileProperties; this.jobIdentifier = jobIdentifier; - } - - public ShouldAssignWorkRequest(Agent agent, String environment, Map properties, JobIdentifier jobIdentifier, ClusterProfileProperties clusterProfileProperties) { - this(agent, environment, properties, jobIdentifier); this.clusterProfileProperties = clusterProfileProperties; } @@ -77,8 +78,8 @@ public String environment() { return environment; } - public Map properties() { - return properties; + public Map elasticProfileProperties() { + return elasticProfileProperties; } public RequestExecutor executor(AgentInstances agentInstances) { @@ -98,7 +99,7 @@ public String toString() { return "ShouldAssignWorkRequest{" + "agent=" + agent + ", environment='" + environment + '\'' + - ", properties=" + properties + + ", properties=" + elasticProfileProperties + ", jobIdentifier=" + jobIdentifier + ", clusterProfileProperties=" + clusterProfileProperties + '}'; diff --git a/src/main/java/cd/go/contrib/elasticagent/utils/Util.java b/src/main/java/cd/go/contrib/elasticagent/utils/Util.java index ae954f05..4a17c79e 100644 --- a/src/main/java/cd/go/contrib/elasticagent/utils/Util.java +++ b/src/main/java/cd/go/contrib/elasticagent/utils/Util.java @@ -26,6 +26,7 @@ import java.io.InputStream; import java.io.StringReader; import java.nio.charset.StandardCharsets; +import java.util.Objects; import java.util.Properties; public class Util { @@ -71,6 +72,10 @@ public static String pluginId() { } } + public static String objectUUID(Object o) { + return Integer.toHexString(Objects.hash(o)); + } + public static String fullVersion() { String s = readResource("/plugin.properties"); try { diff --git a/src/main/resources/plugin-settings.template.html b/src/main/resources/plugin-settings.template.html index c6f07cf2..a922dfbb 100644 --- a/src/main/resources/plugin-settings.template.html +++ b/src/main/resources/plugin-settings.template.html @@ -91,6 +91,13 @@ +
+ + + {{GOINPUTNAME[enable_agent_reuse].$error.server}} + +
+
Cluster Information
diff --git a/src/test/java/cd/go/contrib/elasticagent/KubernetesAgentInstancesIntegrationTest.java b/src/test/java/cd/go/contrib/elasticagent/KubernetesAgentInstancesIntegrationTest.java index 96b6166b..85b9677c 100644 --- a/src/test/java/cd/go/contrib/elasticagent/KubernetesAgentInstancesIntegrationTest.java +++ b/src/test/java/cd/go/contrib/elasticagent/KubernetesAgentInstancesIntegrationTest.java @@ -17,6 +17,7 @@ package cd.go.contrib.elasticagent; import cd.go.contrib.elasticagent.requests.CreateAgentRequest; +import cd.go.contrib.elasticagent.utils.Util; import com.google.gson.Gson; import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.client.KubernetesClient; @@ -92,7 +93,7 @@ public void setUp() { @Test public void shouldCreateKubernetesPodForCreateAgentRequest() { - KubernetesInstance kubernetesInstance = kubernetesAgentInstances.create(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender); + KubernetesInstance kubernetesInstance = kubernetesAgentInstances.requestCreateAgent(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender).get(); assertTrue(kubernetesAgentInstances.instanceExists(kubernetesInstance)); } @@ -100,7 +101,7 @@ public void shouldCreateKubernetesPodForCreateAgentRequest() { @Test public void shouldCreateKubernetesPodWithContainerSpecification() { ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Pod.class); - KubernetesInstance instance = kubernetesAgentInstances.create(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender); + KubernetesInstance instance = kubernetesAgentInstances.requestCreateAgent(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender).get(); verify(pods).resource(argumentCaptor.capture()); Pod elasticAgentPod = argumentCaptor.getValue(); @@ -109,7 +110,7 @@ public void shouldCreateKubernetesPodWithContainerSpecification() { Container gocdAgentContainer = containers.get(0); - assertThat(gocdAgentContainer.getName()).isEqualTo(instance.name()); + assertThat(gocdAgentContainer.getName()).isEqualTo(instance.getPodName()); assertThat(gocdAgentContainer.getImage()).isEqualTo("gocd/custom-gocd-agent-alpine:latest"); assertThat(gocdAgentContainer.getImagePullPolicy()).isEqualTo("IfNotPresent"); @@ -120,9 +121,9 @@ public void shouldCreateKubernetesPodWithContainerSpecification() { @Test public void shouldCreateKubernetesPodWithPrivilegedMod() { - createAgentRequest.properties().put(PRIVILEGED.getKey(), "true"); + createAgentRequest.elasticProfileProperties().put(PRIVILEGED.getKey(), "true"); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Pod.class); - KubernetesInstance instance = kubernetesAgentInstances.create(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender); + KubernetesInstance instance = kubernetesAgentInstances.requestCreateAgent(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender).get(); verify(pods).resource(argumentCaptor.capture()); Pod elasticAgentPod = argumentCaptor.getValue(); @@ -131,7 +132,7 @@ public void shouldCreateKubernetesPodWithPrivilegedMod() { Container gocdAgentContainer = containers.get(0); - assertThat(gocdAgentContainer.getName()).isEqualTo(instance.name()); + assertThat(gocdAgentContainer.getName()).isEqualTo(instance.getPodName()); assertThat(gocdAgentContainer.getSecurityContext().getPrivileged()).isEqualTo(true); verify(mockedPodResource).create(); @@ -140,7 +141,7 @@ public void shouldCreateKubernetesPodWithPrivilegedMod() { @Test public void shouldCreateKubernetesPodWithResourcesLimitSpecificationOnGoCDAgentContainer() { ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Pod.class); - kubernetesAgentInstances.create(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender); + kubernetesAgentInstances.requestCreateAgent(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender).get(); verify(pods).resource(argumentCaptor.capture()); Pod elasticAgentPod = argumentCaptor.getValue(); @@ -160,13 +161,13 @@ public void shouldCreateKubernetesPodWithResourcesLimitSpecificationOnGoCDAgentC @Test public void shouldCreateKubernetesPodWithPodMetadata() { ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Pod.class); - KubernetesInstance instance = kubernetesAgentInstances.create(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender); + KubernetesInstance instance = kubernetesAgentInstances.requestCreateAgent(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender).get(); verify(pods).resource(argumentCaptor.capture()); Pod elasticAgentPod = argumentCaptor.getValue(); assertNotNull(elasticAgentPod.getMetadata()); - assertThat(elasticAgentPod.getMetadata().getName()).isEqualTo(instance.name()); + assertThat(elasticAgentPod.getMetadata().getName()).isEqualTo(instance.getPodName()); verify(mockedPodResource).create(); } @@ -174,7 +175,7 @@ public void shouldCreateKubernetesPodWithPodMetadata() { @Test public void shouldCreateKubernetesPodWithTimeStamp() { ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Pod.class); - kubernetesAgentInstances.create(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender); + kubernetesAgentInstances.requestCreateAgent(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender).get(); verify(pods).resource(argumentCaptor.capture()); Pod elasticAgentPod = argumentCaptor.getValue(); @@ -188,7 +189,7 @@ public void shouldCreateKubernetesPodWithTimeStamp() { @Test public void shouldCreateKubernetesPodWithGoCDElasticAgentContainerContainingEnvironmentVariables() { ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Pod.class); - KubernetesInstance instance = kubernetesAgentInstances.create(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender); + KubernetesInstance instance = kubernetesAgentInstances.requestCreateAgent(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender).get(); verify(pods).resource(argumentCaptor.capture()); Pod elasticAgentPod = argumentCaptor.getValue(); @@ -200,7 +201,7 @@ public void shouldCreateKubernetesPodWithGoCDElasticAgentContainerContainingEnvi expectedEnvVars.add(new EnvVar("GO_EA_AUTO_REGISTER_KEY", createAgentRequest.autoRegisterKey(), null)); expectedEnvVars.add(new EnvVar("GO_EA_AUTO_REGISTER_ENVIRONMENT", createAgentRequest.environment(), null)); - expectedEnvVars.add(new EnvVar("GO_EA_AUTO_REGISTER_ELASTIC_AGENT_ID", instance.name(), null)); + expectedEnvVars.add(new EnvVar("GO_EA_AUTO_REGISTER_ELASTIC_AGENT_ID", instance.getPodName(), null)); expectedEnvVars.add(new EnvVar("GO_EA_AUTO_REGISTER_ELASTIC_PLUGIN_ID", Constants.PLUGIN_ID, null)); List containers = elasticAgentPod.getSpec().getContainers(); @@ -214,15 +215,19 @@ public void shouldCreateKubernetesPodWithGoCDElasticAgentContainerContainingEnvi @Test public void shouldCreateKubernetesPodWithPodAnnotations() { ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Pod.class); - kubernetesAgentInstances.create(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender); + kubernetesAgentInstances.requestCreateAgent(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender).get(); verify(pods).resource(argumentCaptor.capture()); Pod elasticAgentPod = argumentCaptor.getValue(); assertNotNull(elasticAgentPod.getMetadata()); - Map expectedAnnotations = new HashMap<>(); - expectedAnnotations.putAll(createAgentRequest.properties()); + Map expectedAnnotations = new HashMap<>(createAgentRequest.elasticProfileProperties()); expectedAnnotations.put(Constants.JOB_IDENTIFIER_LABEL_KEY, new Gson().toJson(createAgentRequest.jobIdentifier())); + expectedAnnotations.put( + KubernetesInstance.ELASTIC_CONFIG_HASH, + KubernetesInstanceFactory.agentConfigHash( + createAgentRequest.clusterProfileProperties(), + createAgentRequest.elasticProfileProperties())); assertThat(elasticAgentPod.getMetadata().getAnnotations()).isEqualTo(expectedAnnotations); verify(mockedPodResource).create(); @@ -231,7 +236,7 @@ public void shouldCreateKubernetesPodWithPodAnnotations() { @Test public void shouldCreateKubernetesPodWithPodLabels() { ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Pod.class); - kubernetesAgentInstances.create(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender); + kubernetesAgentInstances.requestCreateAgent(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender).get(); verify(pods).resource(argumentCaptor.capture()); Pod elasticAgentPod = argumentCaptor.getValue(); @@ -253,7 +258,7 @@ public void shouldCreateKubernetesPodWithPodLabels() { @Test public void usingPodYamlConfigurations_shouldCreateKubernetesPodForCreateAgentRequest() { createAgentRequest = CreateAgentRequestMother.createAgentRequestUsingPodYaml(); - KubernetesInstance kubernetesInstance = kubernetesAgentInstances.create(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender); + KubernetesInstance kubernetesInstance = kubernetesAgentInstances.requestCreateAgent(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender).get(); assertTrue(kubernetesAgentInstances.instanceExists(kubernetesInstance)); } @@ -263,7 +268,7 @@ public void usingPodYamlConfigurations_shouldCreateKubernetesPodWithContainerSpe createAgentRequest = CreateAgentRequestMother.createAgentRequestUsingPodYaml(); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Pod.class); - kubernetesAgentInstances.create(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender); + kubernetesAgentInstances.requestCreateAgent(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender).get(); verify(pods).resource(argumentCaptor.capture()); Pod elasticAgentPod = argumentCaptor.getValue(); @@ -284,7 +289,7 @@ public void usingPodYamlConfigurations_shouldCreateKubernetesPodWithPodMetadata( createAgentRequest = CreateAgentRequestMother.createAgentRequestUsingPodYaml(); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Pod.class); - KubernetesInstance instance = kubernetesAgentInstances.create(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender); + KubernetesInstance instance = kubernetesAgentInstances.requestCreateAgent(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender).get(); verify(pods).resource(argumentCaptor.capture()); Pod elasticAgentPod = argumentCaptor.getValue(); @@ -292,7 +297,7 @@ public void usingPodYamlConfigurations_shouldCreateKubernetesPodWithPodMetadata( assertNotNull(elasticAgentPod.getMetadata()); assertThat(elasticAgentPod.getMetadata().getName()).contains("test-pod-yaml"); - assertThat(elasticAgentPod.getMetadata().getName()).isEqualTo(instance.name()); + assertThat(elasticAgentPod.getMetadata().getName()).isEqualTo(instance.getPodName()); verify(mockedPodResource).create(); } @@ -302,7 +307,7 @@ public void usingPodYamlConfigurations_shouldCreateKubernetesPodWithTimestamp() createAgentRequest = CreateAgentRequestMother.createAgentRequestUsingPodYaml(); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Pod.class); - kubernetesAgentInstances.create(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender); + kubernetesAgentInstances.requestCreateAgent(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender).get(); verify(pods).resource(argumentCaptor.capture()); Pod elasticAgentPod = argumentCaptor.getValue(); @@ -318,7 +323,7 @@ public void usingPodYamlConfigurations_shouldCreateKubernetesPodWithGoCDElasticA createAgentRequest = CreateAgentRequestMother.createAgentRequestUsingPodYaml(); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Pod.class); - KubernetesInstance instance = kubernetesAgentInstances.create(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender); + KubernetesInstance instance = kubernetesAgentInstances.requestCreateAgent(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender).get(); verify(pods).resource(argumentCaptor.capture()); Pod elasticAgentPod = argumentCaptor.getValue(); @@ -328,7 +333,7 @@ public void usingPodYamlConfigurations_shouldCreateKubernetesPodWithGoCDElasticA expectedEnvVars.add(new EnvVar("GO_EA_SERVER_URL", settings.getGoServerUrl(), null)); expectedEnvVars.add(new EnvVar("GO_EA_AUTO_REGISTER_KEY", createAgentRequest.autoRegisterKey(), null)); expectedEnvVars.add(new EnvVar("GO_EA_AUTO_REGISTER_ENVIRONMENT", createAgentRequest.environment(), null)); - expectedEnvVars.add(new EnvVar("GO_EA_AUTO_REGISTER_ELASTIC_AGENT_ID", instance.name(), null)); + expectedEnvVars.add(new EnvVar("GO_EA_AUTO_REGISTER_ELASTIC_AGENT_ID", instance.getPodName(), null)); expectedEnvVars.add(new EnvVar("GO_EA_AUTO_REGISTER_ELASTIC_PLUGIN_ID", Constants.PLUGIN_ID, null)); List containers = elasticAgentPod.getSpec().getContainers(); @@ -344,16 +349,20 @@ public void usingPodYamlConfigurations_shouldCreateKubernetesPodWithPodAnnotatio createAgentRequest = CreateAgentRequestMother.createAgentRequestUsingPodYaml(); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Pod.class); - kubernetesAgentInstances.create(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender); + kubernetesAgentInstances.requestCreateAgent(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender).get(); verify(pods).resource(argumentCaptor.capture()); Pod elasticAgentPod = argumentCaptor.getValue(); assertNotNull(elasticAgentPod.getMetadata()); - HashMap expectedAnnotations = new HashMap<>(); - expectedAnnotations.putAll(createAgentRequest.properties()); + HashMap expectedAnnotations = new HashMap<>(createAgentRequest.elasticProfileProperties()); expectedAnnotations.put("annotation-key", "my-fancy-annotation-value"); expectedAnnotations.put(Constants.JOB_IDENTIFIER_LABEL_KEY, new Gson().toJson(createAgentRequest.jobIdentifier())); + expectedAnnotations.put( + KubernetesInstance.ELASTIC_CONFIG_HASH, + KubernetesInstanceFactory.agentConfigHash( + createAgentRequest.clusterProfileProperties(), + createAgentRequest.elasticProfileProperties())); assertThat(elasticAgentPod.getMetadata().getAnnotations()).isEqualTo(expectedAnnotations); @@ -365,7 +374,7 @@ public void usingPodYamlConfigurations_shouldCreateKubernetesPodWithPodLabels() createAgentRequest = CreateAgentRequestMother.createAgentRequestUsingPodYaml(); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Pod.class); - kubernetesAgentInstances.create(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender); + kubernetesAgentInstances.requestCreateAgent(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender).get(); verify(pods).resource(argumentCaptor.capture()); Pod elasticAgentPod = argumentCaptor.getValue(); @@ -389,7 +398,7 @@ public void usingPodYamlConfigurations_shouldCreateKubernetesPodWithPodLabels() @Test public void usingRemoteFile_shouldCreateKubernetesPodForCreateAgentRequest() { createAgentRequest = CreateAgentRequestMother.createAgentRequestUsingRemoteFile(); - KubernetesInstance kubernetesInstance = kubernetesAgentInstances.create(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender); + KubernetesInstance kubernetesInstance = kubernetesAgentInstances.requestCreateAgent(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender).get(); assertTrue(kubernetesAgentInstances.instanceExists(kubernetesInstance)); } @@ -399,7 +408,7 @@ public void usingRemoteFile_shouldCreateKubernetesPodWithContainerSpecification( createAgentRequest = CreateAgentRequestMother.createAgentRequestUsingRemoteFile(); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Pod.class); - kubernetesAgentInstances.create(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender); + kubernetesAgentInstances.requestCreateAgent(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender).get(); verify(pods).resource(argumentCaptor.capture()); Pod elasticAgentPod = argumentCaptor.getValue(); @@ -420,7 +429,7 @@ public void usingRemoteFile_shouldCreateKubernetesPodWithPodMetadata() { createAgentRequest = CreateAgentRequestMother.createAgentRequestUsingRemoteFile(); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Pod.class); - KubernetesInstance instance = kubernetesAgentInstances.create(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender); + KubernetesInstance instance = kubernetesAgentInstances.requestCreateAgent(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender).get(); verify(pods).resource(argumentCaptor.capture()); Pod elasticAgentPod = argumentCaptor.getValue(); @@ -428,7 +437,7 @@ public void usingRemoteFile_shouldCreateKubernetesPodWithPodMetadata() { assertNotNull(elasticAgentPod.getMetadata()); assertThat(elasticAgentPod.getMetadata().getName()).contains("test-pod-json"); - assertThat(elasticAgentPod.getMetadata().getName()).isEqualTo(instance.name()); + assertThat(elasticAgentPod.getMetadata().getName()).isEqualTo(instance.getPodName()); verify(mockedPodResource).create(); } @@ -438,7 +447,7 @@ public void usingRemoteFile_shouldCreateKubernetesPodWithTimestamp() { createAgentRequest = CreateAgentRequestMother.createAgentRequestUsingRemoteFile(); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Pod.class); - kubernetesAgentInstances.create(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender); + kubernetesAgentInstances.requestCreateAgent(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender).get(); verify(pods).resource(argumentCaptor.capture()); Pod elasticAgentPod = argumentCaptor.getValue(); @@ -454,7 +463,7 @@ public void usingRemoteFile_shouldCreateKubernetesPodWithGoCDElasticAgentContain createAgentRequest = CreateAgentRequestMother.createAgentRequestUsingRemoteFile(); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Pod.class); - KubernetesInstance instance = kubernetesAgentInstances.create(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender); + KubernetesInstance instance = kubernetesAgentInstances.requestCreateAgent(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender).get(); verify(pods).resource(argumentCaptor.capture()); Pod elasticAgentPod = argumentCaptor.getValue(); @@ -464,7 +473,7 @@ public void usingRemoteFile_shouldCreateKubernetesPodWithGoCDElasticAgentContain expectedEnvVars.add(new EnvVar("GO_EA_SERVER_URL", settings.getGoServerUrl(), null)); expectedEnvVars.add(new EnvVar("GO_EA_AUTO_REGISTER_KEY", createAgentRequest.autoRegisterKey(), null)); expectedEnvVars.add(new EnvVar("GO_EA_AUTO_REGISTER_ENVIRONMENT", createAgentRequest.environment(), null)); - expectedEnvVars.add(new EnvVar("GO_EA_AUTO_REGISTER_ELASTIC_AGENT_ID", instance.name(), null)); + expectedEnvVars.add(new EnvVar("GO_EA_AUTO_REGISTER_ELASTIC_AGENT_ID", instance.getPodName(), null)); expectedEnvVars.add(new EnvVar("GO_EA_AUTO_REGISTER_ELASTIC_PLUGIN_ID", Constants.PLUGIN_ID, null)); List containers = elasticAgentPod.getSpec().getContainers(); @@ -480,16 +489,20 @@ public void usingRemoteFile_shouldCreateKubernetesPodWithPodAnnotations() { createAgentRequest = CreateAgentRequestMother.createAgentRequestUsingRemoteFile(); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Pod.class); - kubernetesAgentInstances.create(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender); + kubernetesAgentInstances.requestCreateAgent(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender).get(); verify(pods).resource(argumentCaptor.capture()); Pod elasticAgentPod = argumentCaptor.getValue(); assertNotNull(elasticAgentPod.getMetadata()); - HashMap expectedAnnotations = new HashMap<>(); - expectedAnnotations.putAll(createAgentRequest.properties()); + HashMap expectedAnnotations = new HashMap<>(createAgentRequest.elasticProfileProperties()); expectedAnnotations.put("annotation-key", "my-fancy-annotation-value"); expectedAnnotations.put(Constants.JOB_IDENTIFIER_LABEL_KEY, new Gson().toJson(createAgentRequest.jobIdentifier())); + expectedAnnotations.put( + KubernetesInstance.ELASTIC_CONFIG_HASH, + KubernetesInstanceFactory.agentConfigHash( + createAgentRequest.clusterProfileProperties(), + createAgentRequest.elasticProfileProperties())); assertThat(elasticAgentPod.getMetadata().getAnnotations()).isEqualTo(expectedAnnotations); @@ -501,7 +514,7 @@ public void usingRemoteFile_shouldCreateKubernetesPodWithPodLabels() { createAgentRequest = CreateAgentRequestMother.createAgentRequestUsingRemoteFile(); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Pod.class); - kubernetesAgentInstances.create(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender); + kubernetesAgentInstances.requestCreateAgent(createAgentRequest, settings, mockedPluginRequest, consoleLogAppender).get(); verify(pods).resource(argumentCaptor.capture()); Pod elasticAgentPod = argumentCaptor.getValue(); diff --git a/src/test/java/cd/go/contrib/elasticagent/KubernetesAgentInstancesTest.java b/src/test/java/cd/go/contrib/elasticagent/KubernetesAgentInstancesTest.java index eea2cee0..0b961655 100644 --- a/src/test/java/cd/go/contrib/elasticagent/KubernetesAgentInstancesTest.java +++ b/src/test/java/cd/go/contrib/elasticagent/KubernetesAgentInstancesTest.java @@ -26,11 +26,9 @@ import io.fabric8.kubernetes.client.dsl.PodResource; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.InOrder; import org.mockito.Mock; import java.io.IOException; -import java.time.Instant; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -75,7 +73,7 @@ public class KubernetesAgentInstancesTest { public void setUp() { openMocks(this); testProperties = new HashMap<>(); - when(mockCreateAgentRequest.properties()).thenReturn(testProperties); + when(mockCreateAgentRequest.elasticProfileProperties()).thenReturn(testProperties); when(mockPluginSettings.getMaxPendingPods()).thenReturn(10); when(factory.client(mockPluginSettings)).thenReturn(mockKubernetesClient); JobIdentifier jobId = new JobIdentifier("test", 1L, "Test pipeline", "test name", "1", "test job", 100L); @@ -88,42 +86,42 @@ public void setUp() { @Test public void shouldCreateKubernetesPodUsingPodYamlAndCacheCreatedInstance() throws IOException { - KubernetesInstance kubernetesInstance = new KubernetesInstance(Instant.now(), "test", "test-agent", new HashMap<>(), 100L, PodState.Running); + KubernetesInstance kubernetesInstance = KubernetesInstance.builder().environment("test").podName("test-agent").jobId(100L).podState(PodState.Running).build(); when(mockKubernetesInstanceFactory.create(mockCreateAgentRequest, mockPluginSettings, mockKubernetesClient, mockPluginRequest)). thenReturn(kubernetesInstance); testProperties.put("PodSpecType", "yaml"); KubernetesAgentInstances agentInstances = new KubernetesAgentInstances(factory, mockKubernetesInstanceFactory); - KubernetesInstance instance = agentInstances.create(mockCreateAgentRequest, mockPluginSettings, mockPluginRequest, consoleLogAppender); + KubernetesInstance instance = agentInstances.requestCreateAgent(mockCreateAgentRequest, mockPluginSettings, mockPluginRequest, consoleLogAppender).get(); assertTrue(agentInstances.instanceExists(instance)); } @Test public void shouldCreateKubernetesPodAndCacheCreatedInstance() throws IOException { - KubernetesInstance kubernetesInstance = new KubernetesInstance(Instant.now(), "test", "test-agent", new HashMap<>(), 100L, PodState.Running); + KubernetesInstance kubernetesInstance = KubernetesInstance.builder().environment("test").podName("test-agent").jobId(100L).podState(PodState.Running).build(); when(mockKubernetesInstanceFactory.create(mockCreateAgentRequest, mockPluginSettings, mockKubernetesClient, mockPluginRequest)). thenReturn(kubernetesInstance); testProperties.put("PodSpecType", "properties"); KubernetesAgentInstances agentInstances = new KubernetesAgentInstances(factory, mockKubernetesInstanceFactory); - KubernetesInstance instance = agentInstances.create(mockCreateAgentRequest, mockPluginSettings, mockPluginRequest, consoleLogAppender); + KubernetesInstance instance = agentInstances.requestCreateAgent(mockCreateAgentRequest, mockPluginSettings, mockPluginRequest, consoleLogAppender).get(); assertTrue(agentInstances.instanceExists(instance)); } @Test public void shouldCreateKubernetesPodFromFileAndCacheCreatedInstance() throws IOException { - KubernetesInstance kubernetesInstance = new KubernetesInstance(Instant.now(), "test", "test-agent", new HashMap<>(), 100L, PodState.Running); + KubernetesInstance kubernetesInstance = KubernetesInstance.builder().environment("test").podName("test-agent").jobId(100L).podState(PodState.Running).build(); when(mockKubernetesInstanceFactory.create(mockCreateAgentRequest, mockPluginSettings, mockKubernetesClient, mockPluginRequest)). thenReturn(kubernetesInstance); testProperties.put("PodSpecType", "remote"); KubernetesAgentInstances agentInstances = new KubernetesAgentInstances(factory, mockKubernetesInstanceFactory); - KubernetesInstance instance = agentInstances.create(mockCreateAgentRequest, mockPluginSettings, mockPluginRequest, consoleLogAppender); + KubernetesInstance instance = agentInstances.requestCreateAgent(mockCreateAgentRequest, mockPluginSettings, mockPluginRequest, consoleLogAppender).get(); assertTrue(agentInstances.instanceExists(instance)); } @Test public void shouldNotCreatePodWhenOutstandingRequestsExistForJobs() throws IOException { - KubernetesInstance kubernetesInstance = new KubernetesInstance(Instant.now(), "test", "test-agent", new HashMap<>(), 100L, PodState.Running); + KubernetesInstance kubernetesInstance = KubernetesInstance.builder().environment("test").podName("test-agent").jobId(100L).podState(PodState.Running).build(); when(mockKubernetesInstanceFactory.create(mockCreateAgentRequest, mockPluginSettings, mockKubernetesClient, mockPluginRequest)). thenReturn(kubernetesInstance); testProperties.put("PodSpecType", "properties"); @@ -131,7 +129,7 @@ public void shouldNotCreatePodWhenOutstandingRequestsExistForJobs() throws IOExc KubernetesAgentInstances agentInstances = new KubernetesAgentInstances(factory, mockKubernetesInstanceFactory); JobIdentifier jobId = new JobIdentifier("test", 1L, "Test pipeline", "test name", "1", "test job", 100L); when(mockCreateAgentRequest.jobIdentifier()).thenReturn(jobId); - agentInstances.create(mockCreateAgentRequest, mockPluginSettings, mockPluginRequest, consoleLogAppender); + agentInstances.requestCreateAgent(mockCreateAgentRequest, mockPluginSettings, mockPluginRequest, consoleLogAppender).get(); verify(mockKubernetesInstanceFactory, times(1)).create(any(), any(), any(), any()); reset(mockKubernetesInstanceFactory); @@ -147,7 +145,7 @@ public void shouldNotCreatePodWhenOutstandingRequestsExistForJobs() throws IOExc when(podList.getItems()).thenReturn(Arrays.asList(pod)); when(mockKubernetesInstanceFactory.fromKubernetesPod(pod)).thenReturn(kubernetesInstance); - agentInstances.create(mockCreateAgentRequest, mockPluginSettings, mockPluginRequest, consoleLogAppender); + agentInstances.requestCreateAgent(mockCreateAgentRequest, mockPluginSettings, mockPluginRequest, consoleLogAppender); verify(mockKubernetesInstanceFactory, times(0)).create(any(), any(), any(), any()); } @@ -157,7 +155,7 @@ public void shouldNotCreatePodsWhenOutstandingLimitOfPendingKubernetesPodsHasRea when(mockPluginSettings.getMaxPendingPods()).thenReturn(1); //pending kubernetes pod - KubernetesInstance kubernetesInstance = new KubernetesInstance(Instant.now(), "test", "test-agent", new HashMap<>(), 100L, PodState.Pending); + KubernetesInstance kubernetesInstance = KubernetesInstance.builder().environment("test").podName("test-agent").jobId(100L).podState(PodState.Running).build(); when(mockKubernetesInstanceFactory.create(mockCreateAgentRequest, mockPluginSettings, mockKubernetesClient, mockPluginRequest)). thenReturn(kubernetesInstance); testProperties.put("PodSpecType", "properties"); @@ -166,7 +164,7 @@ public void shouldNotCreatePodsWhenOutstandingLimitOfPendingKubernetesPodsHasRea KubernetesAgentInstances agentInstances = new KubernetesAgentInstances(factory, mockKubernetesInstanceFactory); JobIdentifier jobId = new JobIdentifier("test", 1L, "Test pipeline", "test name", "1", "test job", 100L); when(mockCreateAgentRequest.jobIdentifier()).thenReturn(jobId); - agentInstances.create(mockCreateAgentRequest, mockPluginSettings, mockPluginRequest, consoleLogAppender); + agentInstances.requestCreateAgent(mockCreateAgentRequest, mockPluginSettings, mockPluginRequest, consoleLogAppender).get(); verify(mockKubernetesInstanceFactory, times(1)).create(any(), any(), any(), any()); reset(mockKubernetesInstanceFactory); @@ -183,21 +181,8 @@ public void shouldNotCreatePodsWhenOutstandingLimitOfPendingKubernetesPodsHasRea when(mockKubernetesInstanceFactory.fromKubernetesPod(pod)).thenReturn(kubernetesInstance); //second create agent request - agentInstances.create(mockCreateAgentRequest, mockPluginSettings, mockPluginRequest, consoleLogAppender); + agentInstances.requestCreateAgent(mockCreateAgentRequest, mockPluginSettings, mockPluginRequest, consoleLogAppender); verify(mockKubernetesInstanceFactory, times(0)).create(any(), any(), any(), any()); } - @Test - public void shouldSyncPodsStateFromClusterBeforeCreatingPod() throws IOException { - when(mockKubernetesInstanceFactory.create(mockCreateAgentRequest, mockPluginSettings, mockKubernetesClient, mockPluginRequest)). - thenReturn(new KubernetesInstance(Instant.now(), "test", "test-agent", new HashMap<>(), 100L, PodState.Running)); - - final KubernetesAgentInstances agentInstances = new KubernetesAgentInstances(factory, mockKubernetesInstanceFactory); - - agentInstances.create(mockCreateAgentRequest, mockPluginSettings, mockPluginRequest, consoleLogAppender); - - InOrder inOrder = inOrder(mockKubernetesInstanceFactory, mockedOperation); - inOrder.verify(mockedOperation).list(); - inOrder.verify(mockKubernetesInstanceFactory).create(mockCreateAgentRequest, mockPluginSettings, mockKubernetesClient, mockPluginRequest); - } } diff --git a/src/test/java/cd/go/contrib/elasticagent/KubernetesInstanceTest.java b/src/test/java/cd/go/contrib/elasticagent/KubernetesInstanceTest.java new file mode 100644 index 00000000..b276fdc6 --- /dev/null +++ b/src/test/java/cd/go/contrib/elasticagent/KubernetesInstanceTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2023 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 cd.go.contrib.elasticagent; + +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class KubernetesInstanceTest { + @Test + void podAnnotationsDefaultToEmptyMap() { + KubernetesInstance instance1 = KubernetesInstance.builder() + .podName("test") + .environment("test") + .jobId(1L) + .build(); + assertEquals(Collections.emptyMap(), instance1.getPodAnnotations()); + } + + @Test + void podAnnotationsSafelyHandleNull() { + KubernetesInstance instance1 = KubernetesInstance.builder() + .podName("test") + .environment("test") + .podAnnotations(null) + .jobId(1L) + .build(); + assertEquals(Collections.emptyMap(), instance1.getPodAnnotations()); + } + + @Test + void podAnnotationsAreCopied() { + Map annotations = new HashMap<>(); + annotations.put("key1", "value1"); + KubernetesInstance instance1 = KubernetesInstance.builder() + .podName("test") + .environment("test") + .jobId(1L) + .podAnnotations(annotations) + .build(); + annotations.put("key2", "value2"); + assertEquals(Map.of("key1", "value1"), instance1.getPodAnnotations()); + } +} diff --git a/src/test/java/cd/go/contrib/elasticagent/PluginRequestTest.java b/src/test/java/cd/go/contrib/elasticagent/PluginRequestTest.java index 1f06317f..cd4776de 100644 --- a/src/test/java/cd/go/contrib/elasticagent/PluginRequestTest.java +++ b/src/test/java/cd/go/contrib/elasticagent/PluginRequestTest.java @@ -36,4 +36,4 @@ public void shouldNotThrowAnExceptionIfConsoleLogAppenderCallFails() { final PluginRequest pluginRequest = new PluginRequest(accessor); pluginRequest.appendToConsoleLog(jobIdentifier, "text1"); } -} \ No newline at end of file +} diff --git a/src/test/java/cd/go/contrib/elasticagent/executors/CreateAgentRequestExecutorTest.java b/src/test/java/cd/go/contrib/elasticagent/executors/CreateAgentRequestExecutorTest.java index 2045ff7f..1b2f106b 100644 --- a/src/test/java/cd/go/contrib/elasticagent/executors/CreateAgentRequestExecutorTest.java +++ b/src/test/java/cd/go/contrib/elasticagent/executors/CreateAgentRequestExecutorTest.java @@ -39,7 +39,7 @@ public void shouldAskDockerContainersToCreateAnAgent() throws Exception { new CreateAgentRequestExecutor(request, agentInstances, pluginRequest).execute(); verify(pluginRequest).appendToConsoleLog(eq(jobIdentifier), contains("Received request to create a pod for job")); - verify(agentInstances).create(eq(request), eq(request.clusterProfileProperties()), eq(pluginRequest), any(ConsoleLogAppender.class)); + verify(agentInstances).requestCreateAgent(eq(request), eq(request.clusterProfileProperties()), eq(pluginRequest), any(ConsoleLogAppender.class)); } @Test @@ -52,7 +52,7 @@ public void shouldLogErrorMessageToConsoleIfAgentCreateFails() throws Exception AgentInstances agentInstances = mock(KubernetesAgentInstances.class); PluginRequest pluginRequest = mock(PluginRequest.class); - when(agentInstances.create(any(), any(), any(), any())).thenThrow(new RuntimeException("Ouch!")); + when(agentInstances.requestCreateAgent(any(), any(), any(), any())).thenThrow(new RuntimeException("Ouch!")); assertThrows(Exception.class, () -> new CreateAgentRequestExecutor(request, agentInstances, pluginRequest).execute()); diff --git a/src/test/java/cd/go/contrib/elasticagent/executors/GetClusterProfileMetadataExecutorTest.java b/src/test/java/cd/go/contrib/elasticagent/executors/GetClusterProfileMetadataExecutorTest.java index fc46f99f..149a9c3d 100644 --- a/src/test/java/cd/go/contrib/elasticagent/executors/GetClusterProfileMetadataExecutorTest.java +++ b/src/test/java/cd/go/contrib/elasticagent/executors/GetClusterProfileMetadataExecutorTest.java @@ -92,6 +92,13 @@ public void assertJsonStructure() throws Exception { " \"required\": false,\n" + " \"secure\": true\n" + " }\n" + + " },\n" + + " {\n" + + " \"key\": \"enable_agent_reuse\",\n" + + " \"metadata\": {\n" + + " \"required\": false,\n" + + " \"secure\": false\n" + + " }\n" + " }\n" + "]"; diff --git a/src/test/java/cd/go/contrib/elasticagent/executors/GetClusterProfileViewRequestExecutorTest.java b/src/test/java/cd/go/contrib/elasticagent/executors/GetClusterProfileViewRequestExecutorTest.java index 8736b5c8..9c4400ba 100644 --- a/src/test/java/cd/go/contrib/elasticagent/executors/GetClusterProfileViewRequestExecutorTest.java +++ b/src/test/java/cd/go/contrib/elasticagent/executors/GetClusterProfileViewRequestExecutorTest.java @@ -57,6 +57,6 @@ public void allFieldsShouldBePresentInView() { } final Elements inputs = document.select("textarea,input[type=text],select,input[type=checkbox]"); - assertThat(inputs).hasSize(GetProfileMetadataExecutor.FIELDS.size() - 3); // do not include SPECIFIED_USING_POD_CONFIGURATION, POD_SPEC_TYPE, REMOTE_FILE_TYPE key + assertThat(inputs).hasSize(GetClusterProfileMetadataExecutor.FIELDS.size()); } } diff --git a/src/test/java/cd/go/contrib/elasticagent/executors/GetPluginSettingsIconExecutorTest.java b/src/test/java/cd/go/contrib/elasticagent/executors/GetPluginSettingsIconExecutorTest.java index 1038750f..bbea65c4 100644 --- a/src/test/java/cd/go/contrib/elasticagent/executors/GetPluginSettingsIconExecutorTest.java +++ b/src/test/java/cd/go/contrib/elasticagent/executors/GetPluginSettingsIconExecutorTest.java @@ -38,4 +38,4 @@ public void rendersIconInBase64() throws Exception { assertThat(hashMap.get("content_type")).isEqualTo("image/svg+xml"); assertThat(Util.readResourceBytes("/kubernetes_logo.svg")).isEqualTo(Base64.getDecoder().decode(hashMap.get("data"))); } -} \ No newline at end of file +} diff --git a/src/test/java/cd/go/contrib/elasticagent/executors/GetProfileMetadataExecutorTest.java b/src/test/java/cd/go/contrib/elasticagent/executors/GetProfileMetadataExecutorTest.java index 210fb7b4..9759daeb 100644 --- a/src/test/java/cd/go/contrib/elasticagent/executors/GetProfileMetadataExecutorTest.java +++ b/src/test/java/cd/go/contrib/elasticagent/executors/GetProfileMetadataExecutorTest.java @@ -117,4 +117,4 @@ public void assertJsonStructure() throws Exception { JSONAssert.assertEquals(expectedJSON, response.responseBody(), true); } -} \ No newline at end of file +} diff --git a/src/test/java/cd/go/contrib/elasticagent/executors/GetProfileViewExecutorTest.java b/src/test/java/cd/go/contrib/elasticagent/executors/GetProfileViewExecutorTest.java index 365fba04..98b8d9da 100644 --- a/src/test/java/cd/go/contrib/elasticagent/executors/GetProfileViewExecutorTest.java +++ b/src/test/java/cd/go/contrib/elasticagent/executors/GetProfileViewExecutorTest.java @@ -62,4 +62,4 @@ public void allFieldsShouldBePresentInView() { final Elements inputs = document.select("textarea,input[type=text],select,input[type=checkbox]"); assertThat(inputs).hasSize(GetProfileMetadataExecutor.FIELDS.size() - 3); // do not include SPECIFIED_USING_POD_CONFIGURATION, POD_SPEC_TYPE, REMOTE_FILE_TYPE key } -} \ No newline at end of file +} diff --git a/src/test/java/cd/go/contrib/elasticagent/executors/JobCompletionRequestExecutorTest.java b/src/test/java/cd/go/contrib/elasticagent/executors/JobCompletionRequestExecutorTest.java index a69bf6d1..6e9cc4ea 100644 --- a/src/test/java/cd/go/contrib/elasticagent/executors/JobCompletionRequestExecutorTest.java +++ b/src/test/java/cd/go/contrib/elasticagent/executors/JobCompletionRequestExecutorTest.java @@ -20,62 +20,67 @@ import cd.go.contrib.elasticagent.model.JobIdentifier; import cd.go.contrib.elasticagent.requests.JobCompletionRequest; import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.InOrder; -import org.mockito.Mock; +import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.MockitoAnnotations.openMocks; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; public class JobCompletionRequestExecutorTest { - @Mock - private PluginRequest mockPluginRequest; - @Mock - private AgentInstances mockAgentInstances; + @Test + public void withAgentReuseDisabledShouldTerminateAgent() throws Exception { + String elasticAgentId = "agent-1"; + JobIdentifier jobIdentifier = new JobIdentifier(100L); + + ClusterProfileProperties clusterProfileProperties = new ClusterProfileProperties(); + clusterProfileProperties.setEnableAgentReuse(false); - @Captor - private ArgumentCaptor> agentsArgumentCaptor; + Agent agent = new Agent(); + agent.setElasticAgentId(elasticAgentId); + List agents = List.of(agent); - @BeforeEach - public void setUp() { - openMocks(this); + KubernetesAgentInstances agentInstances = mock(KubernetesAgentInstances.class); + PluginRequest pluginRequest = mock(PluginRequest.class); + JobCompletionRequest request = new JobCompletionRequest(elasticAgentId, jobIdentifier, Collections.emptyMap(), clusterProfileProperties); + JobCompletionRequestExecutor executor = new JobCompletionRequestExecutor(request, agentInstances, pluginRequest); + GoPluginApiResponse response = executor.execute(); + + verify(pluginRequest, times(1)).disableAgents(agents); + verify(pluginRequest, times(1)).deleteAgents(agents); + verify(agentInstances, times(1)).terminate(elasticAgentId, clusterProfileProperties); + assertEquals(200, response.responseCode()); + assertTrue(response.responseBody().isEmpty()); } @Test - public void shouldTerminateElasticAgentOnJobCompletion() throws Exception { - JobIdentifier jobIdentifier = new JobIdentifier(100L); - ClusterProfileProperties clusterProfileProperties = new ClusterProfileProperties(); + public void withAgentReuseEnabledShouldMarkInstanceIdle() throws Exception { String elasticAgentId = "agent-1"; - JobCompletionRequest request = new JobCompletionRequest(elasticAgentId, jobIdentifier, new HashMap<>(), clusterProfileProperties); - JobCompletionRequestExecutor executor = new JobCompletionRequestExecutor(request, mockAgentInstances, mockPluginRequest); - - GoPluginApiResponse response = executor.execute(); + JobIdentifier jobIdentifier = new JobIdentifier(100L); - InOrder inOrder = inOrder(mockPluginRequest, mockAgentInstances); + ClusterProfileProperties clusterProfileProperties = new ClusterProfileProperties(); + clusterProfileProperties.setEnableAgentReuse(true); - inOrder.verify(mockPluginRequest).disableAgents(agentsArgumentCaptor.capture()); - List agentsToDisabled = agentsArgumentCaptor.getValue(); - assertEquals(1, agentsToDisabled.size()); - assertEquals(elasticAgentId, agentsToDisabled.get(0).elasticAgentId()); - inOrder.verify(mockAgentInstances).terminate(elasticAgentId, clusterProfileProperties); - inOrder.verify(mockPluginRequest).deleteAgents(agentsArgumentCaptor.capture()); - List agentsToDelete = agentsArgumentCaptor.getValue(); + JobCompletionRequest request = new JobCompletionRequest(elasticAgentId, jobIdentifier, Collections.emptyMap(), clusterProfileProperties); - assertEquals(agentsToDisabled, agentsToDelete); + KubernetesInstance instance = KubernetesInstance.builder().agentState(KubernetesInstance.AgentState.Building).build(); + KubernetesAgentInstances instances = new KubernetesAgentInstances( + mock(KubernetesClientFactory.class), + mock(KubernetesInstanceFactory.class), + Map.of(elasticAgentId, instance)); + assertEquals(instances.find(elasticAgentId).getAgentState(), KubernetesInstance.AgentState.Building); + PluginRequest pluginRequest = mock(PluginRequest.class); + JobCompletionRequestExecutor executor = new JobCompletionRequestExecutor(request, instances, pluginRequest); + GoPluginApiResponse response = executor.execute(); + assertEquals(instances.find(elasticAgentId).getAgentState(), KubernetesInstance.AgentState.Idle); assertEquals(200, response.responseCode()); assertTrue(response.responseBody().isEmpty()); - - verifyNoMoreInteractions(mockPluginRequest); } } diff --git a/src/test/java/cd/go/contrib/elasticagent/executors/MemoryMetadataTest.java b/src/test/java/cd/go/contrib/elasticagent/executors/MemoryMetadataTest.java index e4d332c3..48f776f8 100644 --- a/src/test/java/cd/go/contrib/elasticagent/executors/MemoryMetadataTest.java +++ b/src/test/java/cd/go/contrib/elasticagent/executors/MemoryMetadataTest.java @@ -44,4 +44,4 @@ public void shouldValidateMemoryBytesWhenRequireField() throws Exception { assertThat(validate).containsEntry("message", "Disk must not be blank."); assertThat(validate).containsEntry("key", "Disk"); } -} \ No newline at end of file +} diff --git a/src/test/java/cd/go/contrib/elasticagent/executors/ServerPingRequestExecutorTest.java b/src/test/java/cd/go/contrib/elasticagent/executors/ServerPingRequestExecutorTest.java index 4806751d..e246d265 100644 --- a/src/test/java/cd/go/contrib/elasticagent/executors/ServerPingRequestExecutorTest.java +++ b/src/test/java/cd/go/contrib/elasticagent/executors/ServerPingRequestExecutorTest.java @@ -32,10 +32,12 @@ import java.time.Instant; import java.util.*; +import static cd.go.contrib.elasticagent.Constants.ENVIRONMENT_LABEL_KEY; import static cd.go.contrib.elasticagent.Constants.JOB_ID_LABEL_KEY; import static java.time.temporal.ChronoUnit.MINUTES; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.openMocks; @@ -92,9 +94,9 @@ public void testShouldTerminateKubernetesPodsRunningAfterTimeout_forSingleCluste Agent agent2 = new Agent(agentId2, Agent.AgentState.Idle, Agent.BuildState.Idle, Agent.ConfigState.Enabled); //idle just created Agent agent3 = new Agent(agentId3, Agent.AgentState.Building, Agent.BuildState.Building, Agent.ConfigState.Enabled); //running time elapsed - KubernetesInstance k8sPodForAgent1 = new KubernetesInstance(Instant.now().minus(100, MINUTES), null, agentId1, Collections.emptyMap(), 1L, PodState.Running); - KubernetesInstance k8sPodForAgent2 = new KubernetesInstance(Instant.now(), null, agentId2, Collections.emptyMap(), 2L, PodState.Running); - KubernetesInstance k8sPodForAgent3 = new KubernetesInstance(Instant.now().minus(100, MINUTES), null, agentId3, Collections.emptyMap(), 3L, PodState.Running); + KubernetesInstance k8sPodForAgent1 = KubernetesInstance.builder().createdAt(Instant.now().minus(100, MINUTES)).environment("test").podName(agentId1).jobId(1L).podState(PodState.Running).build(); + KubernetesInstance k8sPodForAgent2 = KubernetesInstance.builder().createdAt(Instant.now()).environment("test").podName(agentId2).jobId(2L).podState(PodState.Running).build(); + KubernetesInstance k8sPodForAgent3 = KubernetesInstance.builder().createdAt(Instant.now().minus(100, MINUTES)).environment("test").podName(agentId3).jobId(3L).podState(PodState.Running).build(); final Agents allAgentsInitially = new Agents(Arrays.asList(agent1, agent2, agent3)); final Agents allAgentsAfterDisablingIdleAgents = new Agents(Arrays.asList(agent1AfterDisabling, agent2, agent3)); @@ -114,13 +116,13 @@ public void testShouldTerminateKubernetesPodsRunningAfterTimeout_forSingleCluste when(pluginRequest.listAgents()).thenReturn(allAgentsInitially, allAgentsAfterDisablingIdleAgents, new Agents()); - assertTrue(clusterSpecificInstances.get(clusterProfileProperties.uuid()).hasInstance(k8sPodForAgent1.name())); + assertTrue(clusterSpecificInstances.get(clusterProfileProperties.uuid()).hasInstance(k8sPodForAgent1.getPodName())); - new ServerPingRequestExecutor(serverPingRequest, clusterSpecificInstances, pluginRequest).execute(); + new ServerPingRequestExecutor(serverPingRequest.allClusterProfileProperties(), clusterSpecificInstances, pluginRequest).execute(); verify(pluginRequest, atLeastOnce()).disableAgents(Collections.singletonList(agent1)); verify(pluginRequest, atLeastOnce()).deleteAgents(Collections.singletonList(agent1AfterDisabling)); - assertFalse(clusterSpecificInstances.get(clusterProfileProperties.uuid()).hasInstance(k8sPodForAgent1.name())); + assertFalse(clusterSpecificInstances.get(clusterProfileProperties.uuid()).hasInstance(k8sPodForAgent1.getPodName())); } @Test @@ -146,13 +148,19 @@ public void testShouldTerminateKubernetesPodsRunningAfterTimeout_forMultipleClus Agent agent5 = new Agent(agentId5, Agent.AgentState.Idle, Agent.BuildState.Idle, Agent.ConfigState.Enabled); //idle just created Agent agent6 = new Agent(agentId6, Agent.AgentState.Building, Agent.BuildState.Building, Agent.ConfigState.Enabled); //running time elapsed - KubernetesInstance k8sPodForAgent1 = new KubernetesInstance(Instant.now().minus(100, MINUTES), null, agentId1, Collections.emptyMap(), 1L, PodState.Running); - KubernetesInstance k8sPodForAgent2 = new KubernetesInstance(Instant.now(), null, agentId2, Collections.emptyMap(), 2L, PodState.Running); - KubernetesInstance k8sPodForAgent3 = new KubernetesInstance(Instant.now().minus(100, MINUTES), null, agentId3, Collections.emptyMap(), 3L, PodState.Running); + KubernetesInstance k8sPodForAgent1 = KubernetesInstance.builder() + .createdAt(Instant.now().minus(100, MINUTES)).environment("test").podName(agentId1).jobId(1L).podState(PodState.Running).build(); + KubernetesInstance k8sPodForAgent2 = KubernetesInstance.builder() + .createdAt(Instant.now()).environment("test").podName(agentId2).jobId(2L).podState(PodState.Running).build(); + KubernetesInstance k8sPodForAgent3 = KubernetesInstance.builder() + .createdAt(Instant.now().minus(100, MINUTES)).environment("test").podName(agentId3).jobId(3L).podState(PodState.Running).build(); - KubernetesInstance k8sPodForAgent4 = new KubernetesInstance(Instant.now().minus(100, MINUTES), null, agentId4, Collections.emptyMap(), 1L, PodState.Running); - KubernetesInstance k8sPodForAgent5 = new KubernetesInstance(Instant.now(), null, agentId5, Collections.emptyMap(), 2L, PodState.Running); - KubernetesInstance k8sPodForAgent6 = new KubernetesInstance(Instant.now().minus(100, MINUTES), null, agentId6, Collections.emptyMap(), 3L, PodState.Running); + KubernetesInstance k8sPodForAgent4 = KubernetesInstance.builder() + .createdAt(Instant.now().minus(100, MINUTES)).environment("test").podName(agentId4).jobId(1L).podState(PodState.Running).build(); + KubernetesInstance k8sPodForAgent5 = KubernetesInstance.builder() + .createdAt(Instant.now()).environment("test").podName(agentId5).jobId(2L).podState(PodState.Running).build(); + KubernetesInstance k8sPodForAgent6 = KubernetesInstance.builder() + .createdAt(Instant.now().minus(100, MINUTES)).environment("test").podName(agentId6).jobId(3L).podState(PodState.Running).build(); final Agents allAgentsInitially = new Agents(Arrays.asList(agent1, agent2, agent3, agent4, agent5, agent6)); final Agents allAgentsAfterDisablingIdleAgentsFromCluster1 = new Agents(Arrays.asList(agent1AfterDisabling, agent2, agent3, agent4, agent5, agent6)); @@ -181,10 +189,10 @@ public void testShouldTerminateKubernetesPodsRunningAfterTimeout_forMultipleClus when(pluginRequest.listAgents()).thenReturn(allAgentsInitially, allAgentsAfterDisablingIdleAgentsFromCluster1, allAgentsAfterTerminatingIdleAgentsFromCluster1, allAgentsAfterDisablingIdleAgentsFromCluster2, allAgentsAfterTerminatingIdleAgentsFromCluster2, new Agents()); - assertTrue(clusterSpecificInstances.get(clusterProfilePropertiesForCluster1.uuid()).hasInstance(k8sPodForAgent1.name())); - assertTrue(clusterSpecificInstances.get(clusterProfilePropertiesForCluster2.uuid()).hasInstance(k8sPodForAgent4.name())); + assertTrue(clusterSpecificInstances.get(clusterProfilePropertiesForCluster1.uuid()).hasInstance(k8sPodForAgent1.getPodName())); + assertTrue(clusterSpecificInstances.get(clusterProfilePropertiesForCluster2.uuid()).hasInstance(k8sPodForAgent4.getPodName())); - new ServerPingRequestExecutor(serverPingRequest, clusterSpecificInstances, pluginRequest).execute(); + new ServerPingRequestExecutor(serverPingRequest.allClusterProfileProperties(), clusterSpecificInstances, pluginRequest).execute(); verify(pluginRequest, atLeastOnce()).disableAgents(Collections.singletonList(agent1)); verify(pluginRequest, atLeastOnce()).deleteAgents(Collections.singletonList(agent1AfterDisabling)); @@ -192,8 +200,8 @@ public void testShouldTerminateKubernetesPodsRunningAfterTimeout_forMultipleClus verify(pluginRequest, atLeastOnce()).disableAgents(Collections.singletonList(agent4)); verify(pluginRequest, atLeastOnce()).deleteAgents(Collections.singletonList(agent4AfterDisabling)); - assertFalse(clusterSpecificInstances.get(clusterProfilePropertiesForCluster1.uuid()).hasInstance(k8sPodForAgent1.name())); - assertFalse(clusterSpecificInstances.get(clusterProfilePropertiesForCluster2.uuid()).hasInstance(k8sPodForAgent4.name())); + assertFalse(clusterSpecificInstances.get(clusterProfilePropertiesForCluster1.uuid()).hasInstance(k8sPodForAgent1.getPodName())); + assertFalse(clusterSpecificInstances.get(clusterProfilePropertiesForCluster2.uuid()).hasInstance(k8sPodForAgent4.getPodName())); } @Test @@ -206,7 +214,7 @@ public void testShouldTerminateUnregisteredInstances_forSingleCluster() throws E when(mockedOperation.withName(anyString())).thenReturn(podResource); when(podResource.get()).thenReturn(mockedPod); objectMetadata = new ObjectMeta(); - objectMetadata.setLabels(Collections.singletonMap(JOB_ID_LABEL_KEY, "20")); + objectMetadata.setLabels(Map.of(JOB_ID_LABEL_KEY, "20", ENVIRONMENT_LABEL_KEY, "test")); objectMetadata.setName(unregisteredAgentId1); objectMetadata.setCreationTimestamp(Constants.KUBERNETES_POD_CREATION_TIME_FORMAT.format(Instant.now().minus(20, MINUTES))); @@ -214,8 +222,10 @@ public void testShouldTerminateUnregisteredInstances_forSingleCluster() throws E ClusterProfileProperties clusterProfilePropertiesForCluster1 = new ClusterProfileProperties("https://localhost:8154/go", null, null); - KubernetesInstance k8sUnregisteredCluster1Pod1 = new KubernetesInstance(Instant.now().minus(100, MINUTES), null, unregisteredAgentId1, Collections.emptyMap(), 3L, PodState.Running); - KubernetesInstance k8sUnregisteredCluster1Pod2 = new KubernetesInstance(Instant.now(), null, unregisteredAgentId2, Collections.emptyMap(), 3L, PodState.Running); + KubernetesInstance k8sUnregisteredCluster1Pod1 = KubernetesInstance.builder() + .createdAt(Instant.now().minus(100, MINUTES)).environment("test").podName(unregisteredAgentId1).jobId(3L).podState(PodState.Running).build(); + KubernetesInstance k8sUnregisteredCluster1Pod2 = KubernetesInstance.builder() + .createdAt(Instant.now()).environment("test").podName(unregisteredAgentId2).jobId(3L).podState(PodState.Running).build(); final Agents allAgentsInitially = new Agents(); @@ -233,13 +243,13 @@ public void testShouldTerminateUnregisteredInstances_forSingleCluster() throws E when(pluginRequest.listAgents()).thenReturn(allAgentsInitially); - assertTrue(clusterSpecificInstances.get(clusterProfilePropertiesForCluster1.uuid()).hasInstance(k8sUnregisteredCluster1Pod1.name())); - assertTrue(clusterSpecificInstances.get(clusterProfilePropertiesForCluster1.uuid()).hasInstance(k8sUnregisteredCluster1Pod2.name())); + assertTrue(clusterSpecificInstances.get(clusterProfilePropertiesForCluster1.uuid()).hasInstance(k8sUnregisteredCluster1Pod1.getPodName())); + assertTrue(clusterSpecificInstances.get(clusterProfilePropertiesForCluster1.uuid()).hasInstance(k8sUnregisteredCluster1Pod2.getPodName())); - new ServerPingRequestExecutor(serverPingRequest, clusterSpecificInstances, pluginRequest).execute(); + new ServerPingRequestExecutor(serverPingRequest.allClusterProfileProperties(), clusterSpecificInstances, pluginRequest).execute(); - assertFalse(clusterSpecificInstances.get(clusterProfilePropertiesForCluster1.uuid()).hasInstance(k8sUnregisteredCluster1Pod1.name())); - assertTrue(clusterSpecificInstances.get(clusterProfilePropertiesForCluster1.uuid()).hasInstance(k8sUnregisteredCluster1Pod2.name())); + assertFalse(clusterSpecificInstances.get(clusterProfilePropertiesForCluster1.uuid()).hasInstance(k8sUnregisteredCluster1Pod1.getPodName())); + assertTrue(clusterSpecificInstances.get(clusterProfilePropertiesForCluster1.uuid()).hasInstance(k8sUnregisteredCluster1Pod2.getPodName())); } @Test @@ -263,10 +273,58 @@ public void testShouldDisableAndDeleteMissingAgents() throws Exception { when(pluginRequest.listAgents()).thenReturn(allAgents); - new ServerPingRequestExecutor(serverPingRequest, clusterSpecificInstances, pluginRequest).execute(); + new ServerPingRequestExecutor(serverPingRequest.allClusterProfileProperties(), clusterSpecificInstances, pluginRequest).execute(); verify(pluginRequest, atLeastOnce()).disableAgents(Arrays.asList(agent2, agent1)); verify(pluginRequest, atLeastOnce()).deleteAgents(Arrays.asList(agent2, agent1)); } -} + @Test + public void shouldRefreshPodsForAllClusters() throws Exception { + KubernetesAgentInstances agentInstancesForCluster1 = mock(KubernetesAgentInstances.class); + when(agentInstancesForCluster1.listAgentPods(any())).thenReturn(Collections.emptyList()); + + KubernetesAgentInstances agentInstancesForCluster2 = mock(KubernetesAgentInstances.class); + when(agentInstancesForCluster2.listAgentPods(any())).thenReturn(Collections.emptyList()); + + ClusterProfileProperties clusterProfilePropertiesForCluster1 = new ClusterProfileProperties("https://localhost:8154/go", "https://cluster1", null); + ClusterProfileProperties clusterProfilePropertiesForCluster2 = new ClusterProfileProperties("https://localhost:8154/go", "https://cluster2", null); + + Map clusterSpecificInstances = Map.of( + clusterProfilePropertiesForCluster1.uuid(), agentInstancesForCluster1, + clusterProfilePropertiesForCluster2.uuid(), agentInstancesForCluster2 + ); + + List allClusterProps = List.of(clusterProfilePropertiesForCluster1, clusterProfilePropertiesForCluster2); + + PluginRequest pluginRequest = mock(PluginRequest.class); + + // Use spy to disable methods we're not testing + ServerPingRequestExecutor spy = spy(new ServerPingRequestExecutor(allClusterProps, clusterSpecificInstances, pluginRequest)); + doNothing().when(spy).performCleanupForACluster(any(), any()); + doNothing().when(spy).checkForPossiblyMissingAgents(); + spy.execute(); + verify(agentInstancesForCluster1, times(1)).refreshAll(clusterProfilePropertiesForCluster1); + verify(agentInstancesForCluster2, times(1)).refreshAll(clusterProfilePropertiesForCluster2); + } + + @Test + public void shouldInitializeInstancesAndRefreshPodsForNewClusters() throws Exception { + ClusterProfileProperties clusterProfilePropertiesForCluster1 = new ClusterProfileProperties("https://localhost:8154/go", "https://cluster1", null); + List allClusterProps = List.of(clusterProfilePropertiesForCluster1); + Map clusterSpecificInstances = new HashMap<>(); + KubernetesAgentInstances agentInstancesForCluster1 = mock(KubernetesAgentInstances.class); + PluginRequest pluginRequest = mock(PluginRequest.class); + + // Use spy to disable methods we're not testing + ServerPingRequestExecutor spy = spy(new ServerPingRequestExecutor(allClusterProps, clusterSpecificInstances, pluginRequest)); + doNothing().when(spy).performCleanupForACluster(any(), any()); + doNothing().when(spy).checkForPossiblyMissingAgents(); + when(spy.newKubernetesInstances()).thenReturn(agentInstancesForCluster1); + spy.execute(); + + verify(spy, times(1)).newKubernetesInstances(); + assertThat(clusterSpecificInstances).isEqualTo(Map.of(clusterProfilePropertiesForCluster1.uuid(), agentInstancesForCluster1)); + verify(agentInstancesForCluster1, times(1)).refreshAll(clusterProfilePropertiesForCluster1); + } +} diff --git a/src/test/java/cd/go/contrib/elasticagent/executors/ShouldAssignWorkRequestExecutorTest.java b/src/test/java/cd/go/contrib/elasticagent/executors/ShouldAssignWorkRequestExecutorTest.java index 57ad2ab5..5a1121c2 100644 --- a/src/test/java/cd/go/contrib/elasticagent/executors/ShouldAssignWorkRequestExecutorTest.java +++ b/src/test/java/cd/go/contrib/elasticagent/executors/ShouldAssignWorkRequestExecutorTest.java @@ -32,7 +32,6 @@ import org.mockito.stubbing.Answer; import java.util.Collections; -import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -49,7 +48,8 @@ public class ShouldAssignWorkRequestExecutorTest extends BaseTest { private AgentInstances agentInstances; private KubernetesInstance instance; - private Map properties = new HashMap<>(); + private Map instanceElasticProperties; + private ClusterProfileProperties instanceClusterProps; @Mock private KubernetesClient mockedClient; @@ -88,25 +88,93 @@ public void setUp() throws Exception { }); agentInstances = new KubernetesAgentInstances(factory); - properties.put("foo", "bar"); - properties.put("Image", "gocdcontrib/ubuntu-docker-elastic-agent"); - instance = agentInstances.create(new CreateAgentRequest(UUID.randomUUID().toString(), properties, environment, new JobIdentifier(100L)), createClusterProfileProperties(), pluginRequest, consoleLogAppender); + instanceElasticProperties = Map.of("foo", "bar", "Image", "gocdcontrib/ubuntu-docker-elastic-agent"); + instanceClusterProps = createClusterProfileProperties(); + instance = agentInstances.requestCreateAgent(new CreateAgentRequest(UUID.randomUUID().toString(), instanceElasticProperties, environment, new JobIdentifier(100L)), instanceClusterProps, pluginRequest, consoleLogAppender).get(); } @Test - public void shouldAssignWorkWhenJobIdMatchesPodId() throws Exception { + public void withAgentReuseDisabledShouldAssignWorkWhenJobIdMatchesPodId() throws Exception { + Long jobId = 100L; + assertThat(jobId).isEqualTo(instance.getJobId()); + ClusterProfileProperties clusterProfileProperties = new ClusterProfileProperties(); + clusterProfileProperties.setEnableAgentReuse(false); JobIdentifier jobIdentifier = new JobIdentifier("test-pipeline", 1L, "Test Pipeline", "test-stage", "1", "test-job", 100L); - ShouldAssignWorkRequest request = new ShouldAssignWorkRequest(new Agent(instance.name(), null, null, null), environment, properties, jobIdentifier); + ShouldAssignWorkRequest request = new ShouldAssignWorkRequest( + new Agent(instance.getPodName(), null, null, null), + environment, + instanceElasticProperties, + jobIdentifier, + clusterProfileProperties); GoPluginApiResponse response = new ShouldAssignWorkRequestExecutor(request, agentInstances).execute(); assertThat(response.responseCode()).isEqualTo(200); assertThat(response.responseBody()).isEqualTo("true"); } @Test - public void shouldNotAssignWorkWhenJobIdDiffersFromPodId() throws Exception { - long mismatchingJobId = 200L; - JobIdentifier jobIdentifier = new JobIdentifier("test-pipeline", 1L, "Test Pipeline", "test-stage", "1", "test-job", mismatchingJobId); - ShouldAssignWorkRequest request = new ShouldAssignWorkRequest(new Agent(instance.name(), null, null, null), "FooEnv", properties, jobIdentifier); + public void withAgentReuseDisabledShouldNotAssignWorkWhenJobIdDoesNotMatchPodId() throws Exception { + Long jobId = 333L; + assertThat(jobId).isNotEqualTo(instance.getJobId()); + ClusterProfileProperties clusterProfileProperties = new ClusterProfileProperties(); + clusterProfileProperties.setEnableAgentReuse(false); + JobIdentifier jobIdentifier = new JobIdentifier( + "test-pipeline", + 1L, + "Test Pipeline", + "test-stage", + "1", + "test-job", + jobId); + ShouldAssignWorkRequest request = new ShouldAssignWorkRequest( + new Agent(instance.getPodName(), null, null, null), + environment, + instanceElasticProperties, + jobIdentifier, + clusterProfileProperties); + GoPluginApiResponse response = new ShouldAssignWorkRequestExecutor(request, agentInstances).execute(); + assertThat(response.responseCode()).isEqualTo(200); + assertThat(response.responseBody()).isEqualTo("false"); + } + + @Test + public void withAgentReuseEnabledShouldAssignWorkWhenElasticInfoMatches() { + Long jobId = 333L; + assertThat(jobId).isNotEqualTo(instance.getJobId()); + ClusterProfileProperties clusterProfileProperties = new ClusterProfileProperties(); + clusterProfileProperties.setEnableAgentReuse(true); + JobIdentifier jobIdentifier = new JobIdentifier( + "test-pipeline", + 1L, + "Test Pipeline", + "test-stage", + "1", + "test-job", + jobId); + ShouldAssignWorkRequest request = new ShouldAssignWorkRequest( + new Agent(instance.getPodName(), null, null, null), + environment, + instanceElasticProperties, + jobIdentifier, + clusterProfileProperties); + GoPluginApiResponse response = new ShouldAssignWorkRequestExecutor(request, agentInstances).execute(); + assertThat(response.responseCode()).isEqualTo(200); + assertThat(response.responseBody()).isEqualTo("true"); + } + + @Test + public void withAgentReuseEnabledShouldNotAssignWorkWhenElasticInfoDoesNotMatch() { + Long jobId = 333L; + assertThat(jobId).isNotEqualTo(instance.getJobId()); + + ClusterProfileProperties requestClusterProps = new ClusterProfileProperties("http://foo:8154/go", null, null); + requestClusterProps.setEnableAgentReuse(true); + assertThat(requestClusterProps).isNotEqualTo(instanceClusterProps); + + Map requestElasticProperties = Map.of("something", "different"); + assertThat(requestElasticProperties).isNotEqualTo(instanceElasticProperties); + + JobIdentifier jobIdentifier = new JobIdentifier("test-pipeline", 1L, "Test Pipeline", "test-stage", "1", "test-job", jobId); + ShouldAssignWorkRequest request = new ShouldAssignWorkRequest(new Agent(instance.getPodName(), null, null, null), environment, requestElasticProperties, jobIdentifier, requestClusterProps); GoPluginApiResponse response = new ShouldAssignWorkRequestExecutor(request, agentInstances).execute(); assertThat(response.responseCode()).isEqualTo(200); assertThat(response.responseBody()).isEqualTo("false"); diff --git a/src/test/java/cd/go/contrib/elasticagent/model/JobIdentifierTest.java b/src/test/java/cd/go/contrib/elasticagent/model/JobIdentifierTest.java index e6a1888a..8bb641dd 100644 --- a/src/test/java/cd/go/contrib/elasticagent/model/JobIdentifierTest.java +++ b/src/test/java/cd/go/contrib/elasticagent/model/JobIdentifierTest.java @@ -51,4 +51,4 @@ public void shouldCreateJobDetailsPageLink() { assertThat(jobIdentifier.getJobDetailsPageLink()).isEqualTo("/go/tab/build/detail/up42/98765/stage_1/30000/job_1"); } -} \ No newline at end of file +} diff --git a/src/test/java/cd/go/contrib/elasticagent/model/reports/agent/StatusReportGenerationErrorTest.java b/src/test/java/cd/go/contrib/elasticagent/model/reports/agent/StatusReportGenerationErrorTest.java index 5c4145fc..ea8c5979 100644 --- a/src/test/java/cd/go/contrib/elasticagent/model/reports/agent/StatusReportGenerationErrorTest.java +++ b/src/test/java/cd/go/contrib/elasticagent/model/reports/agent/StatusReportGenerationErrorTest.java @@ -63,4 +63,4 @@ public void shouldReturnErrorResponseWhenItFailsToGenerateErrorView() throws IOE assertThat(response.responseCode()).isEqualTo(500); assertThat(response.responseBody()).isEqualTo("Failed to generate error report: cd.go.contrib.elasticagent.reports.StatusReportGenerationException: Pod is not running."); } -} \ No newline at end of file +} diff --git a/src/test/java/cd/go/contrib/elasticagent/requests/CreateAgentRequestTest.java b/src/test/java/cd/go/contrib/elasticagent/requests/CreateAgentRequestTest.java index 8ff2ba76..c2804f4d 100644 --- a/src/test/java/cd/go/contrib/elasticagent/requests/CreateAgentRequestTest.java +++ b/src/test/java/cd/go/contrib/elasticagent/requests/CreateAgentRequestTest.java @@ -55,7 +55,7 @@ public void shouldDeserializeFromJSON() throws Exception { HashMap expectedElasticAgentProperties = new HashMap<>(); expectedElasticAgentProperties.put("key1", "value1"); expectedElasticAgentProperties.put("key2", "value2"); - assertThat(request.properties()).isEqualTo(expectedElasticAgentProperties); + assertThat(request.elasticProfileProperties()).isEqualTo(expectedElasticAgentProperties); HashMap clusterProfileConfigurations = new HashMap<>(); clusterProfileConfigurations.put("go_server_url", "go-server-url"); diff --git a/src/test/java/cd/go/contrib/elasticagent/requests/ShouldAssignWorkRequestTest.java b/src/test/java/cd/go/contrib/elasticagent/requests/ShouldAssignWorkRequestTest.java index bf28ff2a..790bb868 100644 --- a/src/test/java/cd/go/contrib/elasticagent/requests/ShouldAssignWorkRequestTest.java +++ b/src/test/java/cd/go/contrib/elasticagent/requests/ShouldAssignWorkRequestTest.java @@ -61,7 +61,7 @@ public void shouldDeserializeFromJSON() throws Exception { HashMap expectedElasticAgentProperties = new HashMap<>(); expectedElasticAgentProperties.put("key1", "value1"); expectedElasticAgentProperties.put("key2", "value2"); - assertThat(request.properties()).isEqualTo(expectedElasticAgentProperties); + assertThat(request.elasticProfileProperties()).isEqualTo(expectedElasticAgentProperties); HashMap clusterProfileConfigurations = new HashMap<>(); clusterProfileConfigurations.put("go_server_url", "go-server-url");