plan

package
v0.8.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 2, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Index

Constants

View Source
const (
	TargetAuthAPIKeys = "confluent_cloud_api_keys"
	TargetAuthMTLS    = "mtls"
	TargetAuthOAuth   = "oauth"
)

Target auth method tokens. Customer-facing values written into `plan-inputs.yaml` and rendered back in the AuthDecision JSON.

View Source
const (
	SourceAuthSCRAM  = "scram"
	SourceAuthIAM    = "iam"
	SourceAuthMTLS   = "mtls"
	SourceAuthUnauth = "unauth"
)

Source-auth tokens. Stable strings — these appear in the rendered Plan (AuthDecision.SourceAuths) and key the auth_mapping table in plan-config.yaml. Don't rename without bumping a schema doc.

View Source
const (
	DiscoveredClientAuthIAM             = "IAM"
	DiscoveredClientAuthSASLSCRAM       = "SASL_SCRAM"
	DiscoveredClientAuthTLS             = "TLS"
	DiscoveredClientAuthUnauthenticated = "UNAUTHENTICATED"
	DiscoveredClientAuthUnknown         = "UNKNOWN"
)

DiscoveredClientAuth* mirrors the literal strings that `kcp scan client-inventory` writes into `DiscoveredClient.Auth` (see `cmd/scan/client_inventory/kafka_trace_line_parser.go`).

**Heads-up:** `types.AuthTypeIAM` in `internal/types/types.go` resolves to `"SASL/IAM"` — a DIFFERENT string used elsewhere. Use these constants when comparing against the persisted client inventory, not the `types.AuthType*` constants.

View Source
const (
	DowntimeZero                      = "zero"
	DowntimeSecondsPerService         = "seconds_per_service"
	DowntimeMinutesPerService         = "minutes_per_service"
	DowntimeScheduledWindowSequential = "scheduled_window_sequential"
	DowntimeScheduledWindowAllAtOnce  = "scheduled_window_all_at_once"
	DowntimeLetConfluentChoose        = "let_confluent_choose"
	DowntimeUnsetFallback             = DowntimeLetConfluentChoose
)

downtime_tolerance enum values. Stable identifiers — used as YAML input tokens AND as keys in the style mapping.

View Source
const (
	PrereqNotStarted            = "not_started"
	PrereqStatusInProgressInput = "in_progress"
	PrereqStatusCompleteInput   = "complete"
)

Gateway prereq status enum values.

View Source
const (
	EffortSignalIDIAMClientCount           = "iam_to_scram_client_count"
	EffortSignalIDMM2CheckpointTopics      = "mm2_checkpoint_topic_count"
	EffortSignalIDSelfManagedConnectFleets = "self_managed_connect_fleet_count"
	EffortSignalIDGlueSerializerMigration  = "glue_serializer_migration_count"
)

Stable Effort Signal IDs. Matches the spec row order.

View Source
const (
	RedFlagIDSchemalessSource          = "schemaless_source"
	RedFlagIDKafkaVersionBelowCLFloor  = "kafka_version_below_cl_floor"
	RedFlagIDIAMAuthEnabled            = "iam_auth_enabled"
	RedFlagIDGlueSRInUse               = "glue_sr_in_use"
	RedFlagIDPartitionApproachingCap   = "partition_approaching_cap"
	RedFlagIDMSKConnectPresent         = "msk_connect_present"
	RedFlagIDSelfManagedConnectPresent = "self_managed_connect_present"
	RedFlagIDMultiRegionSource         = "multi_region_source"
	RedFlagIDZeroACLsWithIAM           = "zero_acls_with_iam"
	RedFlagIDClientInventoryGap        = "client_inventory_gap"
	RedFlagIDMSKExpressBrokerTier      = "msk_express_broker_tier"
	RedFlagIDTieredStorageInUse        = "tiered_storage_in_use"
	RedFlagIDEOSInUse                  = "eos_in_use"
	RedFlagIDKafkaStreamsInUse         = "kafka_streams_in_use"
	RedFlagIDBroadTopicPatternMatch    = "broad_topic_pattern_match"
)

Stable Red Flag IDs. Matches the spec's row numbering — kept stable so JSON consumers can branch on the ID without parsing the title.

View Source
const (
	SchemaStrategyUnknown                       = "unknown"
	SchemaStrategyNoSchemas                     = "no_schemas"
	SchemaStrategyAdoptSchemasDuringMigration   = "adopt_schemas_during_migration"
	SchemaStrategyMigrateExistingSchemaRegistry = "migrate_existing_schema_registry"

	SchemaCPEditionEnterprise = "enterprise"
	SchemaCPEditionCommunity  = "community"
)

Plan-input enum tokens for schema migration. Stable customer-facing strings — surfaced in plan-inputs.yaml and the rendered Plan.

View Source
const (
	ConsumerHistoryRequired    = "required"
	ConsumerHistoryNotRequired = "not_required"
	ConsumerHistoryUnknown     = "unknown"

	HistoricalKeepMSKRunning = "keep_msk_running_until_data_expires"
	HistoricalBulkLoadExtern = "bulk_load_historical_via_external_tool"
	HistoricalDeferToAccount = "defer_to_account_team"
)

Plan-input enum tokens for tiered-storage knobs. Stable customer-facing strings; the resolver surfaces them through PlanInputsResolved.ConsumerHistoryRequirement and HistoricalDataStrategy.

Variables

This section is empty.

Functions

func RenderJSON

func RenderJSON(p *Plan) ([]byte, error)

RenderJSON serialises a Plan to canonical 2-space-indented JSON. Stable key ordering comes for free from struct field order — any map members are encoded by Go's stdlib in sorted key order since Go 1.12.

func RenderMarkdown

func RenderMarkdown(p *Plan, cfg *PlanConfig) ([]byte, error)

RenderMarkdown emits the human-readable Plan: Source Environment, Sizing & Cluster Decisions, Actions Needed, and collapsed appendices. `cfg` must be the same PlanConfig the PlanService used to build the plan (product-fact numbers in the Definitions block and the partition cap in the appendix read from it).

Types

type AuthDecision added in v0.8.6

type AuthDecision struct {
	ClusterID      string           `json:"cluster_id"`
	SourceAuths    []string         `json:"source_auths_detected"`
	TargetMappings []AuthMappingRow `json:"target_mappings,omitempty"`
	// OverrideRejected is true when the (cluster-scoped or global)
	// `target_auth_method` override was set to a value outside the
	// recognised enum — the row's `effective_target` reflects the per-
	// source default. The renderer surfaces an inline marker in §4 so
	// a reader scanning the table sees why this cluster's target
	// differs from peers without scrolling to the Actions Needed
	// section to read the typo OQ.
	OverrideRejected bool `json:"override_rejected,omitempty"`
	// RejectedOverrideValue is the customer-supplied invalid value;
	// surfaced in the renderer footnote so the reader recognises the
	// typo at a glance.
	RejectedOverrideValue string `json:"rejected_override_value,omitempty"`
}

AuthDecision is the per-cluster source→target auth mapping. SourceAuths holds the methods detected on the source MSK cluster as stable enum tokens ("scram", "iam", "mtls", "unauth"); the plan does NOT pick one when multiple are enabled — it shows all options.

type AuthMapping added in v0.8.4

type AuthMapping struct {
	Target            string `yaml:"target"`
	GatewayCompatible bool   `yaml:"gateway_compatible"`
	TransparentSwap   bool   `yaml:"transparent_swap"`
	Note              string `yaml:"note"`
	Source            string `yaml:"source"`
	LastVerified      string `yaml:"last_verified"`
}

AuthMapping is one row in the source→target auth lookup table keyed by source-auth token (scram / iam / mtls / unauth). Defaults resolve via this table when the customer doesn't override via `target_auth_method`. Source + LastVerified carry the row's provenance (which Confluent doc the values come from, and when an engineer last checked it).

type AuthMappingRow added in v0.8.6

type AuthMappingRow struct {
	SourceAuth        string `json:"source_auth"`
	EffectiveTarget   string `json:"effective_target"`
	GatewayCompatible bool   `json:"gateway_compatible"`
	TransparentSwap   bool   `json:"transparent_swap"`
	Note              string `json:"note,omitempty"`
	// Source + LastVerified carry per-row provenance from the
	// auth_mapping table in plan-config.yaml — surfaced in the
	// rendered Plan as a footnote so the reviewer can audit where the
	// recommendation comes from.
	Source       string `json:"source,omitempty"`
	LastVerified string `json:"last_verified,omitempty"`
}

