Skip to content

Commit

Permalink
Merge pull request #46 from arkaprovob/main
Browse files Browse the repository at this point in the history
 feat: add symlink support, exception handling, version bump, and httpd image update #46
  • Loading branch information
arkaprovob authored Oct 17, 2023
2 parents 172cf9c + edcaee2 commit 4901a88
Show file tree
Hide file tree
Showing 10 changed files with 247 additions and 24 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>io.spaship</groupId>
<artifactId>spa-deployment-operator</artifactId>
<version>3.4.0</version>
<version>3.4.1</version>
<properties>
<compiler-plugin.version>3.10.1</compiler-plugin.version>
<maven.compiler.release>17</maven.compiler.release>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.spaship.operator.api;

import io.quarkus.security.Authenticated;
import io.smallrye.mutiny.tuples.Tuple2;
import io.spaship.operator.service.k8s.CommandExecutionService;
import io.spaship.operator.type.CommandExecForm;
import io.spaship.operator.type.Environment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("execute")
@Authenticated
public class CommandExecutionController {
private static final Logger LOG = LoggerFactory.getLogger(CommandExecutionController.class);

private final CommandExecutionService exec;
public CommandExecutionController(CommandExecutionService exec) {
this.exec = exec;
}

@POST
@Path("/symlink")
@Produces("text/json")
@Consumes(MediaType.APPLICATION_JSON)
public String createSymlink(CommandExecForm form) {
LOG.debug("form content is as follows {}", form);
Tuple2<String, String> sourceTargetTuple = Tuple2.of(form.metadata().get("source"),
form.metadata().get("target"));
boolean isCreated = exec.createSymlink(form.environment(), sourceTargetTuple);
return "{\"created\":"+isCreated+"}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.spaship.operator.exception;

public class CommandExecutionException extends Exception{
public CommandExecutionException(String message) {
super(message);
}

public CommandExecutionException() {
super();
}

public CommandExecutionException(Throwable cause) {
super(cause);
}

public CommandExecutionException(String s, Exception e) {
super(s, e);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package io.spaship.operator.service.k8s;

import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.Status;
import io.fabric8.kubernetes.client.dsl.ExecListener;
import io.fabric8.kubernetes.client.dsl.ExecWatch;
import io.fabric8.openshift.client.OpenShiftClient;
import io.smallrye.mutiny.tuples.Tuple2;
import io.spaship.operator.exception.CommandExecutionException;
import io.spaship.operator.type.ApplicationConstants;
import io.spaship.operator.type.Environment;
import io.spaship.operator.util.ReUsableItems;
import lombok.SneakyThrows;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;
import java.util.Map;
import java.util.concurrent.CountDownLatch;

@ApplicationScoped
public class CommandExecutionService {


// todo :scope of improvement: read these two vars from the config map
private static final String CONTAINER_NAME = "httpd-server";
private static final String BASE_HTTP_DIR = "/var/www/html";


private final OpenShiftClient ocClient;
private static final Logger LOG = LoggerFactory.getLogger(CommandExecutionService.class);

public CommandExecutionService(@Named("default") OpenShiftClient ocClient) {
this.ocClient = ocClient;
}


public boolean createSymlink(Environment environment,Tuple2<String, String> sourceTargetTuple) {
boolean success = false;
ReUsableItems.checkNull(environment,sourceTargetTuple);
try{
execute(podLabelFrom(environment), environment.getNameSpace(), sourceTargetTuple);
success = true;
}catch(Exception e){
LOG.error("failed to create symlink",e);
}
return success;
}

private Map<String,String> podLabelFrom(Environment environment) {
ReUsableItems.checkNull(environment);
return Map.of(ApplicationConstants.MANAGED_BY, ApplicationConstants.SPASHIP,
ApplicationConstants.WEBSITE, environment.getWebsiteName().toLowerCase(),
ApplicationConstants.ENVIRONMENT, environment.getName().toLowerCase());
}

private void execute(Map<String, String> podLabel, String namespace,
Tuple2<String, String> sourceTargetTuple) {
ReUsableItems.checkNull(podLabel,namespace, sourceTargetTuple);
// even changing in one of the pod will reflect in all the pods as long as the volume is shared
var pod = ocClient.pods().inNamespace(namespace).withLabels(podLabel).list().getItems();
if (pod.isEmpty()) {
throw new RuntimeException("No pod found for label " + podLabel);
}

// TODO :scope of improvement: add support for multiple commands
var command = symbolicLinkCommand(sourceTargetTuple.getItem1(), sourceTargetTuple.getItem2());

pod.forEach(p -> {
try {
executeCommandInContainer(p, command);
} catch (CommandExecutionException e) {
throw new RuntimeException(e);
}
});
}

// todo :must do: 1. check both source and destination exists 2. Validate and sanitize the
// command
private String[] symbolicLinkCommand(String source, String target) {
ReUsableItems.checkNull(source,target);
source = (BASE_HTTP_DIR.concat("/").concat(source)).toLowerCase();
target = (BASE_HTTP_DIR.concat("/").concat(target)).toLowerCase();
LOG.debug("creating a symlink of source {} to {}",source,target);
LOG.debug("command to be executed [ln] [-s] [{}] [{}]",source,target);
return new String[]{"ln", "-s", source, target};
}

private void executeCommandInContainer(Pod httpdPod, String[] command) throws CommandExecutionException {
ReUsableItems.checkNull(httpdPod,command);
CountDownLatch latch = new CountDownLatch(1);
try (ExecWatch ignored = ocClient.pods().inNamespace(httpdPod.getMetadata().getNamespace())
.withName(httpdPod.getMetadata().getName())
.inContainer(CONTAINER_NAME).readingInput(System.in). //TODO replace the deprecated method
// with the new method
writingOutput(System.out).writingError(System.err).withTTY()
.usingListener(new ExecListener() {
@Override
public void onOpen() {
LOG.debug("Executing command in container");
}

@Override
public void onClose(int code, String reason) {
LOG.debug("closing the listener");
latch.countDown();
}

@SneakyThrows
@Override
public void onFailure(Throwable t, Response failureResponse) {
LOG.error("Failed to execute command in container due to {}",failureResponse.body());
latch.countDown();
}

@Override
public void onExit(int code, Status status) {
LOG.error("Command executed in container code {} reason {}",code,status);
latch.countDown();
}
}).exec(command)) {
latch.await();
} catch (Exception e) {
throw new CommandExecutionException("Error while executing command in container", e);
}
}
}
41 changes: 19 additions & 22 deletions src/main/java/io/spaship/operator/service/k8s/Operator.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import io.spaship.operator.business.EventManager;
import io.spaship.operator.exception.ResourceNotFoundException;
import io.spaship.operator.service.Operations;
import io.spaship.operator.type.ApplicationConstants;
import io.spaship.operator.type.Environment;
import io.spaship.operator.type.EventStructure;
import io.spaship.operator.type.OperationResponse;
Expand All @@ -37,10 +38,6 @@
@ApplicationScoped
public class Operator implements Operations {
private static final Logger LOG = LoggerFactory.getLogger(Operator.class);
private static final String MANAGED_BY = "managedBy";
private static final String WEBSITE = "website";
private static final String ENVIRONMENT = "environment";
private static final String SPASHIP = "spaship";
private final OpenShiftClient ocClient;
private final EventManager eventManager;
private final String domain;
Expand Down Expand Up @@ -290,9 +287,9 @@ public String environmentSidecarUrl(Environment environment) {
}

Map<String, String> searchCriteriaLabel(Environment environment) {
return Map.of(MANAGED_BY, SPASHIP,
WEBSITE, environment.getWebsiteName().toLowerCase(),
ENVIRONMENT, environment.getName().toLowerCase());
return Map.of(ApplicationConstants.MANAGED_BY, ApplicationConstants.SPASHIP,
ApplicationConstants.WEBSITE, environment.getWebsiteName().toLowerCase(),
ApplicationConstants.ENVIRONMENT, environment.getName().toLowerCase());
}

private OperationResponse applyDeleteResourceList(Environment environment, KubernetesList resourceList) {
Expand Down Expand Up @@ -349,9 +346,9 @@ public boolean isEnvironmentAvailable(Environment environment) {
Objects.requireNonNull(environment.getWebsiteName(), "website name not found in env object");
Objects.requireNonNull(environment.getNameSpace(), "website namespace not found in env object");
Map<String, String> labels = Map.of(
MANAGED_BY, SPASHIP,
WEBSITE, environment.getWebsiteName(),
ENVIRONMENT, environment.getName());
ApplicationConstants.MANAGED_BY, ApplicationConstants.SPASHIP,
ApplicationConstants.WEBSITE, environment.getWebsiteName(),
ApplicationConstants.ENVIRONMENT, environment.getName());
var pods = ocClient.pods()
.inNamespace(environment.getNameSpace()).withLabels(labels).list().getItems();

Expand Down Expand Up @@ -383,44 +380,44 @@ private void processK8sList(KubernetesList result, UUID tracing, String nameSpac
if (item instanceof Service svc) {
LOG.debug("creating new Service in K8s, tracing = {}", tracing);
ocClient.services().inNamespace(nameSpace).createOrReplace(svc);
eb.websiteName(item.getMetadata().getLabels().get(WEBSITE))
.environmentName(item.getMetadata().getLabels().get(ENVIRONMENT))
eb.websiteName(item.getMetadata().getLabels().get(ApplicationConstants.WEBSITE))
.environmentName(item.getMetadata().getLabels().get(ApplicationConstants.ENVIRONMENT))
.state("service created");
}
if (item instanceof Deployment dep) {
LOG.debug("creating new Deployment in K8s, tracing = {}", tracing);
ocClient.apps().deployments().inNamespace(nameSpace).createOrReplace(dep);
eb.websiteName(item.getMetadata().getLabels().get(WEBSITE))
.environmentName(item.getMetadata().getLabels().get(ENVIRONMENT))
eb.websiteName(item.getMetadata().getLabels().get(ApplicationConstants.WEBSITE))
.environmentName(item.getMetadata().getLabels().get(ApplicationConstants.ENVIRONMENT))
.state("deployment created");
}
if (item instanceof StatefulSet sfs) {
LOG.debug("creating new Deployment in K8s, tracing = {}", tracing);
ocClient.apps().statefulSets().inNamespace(nameSpace).createOrReplace(sfs);
eb.websiteName(item.getMetadata().getLabels().get(WEBSITE))
.environmentName(item.getMetadata().getLabels().get(ENVIRONMENT))
eb.websiteName(item.getMetadata().getLabels().get(ApplicationConstants.WEBSITE))
.environmentName(item.getMetadata().getLabels().get(ApplicationConstants.ENVIRONMENT))
.state("StatefulSet created");

}
if (item instanceof PersistentVolumeClaim pvc) {
LOG.debug("creating new pvc in K8s, tracing = {}", tracing);
ocClient.persistentVolumeClaims().inNamespace(nameSpace).createOrReplace(pvc);
eb.websiteName(item.getMetadata().getLabels().get(WEBSITE))
.environmentName(item.getMetadata().getLabels().get(ENVIRONMENT))
eb.websiteName(item.getMetadata().getLabels().get(ApplicationConstants.WEBSITE))
.environmentName(item.getMetadata().getLabels().get(ApplicationConstants.ENVIRONMENT))
.state("pvc created");
}
if (item instanceof Route route) {
LOG.debug("creating new Route in K8s, tracing = {}", tracing);
ocClient.routes().inNamespace(nameSpace).createOrReplace(route);
eb.websiteName(item.getMetadata().getLabels().get(WEBSITE))
.environmentName(item.getMetadata().getLabels().get(ENVIRONMENT))
eb.websiteName(item.getMetadata().getLabels().get(ApplicationConstants.WEBSITE))
.environmentName(item.getMetadata().getLabels().get(ApplicationConstants.ENVIRONMENT))
.state("route created");
}
if (item instanceof ConfigMap cm) {
LOG.debug("creating new ConfigMap in K8s, tracing = {}", tracing);
ocClient.configMaps().inNamespace(nameSpace).createOrReplace(cm);
eb.websiteName(item.getMetadata().getLabels().get(WEBSITE))
.environmentName(item.getMetadata().getLabels().get(ENVIRONMENT))
eb.websiteName(item.getMetadata().getLabels().get(ApplicationConstants.WEBSITE))
.environmentName(item.getMetadata().getLabels().get(ApplicationConstants.ENVIRONMENT))
.state("configmap created");
}
if (item instanceof Ingress ing) {
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/io/spaship/operator/type/ApplicationConstants.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.spaship.operator.type;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ApplicationConstants {

public static final Logger LOG = LoggerFactory.getLogger(ApplicationConstants.class);
public static final String MANAGED_BY = "managedBy";
public static final String WEBSITE = "website";
public static final String ENVIRONMENT = "environment";
public static final String SPASHIP = "spaship";
}
13 changes: 13 additions & 0 deletions src/main/java/io/spaship/operator/type/CommandExecForm.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.spaship.operator.type;

import java.util.Map;

public record CommandExecForm(Environment environment, Map<String,String> metadata) {
@Override
public String toString() {
return "CommandExecForm{" +
"environment=" + environment +
", metadata=" + metadata +
'}';
}
}
14 changes: 14 additions & 0 deletions src/main/java/io/spaship/operator/util/ReUsableItems.java
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,18 @@ public static String selectContainerizedDeploymentOCTemplate() {
return "/openshift/ssr-deployment-template.yaml";
}


@SafeVarargs
public static <T> void checkNull(T... objects) {
if (Objects.isNull(objects)) {
throw new NullPointerException("The objects parameter itself is null");
}
for (int i = 0; i < objects.length; i++) {
T entry = objects[i];
if (Objects.isNull(entry)) {
throw new NullPointerException("Object at index " + i + " is null");
}
}
}

}
2 changes: 1 addition & 1 deletion src/main/resources/META-INF/resources/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ <h5>Application</h5>
<ul>
<li>GroupId: <code>io.spaship</code></li>
<li>ArtifactId: <code>spa-deployment-operator</code></li>
<li>Version: <code>3.4.0</code></li>
<li>Version: <code>3.4.1</code></li>
<li>Quarkus Version: <code>2.16.3.Final</code></li>
</ul>
</div>
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ mpp.remote.build.cluster.access.token=
mpp.remote.build.imagepull.secret=
mpp.remote.build.ns=

http.dir.path=/var/www/http



Expand Down

0 comments on commit 4901a88

Please sign in to comment.