-
Notifications
You must be signed in to change notification settings - Fork 3
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
Changes from all commits
01a1364
26ccce3
1b8ef11
edcaee2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
} | ||
} | ||
} |
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"; | ||
} |
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 + | ||
'}'; | ||
} | ||
} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#47