AuthMappingRow describes one source→target option. TransparentSwap fires when the CC Gateway can swap credentials without a producer restart (e.g. SCRAM → SASL/PLAIN with API key). GatewayCompatible is false for IAM — IAM clients cannot connect to the gateway and must pre-migrate to SCRAM or mTLS first.

type ClusterCutoverOverride added in v0.8.6

type ClusterCutoverOverride struct {
	ClusterID             string            `json:"cluster_id"`
	Style                 CutoverStyle      `json:"style"`
	SubPattern            CutoverSubPattern `json:"sub_pattern,omitempty"`
	GatewayMediated       GatewayMediated   `json:"gateway_mediated"`
	OverrideRejected      bool              `json:"override_rejected,omitempty"`
	RejectedOverrideValue string            `json:"rejected_override_value,omitempty"`
}

ClusterCutoverOverride captures a single cluster that resolves to a different cutover style than the fleet-wide default. Only the fields that can differ from the fleet decision are carried — alternatives and prereqs come from the fleet entry.

OverrideRejected mirrors AuthDecision's same-named field — set when the customer supplied a per-cluster `downtime_tolerance` (or `sub_pattern`) value that wasn't in the recognised enum. JSON consumers can detect rejected per-cluster overrides structurally without scanning OpenQuestion titles.

type ClusterLinking

type ClusterLinking struct {
	SourceMinKafkaVersion string `yaml:"source_min_kafka_version"`
	ExpressTierSupported  string `yaml:"express_tier_supported"`
	Source                string `yaml:"source"`
}

type ClusterPlanInputs added in v0.8.6

type ClusterPlanInputs struct {
	SLATarget                            *string  `yaml:"sla_target,omitempty"                                 json:"sla_target,omitempty"`
	HeadroomFraction                     *float64 `yaml:"headroom_fraction,omitempty"                          json:"headroom_fraction,omitempty"`
	SpikyWorkloadRatio                   *float64 `yaml:"spiky_workload_ratio,omitempty"                       json:"spiky_workload_ratio,omitempty"`
	EnforceSchemasAtTheBroker            *bool    `yaml:"enforce_schemas_at_the_broker,omitempty"              json:"enforce_schemas_at_the_broker,omitempty"`
	RequiresHighThroughputRESTProduceAPI *bool    `yaml:"requires_high_throughput_rest_produce_api,omitempty"  json:"requires_high_throughput_rest_produce_api,omitempty"`
	Requires9995SLAWithinSingleZone      *bool    `yaml:"requires_99_95_sla_within_a_single_zone,omitempty"    json:"requires_99_95_sla_within_a_single_zone,omitempty"`
	TargetCloud                          *string  `yaml:"target_cloud,omitempty"                               json:"target_cloud,omitempty"`
	ExistingVPCConnectivity              *string  `yaml:"existing_vpc_connectivity,omitempty"                  json:"existing_vpc_connectivity,omitempty"`
	CCEgressRequired                     *bool    `yaml:"cc_egress_required,omitempty"                         json:"cc_egress_required,omitempty"`
	ProjectedPNIGatewayCount             *int     `yaml:"projected_pni_gateway_count,omitempty"                json:"projected_pni_gateway_count,omitempty"`
	// TargetAuthMethod is the per-cluster override for the target
	// auth verdict. Same enum as the global field.
	TargetAuthMethod *string `yaml:"target_auth_method,omitempty" json:"target_auth_method,omitempty"`
	// DowntimeTolerance is the per-cluster override for the cutover
	// style. Same enum as the global field. Use when heterogeneous
	// fleets need different cutover styles per cluster — e.g. a
	// latency-sensitive service on Blue/Green while batch jobs run
	// Stop-Restart-Repeat. Without this, the customer would have to
	// slice the state file and run kcp once per cluster subset.
	DowntimeTolerance *string `yaml:"downtime_tolerance,omitempty" json:"downtime_tolerance,omitempty"`
	// SubPattern is the per-cluster override; only meaningful when the
	// resolved style is Stop-Restart-Repeat. Mirrors the global field.
	SubPattern *string `yaml:"sub_pattern,omitempty" json:"sub_pattern,omitempty"`
}

ClusterPlanInputs is the per-cluster override slice of PlanInputs. All fields are optional pointers so the resolver can detect "not set for this cluster" and fall back to the global default.

**Intentionally global-only** (not in this struct):

  • `sizing_percentile` — the Appendix A1 preamble names a single percentile for the whole plan; mixing P95 + P99 in one run would desync the prose. If a customer genuinely needs different percentiles per cluster, run the plan twice with different inputs and merge — explicit beats invisible.

type ClusterSizing added in v0.8.6

type ClusterSizing struct {
	ClusterID string `json:"cluster_id"`
	// SizedInMBps / SizedOutMBps hold the throughput at the configured
	// `sizing_percentile` (default p95; alternates p99, max). Read
	// `plan.inputs.sizing_percentile` to see which percentile was used.
	SizedInMBps        float64 `json:"sized_in_mbps"`
	SizedOutMBps       float64 `json:"sized_out_mbps"`
	PeakInMBps         float64 `json:"peak_in_mbps"`
	PeakOutMBps        float64 `json:"peak_out_mbps"`
	UserPartitions     int     `json:"user_partitions"`
	InternalPartitions int     `json:"internal_partitions"`
	IngressRatio       float64 `json:"ingress_ratio"`
	EgressRatio        float64 `json:"egress_ratio"`
	PartitionRatio     float64 `json:"partition_ratio"`
	MaxRatio           float64 `json:"max_ratio"`
	// MaxRatioDriver names which of the three dimensions produced MaxRatio:
	// "ingress", "egress", or "partitions". Tracked at compute time so the
	// renderer never has to do a float-equality comparison to recover it.
	MaxRatioDriver      string          `json:"max_ratio_driver,omitempty"`
	SizedECKU           int             `json:"sized_ecku"`
	SLAFloorECKU        int             `json:"sla_floor_ecku"`
	FinalECKU           int             `json:"final_ecku"`
	PeakBurstInRatio    float64         `json:"peak_burst_in_ratio"`
	PeakBurstOutRatio   float64         `json:"peak_burst_out_ratio"`
	PeakBurstECKU       int             `json:"peak_burst_ecku"`
	PeakBurstPctOfPLCap float64         `json:"peak_burst_pct_of_pl_cap"`
	SpikyIngress        bool            `json:"spiky_ingress"`
	SpikyEgress         bool            `json:"spiky_egress"`
	Citations           []FieldCitation `json:"citations"`

	// Degraded is true when throughput metrics were missing from the state
	// file. Numbers fall back to the SLA floor and the renderer surfaces
	// the gap rather than silently asserting a sized eCKU.
	Degraded       bool   `json:"degraded,omitempty"`
	DegradedReason string `json:"degraded_reason,omitempty"`

	// InputsMissing names load-bearing scan signals that weren't
	// available when sizing was computed (e.g. `"topics"`, `"acls"`,
	// `"brokers"`). The verdict from downstream decisions may still be
	// solid if driven by customer-declared flags; the renderer reads
	// this list to mark sizing as provisional rather than blanket-
	// deferring the cluster.
	InputsMissing []string `json:"inputs_missing,omitempty"`
}

type ClusterType added in v0.8.6

type ClusterType string

ClusterType represents the Confluent Cloud cluster verdict. Freight is in the Confluent product but not produced by DecideClusterType today — when a rule that emits Freight lands, add the constant then.

const (
	ClusterTypeEnterprise ClusterType = "Enterprise"
	ClusterTypeDedicated  ClusterType = "Dedicated"
)

func (ClusterType) IsValid added in v0.8.6

func (c ClusterType) IsValid() bool

type ClusterTypeDecision added in v0.8.6

