Skip to content

feat(api): Add deploy endpoint, compose mount, and custom-image create#125

Merged
nfebe merged 1 commit into
mainfrom
feat/deployment-endpoints
May 24, 2026
Merged

feat(api): Add deploy endpoint, compose mount, and custom-image create#125
nfebe merged 1 commit into
mainfrom
feat/deployment-endpoints

Conversation

@nfebe
Copy link
Copy Markdown
Contributor

@nfebe nfebe commented May 24, 2026

Adds POST /deployments/:name/deploy that bundles image pull and the requested lifecycle action in one request, so scripted rollouts no longer need a separate pull-then-restart sequence.

Adds POST /deployments/:name/compose/mount that wires a deployment-directory path into one compose service as a bind mount, with traversal protection on the source and absolute-path enforcement on the target. The mount handler also honors an optional SELinux relabel flag for RHEL-family hosts.

Lets deployment creation request a specific container image and exposes basic validation on it, so the custom-compose path is no longer hardcoded to nginx:alpine.

Closes #21

@sourceant
Copy link
Copy Markdown

sourceant Bot commented May 24, 2026

Code Review Summary

The PR introduces a unified deploy endpoint, support for dynamic volume mounting into Compose files, and allows custom images for deployments. These changes significantly improve the flexibility of the agent's API.

🚀 Key Improvements

  • New /deploy endpoint reduces API round-trips by bundling image pull and service restart.
  • Added /compose/mount provides a programmatic way to manage persistent data via bind mounts.
  • Validation added for custom image names to prevent injection attacks.

💡 Minor Suggestions

  • Support multiple ports in generateDeploymentCompose instead of just the first element.
  • Simplify path normalization logic in internal/api/server.go.

Copy link
Copy Markdown

@sourceant sourceant Bot left a comment

Choose a reason for hiding this comment

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

Review complete. See the overview comment for a summary.

Comment thread internal/api/server.go
@@ -2615,7 +2834,9 @@ type ComposeGenerateRequest struct {

type PortConfig struct {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The PortConfig struct now has redundant fields (ContainerPort/Container and HostPort/Host). This likely stems from supporting different JSON naming conventions. It is cleaner to use standard tags and handle mapping in the logic or use a single naming convention.

Suggested change
type PortConfig struct {
type PortConfig struct {
ContainerPort int `json:"container_port"`
HostPort string `json:"host_port"`
}

Comment thread internal/api/server.go
if len(image) > 255 {
return fmt.Errorf("image name is too long")
}
validImage := regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:/@-]*$`)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The regexp is compiled inside the function. For better performance, move this to a global variable using regexp.MustCompile so it is only compiled once.

Suggested change
validImage := regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:/@-]*$`)
var validImageRegex = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:/@-]*$`)

Comment thread internal/api/server.go
}{
Action: "restart",
}
if c.Request.Body != nil && c.Request.ContentLength != 0 {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The check c.Request.Body != nil is generally true in Gin; checking c.Request.ContentLength == 0 is okay but ShouldBindJSON handles empty bodies depending on the binding. However, since a default restart action is desired, ensure that if a body is present but doesn't contain the field, the default is kept.

Suggested change
if c.Request.Body != nil && c.Request.ContentLength != 0 {
if c.Request.ContentLength > 0 {

Copy link
Copy Markdown

@sourceant sourceant Bot left a comment

Choose a reason for hiding this comment

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

Review complete. See the overview comment for a summary.

Comment thread internal/api/server.go
return "", fmt.Errorf("source_path is invalid")
}

sourcePath = strings.ReplaceAll(sourcePath, "\\", "/")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

When sourcePath starts with a /, it is converted to a relative path by prepending ./. However, the logic then duplicates the ./ check and trimming in a confusing way. Simplifying this to a single path.Join with a base or a more direct prefixing would be clearer.

Suggested change
sourcePath = strings.ReplaceAll(sourcePath, "\\", "/")
sourcePath = strings.ReplaceAll(sourcePath, "\\", "/")
if strings.HasPrefix(sourcePath, "/") {
sourcePath = "." + sourcePath
} else {
sourcePath = "./" + sourcePath
}

Comment thread internal/api/server.go
HostPort: hostPort,
}
if len(ports) > 0 {
composeReq.ContainerPort = ports[0].ContainerPort
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The logic to extract the first port from the ports slice is repetitive. Using the new PortConfig fields (Container and Host) alongside the existing ones without clear priority is messy. It is better to consolidate this into a helper or a more concise conditional block.

Suggested change
composeReq.ContainerPort = ports[0].ContainerPort
p := ports[0]
composeReq.ContainerPort = p.ContainerPort
if composeReq.ContainerPort == 0 {
composeReq.ContainerPort = p.Container
}
host := p.HostPort
if host == "" {
host = p.Host
}
if host != "" {
composeReq.MapPorts = true
composeReq.HostPort = host
}

Adds POST /deployments/:name/deploy that bundles image pull and the
requested lifecycle action in one request, so scripted rollouts no
longer need a separate pull-then-restart sequence.

Adds POST /deployments/:name/compose/mount that wires a deployment-
directory path into one compose service as a bind mount, with traversal
protection on the source and absolute-path enforcement on the target.
The mount handler also honors an optional SELinux relabel flag for
hosts where SELinux is enforcing, so the resulting bind mount stays
readable inside the container on RHEL-family distributions.

Lets deployment creation request a specific container image and exposes
basic validation on it, so the custom-compose path is no longer
hardcoded to nginx:alpine.
@nfebe nfebe force-pushed the feat/deployment-endpoints branch from 890498d to 981e9d7 Compare May 24, 2026 17:59
Copy link
Copy Markdown

@sourceant sourceant Bot left a comment

Choose a reason for hiding this comment

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

Review complete. See the overview comment for a summary.

Comment thread internal/api/server.go
return "", fmt.Errorf("source_path is invalid")
}

sourcePath = strings.ReplaceAll(sourcePath, "\\", "/")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The normalization logic for sourcePath is complex and slightly redundant with the subsequent path.Clean call. It can be simplified by relying on path.Join to ensure a consistent ./ prefix after cleaning.

Suggested change
sourcePath = strings.ReplaceAll(sourcePath, "\\", "/")
sourcePath = strings.ReplaceAll(sourcePath, "\\", "/")
if !strings.HasPrefix(sourcePath, "/") {
sourcePath = "/" + sourcePath
}
cleaned := path.Clean(sourcePath)
if cleaned == "/" {
return ".", nil
}
cleaned = "." + cleaned

@nfebe nfebe merged commit 1f40ef5 into main May 24, 2026
5 checks passed
@nfebe nfebe deleted the feat/deployment-endpoints branch May 24, 2026 18:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Accept just image names

1 participant