Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add symlink support, exception handling, version bump, and httpd image update #46

Merged
merged 4 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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+"}";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enhancement: This is a very generic response, Specific Response to be created for the symlink operations

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, we should make this change in the next iteration

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#47

}
}
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linked issue is #38

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linked issue #38

// 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
Comment on lines +95 to +96
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be formatted #38

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enhancement: In case of exception a proper message should be communicated to the user (via API response/SSE events). To be discussed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#48

}
}
}
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