type ClusterTypeDecision struct {
	ClusterID string             `json:"cluster_id"`
	Verdict   ClusterType        `json:"verdict"`
	Triggers  []HardLimitTrigger `json:"triggers,omitempty"`
	// Topology is populated for Dedicated verdicts (MZ default; SZ when the
	// 99.95% single-zone SLA rule fires). Empty for Enterprise.
	Topology Topology `json:"topology,omitempty"`
	// FinalCKU mirrors the sizing's FinalECKU under the Dedicated unit
	// (Confluent Kafka Unit). Set only when Verdict == Dedicated.
	FinalCKU *int `json:"final_cku,omitempty"`
	// InputsMissing names load-bearing scan / plan-input signals that
	// weren't available when this decision was computed (e.g. `"topics"`,
	// `"acls"`). **Observational only** — set post-hoc in
	// `PlanService.Build` after `DecideClusterType` has already run; the
	// Verdict itself was computed against whatever inputs WERE present.
	// Markdown reads this to flag the sizing column as provisional;
	// JSON consumers can branch on the list.
	InputsMissing []string `json:"inputs_missing,omitempty"`
	// EvaluatedRules is the full audit trail for the hard-limit catalog
	// against this cluster — every rule's outcome (fired / not_fired /
	// skipped) with evidence. Read-only audit shape; the renderer
	// surfaces it as a collapsed appendix.
	EvaluatedRules []RuleEvaluation `json:"evaluated_rules,omitempty"`
}

type CostReconciliationCfg added in v0.8.4

type CostReconciliationCfg struct {
	UsageFamilies []string `yaml:"usage_families"`
}

CostReconciliationCfg pins the AWS Cost Explorer usage-string families that count as MSK broker spend. Today AWS bills MSK under `Kafka.*` and `Express.*`; new broker tiers will land as new family tokens. Admins extend this list when a new tier ships without waiting for a kcp release.

type CostReconciliationSection added in v0.8.6

type CostReconciliationSection struct {
	Candidates []HiddenClusterCandidate `json:"candidates"`
}

CostReconciliationSection lists the candidate hidden MSK clusters per region. Nil when cost data is empty or the diff is clean. When cost data IS empty, the section nils and the detector emits an OQ pointing at `kcp report costs`.

type CutoverDecision added in v0.8.6

type CutoverDecision struct {
	Style                CutoverStyle         `json:"style"`
	SubPattern           CutoverSubPattern    `json:"sub_pattern,omitempty"` // only when Style == StopRestartRepeat
	GatewayMediated      GatewayMediated      `json:"gateway_mediated"`
	RecommendationStatus RecommendationStatus `json:"recommendation_status"`
	// AlternativesShown lists the styles the renderer explains for
	// trust but doesn't recommend — gives the reader the full pattern
	// set without forcing a deeper decision tree.
	AlternativesShown []CutoverStyle `json:"alternatives_shown_for_trust,omitempty"`
	Prereqs           []Prereq       `json:"prereqs,omitempty"`
}

CutoverDecision is the fleet-wide cutover plan — applied to every cluster that doesn't carry a per-cluster override.

type CutoverStyle added in v0.8.6

type CutoverStyle string

CutoverStyle is the strategic shape of the cutover. Mapped 1:1 from `downtime_tolerance` in plan-inputs.yaml.

const (
	// String values match the customer-facing tokens (hyphenated).
	// Go identifiers can't contain hyphens; the renderer translates these
	// to display labels via cutoverStyleName.
	CutoverStopRestartRepeat CutoverStyle = "Stop-Restart-Repeat"
	CutoverStopWaitRestart   CutoverStyle = "Stop-Wait-Restart"
	CutoverRestartAllAtOnce  CutoverStyle = "Restart-All-At-Once"
	CutoverBlueGreen         CutoverStyle = "Blue/Green"
)

type CutoverSubPattern added in v0.8.6

type CutoverSubPattern string

CutoverSubPattern is the team-topology choice within Stop-Restart-Repeat. Empty for any other CutoverStyle.

const (
	SubPatternUnset        CutoverSubPattern = ""
	SubPatternAppByApp     CutoverSubPattern = "app-by-app"
	SubPatternTopicByTopic CutoverSubPattern = "topic-by-topic"
)

type EffortSignal added in v0.8.6

type EffortSignal struct {
	ID    string `json:"id"`
	Label string `json:"label"`
	// Count is the integer signal value. `nil` means the signal is
	// structurally unobservable (the upstream scan didn't run);
	// `0` means the scan ran and returned zero hits.
	Count *int   `json:"count"`
	Note  string `json:"note,omitempty"`
}

EffortSignal is one quantitative input the customer's PM consumes to scope migration effort. Count is the raw integer the signal produced (e.g. number of IAM-auth clients). Count is `*int` (nil = unobservable) so a missing client-inventory scan reads as "unknown", not "zero". Note carries any caveat the spec calls out (e.g. MM2 `IdentityReplicationPolicy` undercounts checkpoint topics).

type EffortSignalsSection added in v0.8.6

type EffortSignalsSection struct {
	Signals []EffortSignal `json:"signals"`
}

EffortSignalsSection is the fleet-wide list of effort signals.

type EnterpriseCaps

type EnterpriseCaps struct {
	PerECKUIngressMBps   int    `yaml:"per_eCKU_ingress_mbps"`
	PerECKUEgressMBps    int    `yaml:"per_eCKU_egress_mbps"`
	PerECKUPartitionRate int    `yaml:"per_eCKU_partition_rate"`
	PrivateLinkMaxECKU   int    `yaml:"privatelink_max_eCKU"`
	PNIMaxECKU           int    `yaml:"pni_max_eCKU"`
	ACLCountCap          int    `yaml:"acl_count_cap"`
	Source               string `yaml:"source"`
}

type FieldCitation added in v0.8.6

type FieldCitation struct {
	Path  string `json:"path"`
	Value any    `json:"value"`
}

type GatewayMediated added in v0.8.6

type GatewayMediated string

GatewayMediated is tri-state: true / false / not_applicable. The last value fires only on Blue/Green, where the gateway doesn't sit on the cutover step at all.

const (
	GatewayMediatedTrue          GatewayMediated = "true"
	GatewayMediatedFalse         GatewayMediated = "false"
	GatewayMediatedNotApplicable GatewayMediated = "not_applicable"
)

type HardLimitTrigger added in v0.8.6

type HardLimitTrigger struct {
	RowID       string `json:"row_id"`
	Description string `json:"description"`
	Evidence    string `json:"evidence"`
	// CustomerDeclared marks rules whose only signal is a customer-set
	// `plan-inputs.yaml` flag. Renderer surfaces a cost callout on these
	// so a wrong `true` doesn't quietly flip the verdict from Enterprise
	// to Dedicated (Dedicated has a higher monthly cost).
	CustomerDeclared bool `json:"customer_declared,omitempty"`
}

type HiddenClusterCandidate added in v0.8.6

type HiddenClusterCandidate struct {
	Region         string  `json:"region"`
	InstanceType   string  `json:"instance_type"`
	TotalSpend     float64 `json:"total_spend"`
	MonthsObserved int     `json:"months_observed,omitempty"`
	DaysObserved   int     `json:"days_observed,omitempty"`
}

HiddenClusterCandidate is one MSK instance type that shows up in the AWS cost report but was NOT discovered by `kcp discover`. Sorted by TotalSpend desc; the customer (FinOps / cloud lead) decides which candidates are real.

type Networking added in v0.8.6

type Networking string

Networking represents the per-cluster networking verdict.

const (
	NetworkingPrivateLink    Networking = "PrivateLink"
	NetworkingPNI            Networking = "PNI"
	NetworkingTransitGateway Networking = "TransitGateway"
	NetworkingVPCPeering     Networking = "VPCPeering"
)

func (Networking) IsValid added in v0.8.6

func (n Networking) IsValid() bool

type NetworkingDecision added in v0.8.6

type NetworkingDecision struct {
	ClusterID       string     `json:"cluster_id"`
	Verdict         Networking `json:"verdict"`
	PeakBurstECKU   int        `json:"peak_burst_ecku"`
	PercentageOfCap float64    `json:"percentage_of_cap"`
	Reason          string     `json:"reason"`
	// InputsMissing — see ClusterTypeDecision.InputsMissing. Populated
	// in lockstep so JSON consumers see the same gating signal on both
	// decisions for an affected cluster. **JSON-consumer-only**: the
	// markdown renderer reads the sister field on `ClusterTypeDecision`
	// (and the same list on `ClusterSizing`) — this copy is here so
	// downstream JSON branching doesn't need a cross-reference lookup.
	InputsMissing []string `json:"inputs_missing,omitempty"`
}

