Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion internal/e2e/e2e_semantic_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ func (s *E2ESuite) TestSemanticPlanPrerequisites() {
assert.Equal(t, semantic.OriginHelm, p.Changes[1].Origin.Kind)
assert.Equal(t, "ConfigMap", p.Changes[1].Resource.Kind)
assert.Equal(t, semantic.Create, p.Changes[1].Action)
assert.Equal(t, semantic.CompletenessPartial, p.Completeness)
require.NotEmpty(t, p.Diagnostics)

cs, _ := s.kubeClients(t)
Expand Down
2 changes: 1 addition & 1 deletion internal/plan/prediction.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (

// semanticPlanFromResults maps [predict.Result] values onto a semantic
// [semantic.Plan]. It does not mutate results or the unstructured objects
// they hold. Current predictor actions never produce [semantic.Replace].
// they hold.
//
// Write FieldManager values come from [kube.ManagedFieldsManager]. This
// helper always emits [semantic.WriteServerSide] writes. [semantic.New]
Expand Down
22 changes: 2 additions & 20 deletions internal/plan/prediction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,10 @@ func TestSemanticPlanFromResults_ActionMapping(t *testing.T) {
require.Len(t, p.Changes, 3)
assert.NotNil(t, p.Tasks)
assert.Empty(t, p.Tasks)
assert.Equal(t, semantic.CompletenessComplete, p.Completeness)

byName := map[string]semantic.ResourceChange{}
for _, c := range p.Changes {
byName[c.Resource.Name] = c
assert.NotEqual(t, semantic.Replace, c.Action)
}
require.Contains(t, byName, "app")
assert.Equal(t, semantic.Create, byName["app"].Action)
Expand Down Expand Up @@ -109,7 +107,6 @@ func TestSemanticPlanFromResults_NoOpOmitted(t *testing.T) {
}})
require.NoError(t, err)
assert.Empty(t, p.Changes)
assert.Equal(t, semantic.CompletenessComplete, p.Completeness)
}

func TestSemanticPlanFromResults_LimitationKeepsPredicted(t *testing.T) {
Expand All @@ -126,7 +123,6 @@ func TestSemanticPlanFromResults_LimitationKeepsPredicted(t *testing.T) {
assert.Equal(t, semantic.Update, p.Changes[0].Action)
require.NotNil(t, p.Changes[0].After)
assert.Equal(t, "new", predictionObjectString(t, p.Changes[0].After.Object, "data", "key"))
assert.Equal(t, semantic.CompletenessPartial, p.Completeness)
require.Len(t, p.Diagnostics, 1)
assert.Equal(t, semantic.CategoryPredictionLimitation, p.Diagnostics[0].Category)
assert.Contains(t, p.Diagnostics[0].Message, predict.LimitationManagedFieldsMigration)
Expand All @@ -144,7 +140,8 @@ func TestSemanticPlanFromResults_LimitationDoesNotFabricateAfter(t *testing.T) {
require.Len(t, p.Changes, 1)
assert.Nil(t, p.Changes[0].After)
assert.Empty(t, p.Changes[0].Fields)
assert.Equal(t, semantic.CompletenessPartial, p.Completeness)
require.Len(t, p.Diagnostics, 1)
assert.Equal(t, semantic.CategoryPredictionLimitation, p.Diagnostics[0].Category)
}

func TestSemanticPlanFromResults_UnknownAction(t *testing.T) {
Expand Down Expand Up @@ -255,7 +252,6 @@ func TestSemanticPlanFromResults_NoOpWithLimitation(t *testing.T) {
}})
require.NoError(t, err)
assert.Empty(t, p.Changes)
assert.Equal(t, semantic.CompletenessPartial, p.Completeness)
require.Len(t, p.Diagnostics, 1)
assert.Equal(t, semantic.CategoryPredictionLimitation, p.Diagnostics[0].Category)
assert.Contains(t, p.Diagnostics[0].Message, predict.LimitationManagedFieldsMigration)
Expand Down Expand Up @@ -296,20 +292,6 @@ func TestSemanticPlanFromResults_NilObjectSnapshot(t *testing.T) {
assert.Nil(t, p.Changes[0].After.Object)
}

func TestSemanticPlanFromResults_NeverEmitsReplace(t *testing.T) {
t.Parallel()
p, err := semanticPlanFromResults(semantic.Header{Release: "web", Namespace: "prod"}, []predict.Result{
{Identity: predictionIdentity("a"), Action: predict.ActionCreate, Predicted: predictionConfigMap("a", "prod", "1")},
{Identity: predictionIdentity("b"), Action: predict.ActionUpdate, Live: predictionConfigMap("b", "prod", "1"), Predicted: predictionConfigMap("b", "prod", "2")},
{Identity: predictionIdentity("c"), Action: predict.ActionDelete, Live: predictionConfigMap("c", "prod", "1")},
{Identity: predictionIdentity("d"), Action: predict.ActionNoOp, Live: predictionConfigMap("d", "prod", "1"), Predicted: predictionConfigMap("d", "prod", "1")},
})
require.NoError(t, err)
for _, c := range p.Changes {
assert.NotEqual(t, semantic.Replace, c.Action)
}
}

func predictionObjectString(tb testing.TB, obj map[string]any, keys ...string) string {
tb.Helper()
var cur any = obj
Expand Down
3 changes: 2 additions & 1 deletion internal/plan/semantic/change.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@

package semantic

// ResourceChange is one Live -> Predicted Kubernetes resource mutation.
// ResourceChange describes a visible Kubernetes resource consequence
// of this invocation.
type ResourceChange struct {
Resource ResourceRef
Origin ResourceOrigin
Expand Down
3 changes: 1 addition & 2 deletions internal/plan/semantic/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// Package semantic is Deployah's unredacted domain model for a Live ->
// Predicted plan.
// Package semantic is Deployah's unredacted semantic plan model.
//
// This package is not the deployah plan command and is not a JSON API.
// Snapshots and field values stay complete. Serialization and secret
Expand Down
11 changes: 0 additions & 11 deletions internal/plan/semantic/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,14 +165,3 @@ func deleteChange(name, value string) semantic.ResourceChange {
Apply: deleteApply(),
}
}

func replaceChange(name, before, after string) semantic.ResourceChange {
return semantic.ResourceChange{
Resource: ref("ConfigMap", name),
Origin: helmOrigin(),
Action: semantic.Replace,
Before: &semantic.ResourceSnapshot{Object: cm(name, before)},
After: &semantic.ResourceSnapshot{Object: cm(name, after)},
Apply: bothApply(),
}
}
14 changes: 3 additions & 11 deletions internal/plan/semantic/order_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,14 +112,6 @@ func TestNew_ActionRank(t *testing.T) {
Before: &semantic.ResourceSnapshot{Object: cm("app", "v1")},
Apply: deleteApply(),
},
{
Resource: res,
Origin: helmOrigin(),
Action: semantic.Replace,
Before: &semantic.ResourceSnapshot{Object: cm("app", "v1")},
After: &semantic.ResourceSnapshot{Object: cm("app", "v2")},
Apply: bothApply(),
},
{
Resource: res,
Origin: helmOrigin(),
Expand All @@ -137,11 +129,11 @@ func TestNew_ActionRank(t *testing.T) {
},
}, nil, nil)
require.NoError(t, err)
require.Len(t, p.Changes, 4)
require.Len(t, p.Changes, 3)
assert.Equal(t, []semantic.Action{
semantic.Create, semantic.Update, semantic.Replace, semantic.Delete,
semantic.Create, semantic.Update, semantic.Delete,
}, []semantic.Action{
p.Changes[0].Action, p.Changes[1].Action, p.Changes[2].Action, p.Changes[3].Action,
p.Changes[0].Action, p.Changes[1].Action, p.Changes[2].Action,
})
}

Expand Down
88 changes: 27 additions & 61 deletions internal/plan/semantic/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,27 @@ import (
"slices"
)

// Plan is the Live -> Predicted semantic result. Construct it with [New].
// Chart CRDs are attached with [AttachChartCRDs]; [New] leaves them empty.
// Snapshots and field values are unredacted. This type is not a JSON
// rendering contract.
// Plan is the semantic plan for one invocation: Deployah's intended
// operations and their visible resource consequences. Construct it with
// [New]. Chart CRDs are attached with [AttachChartCRDs]; [New] leaves
// them empty. Snapshots and field values are unredacted. This type is
// not a JSON rendering contract.
type Plan struct {
Header Header
HelmAction HelmAction
Changes []ResourceChange
Tasks []TaskPlan
ChartCRDs []ChartCRD
Diagnostics []Diagnostic
Summary Summary
Completeness Completeness
Header Header
HelmAction HelmAction
Changes []ResourceChange
Tasks []TaskPlan
ChartCRDs []ChartCRD
Diagnostics []Diagnostic
Summary Summary
}

// New validates helmAction, header, changes, tasks, and diagnostics,
// sorts them, derives [Summary] and [Completeness], and returns a plan
// whose slices are non-nil. ChartCRDs is empty; call [AttachChartCRDs]
// to set them. Task references to [ResourceChange] values must be
// consistent; it fails closed on dangling or duplicate ownership.
// helmAction is stored as provided; [New] does not derive or mutate it.
// sorts them, derives [Summary], and returns a plan whose slices are
// non-nil. ChartCRDs is empty; call [AttachChartCRDs] to set them. Task
// references to [ResourceChange] values must be consistent; it fails
// closed on dangling or duplicate ownership. helmAction is stored as
// provided; [New] does not derive or mutate it.
func New(header Header, helmAction HelmAction, changes []ResourceChange, tasks []TaskPlan, diagnostics []Diagnostic) (Plan, error) {
if !helmAction.valid() {
return Plan{}, fmt.Errorf("invalid helm action %s", helmAction)
Expand Down Expand Up @@ -92,14 +92,13 @@ func New(header Header, helmAction HelmAction, changes []ResourceChange, tasks [
sortDiagnostics(copiedDiags)

return Plan{
Header: header,
HelmAction: helmAction,
Changes: copiedChanges,
Tasks: copiedTasks,
ChartCRDs: []ChartCRD{},
Diagnostics: copiedDiags,
Summary: Summarize(copiedChanges),
Completeness: deriveCompleteness(copiedChanges, copiedDiags),
Header: header,
HelmAction: helmAction,
Changes: copiedChanges,
Tasks: copiedTasks,
ChartCRDs: []ChartCRD{},
Diagnostics: copiedDiags,
Summary: Summarize(copiedChanges),
}, nil
}

Expand Down Expand Up @@ -152,11 +151,10 @@ func (p Plan) HasEffects() bool {
return false
}

// IsNoOp reports whether the plan is a complete, non-install HelmNone
// with no known effects. Keep this expression verbatim.
// IsNoOp reports whether the plan is a non-install [HelmNone] with no
// known effects.
func (p Plan) IsNoOp() bool {
return !p.Header.FreshInstall &&
p.Completeness == CompletenessComplete &&
p.HelmAction == HelmNone &&
!p.HasEffects()
}
Expand All @@ -168,7 +166,7 @@ func normalizeChange(c ResourceChange) (ResourceChange, error) {
c.Apply = copyApply(c.Apply)
c.Fields = copyFields(c.Fields)
switch {
case (c.Action == Update || c.Action == Replace) && c.After != nil:
case c.Action == Update && c.After != nil:
fields, err := DiffFields(snapshotObject(c.Before), snapshotObject(c.After))
if err != nil {
return ResourceChange{}, err
Expand Down Expand Up @@ -323,20 +321,6 @@ func validateDefinition(d HookDefinition) error {
return nil
}

func deriveCompleteness(changes []ResourceChange, diags []Diagnostic) Completeness {
for _, d := range diags {
if d.Category == CategoryPredictionLimitation {
return CompletenessPartial
}
}
for _, c := range changes {
if c.Action == Update && c.After == nil {
return CompletenessPartial
}
}
return CompletenessComplete
}

func validateChange(c ResourceChange, diags []Diagnostic) error {
if !c.Action.valid() {
return fmt.Errorf("invalid action %s", c.Action)
Expand Down Expand Up @@ -428,17 +412,6 @@ func validateApply(action Action, apply ApplySemantics) error {
return fmt.Errorf("delete requires delete semantics")
}
return validateDelete(*apply.Delete)
case Replace:
if apply.Write == nil {
return fmt.Errorf("replace requires write semantics")
}
if apply.Delete == nil {
return fmt.Errorf("replace requires delete semantics")
}
if err := validateWrite(*apply.Write); err != nil {
return err
}
return validateDelete(*apply.Delete)
default:
return fmt.Errorf("invalid action %s", action)
}
Expand Down Expand Up @@ -538,13 +511,6 @@ func validateSnapshots(c ResourceChange, diags []Diagnostic) error {
if c.After != nil {
return fmt.Errorf("delete must not have an after snapshot")
}
case Replace:
if c.Before == nil {
return fmt.Errorf("replace requires a before snapshot")
}
if c.After == nil {
return fmt.Errorf("replace requires an after snapshot")
}
}
return nil
}
Expand Down
Loading
Loading