diff --git a/internal/sync/api/client_test.go b/internal/sync/api/client_test.go index 7fff0135..252306ef 100644 --- a/internal/sync/api/client_test.go +++ b/internal/sync/api/client_test.go @@ -469,7 +469,6 @@ func TestClientArchiveVariationReturnsTransportError(t *testing.T) { func TestClientGetsSyncManifest(t *testing.T) { transport := &recordingClient{Responses: [][]byte{[]byte(`{ - "formatVersion": 1, "source": "git:example.com/acme/repo", "items": [{ "resourceKind": "variation", @@ -494,7 +493,6 @@ func TestClientGetsSyncManifest(t *testing.T) { func TestClientPatchesSyncManifest(t *testing.T) { transport := &recordingClient{Responses: [][]byte{[]byte(`{ - "formatVersion": 1, "source": "git:example.com/acme/repo", "items": [] }`)}} @@ -516,7 +514,6 @@ func TestClientPatchesSyncManifest(t *testing.T) { assert.Equal(t, http.MethodPatch, request.Method) assert.Equal(t, "application/json", request.ContentType) assert.JSONEq(t, `{ - "formatVersion": 1, "source": "git:example.com/acme/repo", "upserts": [{ "resourceKind": "variation", @@ -528,36 +525,23 @@ func TestClientPatchesSyncManifest(t *testing.T) { }`, string(request.Body)) } -// LaunchDarkly answers a project and a source that no sync wrote yet with an -// empty manifest that has no formatVersion. That is an empty manifest, not -// an unsupported one. -func TestClientAcceptsAnUnwrittenSyncManifest(t *testing.T) { - transport := &recordingClient{Responses: [][]byte{[]byte(`{"items":[],"source":"git:example.com/acme/repo"}`)}} +// The manifest API has no format version. An empty manifest and a manifest +// with entries both read as they are. +func TestClientReadsAManifestWithoutAFormatVersion(t *testing.T) { + transport := &recordingClient{Responses: [][]byte{ + []byte(`{"items":[],"source":"git:example.com/acme/repo"}`), + []byte(`{"items":[{"resourceKind":"variation","resourceLookupKey":"support/helpful","fingerprint":"sha256:abc","version":1}],"source":"git:example.com/acme/repo"}`), + }} client := NewClient(transport, "token", "https://example.com") - manifest, err := client.GetSyncManifest("project", "git:example.com/acme/repo") - + empty, err := client.GetSyncManifest("project", "git:example.com/acme/repo") require.NoError(t, err) - assert.Empty(t, manifest.Items) - assert.True(t, manifest.Unwritten()) - - transport = &recordingClient{Responses: [][]byte{[]byte(`{"items":[{"resourceKind":"variation","resourceLookupKey":"support/helpful","fingerprint":"sha256:abc","version":1}],"source":"git:example.com/acme/repo"}`)}} - client = NewClient(transport, "token", "https://example.com") - _, err = client.GetSyncManifest("project", "git:example.com/acme/repo") - require.ErrorContains(t, err, "unsupported formatVersion 0", "entries without a format version are not an unwritten manifest") -} - -func TestClientRejectsUnknownSyncManifestVersion(t *testing.T) { - transport := &recordingClient{Responses: [][]byte{[]byte(`{ - "formatVersion": 99, - "source": "git:example.com/acme/repo", - "items": [] - }`)}} - client := NewClient(transport, "token", "https://example.com") - - _, err := client.GetSyncManifest("project", "git:example.com/acme/repo") + assert.Empty(t, empty.Items) - require.ErrorContains(t, err, "unsupported formatVersion 99") + written, err := client.GetSyncManifest("project", "git:example.com/acme/repo") + require.NoError(t, err) + require.Len(t, written.Items, 1) + assert.Equal(t, 1, written.Items[0].Version) } func TestIsConflictRecognizesAPIStatus(t *testing.T) { diff --git a/internal/sync/api/manifest.go b/internal/sync/api/manifest.go index 417ec444..a80f21fa 100644 --- a/internal/sync/api/manifest.go +++ b/internal/sync/api/manifest.go @@ -9,15 +9,13 @@ import ( syncdomain "github.com/launchdarkly/ldcli/internal/sync" ) -// SyncManifestFormatVersion is the remote sync manifest schema. -const SyncManifestFormatVersion = 1 - // SyncManifest is the sync baseline that LaunchDarkly stores for one project -// and one repository source. +// and one repository source. The API has no format version of its own: the +// path version of the API governs the shape, and the sync.lock file has its +// own formatVersion. type SyncManifest struct { - FormatVersion int `json:"formatVersion"` - Source string `json:"source"` - Items []SyncManifestResource `json:"items"` + Source string `json:"source"` + Items []SyncManifestResource `json:"items"` } // SyncManifestResource is one versioned baseline entry. @@ -58,14 +56,7 @@ func (client Client) GetSyncManifest(projectKey, source string) (SyncManifest, e if err != nil { return SyncManifest{}, err } - manifest, err := decodeJSON[SyncManifest](response, fmt.Sprintf("sync manifest for project %q", projectKey)) - if err != nil { - return SyncManifest{}, err - } - if err := validateSyncManifest(manifest); err != nil { - return SyncManifest{}, fmt.Errorf("sync manifest for project %q: %w", projectKey, err) - } - return manifest, nil + return decodeJSON[SyncManifest](response, fmt.Sprintf("sync manifest for project %q", projectKey)) } // PatchSyncManifest applies versioned changes and returns the new baseline. @@ -85,15 +76,13 @@ func (client Client) PatchSyncManifest( request := mutation{ method: http.MethodPatch, action: "update", resource: "sync manifest for project", key: projectKey, body: struct { - FormatVersion int `json:"formatVersion"` - Source string `json:"source"` - Upserts []SyncManifestUpsert `json:"upserts"` - Deletions []SyncManifestDeletion `json:"deletions"` + Source string `json:"source"` + Upserts []SyncManifestUpsert `json:"upserts"` + Deletions []SyncManifestDeletion `json:"deletions"` }{ - FormatVersion: SyncManifestFormatVersion, - Source: source, - Upserts: upserts, - Deletions: deletions, + Source: source, + Upserts: upserts, + Deletions: deletions, }, } response, err := client.mutate(request, manifestPath(projectKey)...) @@ -107,28 +96,9 @@ func (client Client) PatchSyncManifest( // caller must read the manifest again to find its state. return SyncManifest{}, newMutationError("decode updated", request.resource, projectKey, err) } - if err := validateSyncManifest(manifest); err != nil { - return SyncManifest{}, newMutationError("decode updated", request.resource, projectKey, err) - } return manifest, nil } -// Unwritten reports whether the manifest is the one that LaunchDarkly returns -// for a project and a source that no sync wrote yet: no entries and no format -// version. Sync treats it as an empty manifest of the supported format. -func (manifest SyncManifest) Unwritten() bool { - return manifest.FormatVersion == 0 && len(manifest.Items) == 0 -} - -// validateSyncManifest accepts a manifest of the supported format, and an -// unwritten manifest, which has no format to check. -func validateSyncManifest(manifest SyncManifest) error { - if manifest.Unwritten() || manifest.FormatVersion == SyncManifestFormatVersion { - return nil - } - return fmt.Errorf("unsupported formatVersion %d", manifest.FormatVersion) -} - func manifestPath(projectKey string) []string { return projectPath(projectKey, "configs/sync/manifests") } diff --git a/internal/sync/manifest/baseline_test.go b/internal/sync/manifest/baseline_test.go index 49a6c86b..17198d9a 100644 --- a/internal/sync/manifest/baseline_test.go +++ b/internal/sync/manifest/baseline_test.go @@ -124,7 +124,7 @@ func TestBaselineStaleComparesFingerprints(t *testing.T) { "newer version with a different fingerprint": {remote: remoteManifest(fingerprint("b"), 5), stale: true}, "newer version with the same fingerprint": {remote: remoteManifest(fingerprint("a"), 5)}, "same version with a different fingerprint": {remote: remoteManifest(fingerprint("b"), 4), stale: true}, - "remote entry removed": {remote: syncapi.SyncManifest{FormatVersion: syncapi.SyncManifestFormatVersion, Source: testSource}}, + "remote entry removed": {remote: syncapi.SyncManifest{Source: testSource}}, } for name, test := range tests { @@ -144,8 +144,7 @@ func TestBaselineStoreSaveSendsRemoteVersionsAndWritesLockWithoutThem(t *testing client := &manifestClient{ manifests: map[string]syncapi.SyncManifest{"production": remoteManifest(fingerprint("b"), 5)}, patchResponses: []syncapi.SyncManifest{{ - FormatVersion: syncapi.SyncManifestFormatVersion, - Source: testSource, + Source: testSource, Items: []syncapi.SyncManifestResource{ {ResourceKind: syncdomain.KindVariation, ResourceLookupKey: "support/default", Fingerprint: fingerprint("c"), Version: 6}, {ResourceKind: syncdomain.KindTool, ResourceLookupKey: "search", Fingerprint: fingerprint("d"), Version: 1}, @@ -211,8 +210,7 @@ func TestBaselineStoreSaveRemovesEmptyLock(t *testing.T) { client := &manifestClient{ manifests: map[string]syncapi.SyncManifest{"production": remoteManifest(fingerprint("a"), 4)}, patchResponses: []syncapi.SyncManifest{{ - FormatVersion: syncapi.SyncManifestFormatVersion, - Source: testSource, + Source: testSource, }}, } lock := lockWith(t, fingerprint("a"), 4) @@ -277,7 +275,7 @@ func variationID() syncdomain.ResourceID { } func remoteManifest(fingerprint string, version int) syncapi.SyncManifest { - return syncapi.SyncManifest{FormatVersion: syncapi.SyncManifestFormatVersion, Source: testSource, Items: []syncapi.SyncManifestResource{{ + return syncapi.SyncManifest{Source: testSource, Items: []syncapi.SyncManifestResource{{ ResourceKind: syncdomain.KindVariation, ResourceLookupKey: "support/default", Fingerprint: fingerprint, Version: version, }}} } diff --git a/internal/sync/manifest/store.go b/internal/sync/manifest/store.go index 43b7fe4e..0ebed1d4 100644 --- a/internal/sync/manifest/store.go +++ b/internal/sync/manifest/store.go @@ -133,15 +133,6 @@ func (store Store) patch( } func (store Store) resourcesFromRemote(projectKey string, remote syncapi.SyncManifest) ([]Resource, error) { - // A project and a source that no sync wrote yet have an empty manifest - // without a format version. - if !remote.Unwritten() && remote.FormatVersion != syncapi.SyncManifestFormatVersion { - return nil, fmt.Errorf( - "sync manifest for project %q has unsupported formatVersion %d", - projectKey, - remote.FormatVersion, - ) - } if remote.Source != "" && remote.Source != store.source { return nil, fmt.Errorf( "sync manifest for project %q returned source %q instead of %q", projectKey, remote.Source, store.source, diff --git a/internal/sync/manifest/store_test.go b/internal/sync/manifest/store_test.go index bb952e8d..aad16e27 100644 --- a/internal/sync/manifest/store_test.go +++ b/internal/sync/manifest/store_test.go @@ -49,15 +49,13 @@ func (client *manifestClient) PatchSyncManifest( return response, nil } -// A project that no sync wrote yet has an empty manifest without a format -// version in LaunchDarkly. It loads as no entries, and a manifest with -// entries still needs the supported format. -func TestStoreLoadsAnUnwrittenManifestAsEmpty(t *testing.T) { +// A project that no sync wrote yet has a manifest with no entries, which +// loads as no entries beside the projects that have some. +func TestStoreLoadsAnEmptyManifest(t *testing.T) { client := &manifestClient{manifests: map[string]syncapi.SyncManifest{ "fresh": {Source: "git:example/repo"}, "written": { - FormatVersion: syncapi.SyncManifestFormatVersion, - Source: "git:example/repo", + Source: "git:example/repo", Items: []syncapi.SyncManifestResource{{ ResourceKind: syncdomain.KindVariation, ResourceLookupKey: "config/a", Fingerprint: fingerprint("a"), Version: 2, @@ -70,31 +68,19 @@ func TestStoreLoadsAnUnwrittenManifestAsEmpty(t *testing.T) { require.NoError(t, err) require.Len(t, loaded.Resources, 1) assert.Equal(t, "written", loaded.Resources[0].ProjectKey) - - client.manifests["fresh"] = syncapi.SyncManifest{ - Source: "git:example/repo", - Items: []syncapi.SyncManifestResource{{ - ResourceKind: syncdomain.KindVariation, ResourceLookupKey: "config/b", - Fingerprint: fingerprint("b"), Version: 1, - }}, - } - _, err = NewStore(client, "git:example/repo").Load([]string{"fresh"}) - require.ErrorContains(t, err, "unsupported formatVersion 0") } func TestStoreLoadsProjectManifests(t *testing.T) { client := &manifestClient{manifests: map[string]syncapi.SyncManifest{ "zeta": { - FormatVersion: syncapi.SyncManifestFormatVersion, - Source: "git:example/repo", + Source: "git:example/repo", Items: []syncapi.SyncManifestResource{{ ResourceKind: syncdomain.KindVariation, ResourceLookupKey: "config/b", Fingerprint: fingerprint("b"), Version: 4, }}, }, "alpha": { - FormatVersion: syncapi.SyncManifestFormatVersion, - Source: "git:example/repo", + Source: "git:example/repo", Items: []syncapi.SyncManifestResource{{ ResourceKind: syncdomain.KindVariation, ResourceLookupKey: "config/a", Fingerprint: fingerprint("a"), Version: 2, @@ -113,8 +99,7 @@ func TestStoreLoadsProjectManifests(t *testing.T) { func TestStoreUpdatesWithRemoteVersions(t *testing.T) { client := &manifestClient{patchResponses: []syncapi.SyncManifest{{ - FormatVersion: syncapi.SyncManifestFormatVersion, - Source: "git:example/repo", + Source: "git:example/repo", Items: []syncapi.SyncManifestResource{ { ResourceKind: syncdomain.KindVariation, ResourceLookupKey: "config/existing", @@ -178,8 +163,7 @@ func TestStoreReportsOptimisticConflict(t *testing.T) { func TestStoreReadsBackAnUncertainPatchThatSucceeded(t *testing.T) { applied := syncapi.SyncManifest{ - FormatVersion: syncapi.SyncManifestFormatVersion, - Source: "git:example/repo", + Source: "git:example/repo", Items: []syncapi.SyncManifestResource{{ ResourceKind: syncdomain.KindVariation, ResourceLookupKey: "config/variation", Fingerprint: fingerprint("b"), Version: 2, diff --git a/internal/sync/prompt/acceptance_test.go b/internal/sync/prompt/acceptance_test.go index cd181a51..7b6119be 100644 --- a/internal/sync/prompt/acceptance_test.go +++ b/internal/sync/prompt/acceptance_test.go @@ -2601,7 +2601,7 @@ func runScoped(t *testing.T, root string, api *directAPI, options syncprompt.Opt func runWith(t *testing.T, root string, api *directAPI, runner syncprompt.Runner, options syncprompt.Options) (string, string, error) { t.Helper() if manifestsByRoot[root] == nil { - manifestsByRoot[root] = &syncapi.SyncManifest{FormatVersion: syncapi.SyncManifestFormatVersion, Source: "git:example/repo", Items: []syncapi.SyncManifestResource{}} + manifestsByRoot[root] = &syncapi.SyncManifest{Source: "git:example/repo", Items: []syncapi.SyncManifestResource{}} } api.manifest = manifestsByRoot[root] var output, notes bytes.Buffer @@ -2914,7 +2914,7 @@ func runPrompt(t *testing.T, root string, client resources.Client, arguments ... t.Setenv("XDG_CONFIG_HOME", t.TempDir()) manifest := manifestsByRoot[root] if manifest == nil { - manifest = &syncapi.SyncManifest{FormatVersion: syncapi.SyncManifestFormatVersion, Source: "git:example/repo", Items: []syncapi.SyncManifestResource{}} + manifest = &syncapi.SyncManifest{Source: "git:example/repo", Items: []syncapi.SyncManifestResource{}} manifestsByRoot[root] = manifest } switch api := client.(type) { @@ -3077,7 +3077,7 @@ func writeRemoteManifest(t *testing.T, root string, values ...syncdomain.Variati Kind: syncdomain.KindVariation, ProjectKey: "production", LookupKey: "support/" + value.Key, }, fingerprint(t, value)) } - remote := &syncapi.SyncManifest{FormatVersion: syncapi.SyncManifestFormatVersion, Source: "git:example/repo"} + remote := &syncapi.SyncManifest{Source: "git:example/repo"} for _, resource := range manifest.Resources { remote.Items = append(remote.Items, syncapi.SyncManifestResource{ ResourceKind: resource.ResourceKind,