type OpenQuestion added in v0.8.6

type OpenQuestion struct {
	ID         string `json:"id"`
	ClusterID  string `json:"cluster_id,omitempty"`
	Title      string `json:"title"`
	Body       string `json:"body"`
	HowToClose string `json:"how_to_close"`
}

OpenQuestion is a per-cluster (or plan-level) gap the customer needs to close before the Plan recommendation is fully reliable. State-file gaps (missing metrics, missing ACLs) resolve by re-running a `kcp scan` command; inferred-signal questions (spiky workload) resolve by acknowledging or overriding the input.

type Plan added in v0.8.6

type Plan struct {
	Header              PlanHeader            `json:"header"`
	Inputs              PlanInputsResolved    `json:"inputs"`
	SourceEnvironment   SourceEnvironment     `json:"source_environment"`
	Sizing              []ClusterSizing       `json:"sizing"`
	ClusterTypeDecision []ClusterTypeDecision `json:"cluster_type_decision"`
	NetworkingDecision  []NetworkingDecision  `json:"networking_decision"`
	// Cutover is the fleet-wide cutover decision — the default style
	// applied to every cluster that doesn't carry a per-cluster
	// override in CutoverOverrides. Nil when no clusters were found in
	// the state file.
	Cutover *CutoverDecision `json:"cutover,omitempty"`
	// CutoverOverrides carries clusters whose resolved
	// `downtime_tolerance` (or `sub_pattern`) differs from the fleet
	// — only the clusters that diverge appear here, so an
	// all-homogeneous fleet keeps this slice empty. Heterogeneous
	// fleets previously had to slice the state file and run kcp once
	// per subset; per-cluster overrides remove that workaround.
	CutoverOverrides []ClusterCutoverOverride `json:"cutover_overrides,omitempty"`
	// Auth is per-cluster — source auth methods differ across MSK clusters
	// in the same fleet, so each gets its own source→target mapping.
	Auth []AuthDecision `json:"auth,omitempty"`
	// Schema is fleet-wide (the schema registry is one shared system,
	// not per-cluster). Nil when the section is omitted — currently the
	// `schemaless` path (`sr_detected == none` AND
	// `schema_strategy == no_schemas`).
	Schema *SchemaDecision `json:"schema,omitempty"`
	// RedFlags surfaces the boolean trigger rows over the Plan + state
	// file. Triggered rows are items to discuss with the SE; each row
	// carries its own evidence (field path + value) so the discussion
	// is grounded in the scan, not on inference.
	RedFlags *RedFlagsSection `json:"red_flags,omitempty"`
	// EffortSignals is the list of quantitative signals the customer's
	// PM consumes to scope migration effort. Counts only, no
	// day-estimate.
	EffortSignals *EffortSignalsSection `json:"effort_signals,omitempty"`
	// TieredStorage is a per-cluster section describing the
	// three-dimension trade-off (mechanism / duration / cost direction)
	// for clusters with MSK tiered storage enabled. Nil when no source
	// cluster has TIERED storage.
	TieredStorage *TieredStorageSection `json:"tiered_storage,omitempty"`
	// CostReconciliation lists MSK clusters that show up in the AWS
	// cost report but were NOT discovered by `kcp discover`. Sorted by
	// TotalSpend desc. Nil when cost data is empty or the diff is
	// clean.
	CostReconciliation *CostReconciliationSection `json:"cost_reconciliation,omitempty"`
	SizingAppendix     []SizingMathDetail         `json:"sizing_appendix"`
	OpenQuestions      []OpenQuestion             `json:"open_questions,omitempty"`
}

Plan is the deterministic Migration Plan emitted by `kcp report plan`. Scope: source-environment summary, sizing, cluster-type, networking, cutover, auth (per-cluster), schema migration, red flags, effort signals, tiered storage, and cost-vs-inventory reconciliation. Each section is optional in the JSON and the renderer skips empty ones.

Empty-section conventions across the struct:

  • Always-present per-cluster slices — `Sizing`, `ClusterTypeDecision`, `NetworkingDecision`, `SizingAppendix`. No `omitempty`. JSON renders `[]` when the fleet is empty so a consumer can tell "fleet had no clusters" from "the key is missing because the field was renamed".

  • Conditionally-present per-cluster slices — `Auth`, `CutoverOverrides`, `OpenQuestions`. Tagged `omitempty`, so an empty slice (or nil) drops the key from the JSON entirely. Used when the section either runs for every cluster but may be empty (no clusters found in the state file) or runs only when there's something to say (no overrides → no key; no OQs → no key).

  • Fleet-wide pointer sections — `Cutover`, `Schema`, `RedFlags`, `EffortSignals`, `TieredStorage`, `CostReconciliation`. Tagged `omitempty`. Nil means "section omitted entirely" (no source data, or the path is intentionally skipped, e.g. schemaless).

In all three cases the renderer hides the corresponding §section when the JSON value is empty / nil.

type PlanConfig

type PlanConfig struct {
	SchemaVersion            int    `yaml:"schema_version"`
	LastVerified             string `yaml:"last_verified"`
	KCPVersionAtVerification string `yaml:"kcp_version_at_verification"`

	EnterpriseCaps     EnterpriseCaps         `yaml:"enterprise_caps"`
	ClusterLinking     ClusterLinking         `yaml:"cluster_linking"`
	SchemaLinking      SchemaLinking          `yaml:"schema_linking"`
	PlanInputDefaults  PlanInputDefaults      `yaml:"plan_input_defaults"`
	AuthMapping        map[string]AuthMapping `yaml:"auth_mapping"`
	Thresholds         Thresholds             `yaml:"thresholds"`
	CostReconciliation CostReconciliationCfg  `yaml:"cost_reconciliation"`
}

PlanConfig is the deserialized plan-config.yaml. The embedded copy is the default; an admin-supplied override file replaces only the fields it specifies (standard yaml unmarshal semantics — slices replace whole, maps merge by key).

func LoadPlanConfig

func LoadPlanConfig(overridePath string) (*PlanConfig, error)

LoadPlanConfig returns the embedded plan-config.yaml, optionally overridden by the file at overridePath. Pass an empty string to skip the override.

func (*PlanConfig) Validate

func (c *PlanConfig) Validate() error

type PlanHeader added in v0.8.6

type PlanHeader struct {
	Source        string    `json:"source"`
	StateFilePath string    `json:"state_file_path"`
	KCPVersion    string    `json:"kcp_version"`
	GeneratedAt   time.Time `json:"generated_at"`
	// StateGeneratedAt is the timestamp the source state file was
	// produced (state.Timestamp). Surfaced in the rendered Plan so a
	// reviewer can see how fresh the underlying scan is — important
	// for negative-evidence claims like "0 ACLs" in Appendix A2.
	StateGeneratedAt time.Time `json:"state_generated_at,omitempty"`

	// PlanSchemaVersion identifies the JSON shape of the Plan. Stable
	// at "1" now that the full deterministic decision set ships
	// (sizing / cluster type / networking / cutover / auth / schema /
	// red flags / effort signals / tiered storage / cost
	// reconciliation). Additive top-level keys at this version are
	// allowed; renames or removals require a version bump.
	PlanSchemaVersion string `json:"plan_schema_version"`
}

type PlanInputDefaults

