Skip to content

feat: add custom Docker labels for supporting services#284

Merged
rshoemaker merged 4 commits intomainfrom
feat/PLAT-455/service_labels
Mar 5, 2026
Merged

feat: add custom Docker labels for supporting services#284
rshoemaker merged 4 commits intomainfrom
feat/PLAT-455/service_labels

Conversation

@rshoemaker
Copy link
Contributor

@rshoemaker rshoemaker commented Mar 4, 2026

Summary

  • Add orchestrator_opts to ServiceSpec, enabling custom Docker Swarm labels (e.g., for Traefik routing) on supporting services like MCP
  • Mirrors the existing extra_labels support on Postgres containers
  • Add API validation rejecting pgedge.*-prefixed labels across all three orchestrator_opts locations (database spec, node, and service) to prevent accidental overwrite of system labels

Linter improvement

  • Added --output.text.path stdout to linter so the actual errors show up in the CircleCI output now

Test plan

  • Unit tests for ServiceContainerSpec with extra labels and nil backward compat
  • Unit tests for validateOrchestratorOpts (nil, empty, valid, reserved prefix)
  • Integration into TestValidateServiceSpec (valid labels, rejected pgedge.* labels)
  • Full test suite passes (517 tests)
  • Manual: create a service with orchestrator_opts.swarm.extra_labels and verify with docker service inspect

Sample config

  {
    "database_name": "mydb",
    "nodes": [
      {
        "name": "n1",
        "host_ids": ["host-1"],
        "cpus": "2",
        "memory": "4GiB"
      }
    ],
    "services": [
      {
        "service_id": "mcp-server",
        "service_type": "mcp",
        "version": "latest",
        "host_ids": ["host-1"],
        "config": {
          "llm_provider": "anthropic",
          "llm_model": "claude-sonnet-4-5",
          "anthropic_api_key": "sk-ant-..."
        },
        "orchestrator_opts": {
          "swarm": {
            "extra_labels": {
              "traefik.enable": "true",
              "traefik.http.routers.mcp.rule": "Host(`mcp.example.com`)",
              "traefik.http.services.mcp.loadbalancer.server.port": "8080"
            }
          }
        }
      }
    ]
  }

PLAT-455

Add `orchestrator_opts` to `ServiceSpec`, enabling users to apply
custom Docker Swarm labels (e.g., for Traefik routing) to supporting
services like MCP. This mirrors the existing `extra_labels` support
on Postgres containers.

Also adds API validation rejecting `pgedge.*`-prefixed labels across
all three `orchestrator_opts` locations (database spec, node, and
service) to prevent accidental overwrite of system labels.
@coderabbitai
Copy link

coderabbitai bot commented Mar 4, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 02421006-c02b-4bbd-a048-02567814fea8

📥 Commits

Reviewing files that changed from the base of the PR and between 3153ac1 and f66389b.

📒 Files selected for processing (2)
  • Makefile
  • server/internal/orchestrator/swarm/service_spec_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • server/internal/orchestrator/swarm/service_spec_test.go
  • Makefile

📝 Walkthrough

Walkthrough

Adds orchestrator options to ServiceSpec across API and database layers, validates reserved Swarm extra label prefixes, propagates options through conversion functions, merges user-provided Swarm extra labels into generated swarm service specs, and updates tests and CI lint output.

Changes

Cohort / File(s) Summary
API design
api/apiv1/design/database.go
Added orchestrator_opts JSON field to ServiceSpec and DatabaseSpec declarations.
API ↔ DB conversion
server/internal/api/apiv1/convert.go
Wired OrchestratorOpts round-trip: orchestratorOptsToAPI in serviceSpecToAPI and orchestratorOptsToDatabase in apiToServiceSpec.
Validation
server/internal/api/apiv1/validate.go, server/internal/api/apiv1/validate_test.go
Added validateOrchestratorOpts, reservedLabelPrefix = "pgedge.", path helper, and tests covering nil, empty, valid, and reserved-label cases.
Database model
server/internal/database/spec.go
Added OrchestratorOpts *OrchestratorOpts \json:"orchestrator_opts,omitempty"`toServiceSpecand updatedClone()` to copy it.
Swarm orchestrator integration
server/internal/orchestrator/swarm/service_spec.go, server/internal/orchestrator/swarm/service_spec_test.go
Merge opts.ServiceSpec.OrchestratorOpts.Swarm.ExtraLabels into generated service labels (ContainerSpec labels and Annotations). Added tests for extra-label merging and nil-opts compatibility.
CI tooling
Makefile
golangci-lint CI invocation updated to include text output to stdout alongside JUnit XML when CI is true.

Poem

🐰 I hopped the specs from API down to DB,
I stitched extra labels where Traefik loves to be,
I sniffed out "pgedge." keys and kept them far away,
I cloned each service gently so no labels go astray,
A little rabbit nod for orchestration day.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main feature being added: custom Docker labels for supporting services, which is the primary focus of the changeset.
Description check ✅ Passed The PR description covers the summary, changes, testing details, and references the related ticket (PLAT-455), though some template sections are incomplete or missing.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/PLAT-455/service_labels

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
server/internal/api/apiv1/convert.go (2)

981-993: ⚠️ Potential issue | 🟠 Major

Same nil pointer dereference issue in orchestratorOptsToAPI.

If opts is non-nil but opts.Swarm is nil, this function will panic when accessing opts.Swarm.ExtraVolumes, opts.Swarm.ExtraNetworks, and opts.Swarm.ExtraLabels on lines 987-989.

🐛 Proposed fix to add nil check for Swarm
 func orchestratorOptsToAPI(opts *database.OrchestratorOpts) *api.OrchestratorOpts {
 	if opts == nil {
 		return nil
 	}
+	if opts.Swarm == nil {
+		return &api.OrchestratorOpts{}
+	}
 	return &api.OrchestratorOpts{
 		Swarm: &api.SwarmOpts{
 			ExtraVolumes:  extraVolumesToAPI(opts.Swarm.ExtraVolumes),
 			ExtraNetworks: extraNetworksToAPI(opts.Swarm.ExtraNetworks),
 			ExtraLabels:   maps.Clone(opts.Swarm.ExtraLabels),
 		},
 	}
-
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/api/apiv1/convert.go` around lines 981 - 993, The function
orchestratorOptsToAPI dereferences opts.Swarm without checking for nil, which
can panic; update orchestratorOptsToAPI to check if opts.Swarm is nil and only
populate the returned api.OrchestratorOpts.Swarm when present—otherwise set
Swarm to nil (or return nil) to avoid the dereference; use
extraVolumesToAPI(opts.Swarm.ExtraVolumes),
extraNetworksToAPI(opts.Swarm.ExtraNetworks) and
maps.Clone(opts.Swarm.ExtraLabels) only after verifying opts.Swarm != nil.

968-979: ⚠️ Potential issue | 🟠 Major

Potential nil pointer dereference when opts.Swarm is nil.

If opts is non-nil but opts.Swarm is nil, this function will panic when accessing opts.Swarm.ExtraVolumes, opts.Swarm.ExtraNetworks, and opts.Swarm.ExtraLabels on lines 974-976.

🐛 Proposed fix to add nil check for Swarm
 func orchestratorOptsToDatabase(opts *api.OrchestratorOpts) *database.OrchestratorOpts {
 	if opts == nil {
 		return nil
 	}
+	if opts.Swarm == nil {
+		return &database.OrchestratorOpts{}
+	}
 	return &database.OrchestratorOpts{
 		Swarm: &database.SwarmOpts{
 			ExtraVolumes:  extraVolumesToDatabase(opts.Swarm.ExtraVolumes),
 			ExtraNetworks: extraNetworksToDatabase(opts.Swarm.ExtraNetworks),
 			ExtraLabels:   maps.Clone(opts.Swarm.ExtraLabels),
 		},
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/api/apiv1/convert.go` around lines 968 - 979, The function
orchestratorOptsToDatabase dereferences opts.Swarm without checking for nil;
update orchestratorOptsToDatabase to first handle a nil opts.Swarm (when opts !=
nil) and avoid accessing opts.Swarm.ExtraVolumes/ExtraNetworks/ExtraLabels if
Swarm is nil — e.g., if opts.Swarm == nil return
&database.OrchestratorOpts{Swarm: nil} or construct a database.SwarmOpts with
nil/empty fields accordingly; ensure you still call extraVolumesToDatabase and
extraNetworksToDatabase only when the corresponding Swarm slices are present and
use maps.Clone on ExtraLabels only when ExtraLabels is non-nil to prevent
panics.
🧹 Nitpick comments (1)
server/internal/orchestrator/swarm/service_spec_test.go (1)

453-456: Minor style suggestion from static analysis (optional).

The static analysis tool suggests that got.Annotations.Labels could be simplified to got.Labels since Annotations is an embedded field in swarm.ServiceSpec. However, the explicit form is clearer and this is a test file, so leaving it as-is is fine for readability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/orchestrator/swarm/service_spec_test.go` around lines 453 -
456, The test currently accesses labels via got.Annotations.Labels; simplify
this by using the embedded field directly (got.Labels) to match static analysis
suggestion—update the two occurrences in the test where tt.checkLabels is
invoked (the calls that reference got.TaskTemplate.ContainerSpec.Labels and
got.Annotations.Labels) to use got.Labels for the service-level labels while
keeping ContainerSpec label checks unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@server/internal/api/apiv1/convert.go`:
- Around line 981-993: The function orchestratorOptsToAPI dereferences
opts.Swarm without checking for nil, which can panic; update
orchestratorOptsToAPI to check if opts.Swarm is nil and only populate the
returned api.OrchestratorOpts.Swarm when present—otherwise set Swarm to nil (or
return nil) to avoid the dereference; use
extraVolumesToAPI(opts.Swarm.ExtraVolumes),
extraNetworksToAPI(opts.Swarm.ExtraNetworks) and
maps.Clone(opts.Swarm.ExtraLabels) only after verifying opts.Swarm != nil.
- Around line 968-979: The function orchestratorOptsToDatabase dereferences
opts.Swarm without checking for nil; update orchestratorOptsToDatabase to first
handle a nil opts.Swarm (when opts != nil) and avoid accessing
opts.Swarm.ExtraVolumes/ExtraNetworks/ExtraLabels if Swarm is nil — e.g., if
opts.Swarm == nil return &database.OrchestratorOpts{Swarm: nil} or construct a
database.SwarmOpts with nil/empty fields accordingly; ensure you still call
extraVolumesToDatabase and extraNetworksToDatabase only when the corresponding
Swarm slices are present and use maps.Clone on ExtraLabels only when ExtraLabels
is non-nil to prevent panics.

---

Nitpick comments:
In `@server/internal/orchestrator/swarm/service_spec_test.go`:
- Around line 453-456: The test currently accesses labels via
got.Annotations.Labels; simplify this by using the embedded field directly
(got.Labels) to match static analysis suggestion—update the two occurrences in
the test where tt.checkLabels is invoked (the calls that reference
got.TaskTemplate.ContainerSpec.Labels and got.Annotations.Labels) to use
got.Labels for the service-level labels while keeping ContainerSpec label checks
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2630658c-e924-4fc9-9075-624feef73574

📥 Commits

Reviewing files that changed from the base of the PR and between b81d259 and 305fc7f.

⛔ Files ignored due to path filters (9)
  • api/apiv1/gen/control_plane/service.go is excluded by !**/gen/**
  • api/apiv1/gen/http/control_plane/client/encode_decode.go is excluded by !**/gen/**
  • api/apiv1/gen/http/control_plane/client/types.go is excluded by !**/gen/**
  • api/apiv1/gen/http/control_plane/server/encode_decode.go is excluded by !**/gen/**
  • api/apiv1/gen/http/control_plane/server/types.go is excluded by !**/gen/**
  • api/apiv1/gen/http/openapi.json is excluded by !**/gen/**
  • api/apiv1/gen/http/openapi.yaml is excluded by !**/gen/**
  • api/apiv1/gen/http/openapi3.json is excluded by !**/gen/**
  • api/apiv1/gen/http/openapi3.yaml is excluded by !**/gen/**
📒 Files selected for processing (7)
  • api/apiv1/design/database.go
  • server/internal/api/apiv1/convert.go
  • server/internal/api/apiv1/validate.go
  • server/internal/api/apiv1/validate_test.go
  • server/internal/database/spec.go
  • server/internal/orchestrator/swarm/service_spec.go
  • server/internal/orchestrator/swarm/service_spec_test.go

This should cause any errors to appear in the build log, while
--output.junit-xml.path still writes the XML. Without
--output.text, specifying --output.junit-xml.path causes
golangci-lint v2 to only emit XML.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Makefile`:
- Line 59: Update the CI golangci-lint invocation in the Makefile where the
conditional uses "$(if $(filter true,$(CI)),--output.text
--output.junit-xml.path $@-results.xml)" to replace the invalid flag with the
correct form for v2.5.0: use "--output.text.path stdout" instead of
"--output.text"; ensure the conditional emits "--output.text.path stdout
--output.junit-xml.path $@-results.xml" so CI linting uses the valid flags.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e25411a0-c7f8-447c-b948-096ed96560d6

📥 Commits

Reviewing files that changed from the base of the PR and between 305fc7f and 3153ac1.

📒 Files selected for processing (1)
  • Makefile


golangci-lint=$(gobin)/golangci-lint \
$(if $(filter true,$(CI)),--output.text --output.junit-xml.path $@-results.xml)
$(if $(filter true,$(CI)),--output.text.path stdout --output.junit-xml.path $@-results.xml)
Copy link
Member

Choose a reason for hiding this comment

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

Just FYI in case you haven't seen it, CircleCI consumes these files and makes the results available in a dedicated Tests tab. I can see why you'd also want them in the text output, so I'm OK with adding this.

@rshoemaker rshoemaker merged commit 8eed09e into main Mar 5, 2026
3 checks passed
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.

2 participants