diff --git a/module/services/accounts/code/fixtures/installations.go b/module/services/accounts/code/fixtures/installations.go index d30749f41..ad56195fd 100644 --- a/module/services/accounts/code/fixtures/installations.go +++ b/module/services/accounts/code/fixtures/installations.go @@ -150,8 +150,13 @@ func (i *Installations) Converge(ctx context.Context) error { w.Info("fixture installation installed", wool.Field("installation", row.declared.describe()), wool.Field("installation_id", installation.GetId())) } // The authority core holds the same installation, which the host - // interfaces decide with (coreInstall). - if err := i.coreInstall(ctx, row); err != nil { + // interfaces decide with (coreInstall). A target the core has not + // composed yet keeps the row for the next pass. + coreWaits := false + switch err := i.coreInstall(ctx, row); { + case errors.Is(err, errCoreTargetNotComposed): + coreWaits = true + case err != nil: failures = append(failures, fmt.Errorf("fixture installation %s: %w", row.declared.describe(), err)) } // Each declared team is exposed once the installation admits it (an @@ -164,7 +169,7 @@ func (i *Installations) Converge(ctx context.Context) error { unexposed = append(unexposed, teamID) } } - if row.teamIDs = unexposed; len(row.teamIDs) > 0 { + if row.teamIDs = unexposed; len(row.teamIDs) > 0 || coreWaits { remaining = append(remaining, row) } } @@ -173,6 +178,10 @@ func (i *Installations) Converge(ctx context.Context) error { return errors.Join(failures...) } +// errCoreTargetNotComposed is a declared installation whose target the +// authority core has not composed yet; the next pass installs it there. +var errCoreTargetNotComposed = errors.New("the authority core has not composed this target yet") + // coreInstall performs the declared installation in the authority core as the // host's own setup (InstallAsHost), then grants the installer role to its // owner and its role to each declared team (GrantRoleAsHost), as the local @@ -187,8 +196,17 @@ func (i *Installations) coreInstall(ctx context.Context, row pendingInstallation return err } // A module's installation is accounts' own record, which Converge's install - // made; the core reads it and installs no module itself. - if m, err := core.Composed(ctx, row.declared.Target); err == nil && m.Kind != domain.KindSolution { + // made; the core reads it and installs no module itself. A target the core + // has not composed yet is waited on: the pass that applies its delivery + // composes it. + m, err := core.Composed(ctx, row.declared.Target) + if code, ok := domain.CodeOf(err); ok && code == domain.NotFound { + return fmt.Errorf("%w: %s", errCoreTargetNotComposed, row.declared.Target) + } + if err != nil { + return fmt.Errorf("the authority core: %w", err) + } + if m.Kind != domain.KindSolution { return nil } inst, err := core.InstallAsHost(ctx, row.orgID, row.declared.Target, row.declared.Role, row.ownerID, i.service.AuthorityInstallStep()) @@ -224,7 +242,7 @@ func (i *Installations) ConvergeAuthority(ctx context.Context) error { var failures []error for _, row := range i.pending { err := i.coreInstall(ctx, row) - if code, ok := domain.CodeOf(err); ok && code == domain.NotFound { + if code, ok := domain.CodeOf(err); errors.Is(err, errCoreTargetNotComposed) || ok && code == domain.NotFound { continue } if err != nil { diff --git a/module/services/accounts/code/pkg/adapters/host_interface_conformance_test.go b/module/services/accounts/code/pkg/adapters/host_interface_conformance_test.go index 3a63a8d51..19f5f5975 100644 --- a/module/services/accounts/code/pkg/adapters/host_interface_conformance_test.go +++ b/module/services/accounts/code/pkg/adapters/host_interface_conformance_test.go @@ -828,6 +828,39 @@ func runHostInterfaceFixture(t *testing.T) { require.Greater(t, count, 0, "the suite's revocations are audited") }) + // A solution the composition delivers once the host is running is composed + // in the authority core by the pass that applies it, before the fixture's + // installations converge in that same pass: the declared installation is + // then the core's too, active at the declaring organization alone, and the + // declared team holds its role there. + t.Run("a solution delivered to a running host is installed where the fixture declares it", func(t *testing.T) { + reconciler.AfterEachPass(installations.Converge) + t.Cleanup(func() { reconciler.AfterEachPass(nil) }) + note := wireScope("conformance-late.note", "read") + deliverSolution(t, ctx, conn, signer, conformanceSolution{alias: "conformance-late", generation: 1, ceilings: []*authorityv1.WorkScopeV1{note}, + roles: map[string][]*authorityv1.WorkScopeV1{"installer": {note}, "viewer": {note}}}) + installed := func(caller authority.Person, org string) (domain.Installation, bool) { + t.Helper() + all, err := core.Installations(ctx, caller, org) + require.NoError(t, err) + for _, inst := range all { + if inst.Target == "conformance-late" { + return inst, true + } + } + return domain.Installation{}, false + } + inst, ok := installed(alex, fixtureOrganization) + require.True(t, ok, "the pass that applied the delivery installed it in the core") + require.Equal(t, domain.Active, inst.Status) + _, ok = installed(authority.Person{ID: conformanceZoe, Organization: globexOrganization, SessionID: "conformance-harness"}, globexOrganization) + require.False(t, ok, "and nowhere the fixture does not declare it") + grants, err := core.ListRoleGrants(ctx, alex, fixtureOrganization, inst.ID) + require.NoError(t, err) + finance := teamNamed(t, ctx, svc, fixtureOrganization, "Finance") + require.Contains(t, grants, domain.RoleGrant{InstallationID: inst.ID, Role: "viewer", TeamID: finance}, "the declared team holds its role") + }) + // Last, because the envelope it writes becomes every module's ceiling: a // real envelope write that narrows a module's binding (the write // PutModuleEnvelope makes, without its platform administrator check) is diff --git a/module/services/accounts/code/pkg/adapters/testdata/host-interface-conformance.yaml b/module/services/accounts/code/pkg/adapters/testdata/host-interface-conformance.yaml index e7ec4f9e4..37aa1ab2e 100644 --- a/module/services/accounts/code/pkg/adapters/testdata/host-interface-conformance.yaml +++ b/module/services/accounts/code/pkg/adapters/testdata/host-interface-conformance.yaml @@ -152,3 +152,6 @@ installations: - {org: Acme Corp, target: conformance-proposals, role: viewer, owner: alex@acme.test, teams: [Finance]} # The assistant solution acts for itself down a chain of modules. - {org: Acme Corp, target: conformance-assistant, role: viewer, owner: alex@acme.test} + # Delivered only once the host is running, as a composition delivers a + # solution after boot: installed at Acme alone, and reached by its finance team. + - {org: Acme Corp, target: conformance-late, role: viewer, owner: alex@acme.test, teams: [Finance]} diff --git a/module/services/accounts/code/pkg/business/authority_core_delivery.go b/module/services/accounts/code/pkg/business/authority_core_delivery.go index 7135736a3..3a53612d4 100644 --- a/module/services/accounts/code/pkg/business/authority_core_delivery.go +++ b/module/services/accounts/code/pkg/business/authority_core_delivery.go @@ -138,8 +138,7 @@ func coreDeliveryError(err error) error { // reconcileAuthorityCore composes, in the core, every delivery accounts has // applied: what a host runs once its core is built, so a module delivered // before this host decided through the core, or while its core could not be -// built, is composed there too. Each is read from the inbox's exact applied -// generation, the signed documents accounts verified when it took them. +// built, is composed there too. func (s *Service) reconcileAuthorityCore(ctx context.Context) error { if s.deliveryStore == nil { return nil @@ -150,29 +149,57 @@ func (s *Service) reconcileAuthorityCore(ctx context.Context) error { } var failures []error for _, record := range records { - manifest, err := record.AppliedManifest() - if err != nil || manifest == nil { - continue - } - var delivered *SolutionDeliveryRecord - if err := s.store.WithControlPlane(ctx, func(ctx context.Context) error { - var err error - delivered, err = s.deliveryStore.ReadDeliveredDocument(ctx, record.BindingID, record.Applied.Generation) - return err - }); err != nil { - failures = append(failures, fmt.Errorf("binding %s: %w", record.BindingID, err)) - continue - } - if delivered == nil { - continue - } - var signed *compositionv1.SignedManifestV1 - if len(delivered.AuthorityDocument) > 0 { - signed = &compositionv1.SignedManifestV1{Document: delivered.AuthorityDocument, Envelope: delivered.AuthorityEnvelope} - } - if _, err := s.applyCoreModule(ctx, manifest, delivered.Payload, signed); err != nil { + if err := s.composeAppliedDelivery(ctx, record); err != nil { failures = append(failures, fmt.Errorf("binding %s: %w", record.BindingID, err)) } } return errors.Join(failures...) } + +// composeAppliedDelivery composes one binding's applied generation in the +// core, read from the inbox's exact applied generation: the signed documents +// accounts verified when it took them. Every delivery path reaches the core +// this way, the composition interface's and the host's delivery endpoint +// alike, so a solution delivered to a running host is one the core can +// install. A generation the core already holds changes nothing. +func (s *Service) composeAppliedDelivery(ctx context.Context, record *SolutionHostBindingRecord) error { + if s.deliveryStore == nil || record == nil { + return nil + } + manifest, err := record.AppliedManifest() + if err != nil || manifest == nil { + return nil + } + var delivered *SolutionDeliveryRecord + if err := s.store.WithControlPlane(ctx, func(ctx context.Context) error { + var err error + delivered, err = s.deliveryStore.ReadDeliveredDocument(ctx, record.BindingID, record.Applied.Generation) + return err + }); err != nil { + return err + } + if delivered == nil { + return nil + } + var signed *compositionv1.SignedManifestV1 + if len(delivered.AuthorityDocument) > 0 { + signed = &compositionv1.SignedManifestV1{Document: delivered.AuthorityDocument, Envelope: delivered.AuthorityEnvelope} + } + _, err = s.applyCoreModule(ctx, manifest, delivered.Payload, signed) + return err +} + +// composeAppliedBinding composes binding's applied generation in the core, +// right after a pass applied it. +func (s *Service) composeAppliedBinding(ctx context.Context, binding string) error { + records, err := s.ListSolutionHostBindings(ctx) + if err != nil { + return err + } + for _, record := range records { + if record.BindingID == binding { + return s.composeAppliedDelivery(ctx, record) + } + } + return nil +} diff --git a/module/services/accounts/code/pkg/business/solution_host_bindings.go b/module/services/accounts/code/pkg/business/solution_host_bindings.go index f253c5993..6aab5c03b 100644 --- a/module/services/accounts/code/pkg/business/solution_host_bindings.go +++ b/module/services/accounts/code/pkg/business/solution_host_bindings.go @@ -391,6 +391,14 @@ func (r *SolutionHostBindingReconciler) RunOnce(ctx context.Context) error { } failures = append(failures, fmt.Errorf("apply binding %q generation %d: %w", applied.Document.Binding, applied.Document.Generation, err)) + continue + } + // The applied generation is composed in the authority core at once, + // whichever path delivered it: a solution the host's delivery endpoint + // took is then one the core installs and the host interfaces decide. + if err := r.service.composeAppliedBinding(ctx, applied.Document.Binding); err != nil { + failures = append(failures, fmt.Errorf("compose binding %q generation %d in the authority core: %w", + applied.Document.Binding, applied.Document.Generation, err)) } } diff --git a/module/services/accounts/code/pkg/infra/testdata/control_plane_call_sites.txt b/module/services/accounts/code/pkg/infra/testdata/control_plane_call_sites.txt index bcd45c8a4..46ea6b557 100644 --- a/module/services/accounts/code/pkg/infra/testdata/control_plane_call_sites.txt +++ b/module/services/accounts/code/pkg/infra/testdata/control_plane_call_sites.txt @@ -29,7 +29,7 @@ pkg/auth/pg/session_store.go: (*SessionStore).RotateRefresh: WithControlPlane pkg/business/audit.go: (*DurableAuditEmitter).Emit: WithControlPlane pkg/business/audit.go: (*Service).AggregateAuditLog: WithControlPlane pkg/business/audit.go: (*Service).QueryAuditLog: WithControlPlane -pkg/business/authority_core_delivery.go: (*Service).reconcileAuthorityCore: WithControlPlane +pkg/business/authority_core_delivery.go: (*Service).composeAppliedDelivery: WithControlPlane pkg/business/authority_delivery.go: (*Service).verifyAppliedAuthorityDocument: WithControlPlane pkg/business/connector_credentials.go: (*Service).SourceSigningSecret: WithControlPlane pkg/business/current_installation.go: (*Service).ModuleCurrentInstallation: WithControlPlane