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):
- 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.
- 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/.
- 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.
- 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.
- CI: build both modules; run controller tests against the submodule.
- 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
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."
Problem
The typed CRD surface at
api/v1alpha1/(SeiNode,SeiNodeDeployment, etc.) lives inside the maingithub.com/sei-protocol/sei-k8s-controllermodule. Downstream consumers that want typed access (seictl's newnodedeploymentCLI persei-protocol/seictl/docs/design/nodedeployment-cli.md, future controllers, the seiload harness) cannot import these types directly:sei-k8s-controller/internal/...importsgithub.com/sei-protocol/seictl/sidecar/clientfrom 17+ sites, so a typed import in the reverse direction creates a Go module-level cycle.The seictl
nodedeploymentCLI implementation hits this immediately. The architectural fallback isunstructured.Unstructuredeverywhere — verbose accessors, no compile-time type safety, ongoing drift risk against schema changes — paid by every consumer that wants typed CRD access.Impact
unstructuredand refactor later, or block on the split landing here.unstructuredtax until the split lands.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):api/go.moddeclaring the new module path. Direct dependencies should be limited tok8s.io/apimachinery,k8s.io/api, andsigs.k8s.io/controller-runtime/pkg/scheme— no project-internal imports.api/v1alpha1/is leaf-clean (no imports fromsei-k8s-controller/internal/...orcmd/...). If not, lift the offending shared code intoapi/or push it down intointernal/.go.modto require the api submodule. Use areplacedirective pointing at./apifor local development;go worksyncs across both.sei-k8s-controller/internal/...andcmd/...imports ofapi/v1alpha1to 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.api/v0.1.0independently from main module tags. Subsequent CRD changes bump api/ minor; controller-only changes don't touch it.Standard pattern. References:
kubernetes-sigs/cluster-api/api— separatego.modunderapi/.argoproj/argo-cd/pkg/apis— separate module for typed CRDs.longhorn/longhorn-manager/k8s/pkg/apis— same pattern.Architectural constraints
sei-k8s-controller/internal/,cmd/, or any project-specific package. Any shared utility (e.g., validation helpers) either moves intoapi/or stays controller-side.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.api/vX.Y.Ztags are independent from main module tags. Schema changes bump api; controller-only changes don't.Acceptance criteria
api/go.modexists declaringgithub.com/sei-protocol/sei-k8s-controller/api.go list -m -json github.com/sei-protocol/sei-k8s-controller/apifrom the main module resolves cleanly.cd api && go build ./...succeeds with no project-internal imports.require+replace(local dev) and a tagged version (CI).api/v0.1.0(or first cut) tagged and pushed.sei-protocol/seictl) canimport "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1"and successfully construct a typed client without cycle.Out of scope
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.