Skip to content

Commit 446b719

Browse files
authored
Merge pull request #275 from techulus/automation/release/v0.54.0
Release v0.54.0
2 parents 8c3ddf4 + 374ce70 commit 446b719

92 files changed

Lines changed: 5067 additions & 3804 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/web-build.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545
- name: Set up pnpm
4646
uses: pnpm/action-setup@v4
4747
with:
48-
version: 11
48+
version: 11.20.0
4949

5050
- name: Set up Node
5151
uses: actions/setup-node@v6

AGENT.md renamed to AGENTS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ An open container deployment platform. See README.md for architecture.
2323

2424
- Web tests: `cd web && pnpm test`
2525
- Web typecheck: `cd web && ./node_modules/.bin/tsc --noEmit`
26-
- Web lint/format: `cd web && npx biome check --write <files>`
26+
- Web lint: `cd web && pnpm lint`
27+
- Web format: `cd web && pnpm exec oxfmt --write <files>`
2728
- Go (agent/cli): `go build ./...`, `go test ./...`, `gofmt -l .`
2829
- After deleting or renaming a Next.js route, stale generated types in
2930
`web/.next/types` can fail the typecheck — delete them; they regenerate.

‎CLAUDE.md‎

Lines changed: 0 additions & 1 deletion
This file was deleted.

‎CLAUDE.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@AGENTS.md

‎agent/README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ The agent supports two modes:
1515

1616
### All Nodes
1717
- WireGuard (`wg` and `wg-quick` commands)
18-
- Podman
18+
- Podman 4.8 or newer (required for command execution cleanup)
1919
- BuildKit + buildctl
2020
- Railpack
2121

@@ -226,7 +226,7 @@ WantedBy=multi-user.target
226226
```
227227

228228
`KillMode=process` ensures only the agent process is killed on restart, not container processes.
229-
The rootful Podman API socket at `/run/podman/podman.sock` is required for container metrics collection.
229+
The rootful Podman API socket at `/run/podman/podman.sock` is required for container metrics collection and command execution. Command timeouts force-remove the foreground exec session; deliberately backgrounded descendants are not guaranteed to be removed.
230230

231231
```bash
232232
sudo systemctl daemon-reload

‎agent/internal/agent/handlers.go‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"os/exec"
1111
"path/filepath"
1212
"time"
13+
"unicode/utf8"
1314

1415
"techulus/cloud-agent/internal/build"
1516
"techulus/cloud-agent/internal/container"
@@ -19,6 +20,23 @@ import (
1920
"techulus/cloud-agent/internal/registryauth"
2021
)
2122

23+
func (a *Agent) ProcessCommand(item agenthttp.WorkQueueItem) (container.CommandResult, error) {
24+
var payload struct {
25+
CommandRunID string `json:"commandRunId"`
26+
ServiceID string `json:"serviceId"`
27+
DeploymentID string `json:"deploymentId"`
28+
ContainerID string `json:"containerId"`
29+
Command string `json:"command"`
30+
}
31+
if err := json.Unmarshal([]byte(item.Payload), &payload); err != nil {
32+
return container.CommandResult{}, fmt.Errorf("failed to parse command payload: %w", err)
33+
}
34+
if payload.CommandRunID != item.ID || payload.ServiceID == "" || payload.DeploymentID == "" || payload.ContainerID == "" || payload.Command == "" || utf8.RuneCountInString(payload.Command) > 4096 {
35+
return container.CommandResult{}, fmt.Errorf("invalid command payload")
36+
}
37+
return container.ExecCommand(payload.ContainerID, payload.ServiceID, payload.DeploymentID, payload.Command)
38+
}
39+
2240
func (a *Agent) ProcessRestart(item agenthttp.WorkQueueItem) error {
2341
var payload struct {
2442
DeploymentID string `json:"deploymentId"`
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package agent
2+
3+
import (
4+
"testing"
5+
6+
agenthttp "techulus/cloud-agent/internal/http"
7+
)
8+
9+
func TestProcessCommandRequiresOwnershipIdentifiers(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
payload string
13+
}{
14+
{"service ID", `{"commandRunId":"run","deploymentId":"deployment","containerId":"container","command":"true"}`},
15+
{"deployment ID", `{"commandRunId":"run","serviceId":"service","containerId":"container","command":"true"}`},
16+
{"container ID", `{"commandRunId":"run","serviceId":"service","deploymentId":"deployment","command":"true"}`},
17+
}
18+
for _, tt := range tests {
19+
t.Run(tt.name, func(t *testing.T) {
20+
_, err := (&Agent{}).ProcessCommand(agenthttp.WorkQueueItem{ID: "run", Payload: tt.payload})
21+
if err == nil || err.Error() != "invalid command payload" {
22+
t.Fatalf("expected invalid payload for missing %s, got %v", tt.name, err)
23+
}
24+
})
25+
}
26+
}

‎agent/internal/agent/workqueue.go‎

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os"
88
"time"
99

10+
"techulus/cloud-agent/internal/container"
1011
agenthttp "techulus/cloud-agent/internal/http"
1112
)
1213

@@ -93,7 +94,19 @@ func (a *Agent) processLeasedWorkItem(item agenthttp.WorkQueueItem) {
9394
status := "completed"
9495
errorMsg := ""
9596
restartAfterReport := false
96-
if err := a.ProcessWorkItem(item); err != nil {
97+
var commandResult *container.CommandResult
98+
var processErr error
99+
if item.Type == "command" {
100+
result, err := a.ProcessCommand(item)
101+
processErr = err
102+
if err == nil {
103+
commandResult = &result
104+
}
105+
} else {
106+
processErr = a.ProcessWorkItem(item)
107+
}
108+
if processErr != nil {
109+
err := processErr
97110
if errors.Is(err, errAgentUpgradeRestartNeeded) {
98111
restartAfterReport = true
99112
} else {
@@ -109,12 +122,28 @@ func (a *Agent) processLeasedWorkItem(item agenthttp.WorkQueueItem) {
109122
if !restartAfterReport && a.activeWorkItem != nil && a.activeWorkItem.ID == item.ID && a.activeWorkItem.Attempt == item.Attempt {
110123
a.activeWorkItem = nil
111124
}
112-
a.pendingWorkResults = append(a.pendingWorkResults, agenthttp.CompletedWorkItem{
125+
completed := agenthttp.CompletedWorkItem{
113126
ID: item.ID,
114127
Attempt: item.Attempt,
115128
Status: status,
116129
Error: errorMsg,
117-
})
130+
}
131+
if commandResult != nil {
132+
completed.Result = agenthttp.CommandWorkItemResult{
133+
Type: "command",
134+
Output: commandResult.Output,
135+
ExitCode: &commandResult.ExitCode,
136+
OutputTruncated: commandResult.Truncated,
137+
TimedOut: commandResult.TimedOut,
138+
}
139+
if commandResult.TimedOut {
140+
completed.Status = "failed"
141+
completed.Error = "command timed out after 60 seconds"
142+
} else if commandResult.ExitCode != 0 {
143+
completed.Status = "failed"
144+
}
145+
}
146+
a.pendingWorkResults = append(a.pendingWorkResults, completed)
118147
a.workMutex.Unlock()
119148

120149
a.RequestStatusReport("work item " + status)

0 commit comments

Comments
 (0)