mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 07:24:55 +08:00
fix(release):完善当前分发证明与质量门禁
This commit is contained in:
@@ -1332,7 +1332,7 @@ func validateTranslationWorkerRunParams(params backendrpc.TranslationWorkerRunPa
|
||||
|
||||
func validateTranslationMemoryConfirmParams(params backendrpc.TranslationMemoryConfirmParams) error {
|
||||
if strings.TrimSpace(params.RecordID) == "" || strings.TrimSpace(params.Reviewer) == "" {
|
||||
return errors.New("Translation Memory confirm requires record_id and reviewer")
|
||||
return errors.New("translation memory confirm requires record_id and reviewer")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+116
-8
@@ -91,11 +91,17 @@ func fixtureRPCBackend(t *testing.T, root string) *fakeBackend {
|
||||
doctor: &backendrpc.DoctorReport{Healthy: true, Status: "ok"},
|
||||
catalog: catalog,
|
||||
manifest: &backendrpc.ResourceManifestPage{
|
||||
Available: true,
|
||||
ResourceRoot: root,
|
||||
ManifestVersion: 1,
|
||||
TotalEntries: len(manifestEntries),
|
||||
Entries: manifestEntries,
|
||||
Available: true,
|
||||
Channel: "official",
|
||||
ReleaseID: "official-fixture",
|
||||
ResourceRoot: root,
|
||||
ManifestVersion: 1,
|
||||
PublicationIdentity: "fixture-publication-v1",
|
||||
MappingIdentity: "fixture-mapping-v1",
|
||||
ManifestIdentity: "fixture-manifest-v1",
|
||||
Generation: 1,
|
||||
TotalEntries: len(manifestEntries),
|
||||
Entries: manifestEntries,
|
||||
},
|
||||
releaseStatus: &backendrpc.ReleaseStatusReport{
|
||||
Status: "ready",
|
||||
@@ -821,13 +827,16 @@ type fakeBackend struct {
|
||||
statusCalls int
|
||||
doctorCalls int
|
||||
releaseStatusCalls int
|
||||
attestationCalls int
|
||||
status *backendrpc.DaemonStatusReport
|
||||
doctor *backendrpc.DoctorReport
|
||||
releaseStatus *backendrpc.ReleaseStatusReport
|
||||
releaseStatusErr error
|
||||
attestation *backendrpc.DistributionAttestation
|
||||
catalog json.RawMessage
|
||||
resource *backendrpc.ResourceState
|
||||
manifest *backendrpc.ResourceManifestPage
|
||||
manifestParams []backendrpc.ResourceManifestParams
|
||||
daemonLogs *backendrpc.LogsReport
|
||||
taskList *backendrpc.TaskList
|
||||
taskStatus *backendrpc.TaskRecord
|
||||
@@ -858,6 +867,77 @@ func (f *fakeBackend) ReleaseStatus(ctx context.Context) (*backendrpc.ReleaseSta
|
||||
OfficialDistributionReady: true,
|
||||
}, nil
|
||||
}
|
||||
func (f *fakeBackend) ReleaseAttestation(ctx context.Context) (*backendrpc.DistributionAttestation, error) {
|
||||
f.attestationCalls++
|
||||
// Keep the older test fixture controls useful while discovery moves to the
|
||||
// lightweight RPC: releaseStatus still supplies the desired ready/blocked
|
||||
// state unless a test explicitly installs an attestation.
|
||||
if f.attestation != nil {
|
||||
return f.attestation, nil
|
||||
}
|
||||
f.releaseStatusCalls++
|
||||
if f.releaseStatusErr != nil {
|
||||
return nil, f.releaseStatusErr
|
||||
}
|
||||
releaseStatus := f.releaseStatus
|
||||
if releaseStatus == nil {
|
||||
releaseStatus = &backendrpc.ReleaseStatusReport{
|
||||
Status: "ready",
|
||||
StatusCode: "distribution.ready",
|
||||
OfficialDistributionReady: true,
|
||||
}
|
||||
}
|
||||
releaseID := "fixture-release"
|
||||
resourceRoot := ""
|
||||
if f.manifest != nil {
|
||||
releaseID = f.manifest.ReleaseID
|
||||
resourceRoot = f.manifest.ResourceRoot
|
||||
}
|
||||
var catalog struct {
|
||||
Version struct {
|
||||
ID string `json:"id"`
|
||||
ResourceRoot string `json:"resource_root"`
|
||||
} `json:"version"`
|
||||
}
|
||||
if json.Unmarshal(f.catalog, &catalog) == nil {
|
||||
if catalog.Version.ID != "" {
|
||||
releaseID = catalog.Version.ID
|
||||
}
|
||||
if catalog.Version.ResourceRoot != "" {
|
||||
resourceRoot = catalog.Version.ResourceRoot
|
||||
}
|
||||
}
|
||||
if releaseStatus.OfficialCurrentReleaseID != "" {
|
||||
releaseID = releaseStatus.OfficialCurrentReleaseID
|
||||
}
|
||||
integrity := "verified"
|
||||
for _, release := range releaseStatus.Releases {
|
||||
if release.Channel == "official" && release.Current {
|
||||
integrity = release.DistributionIntegrityStatus
|
||||
break
|
||||
}
|
||||
}
|
||||
return &backendrpc.DistributionAttestation{
|
||||
Available: true,
|
||||
Channel: "official",
|
||||
ReleaseID: releaseID,
|
||||
ResourceRoot: resourceRoot,
|
||||
PublicationIdentity: "fixture-publication-v1",
|
||||
MappingIdentity: "fixture-mapping-v1",
|
||||
ManifestIdentity: "fixture-manifest-v1",
|
||||
EntryCount: func() int {
|
||||
if f.manifest == nil {
|
||||
return 0
|
||||
}
|
||||
return len(f.manifest.Entries)
|
||||
}(),
|
||||
IntegrityStatus: integrity,
|
||||
Status: releaseStatus.Status,
|
||||
StatusCode: releaseStatus.StatusCode,
|
||||
Ready: releaseStatus.OfficialDistributionReady,
|
||||
VerificationGeneration: 1,
|
||||
}, nil
|
||||
}
|
||||
func (f *fakeBackend) ResourceState(ctx context.Context) (*backendrpc.ResourceState, error) {
|
||||
if f.resource != nil {
|
||||
return f.resource, nil
|
||||
@@ -867,8 +947,33 @@ func (f *fakeBackend) ResourceState(ctx context.Context) (*backendrpc.ResourceSt
|
||||
func (f *fakeBackend) CatalogStatus(ctx context.Context) (json.RawMessage, error) {
|
||||
return f.catalog, nil
|
||||
}
|
||||
func (f *fakeBackend) ResourceManifest(ctx context.Context, offset int, limit int) (*backendrpc.ResourceManifestPage, error) {
|
||||
return f.manifest, nil
|
||||
func (f *fakeBackend) ResourceManifest(ctx context.Context, params backendrpc.ResourceManifestParams) (*backendrpc.ResourceManifestPage, error) {
|
||||
if f.manifest == nil {
|
||||
return nil, nil
|
||||
}
|
||||
f.manifestParams = append(f.manifestParams, params)
|
||||
page := *f.manifest
|
||||
if page.ReleaseID == "" {
|
||||
page.ReleaseID = params.ReleaseID
|
||||
}
|
||||
if page.ResourceRoot == "" {
|
||||
page.ResourceRoot = f.manifest.ResourceRoot
|
||||
}
|
||||
if page.PublicationIdentity == "" {
|
||||
page.PublicationIdentity = params.ExpectedPublicationIdentity
|
||||
}
|
||||
if page.ManifestIdentity == "" {
|
||||
page.ManifestIdentity = params.ExpectedManifestIdentity
|
||||
}
|
||||
if page.MappingIdentity == "" {
|
||||
page.MappingIdentity = "fixture-mapping-v1"
|
||||
}
|
||||
if page.Generation == 0 {
|
||||
page.Generation = 1
|
||||
}
|
||||
page.Offset = params.Offset
|
||||
page.Limit = params.Limit
|
||||
return &page, nil
|
||||
}
|
||||
func (f *fakeBackend) DaemonLogs(ctx context.Context, tail int) (*backendrpc.LogsReport, error) {
|
||||
if f.daemonLogs != nil {
|
||||
@@ -1762,9 +1867,12 @@ func (p *pollingBackend) ResourceState(ctx context.Context) (*backendrpc.Resourc
|
||||
func (p *pollingBackend) CatalogStatus(ctx context.Context) (json.RawMessage, error) {
|
||||
return nil, errors.New("unexpected catalog call")
|
||||
}
|
||||
func (p *pollingBackend) ResourceManifest(ctx context.Context, offset int, limit int) (*backendrpc.ResourceManifestPage, error) {
|
||||
func (p *pollingBackend) ResourceManifest(ctx context.Context, params backendrpc.ResourceManifestParams) (*backendrpc.ResourceManifestPage, error) {
|
||||
return nil, errors.New("unexpected manifest call")
|
||||
}
|
||||
func (p *pollingBackend) ReleaseAttestation(ctx context.Context) (*backendrpc.DistributionAttestation, error) {
|
||||
return nil, errors.New("unexpected attestation call")
|
||||
}
|
||||
|
||||
func TestStartRefreshLoopPollsBackend(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
@@ -332,7 +332,8 @@ BAT_API_STATE_DIR=/tmp/bat-pid
|
||||
# BAT_API_SOCKET=/tmp/bat-pid/bat.sock
|
||||
|
||||
# Optional override of the published release root (fixtures / emergency only).
|
||||
# Production obtains resource_root from daemon RPC (catalog.status / resource.manifest).
|
||||
# Production obtains resource_root from daemon RPC (release.attestation +
|
||||
# generation-bound resource.manifest).
|
||||
# BAT_API_RESOURCE_ROOT=
|
||||
|
||||
# Optional server-info JSON for Addressables root rewrite
|
||||
|
||||
@@ -24,12 +24,14 @@ func TestRustContractFixturesPreserveGoMirror(t *testing.T) {
|
||||
availableRaw := readContractFixture(t, "catalog-status.available.json")
|
||||
unavailableRaw := readContractFixture(t, "catalog-status.unavailable.json")
|
||||
manifestRaw := readContractFixture(t, "resource-manifest.page0.json")
|
||||
attestationRaw := readContractFixture(t, "release-attestation.json")
|
||||
snapshotRaw := readContractFixture(t, "official-sync-snapshot.json")
|
||||
|
||||
for name, raw := range map[string][]byte{
|
||||
"catalog available": availableRaw,
|
||||
"catalog unavailable": unavailableRaw,
|
||||
"resource manifest": manifestRaw,
|
||||
"attestation": attestationRaw,
|
||||
"snapshot": snapshotRaw,
|
||||
} {
|
||||
if bytes.Contains(raw, []byte("/tmp/")) {
|
||||
@@ -71,7 +73,12 @@ func TestRustContractFixturesPreserveGoMirror(t *testing.T) {
|
||||
if err := json.Unmarshal(manifestRaw, &manifest); err != nil {
|
||||
t.Fatalf("decode resource manifest: %v", err)
|
||||
}
|
||||
if !manifest.Available || manifest.ManifestVersion != 1 || manifest.TotalEntries != 2 {
|
||||
if !manifest.Available || manifest.Channel != "official" ||
|
||||
manifest.ManifestVersion != 1 || manifest.TotalEntries != 2 ||
|
||||
manifest.ReleaseID != "${VERSION_ID}" ||
|
||||
manifest.PublicationIdentity != "${PUBLICATION_IDENTITY}" ||
|
||||
manifest.ManifestIdentity != "${MANIFEST_IDENTITY}" ||
|
||||
manifest.Generation != 7 {
|
||||
t.Fatalf("manifest header=%+v", manifest)
|
||||
}
|
||||
if len(manifest.Entries) != 2 {
|
||||
@@ -84,6 +91,18 @@ func TestRustContractFixturesPreserveGoMirror(t *testing.T) {
|
||||
t.Fatalf("manifest first entry=%+v", manifest.Entries[0])
|
||||
}
|
||||
|
||||
var attestation backendrpc.DistributionAttestation
|
||||
if err := json.Unmarshal(attestationRaw, &attestation); err != nil {
|
||||
t.Fatalf("decode attestation: %v", err)
|
||||
}
|
||||
if !attestation.Available || !attestation.Ready ||
|
||||
attestation.ReleaseID != "${VERSION_ID}" ||
|
||||
attestation.ManifestIdentity != "${MANIFEST_IDENTITY}" ||
|
||||
attestation.VerificationGeneration != 7 ||
|
||||
attestation.VerifiedAt == nil || *attestation.VerifiedAt != 1000 {
|
||||
t.Fatalf("attestation=%+v", attestation)
|
||||
}
|
||||
|
||||
var snapshot struct {
|
||||
AppVersion string `json:"app_version"`
|
||||
BundleVersion string `json:"bundle_version"`
|
||||
|
||||
@@ -27,9 +27,9 @@ paths:
|
||||
summary: Release readiness
|
||||
responses:
|
||||
"200":
|
||||
description: A release authorized by Rust release.status and fully represented by the local read snapshot is available.
|
||||
description: A current official release authorized by Rust release.attestation and fully represented by the bound local read snapshot is available.
|
||||
"503":
|
||||
description: The Rust whole-release distribution health fact or the local read snapshot is not distributable.
|
||||
description: The Rust current attestation is unavailable, stale, invalid, or the bound local read snapshot is not distributable.
|
||||
/v1/bootstrap:
|
||||
get:
|
||||
summary: Startup resource bootstrap
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"bat-api/internal/backendrpc"
|
||||
)
|
||||
|
||||
func testAttestation(root, releaseID, integrity string, ready bool) *backendrpc.DistributionAttestation {
|
||||
return &backendrpc.DistributionAttestation{
|
||||
Available: true,
|
||||
Channel: "official",
|
||||
ReleaseID: releaseID,
|
||||
ResourceRoot: root,
|
||||
PublicationIdentity: "publication-" + releaseID,
|
||||
MappingIdentity: "mapping-" + releaseID,
|
||||
ManifestIdentity: "manifest-" + releaseID,
|
||||
EntryCount: 3,
|
||||
IntegrityStatus: integrity,
|
||||
Status: integrity,
|
||||
StatusCode: "distribution." + integrity,
|
||||
Ready: ready,
|
||||
VerificationGeneration: 4,
|
||||
}
|
||||
}
|
||||
|
||||
func testManifestPage(attestation *backendrpc.DistributionAttestation, offset int, entries int) *backendrpc.ResourceManifestPage {
|
||||
pageEntries := make([]backendrpc.ResourceManifestEntry, entries)
|
||||
for index := range pageEntries {
|
||||
size := uint64(index + 1)
|
||||
pageEntries[index] = backendrpc.ResourceManifestEntry{
|
||||
URL: "https://example.invalid/" + string(rune('a'+offset+index)),
|
||||
Destination: "resource-" + string(rune('a'+offset+index)),
|
||||
Bytes: &size,
|
||||
BLAKE3: "blake3",
|
||||
}
|
||||
}
|
||||
return &backendrpc.ResourceManifestPage{
|
||||
Available: true,
|
||||
Channel: "official",
|
||||
ReleaseID: attestation.ReleaseID,
|
||||
ResourceRoot: attestation.ResourceRoot,
|
||||
ManifestVersion: 1,
|
||||
PublicationIdentity: attestation.PublicationIdentity,
|
||||
MappingIdentity: attestation.MappingIdentity,
|
||||
ManifestIdentity: attestation.ManifestIdentity,
|
||||
Generation: attestation.VerificationGeneration,
|
||||
TotalEntries: attestation.EntryCount,
|
||||
Offset: offset,
|
||||
Limit: 2,
|
||||
Entries: pageEntries,
|
||||
}
|
||||
}
|
||||
|
||||
type pagedManifestBackend struct {
|
||||
*fakeBackend
|
||||
pages []*backendrpc.ResourceManifestPage
|
||||
params []backendrpc.ResourceManifestParams
|
||||
}
|
||||
|
||||
func (b *pagedManifestBackend) ResourceManifest(_ context.Context, params backendrpc.ResourceManifestParams) (*backendrpc.ResourceManifestPage, error) {
|
||||
b.params = append(b.params, params)
|
||||
pageIndex := len(b.params) - 1
|
||||
page := *b.pages[pageIndex]
|
||||
return &page, nil
|
||||
}
|
||||
|
||||
func TestFetchAllManifestEntriesRejectsMixedPages(t *testing.T) {
|
||||
attestation := testAttestation("/srv/official/current", "official-a", "verified", true)
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(*backendrpc.ResourceManifestPage)
|
||||
}{
|
||||
{
|
||||
name: "release",
|
||||
mutate: func(page *backendrpc.ResourceManifestPage) { page.ReleaseID = "official-b" },
|
||||
},
|
||||
{
|
||||
name: "root",
|
||||
mutate: func(page *backendrpc.ResourceManifestPage) { page.ResourceRoot = "/srv/official/current-b" },
|
||||
},
|
||||
{
|
||||
name: "manifest identity",
|
||||
mutate: func(page *backendrpc.ResourceManifestPage) { page.ManifestIdentity = "manifest-b" },
|
||||
},
|
||||
{
|
||||
name: "total",
|
||||
mutate: func(page *backendrpc.ResourceManifestPage) { page.TotalEntries = 4 },
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
first := testManifestPage(attestation, 0, 2)
|
||||
second := testManifestPage(attestation, 2, 1)
|
||||
test.mutate(second)
|
||||
backend := &pagedManifestBackend{
|
||||
fakeBackend: &fakeBackend{},
|
||||
pages: []*backendrpc.ResourceManifestPage{first, second},
|
||||
}
|
||||
if _, _, _, err := fetchAllManifestEntriesWithPageSize(
|
||||
context.Background(),
|
||||
backend,
|
||||
attestation,
|
||||
2,
|
||||
); err == nil {
|
||||
t.Fatal("expected mixed-page validation error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchAllManifestEntriesAcceptsMatchingGeneration(t *testing.T) {
|
||||
attestation := testAttestation("/srv/official/current", "official-a", "verified", true)
|
||||
backend := &pagedManifestBackend{
|
||||
fakeBackend: &fakeBackend{},
|
||||
pages: []*backendrpc.ResourceManifestPage{
|
||||
testManifestPage(attestation, 0, 2),
|
||||
testManifestPage(attestation, 2, 1),
|
||||
},
|
||||
}
|
||||
entries, version, root, err := fetchAllManifestEntriesWithPageSize(
|
||||
context.Background(),
|
||||
backend,
|
||||
attestation,
|
||||
2,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(entries) != 3 || version != 1 || root != attestation.ResourceRoot {
|
||||
t.Fatalf("entries=%d version=%d root=%q", len(entries), version, root)
|
||||
}
|
||||
if len(backend.params) != 2 ||
|
||||
backend.params[1].ReleaseID != attestation.ReleaseID ||
|
||||
backend.params[1].ExpectedManifestIdentity != attestation.ManifestIdentity {
|
||||
t.Fatalf("params=%+v", backend.params)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverRejectsAttestationThenCatalogCurrentSwitch(t *testing.T) {
|
||||
root := fixtureRoot(t)
|
||||
backend := fixtureRPCBackend(t, root)
|
||||
backend.attestation = testAttestation(root, "official-a", "verified", true)
|
||||
catalog, err := json.Marshal(map[string]any{
|
||||
"available": true,
|
||||
"version": map[string]any{
|
||||
"id": "official-b",
|
||||
"resource_root": root,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
backend.catalog = catalog
|
||||
result, err := DiscoverAndIndex(context.Background(), backend, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Index == nil || result.Index.Summary().Ready || result.Index.Summary().EntryCount != 0 {
|
||||
t.Fatalf("summary=%+v", result.Index.Summary())
|
||||
}
|
||||
if len(backend.manifestParams) != 0 {
|
||||
t.Fatalf("manifest should not be fetched after current switch: %+v", backend.manifestParams)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealthyAttestationThenCurrentSwitchClearsSnapshot(t *testing.T) {
|
||||
root := copyFixtureRoot(t)
|
||||
backend := fixtureRPCBackend(t, root)
|
||||
backend.attestation = testAttestation(root, "official-a", "verified", true)
|
||||
backend.attestation.EntryCount = 2
|
||||
backend.catalog = mustCatalogForTest(t, root, "official-a")
|
||||
backend.manifest.ReleaseID = "official-a"
|
||||
backend.manifest.PublicationIdentity = "publication-official-a"
|
||||
backend.manifest.MappingIdentity = "mapping-official-a"
|
||||
backend.manifest.ManifestIdentity = "manifest-official-a"
|
||||
backend.manifest.Generation = 4
|
||||
cfg := DefaultConfig()
|
||||
cfg.RefreshInterval = 0
|
||||
if err := cfg.Normalize(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server := NewServer(cfg, backend, nil)
|
||||
if err := server.Refresh(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !server.index().Summary().Ready {
|
||||
t.Fatal("initial snapshot should be ready")
|
||||
}
|
||||
|
||||
backend.attestation = testAttestation(root, "official-b", "verified", true)
|
||||
backend.catalog = mustCatalogForTest(t, root, "official-b")
|
||||
if err := server.Refresh(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if summary := server.index().Summary(); summary.Ready || summary.ResourceRoot != "" {
|
||||
t.Fatalf("mixed snapshot was retained: %+v", summary)
|
||||
}
|
||||
}
|
||||
|
||||
func mustCatalogForTest(t *testing.T, root, releaseID string) json.RawMessage {
|
||||
t.Helper()
|
||||
raw, err := json.Marshal(map[string]any{
|
||||
"available": true,
|
||||
"version": map[string]any{
|
||||
"id": releaseID,
|
||||
"resource_root": root,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
func TestStaleOrInvalidMatchingAttestationNeverReadiesIndex(t *testing.T) {
|
||||
for _, integrity := range []string{"stale", "invalid"} {
|
||||
t.Run(integrity, func(t *testing.T) {
|
||||
root := fixtureRoot(t)
|
||||
backend := fixtureRPCBackend(t, root)
|
||||
backend.attestation = testAttestation(root, "official-a", integrity, false)
|
||||
backend.attestation.EntryCount = 2
|
||||
backend.catalog = mustCatalogForTest(t, root, "official-a")
|
||||
backend.manifest.ReleaseID = "official-a"
|
||||
backend.manifest.PublicationIdentity = "publication-official-a"
|
||||
backend.manifest.MappingIdentity = "mapping-official-a"
|
||||
backend.manifest.ManifestIdentity = "manifest-official-a"
|
||||
backend.manifest.Generation = 4
|
||||
result, err := DiscoverAndIndex(context.Background(), backend, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Index == nil || result.Index.Summary().Ready ||
|
||||
result.Index.Summary().Distribution.Ready {
|
||||
t.Fatalf("summary=%+v", result.Index.Summary())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -57,17 +57,25 @@ type GameMainConfigSummary struct {
|
||||
|
||||
// DistributionHealth is the release-level authorization used by read paths.
|
||||
//
|
||||
// In RPC mode Ready is copied from Rust's release.status
|
||||
// official_distribution_ready fact. The local manifest checks only establish
|
||||
// that this process has a usable read snapshot; they do not replace Rust's
|
||||
// release verifier.
|
||||
// In RPC mode these fields are copied from Rust's current official
|
||||
// attestation. The local manifest checks only establish that this process has
|
||||
// a complete, safe read snapshot; they do not replace Rust's verifier.
|
||||
type DistributionHealth struct {
|
||||
Ready bool `json:"ready"`
|
||||
Source string `json:"source"`
|
||||
Status string `json:"status,omitempty"`
|
||||
StatusCode string `json:"status_code,omitempty"`
|
||||
IntegrityStatus string `json:"integrity_status,omitempty"`
|
||||
Diagnostics []string `json:"diagnostics,omitempty"`
|
||||
Ready bool `json:"ready"`
|
||||
Source string `json:"source"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
ReleaseID string `json:"release_id,omitempty"`
|
||||
ResourceRoot string `json:"resource_root,omitempty"`
|
||||
PublicationIdentity string `json:"publication_identity,omitempty"`
|
||||
MappingIdentity string `json:"mapping_identity,omitempty"`
|
||||
ManifestIdentity string `json:"manifest_identity,omitempty"`
|
||||
EntryCount int `json:"entry_count,omitempty"`
|
||||
VerificationGeneration uint64 `json:"verification_generation,omitempty"`
|
||||
VerifiedAt *uint64 `json:"verified_at,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
StatusCode string `json:"status_code,omitempty"`
|
||||
IntegrityStatus string `json:"integrity_status,omitempty"`
|
||||
Diagnostics []string `json:"diagnostics,omitempty"`
|
||||
}
|
||||
|
||||
// ReleaseIndex is the in-memory view of a published resource root.
|
||||
@@ -122,7 +130,11 @@ func (idx *ReleaseIndex) Summary() ReleaseSummary {
|
||||
if distribution.Source == "" {
|
||||
distribution.Ready = localComplete && idx.Source != "rpc" && idx.Source != "rpc+local_manifest"
|
||||
} else {
|
||||
distribution.Ready = distribution.Ready && localComplete
|
||||
distribution.Ready = distribution.Ready &&
|
||||
localComplete &&
|
||||
(idx.Source != "rpc" ||
|
||||
(distribution.ManifestIdentity != "" &&
|
||||
distribution.EntryCount == len(idx.Entries)))
|
||||
}
|
||||
return ReleaseSummary{
|
||||
ResourceRoot: idx.ResourceRoot,
|
||||
|
||||
+161
-61
@@ -12,16 +12,18 @@ import (
|
||||
|
||||
// Backend is the subset of daemon RPC used by bat-api.
|
||||
//
|
||||
// Call order for discovery (per plan review):
|
||||
// Call order for discovery:
|
||||
// 1. daemon.status
|
||||
// 2. daemon.doctor
|
||||
// 3. catalog.status / resource.manifest (and resource.state as needed)
|
||||
// 3. release.attestation
|
||||
// 4. catalog.status / bound resource.manifest (and resource.state as needed)
|
||||
type Backend interface {
|
||||
DaemonStatus(ctx context.Context) (*backendrpc.DaemonStatusReport, error)
|
||||
DaemonDoctor(ctx context.Context) (*backendrpc.DoctorReport, error)
|
||||
ResourceState(ctx context.Context) (*backendrpc.ResourceState, error)
|
||||
CatalogStatus(ctx context.Context) (json.RawMessage, error)
|
||||
ResourceManifest(ctx context.Context, offset int, limit int) (*backendrpc.ResourceManifestPage, error)
|
||||
ResourceManifest(ctx context.Context, params backendrpc.ResourceManifestParams) (*backendrpc.ResourceManifestPage, error)
|
||||
ReleaseAttestation(ctx context.Context) (*backendrpc.DistributionAttestation, error)
|
||||
}
|
||||
|
||||
// ControlBackend is the explicitly allowlisted mutation subset exposed through
|
||||
@@ -146,8 +148,11 @@ func (r RPCClient) ResourceState(ctx context.Context) (*backendrpc.ResourceState
|
||||
func (r RPCClient) CatalogStatus(ctx context.Context) (json.RawMessage, error) {
|
||||
return r.Client.CatalogStatus(ctx)
|
||||
}
|
||||
func (r RPCClient) ResourceManifest(ctx context.Context, offset int, limit int) (*backendrpc.ResourceManifestPage, error) {
|
||||
return r.Client.ResourceManifest(ctx, offset, limit)
|
||||
func (r RPCClient) ResourceManifest(ctx context.Context, params backendrpc.ResourceManifestParams) (*backendrpc.ResourceManifestPage, error) {
|
||||
return r.Client.ResourceManifest(ctx, params)
|
||||
}
|
||||
func (r RPCClient) ReleaseAttestation(ctx context.Context) (*backendrpc.DistributionAttestation, error) {
|
||||
return r.Client.ReleaseAttestation(ctx)
|
||||
}
|
||||
func (r RPCClient) DaemonRestart(ctx context.Context) (*backendrpc.Ack, error) {
|
||||
return r.Client.DaemonRestart(ctx)
|
||||
@@ -310,6 +315,7 @@ type DiscoverResult struct {
|
||||
DoctorHealthy *bool
|
||||
Status *backendrpc.DaemonStatusReport
|
||||
Doctor *backendrpc.DoctorReport
|
||||
Attestation *backendrpc.DistributionAttestation
|
||||
ReleaseStatus *backendrpc.ReleaseStatusReport
|
||||
Distribution DistributionHealth
|
||||
Snapshot *SnapshotSummary
|
||||
@@ -318,10 +324,11 @@ type DiscoverResult struct {
|
||||
Warnings []string
|
||||
}
|
||||
|
||||
// DiscoverAndIndex contacts the daemon (status first, then doctor, then
|
||||
// release.status) and builds a release index from paginated resource.manifest
|
||||
// plus on-disk checks. Rust's release.status is the only release-level
|
||||
// integrity authorization used for the production RPC path.
|
||||
// DiscoverAndIndex contacts the daemon (status first, then doctor, then the
|
||||
// lightweight current-release attestation) and builds a release index from
|
||||
// pages bound to that attestation plus on-disk checks. Rust's attestation is
|
||||
// the only release-level integrity authorization used for the production RPC
|
||||
// path; release.status remains an administrative diagnostic.
|
||||
//
|
||||
// If resourceRootOverride is non-empty, it wins over RPC-reported roots after
|
||||
// RPC health probes (still preferred for production to call status/doctor).
|
||||
@@ -390,30 +397,33 @@ func DiscoverAndIndex(ctx context.Context, backend Backend, resourceRootOverride
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// 3) release.status is the Rust-owned whole-release distribution gate.
|
||||
releaseStatusBackend, ok := backend.(ReleaseStatusBackend)
|
||||
// 3) release.attestation is the Rust-owned current-release distribution gate.
|
||||
attestationBackend, ok := backend.(AttestationBackend)
|
||||
if !ok {
|
||||
out.Warnings = append(out.Warnings, "release.status: backend does not expose Rust release health")
|
||||
out.Warnings = append(out.Warnings, "release.attestation: backend does not expose Rust health proof")
|
||||
return emptyRPCResult(out, nil, "Rust release health is unavailable"),
|
||||
fmt.Errorf("rust release health is unavailable")
|
||||
}
|
||||
releaseStatus, err := releaseStatusBackend.ReleaseStatus(ctx)
|
||||
attestation, err := attestationBackend.ReleaseAttestation(ctx)
|
||||
if err != nil {
|
||||
out.Warnings = append(out.Warnings, fmt.Sprintf("release.status: %v", err))
|
||||
out.Warnings = append(out.Warnings, fmt.Sprintf("release.attestation: %v", err))
|
||||
return emptyRPCResult(out, nil, "Rust release health query failed"),
|
||||
fmt.Errorf("release.status failed: %w", err)
|
||||
fmt.Errorf("release.attestation failed: %w", err)
|
||||
}
|
||||
if releaseStatus == nil {
|
||||
out.Warnings = append(out.Warnings, "release.status: empty response")
|
||||
if attestation == nil {
|
||||
out.Warnings = append(out.Warnings, "release.attestation: empty response")
|
||||
return emptyRPCResult(out, nil, "Rust release health query returned no response"),
|
||||
fmt.Errorf("release.status returned an empty response")
|
||||
fmt.Errorf("release.attestation returned an empty response")
|
||||
}
|
||||
out.Attestation = attestation
|
||||
out.Distribution = rustAttestationHealth(attestation)
|
||||
if !attestation.Available {
|
||||
return emptyRPCResult(out, nil, "Rust current official distribution attestation is unavailable"), nil
|
||||
}
|
||||
out.ReleaseStatus = releaseStatus
|
||||
out.Distribution = rustDistributionHealth(releaseStatus)
|
||||
|
||||
// Catalog / resource discovery
|
||||
var snapshot *SnapshotSummary
|
||||
var resourceRoot string
|
||||
resourceRoot := attestation.ResourceRoot
|
||||
catalogAvailabilityKnown := false
|
||||
catalogAvailable := false
|
||||
|
||||
@@ -429,18 +439,31 @@ func DiscoverAndIndex(ctx context.Context, backend Backend, resourceRootOverride
|
||||
}
|
||||
if snapshot != nil &&
|
||||
snapshot.VersionID != "" &&
|
||||
out.ReleaseStatus.OfficialCurrentReleaseID != "" &&
|
||||
snapshot.VersionID != out.ReleaseStatus.OfficialCurrentReleaseID {
|
||||
attestation.ReleaseID != "" &&
|
||||
snapshot.VersionID != attestation.ReleaseID {
|
||||
return emptyRPCResult(
|
||||
out,
|
||||
snapshot,
|
||||
fmt.Sprintf(
|
||||
"release.status current ID %q does not match catalog current ID %q",
|
||||
out.ReleaseStatus.OfficialCurrentReleaseID,
|
||||
"release.attestation current ID %q does not match catalog current ID %q",
|
||||
attestation.ReleaseID,
|
||||
snapshot.VersionID,
|
||||
),
|
||||
), nil
|
||||
}
|
||||
if resourceRoot != "" &&
|
||||
attestation.ResourceRoot != "" &&
|
||||
resourceRoot != attestation.ResourceRoot {
|
||||
return emptyRPCResult(
|
||||
out,
|
||||
snapshot,
|
||||
fmt.Sprintf(
|
||||
"catalog current root %q does not match attestation root %q",
|
||||
resourceRoot,
|
||||
attestation.ResourceRoot,
|
||||
),
|
||||
), nil
|
||||
}
|
||||
if catalogAvailabilityKnown && !catalogAvailable {
|
||||
return emptyRPCResult(out, snapshot, "catalog.status available=false; no published release"), nil
|
||||
}
|
||||
@@ -485,7 +508,7 @@ func DiscoverAndIndex(ctx context.Context, backend Backend, resourceRootOverride
|
||||
}
|
||||
}
|
||||
|
||||
entries, manifestVersion, rootFromManifest, err := fetchAllManifestEntries(ctx, backend)
|
||||
entries, manifestVersion, rootFromManifest, err := fetchAllManifestEntries(ctx, backend, attestation)
|
||||
if err != nil {
|
||||
out.Warnings = append(out.Warnings, fmt.Sprintf("resource.manifest: %v", err))
|
||||
// Without the RPC manifest there is no evidence that the local
|
||||
@@ -498,6 +521,17 @@ func DiscoverAndIndex(ctx context.Context, backend Backend, resourceRootOverride
|
||||
), nil
|
||||
}
|
||||
if rootFromManifest != "" {
|
||||
if attestation.ResourceRoot != "" && rootFromManifest != attestation.ResourceRoot {
|
||||
return emptyRPCResult(
|
||||
out,
|
||||
snapshot,
|
||||
fmt.Sprintf(
|
||||
"resource.manifest resource root %q does not match attestation root %q",
|
||||
rootFromManifest,
|
||||
attestation.ResourceRoot,
|
||||
),
|
||||
), nil
|
||||
}
|
||||
resourceRoot = rootFromManifest
|
||||
}
|
||||
if resourceRootOverride != "" {
|
||||
@@ -530,6 +564,13 @@ func DiscoverAndIndex(ctx context.Context, backend Backend, resourceRootOverride
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AttestationBackend exposes the lightweight current official health proof.
|
||||
// It is intentionally separate from ReleaseStatusBackend because health
|
||||
// refreshes must not require the historical release scan.
|
||||
type AttestationBackend interface {
|
||||
ReleaseAttestation(ctx context.Context) (*backendrpc.DistributionAttestation, error)
|
||||
}
|
||||
|
||||
func emptyRPCResult(out *DiscoverResult, snapshot *SnapshotSummary, warning string) *DiscoverResult {
|
||||
distribution := out.Distribution
|
||||
if distribution.Source == "" {
|
||||
@@ -553,52 +594,48 @@ func emptyRPCResult(out *DiscoverResult, snapshot *SnapshotSummary, warning stri
|
||||
return out
|
||||
}
|
||||
|
||||
func rustDistributionHealth(report *backendrpc.ReleaseStatusReport) DistributionHealth {
|
||||
func rustAttestationHealth(report *backendrpc.DistributionAttestation) DistributionHealth {
|
||||
health := DistributionHealth{
|
||||
Source: "rust_release_status",
|
||||
Status: "blocked",
|
||||
StatusCode: "distribution.blocked",
|
||||
IntegrityStatus: "unknown",
|
||||
Source: "rust_release_attestation",
|
||||
Status: "unavailable",
|
||||
StatusCode: "distribution.attestation_unavailable",
|
||||
IntegrityStatus: "unavailable",
|
||||
}
|
||||
if report == nil {
|
||||
return health
|
||||
}
|
||||
health.Ready = report.OfficialDistributionReady
|
||||
health.Ready = report.Ready
|
||||
health.Channel = report.Channel
|
||||
health.ReleaseID = report.ReleaseID
|
||||
health.ResourceRoot = report.ResourceRoot
|
||||
health.PublicationIdentity = report.PublicationIdentity
|
||||
health.MappingIdentity = report.MappingIdentity
|
||||
health.ManifestIdentity = report.ManifestIdentity
|
||||
health.EntryCount = report.EntryCount
|
||||
health.VerificationGeneration = report.VerificationGeneration
|
||||
health.VerifiedAt = report.VerifiedAt
|
||||
health.Status = report.Status
|
||||
health.StatusCode = report.StatusCode
|
||||
health.IntegrityStatus = report.IntegrityStatus
|
||||
health.Diagnostics = append([]string(nil), report.Diagnostics...)
|
||||
if health.Status == "" {
|
||||
if health.Ready {
|
||||
health.Status = "ready"
|
||||
} else {
|
||||
health.Status = "blocked"
|
||||
}
|
||||
health.Status = "unavailable"
|
||||
}
|
||||
if health.StatusCode == "" {
|
||||
if health.Ready {
|
||||
health.StatusCode = "distribution.ready"
|
||||
} else {
|
||||
health.StatusCode = "distribution.blocked"
|
||||
}
|
||||
health.StatusCode = "distribution.attestation_unavailable"
|
||||
}
|
||||
for _, release := range report.Releases {
|
||||
if release.Channel == "official" && release.Current {
|
||||
health.IntegrityStatus = release.DistributionIntegrityStatus
|
||||
health.Diagnostics = append([]string(nil), release.Diagnostics...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if health.IntegrityStatus == "valid" && !health.Ready {
|
||||
health.IntegrityStatus = "invalid"
|
||||
if health.IntegrityStatus == "" {
|
||||
health.IntegrityStatus = "unavailable"
|
||||
}
|
||||
return health
|
||||
}
|
||||
|
||||
func unavailableRustDistributionHealth() DistributionHealth {
|
||||
return DistributionHealth{
|
||||
Source: "rust_release_status",
|
||||
Source: "rust_release_attestation",
|
||||
Status: "unavailable",
|
||||
StatusCode: "distribution.health_unavailable",
|
||||
IntegrityStatus: "unknown",
|
||||
StatusCode: "distribution.attestation_unavailable",
|
||||
IntegrityStatus: "unavailable",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -675,25 +712,85 @@ func parseCatalogAvailability(raw json.RawMessage) (bool, bool) {
|
||||
return *payload.Available, true
|
||||
}
|
||||
|
||||
func fetchAllManifestEntries(ctx context.Context, backend Backend) ([]manifestEntry, int, string, error) {
|
||||
const pageSize = 500
|
||||
func fetchAllManifestEntries(
|
||||
ctx context.Context,
|
||||
backend Backend,
|
||||
attestation *backendrpc.DistributionAttestation,
|
||||
) ([]manifestEntry, int, string, error) {
|
||||
return fetchAllManifestEntriesWithPageSize(ctx, backend, attestation, 500)
|
||||
}
|
||||
|
||||
func fetchAllManifestEntriesWithPageSize(
|
||||
ctx context.Context,
|
||||
backend Backend,
|
||||
attestation *backendrpc.DistributionAttestation,
|
||||
pageSize int,
|
||||
) ([]manifestEntry, int, string, error) {
|
||||
if pageSize <= 0 {
|
||||
return nil, 0, "", fmt.Errorf("manifest page size must be positive")
|
||||
}
|
||||
offset := 0
|
||||
var all []manifestEntry
|
||||
var version int
|
||||
var root string
|
||||
var root, releaseID, publicationIdentity, mappingIdentity, manifestIdentity string
|
||||
total := -1
|
||||
for {
|
||||
page, err := backend.ResourceManifest(ctx, offset, pageSize)
|
||||
page, err := backend.ResourceManifest(ctx, backendrpc.ResourceManifestParams{
|
||||
ReleaseID: attestation.ReleaseID,
|
||||
ExpectedPublicationIdentity: attestation.PublicationIdentity,
|
||||
ExpectedManifestIdentity: attestation.ManifestIdentity,
|
||||
Offset: offset,
|
||||
Limit: pageSize,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, "", err
|
||||
}
|
||||
if page == nil {
|
||||
return nil, 0, "", fmt.Errorf("resource.manifest returned nil page")
|
||||
}
|
||||
if !page.Available {
|
||||
return nil, 0, "", fmt.Errorf("resource.manifest available=false")
|
||||
}
|
||||
if root == "" {
|
||||
root = page.ResourceRoot
|
||||
}
|
||||
if version == 0 {
|
||||
releaseID = page.ReleaseID
|
||||
publicationIdentity = page.PublicationIdentity
|
||||
mappingIdentity = page.MappingIdentity
|
||||
manifestIdentity = page.ManifestIdentity
|
||||
version = page.ManifestVersion
|
||||
total = page.TotalEntries
|
||||
} else if page.ResourceRoot != root ||
|
||||
page.ReleaseID != releaseID ||
|
||||
page.PublicationIdentity != publicationIdentity ||
|
||||
page.MappingIdentity != mappingIdentity ||
|
||||
page.ManifestIdentity != manifestIdentity ||
|
||||
page.ManifestVersion != version ||
|
||||
page.TotalEntries != total {
|
||||
return nil, 0, "", fmt.Errorf("resource.manifest page identity or total changed")
|
||||
}
|
||||
if page.Offset != offset {
|
||||
return nil, 0, "", fmt.Errorf(
|
||||
"resource.manifest page offset mismatch: requested=%d actual=%d",
|
||||
offset,
|
||||
page.Offset,
|
||||
)
|
||||
}
|
||||
if page.Limit <= 0 || page.Limit > pageSize {
|
||||
return nil, 0, "", fmt.Errorf("resource.manifest page limit is unreasonable: %d", page.Limit)
|
||||
}
|
||||
if page.TotalEntries < 0 || len(page.Entries) > page.Limit {
|
||||
return nil, 0, "", fmt.Errorf("resource.manifest page entry count is unreasonable")
|
||||
}
|
||||
if total < 0 || offset > total || offset+len(page.Entries) > total {
|
||||
return nil, 0, "", fmt.Errorf("resource.manifest page exceeds declared total")
|
||||
}
|
||||
if attestation != nil && (releaseID != attestation.ReleaseID ||
|
||||
root != attestation.ResourceRoot ||
|
||||
publicationIdentity != attestation.PublicationIdentity ||
|
||||
mappingIdentity != attestation.MappingIdentity ||
|
||||
manifestIdentity != attestation.ManifestIdentity ||
|
||||
total != attestation.EntryCount) {
|
||||
return nil, 0, "", fmt.Errorf("resource.manifest page does not match attestation")
|
||||
}
|
||||
for _, e := range page.Entries {
|
||||
var bytes uint64
|
||||
@@ -708,9 +805,12 @@ func fetchAllManifestEntries(ctx context.Context, backend Backend) ([]manifestEn
|
||||
})
|
||||
}
|
||||
offset += len(page.Entries)
|
||||
if len(page.Entries) == 0 || offset >= page.TotalEntries {
|
||||
if offset == total {
|
||||
break
|
||||
}
|
||||
if len(page.Entries) == 0 || len(page.Entries) < page.Limit {
|
||||
return nil, 0, "", fmt.Errorf("resource.manifest page has a gap before total")
|
||||
}
|
||||
}
|
||||
return all, version, root, nil
|
||||
}
|
||||
|
||||
+3
-1
@@ -7,7 +7,9 @@ contract fixture。JSON 由 Rust 代码路径产出后归一化,只替换本
|
||||
覆盖范围:
|
||||
|
||||
- `catalog.status` 可用与不可用响应。
|
||||
- `resource.manifest` 第一页分页响应。
|
||||
- `resource.manifest` 第一页分页响应,包含 release/publication/mapping/manifest
|
||||
identity 和 generation 绑定字段。
|
||||
- `release.attestation` 当前 official health/publication proof。
|
||||
- 对应 release 的 `official-sync-snapshot.json`。
|
||||
- `launcher_metadata` 与 `game_main_config_bootstrap` 的 Go mirror 解码。
|
||||
- Rust Glossary V1 query 响应,覆盖 alias、approved review、source provenance 和完整 history。
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"available": true,
|
||||
"channel": "official",
|
||||
"diagnostics": [],
|
||||
"entry_count": 2,
|
||||
"integrity_status": "verified",
|
||||
"manifest_identity": "${MANIFEST_IDENTITY}",
|
||||
"mapping_identity": "${MAPPING_IDENTITY}",
|
||||
"max_age_seconds": 900,
|
||||
"publication_identity": "${PUBLICATION_IDENTITY}",
|
||||
"ready": true,
|
||||
"release_id": "${VERSION_ID}",
|
||||
"resource_root": "${RESOURCE_ROOT}",
|
||||
"status": "ready",
|
||||
"status_code": "distribution.ready",
|
||||
"verification_generation": 7,
|
||||
"verified_at": 1000
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"available": true,
|
||||
"channel": "official",
|
||||
"entries": [
|
||||
{
|
||||
"blake3": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
@@ -14,9 +15,14 @@
|
||||
"url": "https://prod-clientpatch.bluearchiveyostar.com/{addressables-root}/TableBundles/TableCatalog.hash"
|
||||
}
|
||||
],
|
||||
"generation": 7,
|
||||
"limit": 2,
|
||||
"manifest_identity": "${MANIFEST_IDENTITY}",
|
||||
"manifest_version": 1,
|
||||
"mapping_identity": "${MAPPING_IDENTITY}",
|
||||
"offset": 0,
|
||||
"publication_identity": "${PUBLICATION_IDENTITY}",
|
||||
"release_id": "${VERSION_ID}",
|
||||
"resource_root": "${RESOURCE_ROOT}",
|
||||
"total_entries": 2
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ func (c *Client) callEnvelope(ctx context.Context, method string, params any) (*
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
if deadline, ok := c.deadline(ctx); ok {
|
||||
_ = conn.SetDeadline(deadline)
|
||||
@@ -877,14 +877,29 @@ type ResourceManifestEntry struct {
|
||||
BLAKE3 string `json:"blake3,omitempty"`
|
||||
}
|
||||
|
||||
// ResourceManifestParams binds every page to one attested official release.
|
||||
type ResourceManifestParams struct {
|
||||
ReleaseID string `json:"release_id,omitempty"`
|
||||
ExpectedPublicationIdentity string `json:"expected_publication_identity,omitempty"`
|
||||
ExpectedManifestIdentity string `json:"expected_manifest_identity,omitempty"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
type ResourceManifestPage struct {
|
||||
Available bool `json:"available"`
|
||||
ResourceRoot string `json:"resource_root,omitempty"`
|
||||
ManifestVersion int `json:"manifest_version,omitempty"`
|
||||
TotalEntries int `json:"total_entries,omitempty"`
|
||||
Offset int `json:"offset,omitempty"`
|
||||
Limit int `json:"limit,omitempty"`
|
||||
Entries []ResourceManifestEntry `json:"entries,omitempty"`
|
||||
Available bool `json:"available"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
ReleaseID string `json:"release_id,omitempty"`
|
||||
ResourceRoot string `json:"resource_root,omitempty"`
|
||||
ManifestVersion int `json:"manifest_version,omitempty"`
|
||||
PublicationIdentity string `json:"publication_identity,omitempty"`
|
||||
MappingIdentity string `json:"mapping_identity,omitempty"`
|
||||
ManifestIdentity string `json:"manifest_identity,omitempty"`
|
||||
Generation uint64 `json:"generation,omitempty"`
|
||||
TotalEntries int `json:"total_entries,omitempty"`
|
||||
Offset int `json:"offset,omitempty"`
|
||||
Limit int `json:"limit,omitempty"`
|
||||
Entries []ResourceManifestEntry `json:"entries,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) DaemonStatus(ctx context.Context) (*DaemonStatusReport, error) {
|
||||
@@ -953,9 +968,35 @@ func (c *Client) ResourceRepair(ctx context.Context) (*TaskAccepted, error) {
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func (c *Client) ResourceManifest(ctx context.Context, offset int, limit int) (*ResourceManifestPage, error) {
|
||||
// DistributionAttestation is the Rust-owned current official health proof.
|
||||
type DistributionAttestation struct {
|
||||
Available bool `json:"available"`
|
||||
Channel string `json:"channel"`
|
||||
ReleaseID string `json:"release_id"`
|
||||
ResourceRoot string `json:"resource_root"`
|
||||
PublicationIdentity string `json:"publication_identity"`
|
||||
MappingIdentity string `json:"mapping_identity"`
|
||||
ManifestIdentity string `json:"manifest_identity"`
|
||||
EntryCount int `json:"entry_count"`
|
||||
IntegrityStatus string `json:"integrity_status"`
|
||||
Status string `json:"status"`
|
||||
StatusCode string `json:"status_code"`
|
||||
Ready bool `json:"ready"`
|
||||
VerificationGeneration uint64 `json:"verification_generation"`
|
||||
VerifiedAt *uint64 `json:"verified_at,omitempty"`
|
||||
MaxAgeSeconds uint64 `json:"max_age_seconds"`
|
||||
Diagnostics []string `json:"diagnostics,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) ReleaseAttestation(ctx context.Context) (*DistributionAttestation, error) {
|
||||
var out DistributionAttestation
|
||||
_, err := c.Call(ctx, "release.attestation", nil, &out)
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func (c *Client) ResourceManifest(ctx context.Context, params ResourceManifestParams) (*ResourceManifestPage, error) {
|
||||
var out ResourceManifestPage
|
||||
_, err := c.Call(ctx, "resource.manifest", pageParam{Offset: offset, Limit: limit}, &out)
|
||||
_, err := c.Call(ctx, "resource.manifest", params, &out)
|
||||
return &out, err
|
||||
}
|
||||
|
||||
|
||||
@@ -249,6 +249,101 @@ func TestParseTextUnitsSendsQuery(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseAttestationMirrorsCurrentOfficialHealth(t *testing.T) {
|
||||
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
||||
if req.Method != "release.attestation" {
|
||||
t.Fatalf("method = %s", req.Method)
|
||||
}
|
||||
return testResponse{
|
||||
Result: testEnvelope{
|
||||
OK: true,
|
||||
Status: "ok",
|
||||
Data: map[string]any{
|
||||
"available": true,
|
||||
"channel": "official",
|
||||
"release_id": "official-a",
|
||||
"resource_root": "/srv/official/versions/official-a",
|
||||
"publication_identity": "odp-v1-publication-a",
|
||||
"mapping_identity": "odm-v1-mapping-a",
|
||||
"manifest_identity": "manifest-a",
|
||||
"entry_count": 2,
|
||||
"integrity_status": "verified",
|
||||
"status": "ready",
|
||||
"status_code": "distribution.ready",
|
||||
"ready": true,
|
||||
"verification_generation": 7,
|
||||
"verified_at": 1234,
|
||||
"max_age_seconds": 900,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
report, err := client.ReleaseAttestation(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("ReleaseAttestation error: %v", err)
|
||||
}
|
||||
if !report.Ready || report.ReleaseID != "official-a" ||
|
||||
report.ManifestIdentity != "manifest-a" ||
|
||||
report.VerificationGeneration != 7 {
|
||||
t.Fatalf("report = %#v", report)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceManifestSendsAttestedGenerationParams(t *testing.T) {
|
||||
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
||||
if req.Method != "resource.manifest" {
|
||||
t.Fatalf("method = %s", req.Method)
|
||||
}
|
||||
var params ResourceManifestParams
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
t.Fatalf("decode params: %v", err)
|
||||
}
|
||||
if params.ReleaseID != "official-a" ||
|
||||
params.ExpectedPublicationIdentity != "odp-v1-publication-a" ||
|
||||
params.ExpectedManifestIdentity != "manifest-a" ||
|
||||
params.Offset != 1 || params.Limit != 100 {
|
||||
t.Fatalf("params = %#v", params)
|
||||
}
|
||||
return testResponse{
|
||||
Result: testEnvelope{
|
||||
OK: true,
|
||||
Status: "ok",
|
||||
Data: map[string]any{
|
||||
"available": true,
|
||||
"channel": "official",
|
||||
"release_id": "official-a",
|
||||
"resource_root": "/srv/official/versions/official-a",
|
||||
"manifest_version": 1,
|
||||
"publication_identity": "odp-v1-publication-a",
|
||||
"mapping_identity": "odm-v1-mapping-a",
|
||||
"manifest_identity": "manifest-a",
|
||||
"generation": 7,
|
||||
"total_entries": 2,
|
||||
"offset": 1,
|
||||
"limit": 100,
|
||||
"entries": []any{},
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
page, err := client.ResourceManifest(context.Background(), ResourceManifestParams{
|
||||
ReleaseID: "official-a",
|
||||
ExpectedPublicationIdentity: "odp-v1-publication-a",
|
||||
ExpectedManifestIdentity: "manifest-a",
|
||||
Offset: 1,
|
||||
Limit: 100,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ResourceManifest error: %v", err)
|
||||
}
|
||||
if page.ReleaseID != "official-a" || page.ManifestIdentity != "manifest-a" ||
|
||||
page.Generation != 7 {
|
||||
t.Fatalf("page = %#v", page)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnityFSPatchFieldSendsTaggedReplacement(t *testing.T) {
|
||||
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
||||
if req.Method != "unityfs.patch_field" {
|
||||
|
||||
Reference in New Issue
Block a user