From 208e73980d741fff95f35a828a1275e8b794ae11 Mon Sep 17 00:00:00 2001 From: Vassiliy Yegorov Date: Wed, 19 Aug 2026 18:02:55 +0700 Subject: [PATCH] feat(service): apply custom marks to the computed diff --- internal/service/service.go | 6 +++ internal/service/service_test.go | 68 +++++++++++++++++++++++++++++--- internal/store/loader.go | 15 ++++--- 3 files changed, 79 insertions(+), 10 deletions(-) diff --git a/internal/service/service.go b/internal/service/service.go index 46646b1..78e975a 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -41,6 +41,11 @@ type DomainRef struct { Provider string SecretEnc string Template dto.TemplateDoc + // CustomKeys are RecordDiff keys the operator marked as deliberately + // outside the template for this domain. Applied in resolve() right after + // the diff is computed — the single marking point, like tmpl.Materialize + // is the single materialisation point. + CustomKeys []string } // ZoneRef is the provider-access subset of a domain, without a template — @@ -102,6 +107,7 @@ func (s *DomainService) resolve(ctx context.Context, projectID, domainID uuid.UU return nil, provider.Credentials{}, ref, diff.Changeset{}, fmt.Errorf("%w: %v", ErrProviderUnavailable, err) } cs := diff.Diff(tmpl.Materialize(ref.Template, ref.ZoneName), actual) + cs.MarkCustom(ref.CustomKeys) return p, creds, ref, cs, nil } diff --git a/internal/service/service_test.go b/internal/service/service_test.go index 8fb8c8b..28e2983 100644 --- a/internal/service/service_test.go +++ b/internal/service/service_test.go @@ -27,10 +27,11 @@ func testCipher(t *testing.T) *crypto.Cipher { // fakeProvider records applied changesets and returns canned zone records. type fakeProvider struct { - actual []model.Record - applied diff.Changeset - getErr error // when set, GetRecords fails with this error - applyErr error // when set, ApplyChanges fails with this error + actual []model.Record + applied diff.Changeset + applyCalls int + getErr error // when set, GetRecords fails with this error + applyErr error // when set, ApplyChanges fails with this error } func (fakeProvider) Name() string { return "selectel" } @@ -44,6 +45,7 @@ func (f *fakeProvider) GetRecords(context.Context, provider.Credentials, string) return f.actual, nil } func (f *fakeProvider) ApplyChanges(_ context.Context, _ provider.Credentials, _ string, cs diff.Changeset) error { + f.applyCalls++ if f.applyErr != nil { return f.applyErr } @@ -70,12 +72,18 @@ type nopRecorder struct{} func (nopRecorder) SaveCheckRun(context.Context, uuid.UUID, diff.Changeset) error { return nil } func setup(t *testing.T, actual []model.Record, tmpl dto.TemplateDoc) (*DomainService, *fakeProvider) { + return setupWithCustomKeys(t, actual, tmpl, nil) +} + +// setupWithCustomKeys mirrors setup but also lets a test populate +// DomainRef.CustomKeys, exercising the resolve() -> MarkCustom wiring. +func setupWithCustomKeys(t *testing.T, actual []model.Record, tmpl dto.TemplateDoc, customKeys []string) (*DomainService, *fakeProvider) { fp := &fakeProvider{actual: actual} reg := registry.New() reg.Register(fp) cipher := testCipher(t) enc, _ := cipher.Encrypt([]byte("secret")) - loader := fakeLoader{ref: DomainRef{ZoneID: "z1", ZoneName: "example.com.", Provider: "selectel", SecretEnc: enc, Template: tmpl}} + loader := fakeLoader{ref: DomainRef{ZoneID: "z1", ZoneName: "example.com.", Provider: "selectel", SecretEnc: enc, Template: tmpl, CustomKeys: customKeys}} return New(loader, nopRecorder{}, reg, cipher), fp } @@ -246,3 +254,53 @@ func TestResolveWrapsProviderError(t *testing.T) { t.Fatalf("expected clean provider message, got %q", msg) } } + +// TestResolveMarksCustomKeys covers the resolve() -> cs.MarkCustom(ref.CustomKeys) +// wiring: a zone record with no template counterpart, whose key is in +// DomainRef.CustomKeys, must be marked Custom rather than surfacing as a +// prune/drift. +func TestResolveMarksCustomKeys(t *testing.T) { + // Zone contains a record absent from the template, and its key is marked custom. + actual := []model.Record{ + {Type: model.CNAME, Name: "admin.example.com.", TTL: 300, Values: []string{"example.com."}}, + } + svc, _ := setupWithCustomKeys(t, actual, dto.TemplateDoc{Records: nil}, []string{"CNAME admin.example.com."}) + + cs, err := svc.Check(context.Background(), uuid.New(), uuid.New()) + if err != nil { + t.Fatal(err) + } + if got := cs.Prunes(); len(got) != 0 { + t.Fatalf("custom record must not appear in Prunes, got %+v", got) + } + if got := cs.Customs(); len(got) != 1 { + t.Fatalf("expected 1 custom diff, got %+v", got) + } + if status := DeriveStatus(cs); status != StatusInSync { + t.Fatalf("a zone whose only deviation is custom must be in_sync, got %q", status) + } +} + +// TestApplyIgnoresCustomKeySentAsPrune covers the case where a client sends a +// custom-marked key in ApplyRequest.Prunes anyway (stale UI state, replay, +// etc): since a custom diff is excluded from cs.Prunes(), Apply's key +// selection never picks it up and the provider is not called at all. +func TestApplyIgnoresCustomKeySentAsPrune(t *testing.T) { + actual := []model.Record{ + {Type: model.CNAME, Name: "admin.example.com.", TTL: 300, Values: []string{"example.com."}}, + } + svc, fp := setupWithCustomKeys(t, actual, dto.TemplateDoc{Records: nil}, []string{"CNAME admin.example.com."}) + + applied, err := svc.Apply(context.Background(), uuid.New(), uuid.New(), ApplyRequest{ + Prunes: []string{"CNAME admin.example.com."}, + }) + if err != nil { + t.Fatal(err) + } + if len(applied.Diffs) != 0 { + t.Fatalf("custom key must never be applied, got %+v", applied.Diffs) + } + if fp.applyCalls != 0 { + t.Fatalf("provider must not be called with an empty change set, got %d calls", fp.applyCalls) + } +} diff --git a/internal/store/loader.go b/internal/store/loader.go index 4b6068e..0eeb7b9 100644 --- a/internal/store/loader.go +++ b/internal/store/loader.go @@ -25,12 +25,17 @@ func (s *Store) LoadDomain(ctx context.Context, projectID, domainID uuid.UUID) ( if row.Doc == nil { return service.DomainRef{}, fmt.Errorf("store: domain %s has no template", domainID) } + keys, err := s.ListCustomKeys(ctx, domainID) + if err != nil { + return service.DomainRef{}, err + } return service.DomainRef{ - ZoneID: row.ZoneID, - ZoneName: row.ZoneName, - Provider: row.Provider, - SecretEnc: row.SecretEnc, - Template: *row.Doc, + ZoneID: row.ZoneID, + ZoneName: row.ZoneName, + Provider: row.Provider, + SecretEnc: row.SecretEnc, + Template: *row.Doc, + CustomKeys: keys, }, nil }