package host import ( "context" "errors" "github.com/carlosframework/platform/internal/park" "path/filepath" "time" "github.com/carlosframework/platform/internal/activator" "github.com/carlosframework/platform/internal/cgroup" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" execlease "github.com/carlosframework/platform/internal/hostexec" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/instancectl" "github.com/carlosframework/platform/internal/provision" ) // Installed only by root construction. An operation adapter must read its // durable per-kind journal and validate the exact observed op, phase, holder // generation, stamp and session. Agent CurrentAttempt caches are not readers. // A missing adapter refuses that kind. No operation adapter ships in rev 2. type claimLaunchAttemptReader interface { ReadAttempt(context.Context, hostclaim.Observed) (hostclaim.WakeRetryAttempt, error) } func (h *ClaimLaunchRoot) authoritativeAttempt(ctx context.Context, c *hostclaim.Claim) (hostclaim.WakeRetryAttempt, error) { if c.Op.Kind == hostclaim.KindServing { retry := c.Holder.WakeRetry if retry == nil || retry.State != "attempting" || retry.EpisodeID == "" || retry.AttemptsReserved < 1 || retry.AttemptsReserved > hostclaim.WakeRetryLimit || retry.Attempt == nil || !retry.NotBefore.IsZero() { return hostclaim.WakeRetryAttempt{}, hostbox.ErrChanged } return *retry.Attempt, nil } switch c.Op.Kind { case hostclaim.KindLanding, hostclaim.KindRelocating: if reader := h.Attempts[c.Op.Kind]; reader != nil { return reader.ReadAttempt(ctx, hostclaim.Observed{Host: c.Host, Claim: c}) } } return hostclaim.WakeRetryAttempt{}, hostbox.ErrUnsupported } func (h *ClaimLaunchRoot) launchSpec(q hostbox.RoleLaunchRequest) (instancectl.Launch, error) { if q.Spec == nil || q.Reservation == nil { return instancectl.Launch{}, hostbox.ErrRoleLaunchShape } t := q.Reservation.Target if launch, bound, err := h.boundLaunchSpec(q); bound || err != nil { return launch, err } paths, err := claimIsolatedPaths(map[string]string{"data": h.DataRoot}, t.Host, ClaimLaunchInputs{DBPath: provision.NestedDBPath(h.DataRoot, t.Host), Limit: cgroup.Limit{MemoryMax: q.MemoryMax}}) if err != nil { return instancectl.Launch{}, err } if h.runRoot != "" { paths.RunDir = filepath.Join(h.runRoot, provision.InstanceID(t.Host)) } argv, _, _, err := activator.IsolatedArgv(t.Host, paths.LaunchDir, paths.RunDir, q.Spec.Args, q.Spec.Env) if err != nil { return instancectl.Launch{}, err } return instancectl.Launch{Schema: instancectl.LaunchSchema, Host: t.Host, Argv: argv, Env: append([]string(nil), q.Spec.Env...)}, nil } // authorize is always called under the host guard, including inside // StartChecked after namespace/tier preparation and immediately before start. func (h *ClaimLaunchRoot) authorize(ctx context.Context, peer StopPeer, q hostbox.RoleLaunchRequest, c *hostclaim.Claim) error { t, r := q.Reservation.Target, *q.Reservation if err := h.checkUnitBinding(ctx, t, false); err != nil { return err } if err := ctx.Err(); err != nil { return err } actor, err := h.Actor(ctx) if err != nil { return err } if !actor.Eligible || actor.Quarantined || actor.Box != t.Box || actor.Enrollment != t.Enrollment || actor.Region != t.Region || actor.Binding == "" || actor.Binding != t.DirectoryRevision { return hostbox.ErrChanged } if c == nil || c.Host != t.Host { return hostbox.ErrChanged } if err := claimLaunchAuthority(c, t, actor.Box); err != nil { return err } a, err := h.authoritativeAttempt(ctx, c) if err != nil { return err } if a.ID == "" || a.InvocationID == "" || a.LaunchInputs == "" || r.ID != a.ID+":main" || r.InvocationID != a.InvocationID || r.Inputs != a.LaunchInputs || a.Session != t.Session || a.Box != t.Box || a.Enrollment != t.Enrollment || a.BootID != peer.BootID || a.ControllerID != peer.ControllerID { return hostbox.ErrChanged } if q.Spec == nil || q.Spec.Identity.Config == "" || q.Spec.Identity.Release == "" || !claimHexDigest(q.Spec.Digest) { return hostbox.ErrRoleLaunchShape } digest, err := q.Spec.Hash() if err != nil || digest != a.LaunchInputs { return hostbox.ErrChanged } if _, err := h.launchSpec(q); err != nil { return err } if err := h.ValidateExecutable(t.Host, q.Executable, q.Spec.Digest); err != nil { return err } if err := h.Kernel.VerifyDigest(ctx, q.Executable, r.Executable, q.Spec.Digest); err != nil { return err } selection := hostbox.Selection{HomeRegion: actor.HomeRegion, FleetHomeRegion: actor.FleetHomeRegion, Host: t.Host, Action: claimSpawnAction(c), Box: t.Box, Region: t.Region, Captured: hostguard.Captured{Incarnation: t.Incarnation, Stamp: t.Stamp}, Kind: c.Op.Kind, Op: c.Op.ID, Phase: c.Op.Phase, Generation: c.Holder.Generation, Session: t.Session} if err := h.Local.Check(ctx, actor, c, selection); err != nil { return err } state, err := h.readExecution(ctx, c) if err != nil { return err } if c.Op.Kind != hostclaim.KindServing { if state.Authority.Box != t.Box || state.Authority.Enrollment != t.Enrollment || state.Peer.KernelBootID != peer.BootID || state.Peer.PID != peer.PID || state.Peer.Start != peer.ProcessStart { return execlease.ErrLost } } now, boot, err := h.Clock.Now() if err != nil { return err } if c.Op.Kind != hostclaim.KindServing && boot >= state.SafeUntil { return execlease.ErrLost } if until := park.QuietUntil(c.QuietAfter, actor.HomeRegion, t.Region); !until.IsZero() && !now.After(until) { return hostbox.ErrChanged } return nil } // waitReady observes one start, never reserves or starts another attempt. // Type=simple returns before namespace setup and the trampoline exec. func (h *ClaimLaunchRoot) waitReady(ctx context.Context, host string, s instancectl.ClaimUnitStarted, want hostbox.LaunchFile) (ClaimLaunchObserved, error) { budget, poll := h.ReadyBudget, h.ReadyPoll if budget <= 0 { budget = 5 * time.Second } if poll <= 0 { poll = 25 * time.Millisecond } ctx, cancel := context.WithTimeout(ctx, budget) defer cancel() var pinned, tenantStart string var tenantPID int for { if err := ctx.Err(); err != nil { return ClaimLaunchObserved{}, err } if err := h.Units.Same(ctx, s); err != nil { return ClaimLaunchObserved{}, err } got, err := h.Kernel.Observe(s) if got.ProcessStart == "" || (pinned != "" && pinned != got.ProcessStart) { return ClaimLaunchObserved{}, hostbox.ErrChanged } pinned = got.ProcessStart if (got.TenantPID == 0) != (got.TenantStart == "") || (tenantPID != 0 && (tenantPID != got.TenantPID || tenantStart != got.TenantStart)) { return ClaimLaunchObserved{}, hostbox.ErrChanged } if got.TenantPID != 0 { tenantPID, tenantStart = got.TenantPID, got.TenantStart } if err != nil && !errors.Is(err, errClaimLaunchPending) { return ClaimLaunchObserved{}, err } if err == nil && got.TenantPID > 0 && got.TenantStart != "" && got.Executable == want && got.Running == want { if err := h.Units.Same(ctx, s); err != nil { return ClaimLaunchObserved{}, err } if h.boundReady == nil || h.boundReady(ctx, host, s) == nil { if err := h.Units.Same(ctx, s); err != nil { return ClaimLaunchObserved{}, err } return got, nil } } select { case <-ctx.Done(): return ClaimLaunchObserved{}, ctx.Err() case <-time.After(poll): } } } var errClaimLaunchPending = errors.New("claim launch: namespace or exec pending")