type PlanInputDefaults struct {
	SizingPercentile   string         `yaml:"sizing_percentile"`
	HeadroomFraction   float64        `yaml:"headroom_fraction"`
	SpikyWorkloadRatio float64        `yaml:"spiky_workload_ratio"`
	SLAFloorECKU       map[string]int `yaml:"sla_floor_eCKU"`

	// Customer-declared hard requirements (all default false).
	EnforceSchemasAtTheBroker            bool `yaml:"enforce_schemas_at_the_broker"`
	RequiresHighThroughputRESTProduceAPI bool `yaml:"requires_high_throughput_rest_produce_api"`
	Requires9995SLAWithinSingleZone      bool `yaml:"requires_99_95_sla_within_a_single_zone"`

	// Target cloud + existing VPC connectivity (Dedicated-path networking).
	TargetCloud             string `yaml:"target_cloud"`
	ExistingVPCConnectivity string `yaml:"existing_vpc_connectivity"`

	// Networking triggers (PNI→PrivateLink) — see PlanInputsResolved.
	CCEgressRequired         bool `yaml:"cc_egress_required"`
	ProjectedPNIGatewayCount int  `yaml:"projected_pni_gateway_count"`

	// Cutover defaults.
	DowntimeTolerance            string `yaml:"downtime_tolerance"`
	SubPattern                   string `yaml:"sub_pattern"`
	PreferGateway                bool   `yaml:"prefer_gateway"`
	ConfluentForKubernetesStatus string `yaml:"confluent_for_kubernetes_status"`
	CCGatewayLicenseStatus       string `yaml:"cc_gateway_license_status"`
	IAMPreMigrationStatus        string `yaml:"iam_pre_migration_status"`

	// Auth defaults.
	TargetAuthMethod string `yaml:"target_auth_method"`

	// Schema-migration defaults. Defaults below resolve to
	// `unknown` so first-run plans always ask the customer to declare
	// strategy + reachability + CP version/edition rather than silently
	// picking a path.
	SchemaStrategy       string `yaml:"schema_strategy"`
	ConfluentSRCPVersion string `yaml:"confluent_sr_cp_version,omitempty"`
	ConfluentSRCPEdition string `yaml:"confluent_sr_cp_edition,omitempty"`
}

type PlanInputs added in v0.8.6

type PlanInputs struct {
	// Sizing overrides (defaults live in plan-config.yaml).
	SLATarget          *string  `yaml:"sla_target,omitempty"          json:"sla_target,omitempty"`
	SizingPercentile   *string  `yaml:"sizing_percentile,omitempty"   json:"sizing_percentile,omitempty"`
	HeadroomFraction   *float64 `yaml:"headroom_fraction,omitempty"   json:"headroom_fraction,omitempty"`
	SpikyWorkloadRatio *float64 `yaml:"spiky_workload_ratio,omitempty" json:"spiky_workload_ratio,omitempty"`

	// Customer-declared hard requirements that force Dedicated.
	// Default `false`; only the customer (or an SE on their behalf) sets these.
	EnforceSchemasAtTheBroker            *bool `yaml:"enforce_schemas_at_the_broker,omitempty"            json:"enforce_schemas_at_the_broker,omitempty"`
	RequiresHighThroughputRESTProduceAPI *bool `yaml:"requires_high_throughput_rest_produce_api,omitempty" json:"requires_high_throughput_rest_produce_api,omitempty"`
	Requires9995SLAWithinSingleZone      *bool `yaml:"requires_99_95_sla_within_a_single_zone,omitempty"  json:"requires_99_95_sla_within_a_single_zone,omitempty"`

	// When source auth includes mTLS AND target_cloud is not `aws`,
	// Dedicated is required because only Dedicated supports mTLS on
	// Azure / GCP. MSK is AWS-only so this defaults to `aws`.
	TargetCloud *string `yaml:"target_cloud,omitempty" json:"target_cloud,omitempty"`

	// Existing VPC connectivity to MSK. When set to `transit_gateway`
	// or `vpc_peering` and the cluster is Dedicated, the same pattern
	// is recommended for Confluent Cloud. `privatelink_or_pni` (default)
	// keeps the AWS-to-AWS Enterprise default (PNI) unless one of the
	// PrivateLink triggers fires (see CCEgressRequired,
	// ProjectedPNIGatewayCount, non-AWS target_cloud).
	ExistingVPCConnectivity *string `yaml:"existing_vpc_connectivity,omitempty" json:"existing_vpc_connectivity,omitempty"`

	// CCEgressRequired = "Do CC-side workloads need to push traffic
	// back into your customer VPC?" PNI does not natively support
	// egress from CC into customer infrastructure; when true the
	// networking default flips from PNI to PrivateLink.
	CCEgressRequired *bool `yaml:"cc_egress_required,omitempty" json:"cc_egress_required,omitempty"`

	// ProjectedPNIGatewayCount — projected PNI gateway count for the
	// deployment. When ≥ 2, the recommendation flips from PNI to
	// PrivateLink. Default 1 (single gateway).
	ProjectedPNIGatewayCount *int `yaml:"projected_pni_gateway_count,omitempty" json:"projected_pni_gateway_count,omitempty"`

	// DowntimeTolerance maps 1:1 to a cutover style. Enum values:
	//   zero                          → Blue/Green
	//   seconds_per_service           → Stop-Restart-Repeat (gateway REQUIRED)
	//   minutes_per_service           → Stop-Restart-Repeat (gateway optional)
	//   scheduled_window_sequential   → Stop-Wait-Restart
	//   scheduled_window_all_at_once  → Restart-All-At-Once
	//   let_confluent_choose          → Stop-Restart-Repeat (Confluent default)
	DowntimeTolerance *string `yaml:"downtime_tolerance,omitempty" json:"downtime_tolerance,omitempty"`

	// SubPattern is only consulted when the resolved style is
	// Stop-Restart-Repeat. Values: `app-by-app` (default) | `topic-by-topic`.
	SubPattern *string `yaml:"sub_pattern,omitempty" json:"sub_pattern,omitempty"`

	// PreferGateway lets the customer opt out of the gateway-mediated
	// recommendation even when they're eligible. Default true mirrors
	// Confluent's canonical recommendation; set false for plain Cluster
	// Linking.
	PreferGateway *bool `yaml:"prefer_gateway,omitempty" json:"prefer_gateway,omitempty"`

	// Gateway prereq statuses. Each is one of `not_started` |
	// `in_progress` | `complete`. Eligibility for the canonical
	// recommendation requires all three to be at `in_progress` or
	// `complete` (with the IAM prereq only relevant when IAM is
	// detected on any source cluster).
	ConfluentForKubernetesStatus *string `yaml:"confluent_for_kubernetes_status,omitempty" json:"confluent_for_kubernetes_status,omitempty"`
	CCGatewayLicenseStatus       *string `yaml:"cc_gateway_license_status,omitempty"       json:"cc_gateway_license_status,omitempty"`
	IAMPreMigrationStatus        *string `yaml:"iam_pre_migration_status,omitempty"        json:"iam_pre_migration_status,omitempty"`

	// TargetAuthMethod overrides the default target auth (looked up
	// per source auth in `auth_mapping`). Enum:
	// `confluent_cloud_api_keys` (default) | `mtls` | `oauth`.
	// Per-cluster override available via `clusters[name].target_auth_method`.
	TargetAuthMethod *string `yaml:"target_auth_method,omitempty" json:"target_auth_method,omitempty"`

	// SchemaStrategy declares the customer's intent for schemas. Enum:
	//   unknown                          → emit OQ (default; first-run safety)
	//   no_schemas                       → schemaless path (omit §Schema)
	//   adopt_schemas_during_migration   → start with no source SR, adopt CC SR
	//   migrate_existing_schema_registry → run the SR-detected path
	SchemaStrategy *string `yaml:"schema_strategy,omitempty" json:"schema_strategy,omitempty"`

	// SourceSROutboundReachableToCC is the customer's network-reachability
	// declaration: can the source Confluent SR reach the CC SR endpoint
	// outbound? Schema Linking's Schema Exporter is one-directional from
	// source SR → CC SR, so without this the gateway-eligible verdict
	// can't hold. Default nil = unknown.
	SourceSROutboundReachableToCC *bool `yaml:"source_sr_outbound_reachable_to_cc,omitempty" json:"source_sr_outbound_reachable_to_cc,omitempty"`

	// ConfluentSRCPVersion is the customer-declared Confluent Platform
	// version of the source Schema Registry. Schema Linking requires
	// CP 7.0 or later. Default nil = unknown — the scanner does not
	// populate this today, so the customer declares it via plan-inputs.
	// Accepted shape: "7.5.1" / "7.0" / "6.2" — string compare against
	// the configured floor.
	ConfluentSRCPVersion *string `yaml:"confluent_sr_cp_version,omitempty" json:"confluent_sr_cp_version,omitempty"`

	// ConfluentSRCPEdition is the customer-declared Confluent Platform
	// edition. Schema Linking requires `enterprise` (CP 7.0 Community
	// does not include it). Enum: `enterprise` | `community`.
	ConfluentSRCPEdition *string `yaml:"confluent_sr_cp_edition,omitempty" json:"confluent_sr_cp_edition,omitempty"`

	// ExactlyOnceTransactionsInUse declares whether the source has
	// EOS / Kafka transactions enabled. There is no detectable state
	// signal for this; customer-only. Default nil = unknown — the
	// Red Flag row surfaces as "not scanned" rather than firing false.
	ExactlyOnceTransactionsInUse *bool `yaml:"exactly_once_transactions_in_use,omitempty" json:"exactly_once_transactions_in_use,omitempty"`

	// KafkaStreamsInUse declares whether Kafka Streams apps consume
	// from the source. The Red Flag detector also runs a
	// topic-pattern scan (`-changelog` / `-repartition`); this flag
	// lets the customer affirm even when topic patterns miss.
	KafkaStreamsInUse *bool `yaml:"kafka_streams_in_use,omitempty" json:"kafka_streams_in_use,omitempty"`

	// ConsumerHistoryRequirement declares whether downstream consumers
	// actually need to replay historical (tiered) data. Enum:
	//   required (default)  → backfill is required; weigh the 3
	//                         dimensions (mechanism / duration / cost)
	//   not_required        → real-time-only consumers; defer to the
	//                         account team for the cascade of
	//                         per-topic + consumer-offset decisions
	//   unknown             → customer hasn't decided yet
	ConsumerHistoryRequirement *string `yaml:"consumer_history_requirement,omitempty" json:"consumer_history_requirement,omitempty"`

	// HistoricalDataStrategy is the customer's preferred path when
	// tiered-storage backfill IS required. Enum:
	//   keep_msk_running_until_data_expires
	//   bulk_load_historical_via_external_tool
	//   defer_to_account_team   (default for the not_required cascade)
	HistoricalDataStrategy *string `yaml:"historical_data_strategy,omitempty" json:"historical_data_strategy,omitempty"`

	// Clusters — per-cluster overrides keyed by source cluster name.
	// Heterogeneous fleets (mixed-SLA, mixed-tier) need finer-grained
	// inputs than the global flags above; without this, flipping a
	// single global flag like `requires_99_95_sla_within_a_single_zone:
	// true` would force Dedicated SZ on every cluster including
	// idle / Zookeeper / Serverless ones. Resolution: per-cluster
	// override wins; fall back to global default. Only the subset of
	// fields that makes sense per-cluster lives here.
	Clusters map[string]ClusterPlanInputs `yaml:"clusters,omitempty" json:"clusters,omitempty"`
}

