Skip to content

Commit 43df00d

Browse files
authored
add the 'arctl deploy' command (#338)
<!-- Thanks for opening a PR! Please delete any sections that don't apply. --> # Description adds the `arctl deploy` command -- the `arctl [mcp/agent] deploy` functionality is not in `arctl deploy create` command. This is the same functionality, just moved to another command. The additional commands that were added: - `arctl deploy list` -- lists all deployments (managed and discovered) - `arctl deploy delete` -- deletes a deployment by ID - `arctl deploy show` -- shows the details of a deployment (including the URL if it's a local deployment) # Change Type ``` /kind feature ``` # Changelog ```release-note adding the `arctl deployments` command ``` # Additional Notes The docs PR is [here](agentregistry-dev/website#16). --------- Signed-off-by: Peter Jausovec <peter.jausovec@solo.io>
1 parent 5afd4b9 commit 43df00d

File tree

13 files changed

+808
-306
lines changed

13 files changed

+808
-306
lines changed

e2e/deploy_test.go

Lines changed: 182 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ var mcpDeployTargets = []deployTarget{
9090
},
9191
}
9292

93-
func TestAgentDeploy(t *testing.T) {
93+
func TestAgentDeployCreate(t *testing.T) {
9494
for _, target := range agentDeployTargets {
9595
t.Run(target.name, func(t *testing.T) {
9696
if target.name == "kubernetes" && !IsK8sBackend() {
@@ -101,10 +101,6 @@ func TestAgentDeploy(t *testing.T) {
101101
agentName := UniqueAgentName("e2edpl" + target.name[:3])
102102
agentImage := fmt.Sprintf("localhost:5001/%s:e2e", agentName)
103103

104-
// Register cleanup at the parent level so it runs after all
105-
// subtests (including verify) complete, not after deploy alone.
106-
// Remove deployment record first (LIFO) so ReconcileAll in
107-
// subsequent tests doesn't try to reconcile stale deployments.
108104
t.Cleanup(func() { RemoveDeploymentsByServerName(t, regURL, agentName) })
109105
if target.cleanup != nil {
110106
t.Cleanup(func() { target.cleanup(t, agentName) })
@@ -140,14 +136,22 @@ func TestAgentDeploy(t *testing.T) {
140136
RequireSuccess(t, result)
141137
})
142138

143-
t.Run("deploy", func(t *testing.T) {
139+
t.Run("deploy_create", func(t *testing.T) {
144140
t.Logf("Deploying agent %q (target: %s)...", agentName, target.name)
145-
args := []string{"agent", "deploy", agentName, "--registry-url", regURL}
141+
args := []string{
142+
"deploy", "create", agentName,
143+
"--type", "agent",
144+
"--registry-url", regURL,
145+
}
146146
args = append(args, target.deplArgs...)
147147
result := RunArctl(t, tmpDir, args...)
148148
RequireSuccess(t, result)
149149
})
150150

151+
t.Run("deploy_list", func(t *testing.T) {
152+
verifyDeploymentInList(t, tmpDir, regURL, agentName, "agent")
153+
})
154+
151155
if target.verify != nil {
152156
t.Run("verify", func(t *testing.T) {
153157
t.Logf("Verifying deployment health (target: %s)...", target.name)
@@ -158,7 +162,7 @@ func TestAgentDeploy(t *testing.T) {
158162
}
159163
}
160164

161-
func TestMCPDeploy(t *testing.T) {
165+
func TestMCPDeployCreate(t *testing.T) {
162166
for _, target := range mcpDeployTargets {
163167
t.Run(target.name, func(t *testing.T) {
164168
if target.name == "kubernetes" && !IsK8sBackend() {
@@ -173,10 +177,6 @@ func TestMCPDeploy(t *testing.T) {
173177

174178
// Delete any stale server entry from a previous interrupted run.
175179
RunArctl(t, tmpDir, "mcp", "delete", serverName, "--version", version, "--registry-url", regURL)
176-
// Register cleanup at the parent level so it runs after all
177-
// subtests (including verify) complete, not after deploy alone.
178-
// Remove deployment record first (LIFO) so ReconcileAll in
179-
// subsequent tests doesn't try to reconcile stale deployments.
180180
t.Cleanup(func() { RemoveDeploymentsByServerName(t, regURL, serverName) })
181181
t.Cleanup(func() {
182182
RunArctl(t, tmpDir, "mcp", "delete", serverName, "--version", version, "--registry-url", regURL)
@@ -219,14 +219,23 @@ func TestMCPDeploy(t *testing.T) {
219219
RequireSuccess(t, result)
220220
})
221221

222-
t.Run("deploy", func(t *testing.T) {
222+
t.Run("deploy_create", func(t *testing.T) {
223223
t.Logf("Deploying MCP server %q (target: %s)...", serverName, target.name)
224-
args := []string{"mcp", "deploy", serverName, "--version", version, "--registry-url", regURL}
224+
args := []string{
225+
"deploy", "create", serverName,
226+
"--type", "mcp",
227+
"--version", version,
228+
"--registry-url", regURL,
229+
}
225230
args = append(args, target.deplArgs...)
226231
result := RunArctl(t, tmpDir, args...)
227232
RequireSuccess(t, result)
228233
})
229234

235+
t.Run("deploy_list", func(t *testing.T) {
236+
verifyDeploymentInList(t, tmpDir, regURL, serverName, "mcp")
237+
})
238+
230239
if target.verify != nil {
231240
t.Run("verify", func(t *testing.T) {
232241
t.Logf("Verifying deployment health (target: %s)...", target.name)
@@ -237,6 +246,124 @@ func TestMCPDeploy(t *testing.T) {
237246
}
238247
}
239248

249+
// TestDeployDeleteLifecycle exercises the full CLI lifecycle:
250+
// create a deployment, extract its ID via "deploy list", delete it via
251+
// "arctl deploy delete <prefix>" (testing ID-prefix resolution), and verify
252+
// it no longer appears in "deploy list".
253+
func TestDeployDeleteLifecycle(t *testing.T) {
254+
regURL := RegistryURL(t)
255+
tmpDir := t.TempDir()
256+
agentName := UniqueAgentName("e2edeldpl")
257+
agentImage := fmt.Sprintf("localhost:5001/%s:e2e", agentName)
258+
259+
t.Cleanup(func() { RemoveDeploymentsByServerName(t, regURL, agentName) })
260+
t.Cleanup(func() { removeLocalDeployment(t) })
261+
262+
// 1. Init, build, and publish
263+
result := RunArctl(t, tmpDir,
264+
"agent", "init", "adk", "python",
265+
"--model-name", "gemini-2.5-flash",
266+
"--image", agentImage,
267+
agentName,
268+
)
269+
RequireSuccess(t, result)
270+
271+
result = RunArctl(t, tmpDir, "agent", "build", agentName,
272+
"--image", agentImage)
273+
RequireSuccess(t, result)
274+
275+
agentDir := filepath.Join(tmpDir, agentName)
276+
result = RunArctl(t, tmpDir,
277+
"agent", "publish", agentDir,
278+
"--registry-url", regURL,
279+
)
280+
RequireSuccess(t, result)
281+
282+
// 2. Deploy (local provider)
283+
result = RunArctl(t, tmpDir,
284+
"deploy", "create", agentName,
285+
"--type", "agent",
286+
"--registry-url", regURL,
287+
)
288+
RequireSuccess(t, result)
289+
290+
// 3. Extract the deployment ID from "deploy list -o json"
291+
deploymentID := extractDeploymentID(t, tmpDir, regURL, agentName, "agent")
292+
if deploymentID == "" {
293+
t.Fatal("Could not find deployment ID after deploy create")
294+
}
295+
t.Logf("Deployment ID: %s", deploymentID)
296+
297+
// 4. Delete using truncated ID prefix (tests ID-prefix resolution)
298+
prefix := deploymentID[:8]
299+
result = RunArctl(t, tmpDir,
300+
"deploy", "delete", prefix,
301+
"--registry-url", regURL,
302+
)
303+
RequireSuccess(t, result)
304+
RequireOutputContains(t, result, "deleted")
305+
306+
// 5. Verify deployment is gone from "deploy list"
307+
result = RunArctl(t, tmpDir,
308+
"deploy", "list",
309+
"--type", "agent",
310+
"-o", "json",
311+
"--registry-url", regURL,
312+
)
313+
RequireSuccess(t, result)
314+
315+
if strings.TrimSpace(result.Stdout) != "" {
316+
var remaining []struct {
317+
ID string `json:"id"`
318+
ServerName string `json:"serverName"`
319+
}
320+
if err := json.Unmarshal([]byte(result.Stdout), &remaining); err == nil {
321+
for _, d := range remaining {
322+
if d.ID == deploymentID {
323+
t.Fatalf("Deployment %s still present after delete", deploymentID)
324+
}
325+
}
326+
}
327+
}
328+
329+
// 6. Verify deleting the same ID again fails
330+
result = RunArctl(t, tmpDir,
331+
"deploy", "delete", prefix,
332+
"--registry-url", regURL,
333+
)
334+
RequireFailure(t, result)
335+
}
336+
337+
// extractDeploymentID runs "deploy list -o json" and returns the ID of the
338+
// deployment matching the given resource name and type.
339+
func extractDeploymentID(t *testing.T, workDir, regURL, resourceName, resourceType string) string {
340+
t.Helper()
341+
342+
result := RunArctl(t, workDir,
343+
"deploy", "list",
344+
"--type", resourceType,
345+
"-o", "json",
346+
"--registry-url", regURL,
347+
)
348+
RequireSuccess(t, result)
349+
350+
var deployments []struct {
351+
ID string `json:"id"`
352+
ServerName string `json:"serverName"`
353+
ResourceType string `json:"resourceType"`
354+
}
355+
if err := json.Unmarshal([]byte(result.Stdout), &deployments); err != nil {
356+
t.Fatalf("Failed to parse deploy list JSON: %v\nOutput: %s", err, result.Stdout)
357+
}
358+
359+
for _, d := range deployments {
360+
if d.ServerName == resourceName && d.ResourceType == resourceType {
361+
return d.ID
362+
}
363+
}
364+
return ""
365+
}
366+
240367
// waitForComposeService polls until a container with the given service name in
241368
// the agentregistry_runtime compose project is running, or fails after timeout.
242369
// Uses docker ps with label filters instead of docker compose ps, because the
@@ -377,7 +504,8 @@ func TestDeleteDeploymentRemovesKubernetesResources(t *testing.T) {
377504

378505
t.Log("Deploying agent to Kubernetes...")
379506
result = RunArctl(t, tmpDir,
380-
"agent", "deploy", agentName,
507+
"deploy", "create", agentName,
508+
"--type", "agent",
381509
"--registry-url", regURL,
382510
"--provider-id", "kubernetes-default",
383511
"--namespace", "default",
@@ -516,6 +644,39 @@ func escapeSQLLiteral(value string) string {
516644
return strings.ReplaceAll(value, "'", "''")
517645
}
518646

647+
// verifyDeploymentInList runs "arctl deploy list" with JSON output and asserts
648+
// that a deployment with the given resource name and type appears in the results.
649+
func verifyDeploymentInList(t *testing.T, workDir, regURL, resourceName, resourceType string) {
650+
t.Helper()
651+
652+
result := RunArctl(t, workDir,
653+
"deploy", "list",
654+
"--type", resourceType,
655+
"-o", "json",
656+
"--registry-url", regURL,
657+
)
658+
RequireSuccess(t, result)
659+
660+
var deployments []struct {
661+
ServerName string `json:"serverName"`
662+
ResourceType string `json:"resourceType"`
663+
Status string `json:"status"`
664+
}
665+
if err := json.Unmarshal([]byte(result.Stdout), &deployments); err != nil {
666+
t.Fatalf("Failed to parse deploy list JSON output: %v\nOutput: %s", err, result.Stdout)
667+
}
668+
669+
for _, d := range deployments {
670+
if d.ServerName == resourceName && d.ResourceType == resourceType {
671+
t.Logf("Found deployment: name=%s type=%s status=%s", d.ServerName, d.ResourceType, d.Status)
672+
return
673+
}
674+
}
675+
676+
t.Fatalf("Expected deployment name=%q type=%q not found in deploy list output (%d deployments)",
677+
resourceName, resourceType, len(deployments))
678+
}
679+
519680
func dockerContainerRunning(name string) bool {
520681
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
521682
defer cancel()
@@ -843,8 +1004,12 @@ func TestAgentDeployWithPrompts(t *testing.T) {
8431004
RequireSuccess(t, result)
8441005
})
8451006

846-
t.Run("deploy", func(t *testing.T) {
847-
args := []string{"agent", "deploy", agentName, "--registry-url", regURL}
1007+
t.Run("deploy_create", func(t *testing.T) {
1008+
args := []string{
1009+
"deploy", "create", agentName,
1010+
"--type", "agent",
1011+
"--registry-url", regURL,
1012+
}
8481013
args = append(args, target.deplArgs...)
8491014
result := RunArctl(t, tmpDir, args...)
8501015
RequireSuccess(t, result)

internal/cli/agent/agent.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ func init() {
3535
AgentCmd.AddCommand(AddMcpCmd)
3636
AgentCmd.AddCommand(PublishCmd)
3737
AgentCmd.AddCommand(DeleteCmd)
38-
AgentCmd.AddCommand(DeployCmd)
3938
AgentCmd.AddCommand(ListCmd)
4039
AgentCmd.AddCommand(ShowCmd)
4140
}

0 commit comments

Comments
 (0)