mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 09:01:31 +00:00
test: characterize EKS add-on v1 compatibility with added fields
Pin which owner writes drop unknown record and manifest fields, that reads and deletes ignore or erase them, that one undecodable entry fails a listing, and that v1 wire consumers ignore extra fields.
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
package eksv1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// A v1 decoder ignores fields a later sender adds, so a report carrying them
|
||||
// decodes to the same value as one without.
|
||||
func TestAddonStatusReport_IgnoresUnknownFields(t *testing.T) {
|
||||
body := `{"addon":"argocd","version":"3.0.23","phase":"ready","message":"","ts":1736935200,` +
|
||||
`"incarnationId":"inc-1","generation":4,"executorEpoch":7}`
|
||||
var got AddonStatusReport
|
||||
require.NoError(t, json.Unmarshal([]byte(body), &got))
|
||||
require.Equal(t, AddonStatusReport{Addon: "argocd", Version: "3.0.23", Phase: AddonPhaseReady, TS: 1736935200}, got)
|
||||
}
|
||||
|
||||
// Both deployed body forms tolerate added top-level and per-add-on fields.
|
||||
func TestInternalAddonsResponse_IgnoresUnknownFields(t *testing.T) {
|
||||
for name, body := range map[string]string{
|
||||
"rest-json": `{"Addons":[{"AddonName":"spinifex-noop","AddonVersion":"0.1.0","ServiceAccountRoleArn":"",` +
|
||||
`"ConfigurationValues":"","IncarnationId":"inc-1","Generation":4}],"ExecutorEpoch":7}`,
|
||||
"encoding-json": `{"addons":[{"addonName":"spinifex-noop","addonVersion":"0.1.0",` +
|
||||
`"incarnationId":"inc-1","generation":4}],"executorEpoch":7}`,
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var got InternalAddonsResponse
|
||||
require.NoError(t, json.Unmarshal([]byte(body), &got))
|
||||
require.Equal(t, InternalAddonsResponse{Addons: []StagedAddonManifest{{AddonName: "spinifex-noop", AddonVersion: "0.1.0"}}}, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package gatewayfetch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Fields a later gateway adds never reach the four TSV columns the on-VM
|
||||
// addon-sync agent reads.
|
||||
func TestEmitAddonsTSV_IgnoresUnknownFields(t *testing.T) {
|
||||
body := []byte(`{"Addons":[{"AddonName":"aws-load-balancer-controller","AddonVersion":"2.11.0",` +
|
||||
`"ServiceAccountRoleArn":"arn:aws:iam::123456789012:role/alb","ConfigurationValues":"a\tb",` +
|
||||
`"IncarnationId":"inc-1","Generation":4}],"ExecutorEpoch":7}`)
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := emitAddonsTSV(&buf, body); err != nil {
|
||||
t.Fatalf("emitAddonsTSV: %v", err)
|
||||
}
|
||||
want := "aws-load-balancer-controller\t2.11.0\tarn:aws:iam::123456789012:role/alb\tYQli\n"
|
||||
if buf.String() != want {
|
||||
t.Fatalf("TSV mismatch:\n got: %q\nwant: %q", buf.String(), want)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package addon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// newerRecordJSON is a record as a later binary might store it: every v1 field
|
||||
// plus fields this binary's Record does not declare.
|
||||
const newerRecordJSON = `{"addonName":"argocd","addonVersion":"3.0.23","status":"ACTIVE",` +
|
||||
`"arn":"arn:aws:eks:us-east-1:111122223333:addon/c1/argocd",` +
|
||||
`"createdAt":"2026-06-08T00:00:00Z","modifiedAt":"2026-06-08T00:00:00Z",` +
|
||||
`"incarnationId":"inc-1","generation":4,"observedGeneration":3,"origin":"user"}`
|
||||
|
||||
const newerManifestJSON = `{"addonName":"argocd","addonVersion":"3.0.23",` +
|
||||
`"incarnationId":"inc-1","generation":4}`
|
||||
|
||||
func seedNewerRecord(t *testing.T, kv jetstream.KeyValue) {
|
||||
t.Helper()
|
||||
_, err := kv.Put(t.Context(), "clusters/c1/addons/argocd", []byte(newerRecordJSON))
|
||||
require.NoError(t, err)
|
||||
_, err = kv.Put(t.Context(), "clusters/c1/addons/argocd/manifest", []byte(newerManifestJSON))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func storedKeys(t *testing.T, kv jetstream.KeyValue, key string) []string {
|
||||
t.Helper()
|
||||
entry, err := kv.Get(t.Context(), key)
|
||||
require.NoError(t, err)
|
||||
var top map[string]json.RawMessage
|
||||
require.NoError(t, json.Unmarshal(entry.Value(), &top))
|
||||
keys := make([]string, 0, len(top))
|
||||
for k := range top {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
slices.Sort(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
// Every owner write is decode-into-Record then re-encode, so fields this
|
||||
// binary does not declare are erased from the record and the re-staged manifest.
|
||||
func TestOwnerWrites_DropUnknownRecordAndManifestFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
failed := "image pull"
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
write func(t *testing.T, o *Owner, kv jetstream.KeyValue)
|
||||
wantRecord []string
|
||||
wantManifest []string
|
||||
}{
|
||||
{
|
||||
name: "Update",
|
||||
write: func(t *testing.T, o *Owner, kv jetstream.KeyValue) {
|
||||
_, err := o.Update(t.Context(), kv, testAccountID, "c1", "argocd", Change{})
|
||||
require.NoError(t, err)
|
||||
},
|
||||
wantRecord: []string{"addonName", "addonVersion", "arn", "createdAt", "modifiedAt", "status"},
|
||||
wantManifest: []string{"addonName", "addonVersion"},
|
||||
},
|
||||
{
|
||||
name: "ApplyReport failed",
|
||||
write: func(t *testing.T, o *Owner, kv jetstream.KeyValue) {
|
||||
require.NoError(t, o.ApplyReport(t.Context(), kv, "c1", Report{Addon: "argocd", Phase: PhaseFailed, Message: failed}))
|
||||
},
|
||||
wantRecord: []string{"addonName", "addonVersion", "arn", "createdAt", "health", "modifiedAt", "status"},
|
||||
wantManifest: []string{"addonName", "addonVersion", "generation", "incarnationId"},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
kv := newTestKV(t)
|
||||
seedNewerRecord(t, kv)
|
||||
o := New("us-east-1", NewStagingInstaller(func(_ context.Context, _ string) (jetstream.KeyValue, error) { return kv, nil }))
|
||||
tc.write(t, o, kv)
|
||||
assert.Equal(t, tc.wantRecord, storedKeys(t, kv, "clusters/c1/addons/argocd"))
|
||||
assert.Equal(t, tc.wantManifest, storedKeys(t, kv, "clusters/c1/addons/argocd/manifest"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyReport skips the write when nothing changes, so a no-op report leaves
|
||||
// unknown fields in place.
|
||||
func TestOwnerApplyReport_NoOpKeepsUnknownFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
kv := newTestKV(t)
|
||||
seedNewerRecord(t, kv)
|
||||
o := New("us-east-1", &fakeInstaller{})
|
||||
|
||||
require.NoError(t, o.ApplyReport(t.Context(), kv, "c1", Report{Addon: "argocd", Phase: PhaseReady}))
|
||||
|
||||
entry, err := kv.Get(t.Context(), "clusters/c1/addons/argocd")
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, newerRecordJSON, string(entry.Value()))
|
||||
}
|
||||
|
||||
func TestOwnerDelete_ErasesRecordAndManifestWithUnknownFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
kv := newTestKV(t)
|
||||
seedNewerRecord(t, kv)
|
||||
inst := &fakeInstaller{}
|
||||
o := New("us-east-1", inst)
|
||||
|
||||
rec, err := o.Delete(t.Context(), kv, testAccountID, "c1", "argocd")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, StatusDeleting, rec.Status)
|
||||
assert.Equal(t, []string{"argocd"}, inst.uninstalls)
|
||||
|
||||
_, err = kv.Get(t.Context(), "clusters/c1/addons/argocd")
|
||||
require.ErrorIs(t, err, jetstream.ErrKeyNotFound)
|
||||
_, err = kv.Get(t.Context(), "clusters/c1/addons/argocd/manifest")
|
||||
require.ErrorIs(t, err, jetstream.ErrKeyNotFound)
|
||||
}
|
||||
|
||||
// Reads ignore unknown fields and never write back.
|
||||
func TestReads_IgnoreUnknownFieldsWithoutRewriting(t *testing.T) {
|
||||
t.Parallel()
|
||||
kv := newTestKV(t)
|
||||
seedNewerRecord(t, kv)
|
||||
|
||||
rec, err := Get(t.Context(), kv, "c1", "argocd")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, StatusActive, rec.Status)
|
||||
|
||||
recs, err := List(t.Context(), kv, "c1")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, recs, 1)
|
||||
assert.Equal(t, "3.0.23", recs[0].AddonVersion)
|
||||
|
||||
ms, err := ListManifests(t.Context(), kv, "c1")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []Manifest{{AddonName: "argocd", AddonVersion: "3.0.23"}}, ms)
|
||||
|
||||
entry, err := kv.Get(t.Context(), "clusters/c1/addons/argocd")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint64(1), entry.Revision())
|
||||
}
|
||||
|
||||
// A field whose JSON type changes is not ignored: one undecodable record fails
|
||||
// the whole cluster listing, and one undecodable manifest fails guest delivery.
|
||||
func TestList_OneUndecodableEntryFailsTheListing(t *testing.T) {
|
||||
t.Parallel()
|
||||
kv := newTestKV(t)
|
||||
require.NoError(t, put(t.Context(), kv, "c1", sampleRecord("argocd")))
|
||||
_, err := kv.Put(t.Context(), "clusters/c1/addons/coredns", []byte(`{"addonName":"coredns","status":1}`))
|
||||
require.NoError(t, err)
|
||||
_, err = kv.Put(t.Context(), "clusters/c1/addons/coredns/manifest", []byte(`{"addonName":"coredns","addonVersion":2}`))
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = List(t.Context(), kv, "c1")
|
||||
require.ErrorContains(t, err, "unmarshal addon clusters/c1/addons/coredns")
|
||||
_, err = ListManifests(t.Context(), kv, "c1")
|
||||
require.ErrorContains(t, err, "unmarshal staged manifest clusters/c1/addons/coredns/manifest")
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package handlers_eks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
eksv1 "github.com/mulgadc/spinifex/contracts/eks/v1"
|
||||
"github.com/mulgadc/spinifex/spinifex/domains/eks/addon"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// The reconciler's subscriber decodes then applies; a report carrying fields
|
||||
// this binary does not know is applied exactly as one without them.
|
||||
func TestAddonStatusReport_UnknownFieldsAreAppliedAsToday(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, kv := setupStagingAddonService(t)
|
||||
_, err := kv.Put(t.Context(), "clusters/c1/addons/argocd", []byte(`{"addonName":"argocd","addonVersion":"3.0.23",`+
|
||||
`"status":"ACTIVE","arn":"arn","createdAt":"2026-06-08T00:00:00Z","modifiedAt":"2026-06-08T00:00:00Z"}`))
|
||||
require.NoError(t, err)
|
||||
r := &ClusterReconciler{acctKV: kv, clusterName: "c1", addonReports: addon.New("", nil)}
|
||||
|
||||
report, err := unmarshalAddonStatusReport([]byte(`{"addon":"argocd","version":"3.0.23","phase":"failed",` +
|
||||
`"message":"rollout stalled","ts":1,"incarnationId":"stale-inc","generation":1}`))
|
||||
require.NoError(t, err)
|
||||
r.applyAddonStatusReport(t.Context(), report)
|
||||
|
||||
rec, err := addon.Get(t.Context(), kv, "c1", "argocd")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, addon.StatusDegraded, rec.Status)
|
||||
assert.Equal(t, "rollout stalled", rec.Health)
|
||||
}
|
||||
|
||||
// The daemon copies the four v1 manifest fields onto the NATS reply, so a
|
||||
// staged manifest's unknown fields never reach the gateway or the guest.
|
||||
func TestListStagedAddonManifests_ProjectsOnlyV1Fields(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc, kv := setupStagingAddonService(t)
|
||||
_, err := kv.Put(t.Context(), albManifestKey, []byte(`{"addonName":"aws-load-balancer-controller",`+
|
||||
`"addonVersion":"2.11.0","serviceAccountRoleArn":"`+addonLifecycleRoleARN+`","incarnationId":"inc-1","generation":4}`))
|
||||
require.NoError(t, err)
|
||||
|
||||
out, err := svc.ListStagedAddonManifests(context.Background(), &ListStagedAddonManifestsInput{ClusterName: "c1"}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []eksv1.StagedAddonManifest{{
|
||||
AddonName: "aws-load-balancer-controller", AddonVersion: "2.11.0", ServiceAccountRoleArn: addonLifecycleRoleARN,
|
||||
}}, out.Manifests)
|
||||
}
|
||||
Reference in New Issue
Block a user