api refactor: deployments to ID-based APIs#240
Conversation
- finish rename config -> env - call deployment adapters from within registry service - add extraroutes
There was a problem hiding this comment.
Pull request overview
This PR completes the break to the new /v0 deployment contract by making deployment operations strictly ID-based, removing legacy compatibility fields, and aligning server/client behavior around canonical deployment metadata (env, provider config/metadata, and deployment summaries in _meta).
Changes:
- Refactors deployment model + storage to use
envplus provider-agnosticprovider_config/provider_metadataJSON, and adds deployment summaries into server/agent response metadata. - Updates service + handlers to dispatch deployment lifecycle operations through platform adapters via new service methods (
CreateDeployment,UndeployDeployment,GetDeploymentLogs,CancelDeployment). - Updates the client + tests for ID-based deployment operations and restores SSE “no-timeout” behavior for streaming indexing.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/types/types.go | Adds ExtraRoutes hook for integrations to register routes under the same API/prefix. |
| pkg/models/server_response.go | Adds deployments metadata pointer to server response meta. |
| pkg/models/agent.go | Adds deployments metadata pointer to agent response meta. |
| pkg/models/deployment.go | Refactors deployment model (env, JSONObject, provider config/metadata) and adds deployment summary/meta structs. |
| pkg/models/deployment_test.go | Adds tests for JSONObject conversion helpers. |
| pkg/cli/root.go | Removes legacy globals and wires client setup through setters only. |
| internal/registry/registry_app.go | Plumbs ExtraRoutes into router options. |
| internal/registry/api/router/v0.go | Invokes ExtraRoutes callback after registering core /v0 routes. |
| internal/registry/api/handlers/v0/deployments.go | Routes deployment create/delete/logs/cancel through new service dispatch methods and updates request docs. |
| internal/registry/api/handlers/v0/deployment_adapters.go | Splits local/kubernetes adapters into explicit types with stricter validation. |
| internal/registry/api/handlers/v0/deployment_meta_bridge.go | Adds deployment→resource indexing and attaches deployment summaries into server/agent responses. |
| internal/registry/api/handlers/v0/servers.go, agents.go, edit.go | Enriches list/get/create/edit responses with deployment metadata. |
| internal/registry/api/handlers/v0/*_test.go | Updates/extends handler tests for adapter dispatch + metadata behavior. |
| internal/registry/service/service.go | Extends RegistryService interface with adapter-dispatch lifecycle methods. |
| internal/registry/service/registry_service.go | Implements adapter resolution and dispatch methods; updates deployment/env handling and provider metadata. |
| internal/registry/service/testing/fake_registry.go | Extends fake service for new deployment lifecycle dispatch methods. |
| internal/registry/service/registry_service_test.go | Updates tests for env/provider handling and cleanup signatures. |
| internal/registry/database/postgres.go | Updates deployment persistence to store env + provider config/metadata and removes dropped columns from queries. |
| internal/registry/database/migrations/007_unify_deployment_provider_metadata.sql | Adds provider_config/provider_metadata; drops region/cloud resource fields and related indexes. |
| internal/client/client.go | Removes legacy client helpers; adds ID-based deployment methods and improved SSE request/client construction. |
| internal/client/client_integration_test.go | Adds in-process integration tests for catalog + ID-based deployments + client behavior. |
| internal/cli/mcp/deploy.go | Removes preflight server existence lookup; deploys by name/version directly. |
| internal/mcp/registryserver/deployments_test.go | Updates deployment tool tests to use Env. |
| go.mod | Promotes github.com/google/uuid to a direct dependency. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ResourceType: resourceType, | ||
| Origin: "managed", | ||
| Env: input.Body.Env, | ||
| ProviderConfig: input.Body.ProviderConfig, |
There was a problem hiding this comment.
models.Deployment.ProviderConfig is a models.JSONObject, but this assigns a map[string]any (input.Body.ProviderConfig). Go won’t allow assigning a plain map to a distinct named map type, so this should be converted (or the field type adjusted) before constructing deploymentReq.
| ProviderConfig: input.Body.ProviderConfig, | |
| ProviderConfig: models.JSONObject(input.Body.ProviderConfig), |
| if err == nil && strings.ToLower(strings.TrimSpace(provider.Platform)) == platformKubernetes { | ||
| s.cleanupKubernetesResources(ctx, existing, serverName, version, resourceType) | ||
| } | ||
| if existing != nil && platform == platformKubernetes { |
There was a problem hiding this comment.
cleanupExistingDeployment decides whether to run Kubernetes runtime cleanup based on the incoming platform argument rather than the platform of the existing deployment being removed. If a stale record belongs to a different provider/platform than the new request, this can either skip necessary Kubernetes cleanup (leaking resources) or attempt Kubernetes deletes for a non-Kubernetes deployment. Consider resolving the platform from existing.ProviderID (similar to removeDeploymentRecord) instead of using the caller-provided platform.
| if existing != nil && platform == platformKubernetes { | |
| if existing != nil && existing.ProviderID == kubernetesProviderID { |
| func deploymentResourceIndex(ctx context.Context, registry service.RegistryService) map[deploymentResourceKey][]models.DeploymentSummary { | ||
| deployments, err := registry.GetDeployments(ctx, nil) | ||
| if err != nil { | ||
| return map[deploymentResourceKey][]models.DeploymentSummary{} | ||
| } |
There was a problem hiding this comment.
deploymentResourceIndex calls registry.GetDeployments(ctx, nil) (no filter) and builds an index over all deployments on every servers/agents request that attaches deployment metadata. This can become a scalability bottleneck as the deployments table grows. If possible, narrow the query to the relevant resource type and names for the current response (or add a targeted DB/API method) to avoid full-table reads per request.
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestDeploymentResourceIndexFiltersInactiveStatuses(t *testing.T) { |
There was a problem hiding this comment.
This test name suggests inactive statuses are filtered out, but the assertions expect both a deployed and a cancelled deployment to be present in the index. Renaming the test to reflect the actual behavior would make the intent clearer.
| func TestDeploymentResourceIndexFiltersInactiveStatuses(t *testing.T) { | |
| func TestDeploymentResourceIndexIncludesAllStatuses(t *testing.T) { |
| return []*models.Deployment{ | ||
| {ID: "dep-active", ServerName: "io.test/server", ResourceType: "mcp", Status: "deployed"}, | ||
| {ID: "dep-cancelled", ServerName: "io.test/server", ResourceType: "mcp", Status: "cancelled"}, | ||
| }, nil | ||
| }, | ||
| } | ||
|
|
||
| index := deploymentResourceIndex(context.Background(), reg) | ||
| key := deploymentResourceKey{resourceType: "mcp", resourceName: "io.test/server"} | ||
|
|
||
| require.Len(t, index[key], 2) | ||
| assert.Equal(t, "dep-active", index[key][0].ID) | ||
| assert.Equal(t, "deployed", index[key][0].Status) | ||
| assert.Equal(t, "dep-cancelled", index[key][1].ID) | ||
| assert.Equal(t, "cancelled", index[key][1].Status) |
There was a problem hiding this comment.
deploymentResourceIndex sorts summaries by UpdatedAt, but in this test both deployments have zero UpdatedAt values. Since slices.SortFunc is not stable, relying on the original order (and asserting [0]/[1] IDs) can be flaky. Set distinct UpdatedAt timestamps in the fixtures or assert order-insensitively.
| deployment, err := registry.CreateDeployment(ctx, deploymentReq, platform) | ||
| if err != nil { | ||
| if errors.Is(err, database.ErrInvalidInput) { | ||
| return nil, huma.Error400BadRequest("Invalid deployment request") |
There was a problem hiding this comment.
registry.CreateDeployment can return database.ErrInvalidInput for multiple reasons (e.g., missing/unsupported platform adapter). Mapping all ErrInvalidInput cases to a generic "Invalid deployment request" makes unsupported provider/platform failures hard to diagnose. Consider returning a more specific error message/status when the platform isn’t supported vs. when the request body is invalid.
| return nil, huma.Error400BadRequest("Invalid deployment request") | |
| // Distinguish unsupported provider/platform from generic invalid input | |
| lowerMsg := strings.ToLower(err.Error()) | |
| if strings.Contains(lowerMsg, "platform") || strings.Contains(lowerMsg, "provider") { | |
| return nil, huma.Error400BadRequest("Unsupported provider or platform for deployment") | |
| } | |
| return nil, huma.Error400BadRequest("Invalid deployment request: " + err.Error()) |
| // Deployment record already exists — clean up stale record and retry | ||
| log.Printf("Deployment for %s/%s already exists, replacing stale record", serverName, deployment.Version) | ||
| if cleanupErr := s.cleanupExistingDeployment(ctx, deployment.ID, provider.Platform); cleanupErr != nil { | ||
| if cleanupErr := s.cleanupExistingDeployment(ctx, serverName, deployment.Version, resourceTypeMCP, provider.Platform); cleanupErr != nil { |
There was a problem hiding this comment.
if we're not using the ID, does that mean I can only deploy an artifact once -- like one MCPServer or Agent deployment in kubernetes?
There was a problem hiding this comment.
yes although this is already the existing behavior; current db shape still enforces single-row identity from legacy semantics (not ID semantics). supporting parallel deployments across providers would require a schema + service semantics change (we can leave this for the post-api phase of the refactor)
CREATE TABLE deployments (
-- Primary identifiers
server_name VARCHAR(255) NOT NULL,
version VARCHAR(255) NOT NULL,
...
-- Primary key
CONSTRAINT deployments_pkey PRIMARY KEY (server_name, version)
);
internal/cli/mcp/list.go
Outdated
| } | ||
| } | ||
|
|
||
| func buildMCPDeploymentCounts(deployedServers []*client.DeploymentResponse) map[string]map[string]int { |
There was a problem hiding this comment.
would it make sense to extract these two functions to a common place, since they are more or less the same as the one in agent list?
| return sanitizeK8sName(base) | ||
| } | ||
|
|
||
| func RemoteMCPResourceName(name, deploymentID string) string { |
There was a problem hiding this comment.
these two functions are exactly the same
|
|
||
| // cleanupKubernetesResources deletes Kubernetes runtime resources for a stale deployment. | ||
| // Errors are logged but not returned, since the resources may already be gone. | ||
| func (s *registryServiceImpl) cleanupKubernetesResources(ctx context.Context, existing *models.Deployment) { |
There was a problem hiding this comment.
are we not cleaning these up anymore?
please make sure this scenario works:
- deploy an agent to k8s (arctl agent deploy)
- manually delete the agent from k8s
- deploy the agent again to k8s (arctl agent deploy)
There was a problem hiding this comment.
we no longer do name/version cleanup on deploy, because that breaks once multiple deployments can exist for the same registry artifact. deployments are now 1:1 by deployment ID, and k8s resources are named/labeled with that ID.
for your scenario (deploy -> manually delete in k8s -> deploy again), this should work. redeploy creates a new deployment ID and does not collide with previous runtime state.
note that deleting the deployment from the DB if it is deleted directly from k8s will require a follow up PR (as we discussed to implement db reconciliation feature)
i plan to address this once the api refactor is done
# Description
This PR finalizes the deployment API refactor as a clean break, removing
compatibility shims and aligning client/server behavior with the new
`/v0` design.
- **Motivation:** Eliminate ambiguous deployment identity and deprecated
payload shapes, and enforce one canonical contract.
- **What changed:**
- Deployment create/get/delete flows now consistently use deployment
`id` (UUID) as the only identifier.
- Removed handler-side ID override so IDs come from the service/adapter
layer source of truth.
- Updated client methods to ID-based deployment operations
(`GetDeploymentByID`, `RemoveDeploymentByID`), removing legacy
name/version deployment paths.
- Removed deprecated deployment response compatibility fields from
client (`config`, `runtime`), keeping canonical `env`.
- Simplified client create semantics (removed `Push*` compatibility
wrappers; `Create*` directly maps to canonical endpoints).
- Fixed client retry behavior and restored SSE no-timeout behavior for
streaming indexing.
- Added/updated tests to cover ID round-trip behavior and `JSONObject`
conversion correctness.
# Change Type
/kind breaking_change
/kind fix
/kind cleanup
# Changelog
```release-note
BREAKING: deployment APIs and client operations are now strictly ID-based (`/v0/deployments/{id}`), and deprecated deployment compatibility fields (`config`, `runtime`) have been removed in favor of canonical `env`.
```
# Additional Notes
- This PR intentionally does **not** preserve backwards compatibility
for deprecated deployment fields or legacy deployment route shapes.
Description
This PR finalizes the deployment API refactor as a clean break, removing compatibility shims and aligning client/server behavior with the new
/v0design.id(UUID) as the only identifier.GetDeploymentByID,RemoveDeploymentByID), removing legacy name/version deployment paths.config,runtime), keeping canonicalenv.Push*compatibility wrappers;Create*directly maps to canonical endpoints).JSONObjectconversion correctness.Change Type
/kind breaking_change
/kind fix
/kind cleanup
Changelog
Additional Notes