package console import ( "context" "log" "time" "github.com/carlosframework/platform/internal/boxid" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostops" ) // The console's automatic enrollment pass (region HA spec 2 ยง5 step 5, // H16, H28): every enrollPassInterval it takes the next step for each box // its region's group launched that the roster does not list yet // (hostactors.AutoEnroller: attempt, invitation, guard-ready, enrollment). // It runs beside claim recovery, on a host-claim console with region HA // on, and signs with the root key this console already holds. It confirms // each box's group membership itself, with its own read-only Auto Scaling // and EC2 describes (consoleMembershipReader), immediately before it // invites and before it enrolls. The console is the only writer of the // roster and the only membership confirmer, so it is the only place this // can run. // enrollPassInterval is how often the pass runs: a box is invited, and // enrolled once its guard-ready report is published, within ten seconds. const enrollPassInterval = 10 * time.Second // enrollPatience is how long the pass keeps an attempt the cloud refused // before it begins another (doubling with each refusal). const enrollPatience = 2 * time.Minute // newAutoEnroller is the pass over this console's claim authority (its // roster, root signer, report evidence and namespace, and the default // pool) and its own membership reader. An error when either is missing. func newAutoEnroller(a *hostops.ProductionAuthority, members hostactors.MembershipReader) (*hostactors.AutoEnroller, error) { if a == nil || members == nil { return nil, hostops.ErrContract } certs, err := boxid.EmbeddedCerts() if err != nil { return nil, err } return &hostactors.AutoEnroller{Directory: a.Directory, Signer: a.Signer, Evidence: a.Evidence, Members: members, Certs: certs, Namespace: a.Namespace, Pool: "default", Now: time.Now, Patience: enrollPatience, Logf: log.Printf}, nil } // runEnrollPass runs the pass until ctx ends. Each box's step is logged // when it changes, and a failed pass on its first failure and every // tenth. func runEnrollPass(ctx context.Context, e *hostactors.AutoEnroller, every time.Duration, logf func(string, ...any)) { logf("console: automatic box enrollment armed (every %s)", every) last := map[string]string{} fails := 0 for { steps, err := e.EnrollPass(ctx) switch { case err != nil: fails++ if fails == 1 || fails%10 == 0 { logf("console: automatic box enrollment (failure #%d): %v", fails, err) } default: fails = 0 seen := map[string]bool{} for _, st := range steps { seen[st.Box] = true if last[st.Box] != st.Step { logf("console: automatic box enrollment: %s: %s", st.Box, st.Step) last[st.Box] = st.Step } } for box := range last { if !seen[box] { delete(last, box) } } } select { case <-ctx.Done(): return case <-time.After(every): } } } // startEnrollPass starts the pass for this server, or logs why it cannot. func (s *Server) startEnrollPass(ctx context.Context) { members, err := consoleMembershipReader(ctx) if err != nil { log.Printf("console: automatic box enrollment: not started: %v", err) return } e, err := newAutoEnroller(s.cfg.ClaimAuthority, members) if err != nil { log.Printf("console: automatic box enrollment: not started: %v", err) return } go runEnrollPass(ctx, e, enrollPassInterval, log.Printf) }