Skip to content

Commit 33472dd

Browse files
committed
Support docker compose ssh deployment
1 parent d78a030 commit 33472dd

File tree

6 files changed

+71
-3
lines changed

6 files changed

+71
-3
lines changed

ctfcli/core/challenge.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,10 @@ def _process_challenge_image(self, challenge_image: Optional[str]) -> Optional[I
193193
if not challenge_image:
194194
return None
195195

196+
# Check if challenge_image is explicitly marked as __compose__
197+
if challenge_image == "__compose__":
198+
return Image(challenge_image)
199+
196200
# Check if challenge_image is explicitly marked with registry:// prefix
197201
if challenge_image.startswith("registry://"):
198202
challenge_image = challenge_image.replace("registry://", "")
@@ -732,8 +736,8 @@ def lint(self, skip_hadolint=False, flag_format="flag{") -> bool:
732736
issues["fields"].append(f"challenge.yml is missing required field: {field}")
733737

734738
# Check that the image field and Dockerfile match
735-
if (self.challenge_directory / "Dockerfile").is_file() and challenge.get("image", "") != ".":
736-
issues["dockerfile"].append("Dockerfile exists but image field does not point to it")
739+
if (self.challenge_directory / "Dockerfile").is_file() and challenge.get("image", "") not in [".", "__compose__"]:
740+
issues["dockerfile"].append("Dockerfile exists but image field does not point to it or compose")
737741

738742
# Check that Dockerfile exists and is EXPOSE'ing a port
739743
if challenge.get("image") == ".":

ctfcli/core/deployment/registry.py

+6
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ def deploy(self, skip_login=False, *args, **kwargs) -> DeploymentResult:
2525
)
2626
return DeploymentResult(False)
2727

28+
if self.challenge.image.compose:
29+
click.secho(
30+
"Cannot use registry deployer with __compose__ stacks", fg="red"
31+
)
32+
return DeploymentResult(False)
33+
2834
# resolve a location for the image push
2935
# e.g. registry.example.com/test-project/challenge-image-name
3036
# challenge image name is appended to the host provided for the deployment

ctfcli/core/deployment/ssh.py

+32
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,38 @@ def deploy(self, *args, **kwargs) -> DeploymentResult:
1919
)
2020
return DeploymentResult(False)
2121

22+
if self.challenge.image.compose:
23+
return self._deploy_compose_stack(*args, **kwargs)
24+
25+
return self._deploy_single_image(*args, **kwargs)
26+
27+
def _deploy_compose_stack(self, *args, **kwargs) -> DeploymentResult:
28+
host_url = urlparse(self.host)
29+
target_path = host_url.path or "~/"
30+
try:
31+
subprocess.run(["ssh", host_url.netloc, f"mkdir -p {target_path}/"], check=True)
32+
subprocess.run(
33+
["rsync", "-a", "--delete", self.challenge.challenge_directory, f"{host_url.netloc}:{target_path}"],
34+
check=True,
35+
)
36+
subprocess.run(
37+
[
38+
"ssh",
39+
host_url.netloc,
40+
f"cd {target_path}/{self.challenge.challenge_directory.name} && "
41+
"docker compose up -d --build --remove-orphans -y",
42+
],
43+
check=True,
44+
)
45+
46+
except subprocess.CalledProcessError as e:
47+
click.secho("Failed to deploy compose stack!", fg="red")
48+
click.secho(str(e), fg="red")
49+
return DeploymentResult(False)
50+
51+
return DeploymentResult(True)
52+
53+
def _deploy_single_image(self, *args, **kwargs) -> DeploymentResult:
2254
if self.challenge.image.built:
2355
if not self.challenge.image.pull():
2456
click.secho("Could not pull the image. Please check docker output above.", fg="red")

ctfcli/core/exceptions.py

+6
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ class InvalidChallengeFile(ChallengeException):
2222
class RemoteChallengeNotFound(ChallengeException):
2323
pass
2424

25+
class ImageException(ChallengeException):
26+
pass
27+
28+
class InvalidComposeOperation(ImageException):
29+
pass
30+
2531

2632
class LintException(Exception):
2733
def __init__(self, *args, issues: Dict[str, List[str]] = None):

ctfcli/core/image.py

+18
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from os import PathLike
55
from pathlib import Path
66
from typing import Optional, Union
7+
from ctfcli.core.exceptions import InvalidComposeOperation
78

89

910
class Image:
@@ -16,6 +17,11 @@ def __init__(self, name: str, build_path: Optional[Union[str, PathLike]] = None)
1617
if "/" in self.name or ":" in self.name:
1718
self.basename = self.name.split(":")[0].split("/")[-1]
1819

20+
if self.name == "__compose__":
21+
self.compose = True
22+
else:
23+
self.compose = False
24+
1925
self.built = True
2026

2127
# if the image provides a build path, assume it is not built yet
@@ -24,6 +30,9 @@ def __init__(self, name: str, build_path: Optional[Union[str, PathLike]] = None)
2430
self.built = False
2531

2632
def build(self) -> Optional[str]:
33+
if self.compose:
34+
raise InvalidComposeOperation("Local build not supported for docker compose challenges")
35+
2736
docker_build = subprocess.call(
2837
["docker", "build", "--load", "-t", self.name, "."], cwd=self.build_path.absolute()
2938
)
@@ -34,13 +43,19 @@ def build(self) -> Optional[str]:
3443
return self.name
3544

3645
def pull(self) -> Optional[str]:
46+
if self.compose:
47+
raise InvalidComposeOperation("Local pull not supported for docker compose challenges")
48+
3749
docker_pull = subprocess.call(["docker", "pull", self.name])
3850
if docker_pull != 0:
3951
return
4052

4153
return self.name
4254

4355
def push(self, location: str) -> Optional[str]:
56+
if self.compose:
57+
raise InvalidComposeOperation("Local push not supported for docker compose challenges")
58+
4459
if not self.built:
4560
self.build()
4661

@@ -53,6 +68,9 @@ def push(self, location: str) -> Optional[str]:
5368
return location
5469

5570
def export(self) -> Optional[str]:
71+
if self.compose:
72+
raise InvalidComposeOperation("Local export not supported for docker compose challenges")
73+
5674
if not self.built:
5775
self.build()
5876

ctfcli/spec/challenge-example.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ type: standard
2222
# Settings used for Dockerfile deployment
2323
# If not used, remove or set to null
2424
# If you have a Dockerfile set to .
25+
# If you have a docker-compose.yaml file, set to __compose__. Note that this will send the entire challenge directory to the remote server and build it there.
26+
# Only compatible with ssh, not registry.
2527
# If you have an imaged hosted on Docker set to the image url (e.g. python/3.8:latest, registry.gitlab.com/python/3.8:latest)
2628
# Follow Docker best practices and assign a tag
2729
image: null
@@ -122,4 +124,4 @@ state: hidden
122124

123125
# Specifies what version of the challenge specification was used.
124126
# Subject to change until ctfcli v1.0.0
125-
version: "0.1"
127+
version: "0.1"

0 commit comments

Comments
 (0)