PlanInputs is the parsed plan-inputs.yaml. Pointers distinguish "unset" from the zero value so the loader can echo only the fields the customer explicitly set.

Customer-declared flags that flip the cluster-type verdict (`EnforceSchemasAtTheBroker`, `RequiresHighThroughputRESTProduceAPI`, `Requires9995SLAWithinSingleZone`) are wrong-click sensitive — a misfired `true` quietly raises the monthly cost. The renderer surfaces a ⚠ cost callout on the Dedicated verdict when any of them is the reason.

func LoadPlanInputs

func LoadPlanInputs(path string) (*PlanInputs, error)

LoadPlanInputs reads plan-inputs.yaml. Returns nil *PlanInputs if path is empty (no file supplied is a valid state — the Plan still generates from defaults). Missing fields never fail because no field is required at the YAML schema level.

type PlanInputsResolved added in v0.8.6

type PlanInputsResolved struct {
	// Raw preserves the original customer-supplied `PlanInputs`. **Note:**
	// the flat fields below carry the *global* resolved view; per-cluster
	// overrides (`Raw.Clusters[<name>]`) are layered on top per cluster
	// inside `PlanService.Build` via `ResolvePlanInputsForCluster` — they
	// are NOT applied to the values stored here. JSON consumers wanting
	// the per-cluster view should re-run the resolver against `Raw`.
	Raw *PlanInputs `json:"raw,omitempty"`

	SLATarget          string  `json:"sla_target"`
	SizingPercentile   string  `json:"sizing_percentile"`
	HeadroomFraction   float64 `json:"headroom_fraction"`
	SpikyWorkloadRatio float64 `json:"spiky_workload_ratio"`

	// Customer-declared hard requirements. Booleans (not *bool) — defaults
	// resolve to false, which is the safe verdict (no escalation to Dedicated).
	EnforceSchemasAtTheBroker            bool `json:"enforce_schemas_at_the_broker"`
	RequiresHighThroughputRESTProduceAPI bool `json:"requires_high_throughput_rest_produce_api"`
	Requires9995SLAWithinSingleZone      bool `json:"requires_99_95_sla_within_a_single_zone"`

	// Target cloud + existing VPC connectivity (Dedicated-path networking).
	TargetCloud             string `json:"target_cloud"`
	ExistingVPCConnectivity string `json:"existing_vpc_connectivity"`

	// Networking triggers that flip the AWS-Enterprise default from PNI
	// to PrivateLink. CCEgressRequired and ProjectedPNIGatewayCount are
	// workload properties, not state-derived.
	CCEgressRequired         bool `json:"cc_egress_required"`
	ProjectedPNIGatewayCount int  `json:"projected_pni_gateway_count"`

	// Cutover — `downtime_tolerance` drives the style; the others
	// govern gateway eligibility + opt-out.
	DowntimeTolerance            string `json:"downtime_tolerance"`
	SubPattern                   string `json:"sub_pattern"`
	PreferGateway                bool   `json:"prefer_gateway"`
	ConfluentForKubernetesStatus string `json:"confluent_for_kubernetes_status"`
	CCGatewayLicenseStatus       string `json:"cc_gateway_license_status"`
	IAMPreMigrationStatus        string `json:"iam_pre_migration_status"`

	// Auth — target verdict; cluster-level override flows through
	// `applyClusterOverride` per the heterogeneous-fleet rule.
	TargetAuthMethod string `json:"target_auth_method"`

	// Schema migration. Strings/bools resolved from the customer
	// input with `unknown` / nil-tri-state semantics: SchemaStrategy
	// defaults to `unknown` so first-run plans don't fire spurious
	// schemaless verdicts. The CP version + edition + outbound
	// reachability stay nil-tri-state because "false" and "unknown"
	// produce different OQs.
	SchemaStrategy                string `json:"schema_strategy"`
	SourceSROutboundReachableToCC *bool  `json:"source_sr_outbound_reachable_to_cc,omitempty"`
	ConfluentSRCPVersion          string `json:"confluent_sr_cp_version,omitempty"`
	ConfluentSRCPEdition          string `json:"confluent_sr_cp_edition,omitempty"`

	// Red Flags customer flags — nil tri-state.
	ExactlyOnceTransactionsInUse *bool `json:"exactly_once_transactions_in_use,omitempty"`
	KafkaStreamsInUse            *bool `json:"kafka_streams_in_use,omitempty"`

	// Tiered Storage knobs. Strings normalized to lowercase tokens by
	// the resolver. `ConsumerHistoryRequirement` empty means "not
	// declared" and is defaulted to `required` by the detector (so the
	// section surfaces the trade-off rather than the unknown-OQ
	// branch). `HistoricalDataStrategy` empty is "not declared" and
	// stays empty unless `ConsumerHistoryRequirement == not_required`,
	// in which case it cascades to `defer_to_account_team`.
	ConsumerHistoryRequirement string `json:"consumer_history_requirement,omitempty"`
	HistoricalDataStrategy     string `json:"historical_data_strategy,omitempty"`
}

PlanInputsResolved is the customer's PlanInputs merged with pinned defaults from plan-config.yaml. Raw preserves the original customer input (nil pointers for fields they didn't set); the flat fields below are the merged values PlanService computes against.

func ResolvePlanInputs

func ResolvePlanInputs(in *PlanInputs, cfg *PlanConfig) PlanInputsResolved

ResolvePlanInputs merges customer-supplied PlanInputs with pinned defaults from PlanConfig. Customer-set fields win; everything else falls back to PlanConfig.PlanInputDefaults. Raw is preserved so downstream consumers can detect which sizing fields the customer explicitly set (HeadroomFraction, SLATarget, SizingPercentile, etc.).

func ResolvePlanInputsForCluster

func ResolvePlanInputsForCluster(in *PlanInputs, cfg *PlanConfig, clusterName string) PlanInputsResolved

ResolvePlanInputsForCluster layers a cluster-specific override on top of the resolved globals. If `in.Clusters[clusterName]` is set, every non-nil field in it wins over the global. Otherwise the global view is returned unchanged. Caller uses this per-cluster during Plan build so heterogeneous fleets get the right verdicts without one global flag flipping every cluster's tier.

