@@ -99,6 +99,7 @@ type launcher struct {
9999 pendingWithdrawals map[string]bool
100100 envelope *gitops.EnvelopeWriter
101101 stop chan struct{}
102+ stopOnce sync.Once
102103
103104 // schedule bounds each workload's wait for the host; waiters are the
104105 // deliveries in flight, one per workload.
@@ -115,31 +116,51 @@ var (
115116// attachLauncher wires the run as the composition's cluster when its
116117// environment declares a host and the run is native. It is a no-op otherwise.
117118func attachLauncher(ctx context.Context, flow *orchestration.Flow, workspace *resources.Workspace, env *environments.Environment, fixture string) error {
118- if env == nil || env.Host == nil || runtimeContext == resources.RuntimeContextContainer {
119- return nil
120- }
121- w := wool.Get(ctx).In("launcher")
122119 launcherMu.Lock()
123120 defer launcherMu.Unlock()
124121 if launcherActive != nil {
125122 launcherActive.close()
126123 launcherActive = nil
127124 }
128- if err := env.Host.Validate(); err != nil {
125+ cluster, err := newNativeLauncher(ctx, flow, workspace, env, runtimeContext, fixture)
126+ if err != nil {
129127 return err
130128 }
129+ launcherActive = cluster
130+ return nil
131+ }
132+
133+ // AttachNativeLauncher gives test entry points the same native authority
134+ // admission as run, with explicit configuration and flow-owned teardown.
135+ // Call after InitManagers determines whether this test starts any service.
136+ func AttachNativeLauncher(ctx context.Context, flow *orchestration.Flow, workspace *resources.Workspace, env *environments.Environment, selectedRuntime, fixture string) error {
137+ if len(flow.LocalRuntimeServices()) == 0 {
138+ return nil
139+ }
140+ _, err := newNativeLauncher(ctx, flow, workspace, env, selectedRuntime, fixture)
141+ return err
142+ }
143+
144+ func newNativeLauncher(ctx context.Context, flow *orchestration.Flow, workspace *resources.Workspace, env *environments.Environment, selectedRuntime, fixture string) (*launcher, error) {
145+ if env == nil || env.Host == nil || selectedRuntime == resources.RuntimeContextContainer {
146+ return nil, nil
147+ }
148+ w := wool.Get(ctx).In("launcher")
149+ if err := env.Host.Validate(); err != nil {
150+ return nil, err
151+ }
131152 identities, err := gitops.ResolveNativeIdentities(ctx, workspace, env)
132153 if err != nil {
133- return err
154+ return nil, err
134155 }
135156 signer, err := signing.LoadDeveloperKey()
136157 if err != nil {
137- return fmt.Errorf("a native run delivers with the developer key, and none is held: %w", err)
158+ return nil, fmt.Errorf("a native run delivers with the developer key, and none is held: %w", err)
138159 }
139160 stateDir := filepath.Join(workspace.Dir(), ".codefly", "run", env.Name)
140161 launcherIdentity, err := identity.Start(env.Host.TrustDomain, env.Host.Audience, stateDir)
141162 if err != nil {
142- return err
163+ return nil, err
143164 }
144165 cluster := &launcher{
145166 vouch: launcherIdentity, workspace: workspace, env: env, flow: flow, fixture: fixture,
@@ -154,30 +175,23 @@ func attachLauncher(ctx context.Context, flow *orchestration.Flow, workspace *re
154175 }
155176 parts := strings.Split(env.Host.Delivery, "/")
156177 if len(parts) != 3 {
157- return fmt.Errorf("host.delivery %q is not <module>/<service>/<endpoint>", env.Host.Delivery)
178+ return nil, fmt.Errorf("host.delivery %q is not <module>/<service>/<endpoint>", env.Host.Delivery)
158179 }
159180 cluster.hostModule, cluster.hostService = parts[0], parts[1]
160181 // The run vouches for exactly what it composes: each identity workload,
161182 // each module's delivery carrier, and the applied observer it acts as.
162183 cluster.declare(identities)
163184 if err := cluster.projectHost(); err != nil {
164- return err
185+ return nil, err
165186 }
166187 world := flow.World()
167188 world.RenderOwnedGroups = cluster.renderOwnedGroups
168189 world.RuntimeOverrides = cluster.runtimeOverrides
169- cluster.registerRunningHook(ctx, flow.WithRunningHook)
190+ world.BeginRuntimeStart = cluster.beginRuntimeStart
170191 go cluster.refreshTokens(ctx)
171192 w.Info("native run: the run vouches for the composition's workloads", wool.Field("anchor", launcherIdentity.AnchorFingerprint()[:16]))
172- launcherActive = cluster
173- return nil
174- }
175-
176- // registerRunningHook binds delivery to the lifetime of this native run.
177- func (cluster *launcher) registerRunningHook(ctx context.Context, register func(func(string, int))) {
178- register(func(service string, _ int) {
179- go cluster.onRunning(ctx, service)
180- })
193+ world.BeforeRuntimeStop = cluster.close
194+ return cluster, nil
181195}
182196
183197func stopLauncher() {
@@ -190,11 +204,11 @@ func stopLauncher() {
190204}
191205
192206func (cluster *launcher) close() {
193- select {
194- case <- cluster.stop:
195- default:
196- close(cluster.stop)
197- }
207+ cluster.stopOnce.Do(func() {
208+ if cluster.stop != nil {
209+ close(cluster.stop)
210+ }
211+ })
198212}
199213
200214// declare records identity workloads the run vouches for, with their delivery
@@ -338,12 +352,7 @@ func (cluster *launcher) runtimeOverrides(service *resources.Service) map[string
338352 if ok {
339353 // A module-identity service reads its token from the path a cell's
340354 // render mounts unless told otherwise; this is the otherwise.
341- credential, err := cluster.project(unique, identity)
342- if err != nil {
343- wool.Get(context.Background()).In("launcher").Warn("cannot project the module's credential", wool.Field("service", unique), wool.ErrField(err))
344- return out
345- }
346- out[workloadTokenFileVariable] = credential.File
355+ out[workloadTokenFileVariable] = cluster.vouch.ProjectionPath(identity.Workload(cluster.env.Host.TrustDomain))
347356 }
348357 if unique == resources.ServiceUnique(cluster.hostModule, cluster.hostService) {
349358 out[nativeidentity.AnchorEnvironment] = cluster.hostIdentityAnchor
@@ -369,11 +378,11 @@ func (cluster *launcher) renderOwnedGroups(ctx context.Context, service *resourc
369378 out = append(out, group)
370379 }
371380 if slices.Contains(service.WorkspaceConfigurationDependencies, environments.ArtifactCacheConfiguration) {
372- group, err := artifactCacheGroup(cluster.composed().Dir())
373- if err != nil {
374- return nil, err
381+ cacheGroup, cacheErr := artifactCacheGroup(cluster.composed().Dir())
382+ if cacheErr != nil {
383+ return nil, cacheErr
375384 }
376- out = append(out, group )
385+ out = append(out, cacheGroup )
377386 }
378387 if !slices.Contains(service.WorkspaceConfigurationDependencies, environments.ModuleAuthorityConfiguration) {
379388 return out, nil
@@ -482,39 +491,25 @@ func (cluster *launcher) refreshTokens(ctx context.Context) {
482491 }
483492}
484493
485- // onRunning is the cell's delivery, for one identity workload that just
486- // started: its module's manifest, listing it — once the host is ready to take
487- // it, retrying while it is not serving yet (hostDelivery). A workload that is
488- // not delivered is said so plainly: the gateway routes nothing to its module.
489- func (cluster *launcher) onRunning(ctx context.Context, unique string) {
490- w := wool.Get(ctx).In("launcher.deliver", wool.Field("service", unique))
491- identity, ok := cluster.identity(unique)
492- if !ok || !cluster.delivers(identity) {
493- return
494- }
495- cluster.mu.Lock()
496- cluster.running[unique] = true
497- cluster.mu.Unlock()
498- waiter := cluster.waitFor(unique)
494+ // runReadyDelivery publishes the latest readiness after an attempt. Failed
495+ // restarts remove stale routes through the same nonterminal signed manifest.
496+ func (cluster *launcher) runReadyDelivery(ctx context.Context, unique string, identity gitops.NativeIdentity, waiter *deliveryWaiter, failed bool) {
499497 defer cluster.doneWaiting(unique, waiter)
498+ w := wool.Get(ctx).In("launcher.deliver", wool.Field("service", unique))
500499 err := hostDelivery{
501500 schedule: cluster.schedule, stop: cluster.stop, superseded: waiter.superseded,
502501 log: w.With(wool.Field("module", identity.Module)),
503502 ready: func(ctx context.Context) error {
504- // A refreshed composition can withdraw the workload while its
505- // delivery waits; nothing is then delivered on its behalf.
506503 if _, composed := cluster.identity(unique); !composed {
507504 return errWorkloadGone
508505 }
509506 return cluster.hostReady(ctx)
510507 },
511- deliver: func(ctx context.Context) error { return cluster.deliver (ctx, identity.Module) },
508+ deliver: func(ctx context.Context) error { return cluster.deliverReadiness (ctx, identity.Module, failed ) },
512509 }.run(ctx)
513510 if deliveryAbandoned(err) {
514511 w.Debug("delivery abandoned", wool.ErrField(err))
515- return
516- }
517- if err != nil {
512+ } else if err != nil {
518513 w.Error(fmt.Sprintf("delivery completion was not confirmed for module %s, workload %s; routes to module %s may be unavailable (the host may already have applied the manifest)", identity.Module, unique, identity.Module), wool.ErrField(err))
519514 }
520515}
@@ -560,7 +555,8 @@ func (cluster *launcher) newDeliverer(ctx context.Context) (*gitops.NativeDelive
560555 return gitops.Roles(m.Dir())
561556 })
562557 return &gitops.NativeDeliverer{
563- Workspace: cluster.composed(), Env: cluster.env, Signer: cluster.signer,
558+ CloseTransport: func() { _ = conn.Close() },
559+ Workspace: cluster.composed(), Env: cluster.env, Signer: cluster.signer,
564560 StateDir: filepath.Join(cluster.stateDir, "delivery"), DeliveryURL: deliveryURL,
565561 Token: func(module string) (string, error) {
566562 return cluster.vouch.Issue(cluster.vouch.Workload(module, workloadidentity.DeliveryService))
@@ -616,7 +612,7 @@ func (cluster *launcher) withdraw(ctx context.Context, uniques []string) ([]stri
616612 sort.Strings(pending)
617613 var withdrawn []string
618614 for _, module := range pending {
619- if len(cluster.startedIdentities (module)) > 0 {
615+ if len(cluster.declaredIdentities (module)) > 0 {
620616 if err := cluster.deliver(ctx, module); err != nil {
621617 return withdrawn, fmt.Errorf("%s: deliver the workloads that remain: %w", module, err)
622618 }
@@ -659,6 +655,9 @@ func (cluster *launcher) withdrawModule(ctx context.Context, module string) (boo
659655 if err != nil {
660656 return false, err
661657 }
658+ if deliverer.CloseTransport != nil {
659+ defer deliverer.CloseTransport()
660+ }
662661 receipt, err := deliverer.Deliver(ctx, withdrawal)
663662 if err != nil {
664663 return false, err
@@ -723,61 +722,96 @@ func (cluster *launcher) startedIdentities(module string) []gitops.NativeIdentit
723722// that runs now, with the addresses this run allocated for its routed
724723// endpoints, after making sure the host holds the composition's envelope.
725724func (cluster *launcher) deliver(ctx context.Context, module string) error {
725+ return cluster.deliverReadiness(ctx, module, false)
726+ }
727+
728+ func (cluster *launcher) deliverReadiness(ctx context.Context, module string, early bool) error {
726729 if err := cluster.acquireDelivery(ctx); err != nil {
727730 return err
728731 }
729732 defer cluster.delivery.Release(1)
730- started := cluster.startedIdentities(module)
731- if len(started) == 0 {
733+ // Failure may precede admission. Replace a previous host snapshot, but do
734+ // not admit a new module just to clean up an unstarted attempt.
735+ if early && cluster.delivered[module] == nil {
732736 return nil
733737 }
734- loaded, err := cluster.composed().LoadModuleFromName(ctx, module)
738+ if len(cluster.declaredIdentities(module)) == 0 {
739+ return nil
740+ }
741+ deliverer, gatewayURL, err := cluster.newDeliverer(ctx)
735742 if err != nil {
736743 return err
737744 }
745+ if deliverer.CloseTransport != nil {
746+ defer deliverer.CloseTransport()
747+ }
748+ manifest, err := cluster.renderDelivery(ctx, deliverer, module, early)
749+ if err != nil {
750+ return err
751+ }
752+ return cluster.applyDelivery(ctx, deliverer, gatewayURL, module, manifest)
753+ }
754+
755+ func (cluster *launcher) renderDelivery(ctx context.Context, deliverer *gitops.NativeDeliverer, module string, early bool) (*solutionhost.ModuleManifest, error) {
756+ loaded, err := cluster.composed().LoadModuleFromName(ctx, module)
757+ if err != nil {
758+ return nil, err
759+ }
738760 services := make([]*resources.Service, 0, len(loaded.ServiceReferences))
739761 for _, reference := range loaded.ServiceReferences {
740762 service, loadErr := cluster.composed().LoadService(ctx, &resources.ServiceWithModule{Name: reference.Name, Module: module})
741763 if loadErr != nil {
742- return loadErr
764+ return nil, loadErr
743765 }
744766 services = append(services, service)
745767 }
746- deliverer, gatewayURL, err := cluster.newDeliverer(ctx)
747- if err != nil {
748- return err
749- }
750- // A host reached through the composition interface holds the manifest as the
751- // ceiling and takes no envelope.
752- if gatewayURL != "" {
753- if err = cluster.ensureEnvelope(ctx, gatewayURL); err != nil {
754- return err
768+ var bundle *solutionhost.FrontendPayload
769+ if early {
770+ // Deferral removes a page. Keep a previously published payload during
771+ // unrelated partial startup instead of accidentally unpublishing it.
772+ if previous := cluster.delivered[module]; previous != nil && previous.Frontend != nil {
773+ bundle = previous.Frontend.Payload
774+ }
775+ } else {
776+ bundle, err = cluster.frontendBundle(ctx, loaded)
777+ if err != nil {
778+ return nil, err
755779 }
756780 }
757- bundle, err := cluster.frontendBundle(ctx, loaded)
758- if err != nil {
759- return err
781+ started := cluster.startedIdentities(module)
782+ var ready []string
783+ for _, identity := range started {
784+ ready = append(ready, resources.ServiceUnique(identity.Module, identity.Service))
760785 }
761- carriers, err := cluster.flow.EndpointCarriers (ctx, module, resources.NewNativeNetworkAccess())
786+ carriers, err := cluster.flow.ReadyEndpointCarriers (ctx, module, resources.NewNativeNetworkAccess(), ready )
762787 if err != nil {
763- return err
788+ return nil, err
764789 }
765- manifest, err := deliverer.Render(ctx, &gitops.NativeWorkload{Module: loaded, Services: services, Started: started, Carriers: carriers, FrontendBundle: bundle})
766- if err != nil {
767- return err
790+ return deliverer.Render(ctx, &gitops.NativeWorkload{
791+ Module: loaded, Services: services, Declared: cluster.declaredIdentities(module),
792+ Started: started, Carriers: carriers, FrontendBundle: bundle, DeferFrontend: early && bundle == nil,
793+ })
794+ }
795+
796+ // Caller holds delivery across rendering, generation reservation and Applied.
797+ func (cluster *launcher) applyDelivery(ctx context.Context, deliverer *gitops.NativeDeliverer, gatewayURL, module string, manifest *solutionhost.ModuleManifest) error {
798+ if gatewayURL != "" {
799+ if err := cluster.ensureEnvelope(ctx, gatewayURL); err != nil {
800+ return err
801+ }
768802 }
769803 receipt, err := deliverer.Deliver(ctx, manifest)
770804 if err != nil {
771805 return err
772806 }
773807 cluster.delivered[module] = manifest
774- wool.Get(ctx).In("launcher.deliver").Info("delivered", wool.Field("module", module), wool.Field("workloads", len(manifest.Workloads)),
808+ w := wool.Get(ctx).In("launcher.deliver")
809+ w.Info("delivered", wool.Field("module", manifest.Alias), wool.Field("workloads", len(manifest.Workloads)),
775810 wool.Field("disposition", receipt.Disposition), wool.Field("generation", receipt.Generation))
776- // Accepted into the inbox is not applied. The host says which, and why.
777811 if err := deliverer.WaitForApplied(ctx, manifest); err != nil {
778- return fmt.Errorf("module %s is delivered and not served: %w", module , err)
812+ return fmt.Errorf("module %s is delivered and not served: %w", manifest.Alias , err)
779813 }
780- wool.Get(ctx).In("launcher.deliver"). Info("applied", wool.Field("module", module ), wool.Field("generation", manifest.Generation))
814+ w. Info("applied", wool.Field("module", manifest.Alias ), wool.Field("generation", manifest.Generation))
781815 return nil
782816}
783817
0 commit comments