Skip to content

feat(api): split api/ into a Go submodule for cross-repo typed import #175

@bdchatham

Description

@bdchatham

Problem

The typed CRD surface at api/v1alpha1/ (SeiNode, SeiNodeDeployment, etc.) lives inside the main github.com/sei-protocol/sei-k8s-controller module. Downstream consumers that want typed access (seictl's new nodedeployment CLI per sei-protocol/seictl/docs/design/nodedeployment-cli.md, future controllers, the seiload harness) cannot import these types directly: sei-k8s-controller/internal/... imports github.com/sei-protocol/seictl/sidecar/client from 17+ sites, so a typed import in the reverse direction creates a Go module-level cycle.

The seictl nodedeployment CLI implementation hits this immediately. The architectural fallback is unstructured.Unstructured everywhere — verbose accessors, no compile-time type safety, ongoing drift risk against schema changes — paid by every consumer that wants typed CRD access.

Impact

  • seictl's PR-1 for the new CLI needs to choose: either ship with unstructured and refactor later, or block on the split landing here.
  • Future consumers (additional controllers, fuzzers, upcoming harnesses) each pay the same unstructured tax until the split lands.
  • "Single source of truth for CRD types" is the standard K8s ecosystem pattern. Cluster-API, Argo CD, Longhorn, Karpenter, and Crossplane all split api/ into a leaf module for exactly this reason.

Relevant experts

  • kubernetes-specialist — kubebuilder/controller-runtime conventions, module structure.

Proposed approach

Extract api/ into a Go submodule (github.com/sei-protocol/sei-k8s-controller/api):

  1. Add api/go.mod declaring the new module path. Direct dependencies should be limited to k8s.io/apimachinery, k8s.io/api, and sigs.k8s.io/controller-runtime/pkg/scheme — no project-internal imports.
  2. Verify api/v1alpha1/ is leaf-clean (no imports from sei-k8s-controller/internal/... or cmd/...). If not, lift the offending shared code into api/ or push it down into internal/.
  3. Update the main module's go.mod to require the api submodule. Use a replace directive pointing at ./api for local development; go work syncs across both.
  4. Update all sei-k8s-controller/internal/... and cmd/... imports of api/v1alpha1 to reference the new module path. The package import path stays the same (github.com/sei-protocol/sei-k8s-controller/api/v1alpha1); only the module boundary changes.
  5. CI: build both modules; run controller tests against the submodule.
  6. Release: tag api/v0.1.0 independently from main module tags. Subsequent CRD changes bump api/ minor; controller-only changes don't touch it.

Standard pattern. References:

Architectural constraints

  • Leaf-clean. The api submodule must not import anything from sei-k8s-controller/internal/, cmd/, or any project-specific package. Any shared utility (e.g., validation helpers) either moves into api/ or stays controller-side.
  • Same import path. Consumers should continue to write import "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1". Only the module boundary changes. This avoids breaking any existing internal consumer of the same package.
  • Independent versioning. api/vX.Y.Z tags are independent from main module tags. Schema changes bump api; controller-only changes don't.

Acceptance criteria

  • api/go.mod exists declaring github.com/sei-protocol/sei-k8s-controller/api.
  • go list -m -json github.com/sei-protocol/sei-k8s-controller/api from the main module resolves cleanly.
  • cd api && go build ./... succeeds with no project-internal imports.
  • Main module imports api via require + replace (local dev) and a tagged version (CI).
  • All controller unit / integration / envtest suites pass against the submodule.
  • api/v0.1.0 (or first cut) tagged and pushed.
  • A consumer outside this repo (initially: sei-protocol/seictl) can import "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" and successfully construct a typed client without cycle.

Out of scope

  • Moving controller reconcile logic, planner, or task code. This issue is structural-only.
  • Renaming the CRD or any of its fields. Additive schema changes only.
  • Splitting the controller itself into a separate repo. Submodule, not subrepo.

References

  • sei-protocol/seictl#135 — design doc that hit this constraint and worked around it with unstructured.
  • sei-protocol/seictl/docs/design/nodedeployment-cli.md — consumer side; the "Implementation: build on existing libraries" section currently assumes unstructured pending this split.
  • Brandon's directive 2026-05-05: "break the CRD spec model from the sei-k8s-controller and import in both places."

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions