Skip to content
Open
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
2 changes: 1 addition & 1 deletion internal/controllers/reconciliation/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ func (c *Controller) reconcileSnapshot(ctx context.Context, comp *apiv1.Composit
reconciliationLatency.Observe(float64(time.Since(start).Milliseconds()))
}()

if res.Deleted() {
if res.Deleted() || (res.Recreate && prev != nil && res.CompareManifest(prev) != 0) {
if current == nil || current.GetDeletionTimestamp() != nil || res.Orphan || (comp.Labels != nil && comp.Labels["eno.azure.io/symphony-deleting"] == "true") {
return false, nil // already deleted - nothing to do
}
Expand Down
59 changes: 59 additions & 0 deletions internal/controllers/reconciliation/edgecase_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -730,3 +730,62 @@ func TestFailOpen_WithAnnotationTrue(t *testing.T) {
return err == nil && comp.Status.CurrentSynthesis != nil && comp.Status.CurrentSynthesis.Reconciled != nil
})
}

func TestRecreateImmutableConfigmap(t *testing.T) {
mgr := testutil.NewManager(t)
setupTestSubject(t, mgr)
testRecreateImmutableConfigmap(t, mgr)
}

func TestRecreateImmutableConfigmap_DisableSSA(t *testing.T) {
mgr := testutil.NewManager(t)
setupTestSubjectForOptions(t, mgr, Options{
Manager: mgr.Manager,
Timeout: time.Minute,
ReadinessPollInterval: time.Hour,
DisableServerSideApply: true,
FailOpen: false,
})
testRecreateImmutableConfigmap(t, mgr)
}

func testRecreateImmutableConfigmap(t *testing.T, mgr *testutil.Manager) {
ctx := testutil.NewContext(t)
upstream := mgr.GetClient()

registerControllers(t, mgr)
testutil.WithFakeExecutor(t, mgr, func(ctx context.Context, s *apiv1.Synthesizer, input *krmv1.ResourceList) (*krmv1.ResourceList, error) {
output := &krmv1.ResourceList{}
output.Items = []*unstructured.Unstructured{{
Object: map[string]any{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]any{
"name": "test-obj",
"namespace": "default",
"annotations": map[string]any{
"eno.azure.io/recreate": "true",
},
},
"immutable": true,
"data": map[string]any{
"synthImage": s.Spec.Image,
},
},
}}
return output, nil
})

mgr.Start(t)
synth, comp := writeGenericComposition(t, upstream)
waitForReadiness(t, mgr, comp, synth, nil)

// Updating the configmap should be possible even though it's immutable because Eno will recreate it
err := retry.RetryOnConflict(testutil.Backoff, func() error {
upstream.Get(ctx, client.ObjectKeyFromObject(synth), synth)
synth.Spec.Image = "updated"
return upstream.Update(ctx, synth)
})
require.NoError(t, err)
waitForReadiness(t, mgr, comp, synth, nil)
}
10 changes: 6 additions & 4 deletions internal/resource/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,10 +231,8 @@ func newResource(ctx context.Context, parsed *unstructured.Unstructured, strict

func (r *Resource) State() *apiv1.ResourceState { return r.latestKnownState.Load() }

// Less returns true when r < than.
// Used to establish determinstic ordering for conflicting resources.
func (r *Resource) Less(than *Resource) bool {
return bytes.Compare(r.manifestHash, than.manifestHash) < 0
func (r *Resource) CompareManifest(than *Resource) int {
return bytes.Compare(r.manifestHash, than.manifestHash)
}

// group returns the readiness or deletion group index that is relevant to the resource's current deletion state.
Expand Down Expand Up @@ -284,6 +282,9 @@ func (r *Resource) SnapshotWithOverrides(ctx context.Context, comp *apiv1.Compos
const replaceKey = "eno.azure.io/replace"
snap.Replace = cascadeAnnotation(comp, copy, replaceKey) == "true"

const recreateKey = "eno.azure.io/recreate"
snap.Recreate = cascadeAnnotation(comp, copy, recreateKey) == "true"

const deletionStratKey = "eno.azure.io/deletion-strategy"
snap.Orphan = strings.EqualFold(cascadeAnnotation(comp, copy, deletionStratKey), "orphan")
snap.Orphan = !r.isPatch && strings.EqualFold(cascadeAnnotation(comp, copy, deletionStratKey), "orphan")
Expand Down Expand Up @@ -314,6 +315,7 @@ type Snapshot struct {
Disable bool
DisableUpdates bool
Replace bool
Recreate bool
Orphan bool
ForegroundDeletion bool

Expand Down
2 changes: 1 addition & 1 deletion internal/resource/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ func TestResourceOrdering(t *testing.T) {
{manifestHash: []byte("c")},
}
sort.Slice(resources, func(i, j int) bool {
return resources[i].Less(resources[j])
return resources[i].CompareManifest(resources[j]) < 0
})

assert.Equal(t, []*Resource{
Expand Down
2 changes: 1 addition & 1 deletion internal/resource/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func (b *treeBuilder) Add(resource *Resource) {
b.init()

// Handle conflicting refs deterministically
if existing, ok := b.byRef[resource.Ref]; ok && resource.Less(existing.Resource) {
if existing, ok := b.byRef[resource.Ref]; ok && resource.CompareManifest(existing.Resource) < 0 {
return
}

Expand Down
Loading