**Hot path note:** when iterating many clusters, prefer `applyClusterOverride` against a pre-resolved global view — this function re-resolves globals on every call.

type PlanService

type PlanService struct {
	// contains filtered or unexported fields
}

Function-naming convention across this package:

  • `decide*` — produces a verdict / path from inputs. One decision per cluster (sizing, cluster type, networking, auth) or one per fleet (cutover, schema). Output is the rendered §section's primary recommendation.

  • `detect*` — enumerates findings or signals from `state` / `plan` (red flags, effort signals, tiered storage, cost reconciliation, all OQ detectors). Output is a list/section, not a single verdict. Some detectors also apply small input cascades (e.g. `detectTieredStorage` defaults `HistoricalDataStrategy`) — that's deliberate: the cascade is scoped to the section's findings, not a standalone decision.

  • `compute*` — pure numeric / structural transforms with no verdict (e.g. `computeClusterSizing`, `computeCutoverOverrides`). Output is intermediate data the renderer + decide* consume.

PlanService orchestrates the deterministic plan-generation pipeline. Each Build step is a pure function so the test surface is the orchestration, not its parts.

func NewPlanService

func NewPlanService(cfg *PlanConfig, now func() time.Time) *PlanService

NewPlanService wires a PlanConfig into a PlanService. now is overridable for deterministic tests; pass nil for time.Now.

func (*PlanService) Build

func (s *PlanService) Build(state report.ProcessedState, inputs PlanInputsResolved, stateFilePath string) (*Plan, error)

Build produces a Plan from a ProcessedState and resolved plan-inputs. Each step is a pure function so the test surface is the orchestration, not its parts.

type Prereq added in v0.8.6

type Prereq struct {
	Description string       `json:"description"`
	Status      PrereqStatus `json:"status"`
}

Prereq is one item in the rendered Prerequisites table on the cutover section. Description is the human-readable label; the renderer doesn't transform it.

type PrereqStatus added in v0.8.6

type PrereqStatus string

PrereqStatus mirrors the customer-facing plan-inputs status values (`not_started` → blocked, `in_progress` → in-progress, `complete` → met) plus an `unconfirmed` fallback for prereqs whose source data isn't pinned (e.g. Express tier compatibility per release).

const (
	PrereqMet         PrereqStatus = "met"
	PrereqInProgress  PrereqStatus = "in_progress"
	PrereqBlocked     PrereqStatus = "blocked"
	PrereqUnconfirmed PrereqStatus = "unconfirmed"
)

type RecommendationStatus added in v0.8.6

type RecommendationStatus string

RecommendationStatus signals how confident the plan is in the recommendation, and what (if anything) the customer should resolve to move it forward.

const (
	// RecommendationCanonical: customer is gateway-eligible and the
	// Stop-Restart-Repeat + Gateway pairing is being recommended.
	RecommendationCanonical RecommendationStatus = "canonical"
	// RecommendationCustomerChoice: customer either opted out of the
	// gateway (prefer_gateway: false) or picked Blue/Green.
	RecommendationCustomerChoice RecommendationStatus = "customer_choice"
	// RecommendationDegradedAwaitingOQ: prefer_gateway is still default
	// and all three gateway prereqs are not_started — customer hasn't
	// engaged with the gateway question. Plan falls back to plain CL.
	RecommendationDegradedAwaitingOQ RecommendationStatus = "degraded_awaiting_oq"
	// RecommendationDegradedPrereqsPending: prefer_gateway is true but
	// at least one prereq is still at not_started. Some engagement,
	// not finished.
	RecommendationDegradedPrereqsPending RecommendationStatus = "degraded_prereqs_pending"
)

type RedFlag added in v0.8.6

type RedFlag struct {
	ID     string        `json:"id"`
	Title  string        `json:"title"`
	Status RedFlagStatus `json:"status"`
	// Evidence is the human-readable prose surfaced in the rendered
	// Plan. Keep it under control of the row's evaluator so a reader
	// can tell at a glance WHY the row fired.
	Evidence string `json:"evidence,omitempty"`
	// EvidenceFields carries the structured signals the evaluator
	// computed: scalar counts, cluster lists, version strings, etc.
	// Downstream JSON consumers branch on these instead of parsing
	// `Evidence`. Stable shape (additive only) at
	// `plan_schema_version: "1"`.
	EvidenceFields map[string]any `json:"evidence_fields,omitempty"`
	ClusterID      string         `json:"cluster_id,omitempty"`
}

RedFlag is one row in §Red Flags. Title is the customer-facing label; Evidence is the field path + value that drove the verdict so the SE-customer discussion can ground in scan facts. ClusterID is populated only for per-cluster rows; fleet-level rows leave it empty.

type RedFlagStatus added in v0.8.6

type RedFlagStatus string

RedFlagStatus is the tri-state verdict for one Red Flag row:

  • `triggered` — the boolean predicate over the state file is true.
  • `not_triggered` — the predicate is false and we have enough scan data to say so with confidence.
  • `unknown` — the underlying signal isn't available (scan didn't run, customer-declared flag wasn't set, etc.). The rendered Plan surfaces it as "not scanned" rather than silently defaulting to `not_triggered`.
const (
	RedFlagTriggered    RedFlagStatus = "triggered"
	RedFlagNotTriggered RedFlagStatus = "not_triggered"
	RedFlagUnknown      RedFlagStatus = "unknown"
)

type RedFlagsSection added in v0.8.6

type RedFlagsSection struct {
	Rows []RedFlag `json:"rows"`
}

RedFlagsSection is the fleet-wide Red Flags decision output. Rows is the full set evaluated in row order; the renderer leads with triggered rows and collapses not-triggered/unknown into a tail summary.

type RuleEvaluation added in v0.8.6

type RuleEvaluation struct {
	RowID       string      `json:"row_id"`
	Description string      `json:"description"`
	Outcome     RuleOutcome `json:"outcome"`
	Evidence    string      `json:"evidence,omitempty"`
	SkipReason  string      `json:"skip_reason,omitempty"`
}

RuleEvaluation is one row of the per-cluster hard-limit audit trail. Triggers (above) carries only the fired rules — this carries every evaluation including not-fired and skipped, so the rendered appendix can show negative evidence ("ACL cap not exceeded: 47 < 4000") and skip rationales ("AclsScanned == false; rule inconclusive").

type RuleOutcome added in v0.8.6

type RuleOutcome string

RuleOutcome enumerates the possible outcomes of evaluating one hard-limit rule against a cluster. Persisted in the audit trail so a reviewer can replay "what would the rules engine have said given this state" without re-running the tool.

The string values below ("fired" / "not_fired" / "skipped") are **stable** and intended for downstream consumers to match on by equality. Don't rename without a coordinated migration of consumers + a plan_schema_version bump.

const (
	RuleFired    RuleOutcome = "fired"     // rule fired — its evidence is recorded
	RuleNotFired RuleOutcome = "not_fired" // rule evaluated and explicitly didn't fire
	RuleSkipped  RuleOutcome = "skipped"   // rule was not evaluated (missing inputs)
)

type SchemaDecision added in v0.8.6

type SchemaDecision struct {
	Source SchemaSource `json:"source"`
	// Paths lists every recommended verdict that applies, in
	// rendering order. Single-source cases have len(Paths)==1; the
	// dual-source `confluent_and_glue` case has 2 entries (Glue
	// first — it's the automatable path; Confluent path second).
	//
	// **JSON-consumer note for dual-source.** When Source is
	// `confluent_and_glue` and len(Paths)==1 (only the Glue arm
	// landed), the Confluent arm is in one of two states:
	//   (a) eligibility flags undeclared — pending. Disambiguate by
	//       reading the OpenQuestions for `schema_linking_eligibility_unknown`.
	//   (b) all three eligibility flags resolved AND at least one is
	//       false — verified ineligible. The OQ
	//       `schema_linking_ineligible` will be present.
	// Reading `MeetsCPVersionFloor` / `MeetsCPEditionRequirement` /
	// `SourceSROutboundReachable` tri-states gives the same signal.
	Paths []SchemaPath `json:"paths"`
	// Schema-Linking eligibility constraints — all three must hold for
	// SchemaPathSchemaLinking. Populated only when Source includes
	// `confluent`; the renderer prints a 3-row eligibility table from
	// these flags. *KnownAsTrue distinguishes "verified true" from
	// "verified false" vs "unknown" — when any one is unknown the
	// resulting Paths include `unknown` and the OQ asks the customer
	// to confirm rather than guessing.
	MeetsCPVersionFloor       *bool `json:"meets_cp_version_floor,omitempty"`
	MeetsCPEditionRequirement *bool `json:"meets_cp_edition_requirement,omitempty"`
	SourceSROutboundReachable *bool `json:"source_sr_outbound_reachable,omitempty"`
	// GlueRegistries lists the Glue registry names detected on the
	// source side — surfaced in the Terraform command the renderer
	// emits for SchemaPathMigrateGlue (one apply per registry).
	GlueRegistries []string `json:"glue_registries,omitempty"`
	// ConfluentSRURLs lists the Confluent SR URLs detected. Surfaced
	// in the eligibility table header so a reader knows which SR the
	// verdict applies to.
	ConfluentSRURLs []string `json:"confluent_sr_urls,omitempty"`
}

