Skip to content

refactor: drop taskParamser in favor of client.TaskBuilder typed wrappers #161

@bdchatham

Description

@bdchatham

Problem

The controller's internal/task/ defines a taskParamser interface (internal/task/sidecar.go:16-19) implemented by 14 task-params types:

type taskParamser interface {
    toRequestParams() *map[string]any
    taskType() string
}

Each *Params type has a toRequestParams() method that hand-builds a *map[string]any from its fields — duplicating exactly what json.Marshal of the same struct already produces via its json:"camelCase" tags. Pure ceremony.

The sidecar/client package in seictl (github.com/sei-protocol/seictl/sidecar/client/tasks.go) provides typed Task wrappers (SnapshotRestoreTask, ConfigureGenesisTask, AssembleAndUploadGenesisTask, etc.) implementing a parallel TaskBuilder interface:

type TaskBuilder interface {
    TaskType() string
    Validate() error
    ToTaskRequest() TaskRequest
}

These are the canonical ergonomic shape — single source of truth for wire format AND validation, with strict bech32 / shape checks living next to the field declarations.

The two interfaces overlap in purpose. The controller-side taskParamser predates the seictl-side TaskBuilder and is now redundant for tasks that have a typed wrapper.

Impact

Drift hazard. When a task gets a new field, both the seictl-side wrapper AND the controller-side params shell must be updated. We just hit this in #160 — the sidecar PR shipped the server-side change but missed the client-side typed wrapper, and the controller-side scaffolding in turn duplicated the wire format with a hand-rolled bech32 check. Caught in review (a third PR + this follow-up), but the gap is structural.

Onboarding friction. A new task type today requires duplicate work: server impl in seictl/sidecar/tasks/, client wrapper in seictl/sidecar/client/tasks.go, AND a controller-side params shell in sei-k8s-controller/internal/task/. The third is mechanical and adds no value.

Operator visibility. PlannedTask.Params.Raw storage uses lowercase camelCase via the controller's json tags. Migrating to client.*Task directly (without json tags on the wrappers) would change the storage format to capital field names — operator-visible cosmetic regression unless the wrappers also gain json tags.

Relevant experts

  • kubernetes-specialist — controller-runtime conventions, runtime.RawExtension round-trip semantics, registry-based deserialization
  • platform-engineer — sidecar client API surface, breaking-change strategy for the seictl module

Proposed approach

Phased migration; can be done incrementally per task type.

Phase 1 — sidecar client surface

For each of the 14 task types:

  • Confirm a client.*Task typed wrapper exists. Add one if it doesn't (e.g., ConfigPatch, ConfigureStateSync already exist; verify the rest).
  • Add json:"camelCase" tags to the typed wrapper fields so direct json.Marshal produces the same wire format the existing toRequestParams() produces. Drop TaskMeta.ID from JSON output via json:"-" since it's controller-internal metadata, not wire payload.
  • Cut a seictl release with the json-tagged wrappers.

Phase 2 — controller migration

  • Extend the controller's executor (internal/task/sidecar.go) to accept client.TaskBuilder directly, calling ToTaskRequest() to build the sidecar.TaskRequest. Override Id with the deterministic task ID at submit time.
  • For each migrated task type:
    • Drop the controller's *Params shell.
    • Update planner construction sites to use client.*Task{...} directly.
    • Update Deserialize registry to unmarshal into the typed wrapper.
    • Update tests.
  • Remove taskParamser interface and the compile-time assertion list once all 14 are migrated.

Phase 3 (optional) — drop the JSON round-trip

Today: planner builds typed struct → json.MarshalPlannedTask.Params.Rawjson.Unmarshal back into typed struct → toRequestParams() → JSON-marshal the map → wire bytes. Two round-trips for the same payload.

Cleaner: planner builds typed wrapper → ToTaskRequest() → store the Params map directly in PlannedTask.Params.Raw → executor reads bytes and submits verbatim. One round-trip.

Worth doing only if Phase 1+2 reveal it's cheap — the executor's current e.params T shape is generic, so a refactor to "store wire bytes" needs care.

Architectural constraints

  • TaskMeta.ID field on every typed wrapper must serialize as json:"-" (or be unexported) so it doesn't pollute PlannedTask.Params.Raw.
  • Storage format compatibility. Existing in-flight TaskPlans with Params.Raw in the old shape need to deserialize cleanly. If field names change (capital ↔ lowercase), the deserialization will silently produce zero-valued fields. Migrating in two steps (add json tags first, then migrate consumers) avoids this.
  • One-way door: removing the controller's *Params types is breaking for any tooling that imports them. None known today; verify before commit.

Acceptance criteria

  • Each of the 14 task types has a typed client.*Task wrapper with json tags.
  • taskParamser interface and the compile-time assertion list at internal/task/sidecar.go:22-37 are removed.
  • Controller's executor accepts client.TaskBuilder.
  • No regression in kubectl get seinodedeploymentplan storage format (operator-visible).
  • All existing tests pass; new typed-wrapper tests cover the round-trip.

Out of scope

  • Adding new task types to seictl/sidecar/client beyond the existing 14.
  • CRD schema changes.
  • Migrating non-sidecar task executions (e.g., controller-internal tasks like CreateExporter) — they don't go through the sidecar HTTP API and don't use taskParamser.

References

  • sei-protocol/sei-k8s-controller#160 introduced the delegating-shell pattern (Phase 0): controller's AssembleAndUploadGenesisParams and AssembleForkGenesisParams keep their shell but delegate Validate() and toRequestParams() to client.AssembleAndUploadGenesisTask / client.AssembleGenesisForkTask. This issue carries the migration to its logical conclusion.
  • sei-protocol/seictl#121 added the typed wrappers + strict bech32 validation for the assemble-genesis tasks.
  • internal/task/sidecar.go:13-37taskParamser interface + compile-time assertions for the 14 implementing types.
  • internal/task/sidecar.go:73-87 — executor's current dispatch site that would shift to TaskBuilder.
  • internal/task/task.go:248-262deserializeSidecar[T] generic helper that hydrates e.params T from Params.Raw.

🤖 Generated with Claude Code

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