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
11 changes: 11 additions & 0 deletions docs/reconciliation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ Exceptions:

- The eno-reconciler process can fall back to client-side three-way merge patch by setting `--disable-ssa`
- Merge can be disabled for a resource by setting the `eno.azure.io/replace: "true"` annotation (a full `update` request will be used instead of a `patch`)
- Merge can be disabled conditionally by setting the `eno.azure.io/replace-when` annotation with a CEL expression, it takes precedence over the non-conditional replace:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Precedence is arguable, maybe we should fail closed if they're both present.

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: foo
annotations:
eno.azure.io/replace-when: "has(self.data.replaceMe)"
data:
replaceMe: "yes"
```
- All updates can be disabled for a resource by setting the `eno.azure.io/disable-updates: "true"` annotation

## Deletion
Expand Down
25 changes: 25 additions & 0 deletions internal/resource/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"time"

apiv1 "github.com/Azure/eno/api/v1"
enocel "github.com/Azure/eno/internal/cel"
"github.com/Azure/eno/internal/readiness"
"github.com/Azure/eno/internal/resource/mutation"
"github.com/go-logr/logr"
Expand All @@ -25,6 +26,7 @@ import (
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"github.com/google/cel-go/cel"
)

var patchGVK = schema.GroupVersionKind{
Expand Down Expand Up @@ -65,6 +67,7 @@ type Resource struct {
manifestDeleted bool
readinessGroup int
overrides []*mutation.Op
replaceWhen cel.Program
latestKnownState atomic.Pointer[apiv1.ResourceState]
}

Expand Down Expand Up @@ -113,6 +116,17 @@ func (r *Resource) Snapshot(ctx context.Context, comp *apiv1.Composition, actual
const replaceKey = "eno.azure.io/replace"
snap.Replace = anno[replaceKey] == "true"

// Handle replace-when annotation
if r.replaceWhen != nil {
// Evaluate the condition
val, err := enocel.Eval(ctx, r.replaceWhen, comp, actual, nil)
if err != nil {
logr.FromContextOrDiscard(ctx).V(0).Info("error evaluating replace-when condition", "error", err)
} else if b, ok := val.Value().(bool); ok && b {
snap.Replace = true
}
}

const reconcileIntervalKey = "eno.azure.io/reconcile-interval"
if str, ok := anno[reconcileIntervalKey]; ok {
reconcileInterval, err := time.ParseDuration(str)
Expand Down Expand Up @@ -259,6 +273,17 @@ func NewResource(ctx context.Context, slice *apiv1.ResourceSlice, index int) (*R
}
}

// Parse replace-when condition
const replaceWhenKey = "eno.azure.io/replace-when"
if condition, ok := anno[replaceWhenKey]; ok {
prgm, err := enocel.Parse(condition)
if err != nil {
logger.Error(err, "invalid replace-when condition")
} else {
res.replaceWhen = prgm
}
}

const readinessGroupKey = "eno.azure.io/readiness-group"
if str, ok := anno[readinessGroupKey]; ok {
rg, err := strconv.Atoi(str)
Expand Down
93 changes: 92 additions & 1 deletion internal/resource/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,92 @@ var newResourceTests = []struct {
assert.Len(t, r.overrides, 0)
},
},
{
Name: "replace-when-condition-true",
Manifest: `{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "foo",
"annotations": {
"eno.azure.io/replace-when": "true"
}
}
}`,
Assert: func(t *testing.T, r *Snapshot) {
assert.True(t, r.Replace)
},
},
{
Name: "replace-when-condition-false",
Manifest: `{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "foo",
"annotations": {
"eno.azure.io/replace-when": "false"
}
}
}`,
Assert: func(t *testing.T, r *Snapshot) {
assert.False(t, r.Replace)
},
},
{
Name: "replace-when-with-self-condition",
Manifest: `{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "foo",
"annotations": {
"eno.azure.io/replace-when": "has(self.data.replaceMe)"
}
},
"data": {
"replaceMe": "yes"
}
}`,
Assert: func(t *testing.T, r *Snapshot) {
assert.True(t, r.Replace)
},
},
{
Name: "replace-when-invalid-condition",
Manifest: `{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "foo",
"annotations": {
"eno.azure.io/replace-when": "invalid CEL expression !@#$"
}
}
}`,
Assert: func(t *testing.T, r *Snapshot) {
// Should remain false when condition parsing fails
assert.False(t, r.Replace)
},
},
{
Name: "replace-when-overrides-replace",
Manifest: `{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "foo",
"annotations": {
"eno.azure.io/replace": "false",
"eno.azure.io/replace-when": "true"
}
}
}`,
Assert: func(t *testing.T, r *Snapshot) {
// replace-when should override the replace annotation when condition is true
assert.True(t, r.Replace)
},
},
{
Name: "labels",
Manifest: `{
Expand Down Expand Up @@ -359,7 +445,12 @@ func TestNewResource(t *testing.T) {
}, 0)
require.NoError(t, err)

rs, err := r.Snapshot(t.Context(), &apiv1.Composition{}, nil)
// Create actual resource for snapshot evaluation
actual := &unstructured.Unstructured{}
err = actual.UnmarshalJSON([]byte(tc.Manifest))
require.NoError(t, err)

rs, err := r.Snapshot(t.Context(), &apiv1.Composition{}, actual)
require.NoError(t, err)
tc.Assert(t, rs)

Expand Down
Loading