SchemaDecision is the fleet-wide Schema Migration recommendation. Source describes what was scanned; Paths describes every verdict that applies — usually a single-element slice (one source → one path), but the dual `confluent_and_glue` case carries two so JSON consumers branching on a single path-slot don't miss the second arm.

The eligibility flags are populated only when Source includes `confluent` so the renderer can show why Schema Linking was or wasn't chosen.

type SchemaLinking added in v0.8.4

type SchemaLinking struct {
	MinCPVersion      string `yaml:"min_cp_version"`
	RequiresCPEdition string `yaml:"requires_cp_edition"`
	Source            string `yaml:"source"`
	LastVerified      string `yaml:"last_verified"`
}

SchemaLinking pins the version + edition floor that source Confluent SR must clear for the Schema Linking path. Customers below these floors get the `defer_to_account_team` verdict (REST API export / import is technically possible but kcp doesn't drive it). The values come from the [Schema Linking on CP docs](https://docs.confluent.io/platform/current/schema-registry/schema-linking-cp.html); last-verified date below tracks when an engineer cross-checked them.

type SchemaPath added in v0.8.6

type SchemaPath string

SchemaPath is the recommended migration path:

  • `schema_linking` — Schema Linking from source Confluent SR → CC SR (zero-data-loss mirror; all three eligibility constraints hold).
  • `kcp_migrate_schemas_glue` — `kcp create-asset migrate-schemas --glue-registry` generates Terraform that imports every Glue schema into CC SR in one apply.
  • `defer_to_account_team` — Confluent SR detected but Schema-Linking eligibility fails (CP < 7.0, Community edition, or no outbound reachability). REST API export/import is technically possible but not deterministically described by kcp.
  • `schemaless` — no source SR + customer declared `no_schemas`.
  • `unknown` — fallback when an Open Question must close before the path is decidable (typically `schema_strategy: unknown`).
const (
	SchemaPathSchemaLinking  SchemaPath = "schema_linking"
	SchemaPathMigrateGlue    SchemaPath = "kcp_migrate_schemas_glue"
	SchemaPathDeferToAccount SchemaPath = "defer_to_account_team"
	SchemaPathSchemaless     SchemaPath = "schemaless"
	SchemaPathUnknown        SchemaPath = "unknown"
)

type SchemaSource added in v0.8.6

type SchemaSource string

SchemaSource is the detected source-side schema registry, derived from the scanner's `state.schema_registries`. `none` means the scanner ran but found neither a Confluent SR nor a Glue registry. `confluent_and_glue` covers the rare both-registries deployment — the decision applies each path independently.

const (
	SchemaSourceNone             SchemaSource = "none"
	SchemaSourceConfluent        SchemaSource = "confluent"
	SchemaSourceGlue             SchemaSource = "glue"
	SchemaSourceConfluentAndGlue SchemaSource = "confluent_and_glue"
)

type SizingMathDetail added in v0.8.6

type SizingMathDetail struct {
	ClusterID         string          `json:"cluster_id"`
	Formula           string          `json:"formula"`
	IntermediateSteps []string        `json:"intermediate_steps"`
	Citations         []FieldCitation `json:"citations,omitempty"`
}

type SourceClusterSummary added in v0.8.6

type SourceClusterSummary struct {
	ClusterID    string `json:"cluster_id"`
	Region       string `json:"region"`
	BrokerCount  int    `json:"broker_count"`
	TopicCount   int    `json:"topic_count"`
	KafkaVersion string `json:"kafka_version,omitempty"`
	// IsServerless flags MSK Serverless source clusters. Set to true
	// when MskClusterConfig.ClusterType == "SERVERLESS"; the renderer
	// uses it to suppress Provisioned-only framing (broker counts,
	// "incomplete scan" guidance) that doesn't apply to Serverless.
	IsServerless bool `json:"is_serverless,omitempty"`
	// SourceAuths lists the auth methods enabled on the source cluster
	// (stable enum tokens: scram, iam, mtls, unauth). Drawn from the
	// same MSK ClientAuthentication signals AuthDecision reads — surfaced here in
	// §1 so a reader scanning the Source Environment table sees the
	// auth posture alongside brokers / topics before the §4 mapping.
	SourceAuths []string `json:"source_auths,omitempty"`
}

type SourceEnvironment added in v0.8.6

type SourceEnvironment struct {
	Clusters     []SourceClusterSummary `json:"clusters"`
	TotalRegions int                    `json:"total_regions"`
}

type Thresholds added in v0.8.4

type Thresholds struct {
	// StaleStateDays — emit the state-file-stale OQ when the state
	// snapshot is older than this many days.
	StaleStateDays int `yaml:"stale_state_days"`
	// PNIGatewayBreakeven — projected PNI gateway count at or above
	// which the recommendation flips from PNI to PrivateLink.
	PNIGatewayBreakeven int `yaml:"pni_gateway_breakeven"`
	// PartitionApproachingFraction — Red Flag row 5 fires when
	// user-topic partitions exceed this fraction of the cluster's
	// sized eCKU capacity (per_eCKU_partition_rate * FinalECKU).
	// 0.30 means "fire at 30% of sized capacity"; pre-fix this was
	// hardcoded in red_flags.go.
	PartitionApproachingFraction float64 `yaml:"partition_approaching_fraction"`
}

Thresholds collects numeric cutoffs that the rule engine and the renderer need but that aren't customer-facing knobs — pulled out so an admin can tune them without code edits if a tenant has a genuinely different operating envelope.

type TieredStorageCluster added in v0.8.6

type TieredStorageCluster struct {
	ClusterID   string `json:"cluster_id"`
	StorageMode string `json:"storage_mode"`
	// RemoteLogSizeBytes is the peak observed footprint (CloudWatch
	// Maximum aggregate) — appropriate for a monotonically-
	// accumulating gauge. Falls back to Average when Max isn't
	// populated. Zero when the metric wasn't collected or the
	// cluster doesn't have tiered data yet.
	RemoteLogSizeBytes float64 `json:"remote_log_size_bytes,omitempty"`
}

TieredStorageCluster is the per-cluster tiered-storage view: which cluster has TIERED storage, the peak GB volume from CloudWatch (`RemoteLogSizeBytes` Maximum — informational, not the basis for a dollar estimate), and whether the customer's `consumer_history_requirement` indicates the data must be carried forward.

type TieredStorageSection added in v0.8.6

type TieredStorageSection struct {
	Clusters                   []TieredStorageCluster `json:"clusters"`
	ConsumerHistoryRequirement string                 `json:"consumer_history_requirement"`
	HistoricalDataStrategy     string                 `json:"historical_data_strategy"`
}

TieredStorageSection surfaces the three-dimension trade-off (mechanism / duration / cost direction) for fleets with at least one TIERED-storage cluster. Customer-decision shaped: kcp does not pick a path, it makes the trade-off legible.

type Topology added in v0.8.6

type Topology string

Topology distinguishes Multi-Zone (MZ) from Single-Zone (SZ) Dedicated clusters. Only meaningful when Verdict == Dedicated; Enterprise clusters have no topology dimension at this layer.

const (
	TopologyNotApplicable Topology = ""
	TopologyMultiZone     Topology = "MultiZone"
	TopologySingleZone    Topology = "SingleZone"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL