| index a9b8b5e64..c32c38773 100644 |
| --- a/internal/console/apiinstancemove_offline.go |
| +++ b/internal/console/apiinstancemove_offline.go |
| @@ -137,13 +137,17 @@ func (s *Server) offlineWaitRefusal(box, from, host string) *offlineRefusal { |
| // (harness Run 9, finding 7: flip answered MV-O31 for a stale report, and an |
| // operator went looking for a process that was not there). |
| func offlineStaleRefusal(box string) *offlineRefusal { |
| - return &offlineRefusal{status: http.StatusConflict, msg: fmt.Sprintf("the report from %s is not current. it reports again within a minute. try again then", box)} |
| + return &offlineRefusal{status: http.StatusConflict, msg: fmt.Sprintf("the report from %s is not current. it reports again within a minute. try again then", box), transient: true} |
| } |
| |
| // offlineRefusal is what a door refuses with: the status and the sentence. |
| +// transient marks a refusal that clears on its own (a read that failed, a |
| +// report not current yet): the doors answer it as any other, and the |
| +// self-serve job waits it out instead of aborting the move (Task 7). |
| type offlineRefusal struct { |
| - status int |
| - msg string |
| + status int |
| + msg string |
| + transient bool |
| } |
| |
| func writeRefusal(w http.ResponseWriter, ref *offlineRefusal) { |
| @@ -454,7 +458,7 @@ func (s *Server) offlineSelect(ctx context.Context, f offlineFleet, host, from, |
| // claim backfill (MV-O20). A read that fails is a 502 and never an absence. |
| func (s *Server) offlineClaimEvidence(ctx context.Context, acct, app, host string, rec *provision.Record) *offlineRefusal { |
| has := &offlineRefusal{status: http.StatusConflict, msg: fmt.Sprintf("%s has a host claim. offline moves are only for hosts without one", host)} |
| - unreadable := &offlineRefusal{status: http.StatusBadGateway, msg: "could not read the host claim"} |
| + unreadable := &offlineRefusal{status: http.StatusBadGateway, msg: "could not read the host claim", transient: true} |
| if rec.Acquisition != "" { |
| return has |
| } |
| @@ -543,14 +547,14 @@ func (s *Server) offlineRegionChecks(ctx context.Context, acct, app, host string |
| m, err := provision.GetManifest(ctx, s.cfg.Releases, acct, app) |
| if err != nil && !errors.Is(err, os.ErrNotExist) { |
| log.Printf("console: offline move %s: manifest: %v", host, err) |
| - return offlineSelection{}, &offlineRefusal{status: http.StatusServiceUnavailable, msg: "could not read the app's instances manifest. try again"} |
| + return offlineSelection{}, &offlineRefusal{status: http.StatusServiceUnavailable, msg: "could not read the app's instances manifest. try again", transient: true} |
| } |
| if m != nil && !s.moveRegionAllowed(m.Regions, to) { |
| return offlineSelection{}, &offlineRefusal{status: http.StatusBadRequest, msg: "region " + region + " is not one of this app's regions: " + strings.Join(m.Regions, ", ")} |
| } |
| fleet, ok := s.readOfflineFleet(ctx) |
| if !ok { |
| - return offlineSelection{}, &offlineRefusal{status: http.StatusConflict, msg: "the fleet's edges files could not all be read. try again"} |
| + return offlineSelection{}, &offlineRefusal{status: http.StatusConflict, msg: "the fleet's edges files could not all be read. try again", transient: true} |
| } |
| if to == "" || rec.Region == "" { |
| // A box that sets CARLOS_REGION to the home's name is home to this |
| @@ -701,7 +705,7 @@ func (s *Server) participantIdentityFor(ctx context.Context, f offlineFleet) (pa |
| case errors.Is(err, os.ErrNotExist): |
| case err != nil: |
| log.Printf("console: self-serve move: the host roster: %v", err) |
| - return participantIdentity{}, &offlineRefusal{status: http.StatusServiceUnavailable, msg: "could not read the host roster. try again"} |
| + return participantIdentity{}, &offlineRefusal{status: http.StatusServiceUnavailable, msg: "could not read the host roster. try again", transient: true} |
| default: |
| refs := make([]string, 0, len(o.Roster.Boxes)) |
| for ref := range o.Roster.Boxes { |
| @@ -749,7 +753,7 @@ func (s *Server) pinParticipant(ctx context.Context, f offlineFleet, b edgesFile |
| return p, nil |
| } |
| if !current { |
| - return provision.Participant{}, &offlineRefusal{status: http.StatusConflict, msg: fmt.Sprintf("the box %s has not reported recently", b.Box)} |
| + return provision.Participant{}, &offlineRefusal{status: http.StatusConflict, msg: fmt.Sprintf("the box %s has not reported recently", b.Box), transient: true} |
| } |
| raw, err := boxid.Load(ctx, s.cfg.Releases, p.BoxID) |
| switch { |
| @@ -759,7 +763,7 @@ func (s *Server) pinParticipant(ctx context.Context, f offlineFleet, b edgesFile |
| return provision.Participant{}, &offlineRefusal{status: http.StatusConflict, msg: fmt.Sprintf("box %s has an identity record that does not verify", b.Box)} |
| case err != nil: |
| log.Printf("console: self-serve move: the identity record of %s (%s): %v", b.Box, p.BoxID, err) |
| - return provision.Participant{}, &offlineRefusal{status: http.StatusBadGateway, msg: fmt.Sprintf("could not read the identity record for box %s", b.Box)} |
| + return provision.Participant{}, &offlineRefusal{status: http.StatusBadGateway, msg: fmt.Sprintf("could not read the identity record for box %s", b.Box), transient: true} |
| } |
| ev, err := id.verifier.Enroll(raw, boxid.Expect{Box: p.BoxID, AWSRegion: id.aws[code], Roster: id.roster}) |
| if err != nil || ev.Cloud.InstanceID != p.InstanceID { |
| @@ -1177,7 +1181,7 @@ func (s *Server) offlineStepFor(ctx context.Context, d offlineDoor, mv *provisio |
| // door's, so the view's answer stays main's. |
| fleet, ok := s.readOfflineFleet(ctx) |
| if !ok { |
| - return offlineStep{}, &offlineRefusal{status: http.StatusConflict, msg: "the fleet's edges files could not all be read. try again"} |
| + return offlineStep{}, &offlineRefusal{status: http.StatusConflict, msg: "the fleet's edges files could not all be read. try again", transient: true} |
| } |
| if ref := s.offlineSelect(ctx, fleet, host, mv.From, mv.To); ref != nil { |
| return offlineStep{}, ref |
| @@ -1194,7 +1198,7 @@ func (s *Server) offlineStepFor(ctx context.Context, d offlineDoor, mv *provisio |
| case provision.OfflineStopping: |
| fleet, ok := s.readOfflineFleet(ctx) |
| if !ok { |
| - return offlineStep{}, &offlineRefusal{status: http.StatusConflict, msg: "the fleet's edges files could not all be read. try again"} |
| + return offlineStep{}, &offlineRefusal{status: http.StatusConflict, msg: "the fleet's edges files could not all be read. try again", transient: true} |
| } |
| sealer, boxes, ref := s.offlineSealCheck(fleet, ev, mv, host, from, now) |
| if ref != nil { |
| @@ -1230,7 +1234,7 @@ func (s *Server) offlineStepFor(ctx context.Context, d offlineDoor, mv *provisio |
| if mv.Sealer == "" { |
| fleet, ok := s.readOfflineFleet(ctx) |
| if !ok { |
| - return offlineStep{}, &offlineRefusal{status: http.StatusConflict, msg: "the fleet's edges files could not all be read. try again"} |
| + return offlineStep{}, &offlineRefusal{status: http.StatusConflict, msg: "the fleet's edges files could not all be read. try again", transient: true} |
| } |
| boxes, sealerPhase = nil, "" |
| for _, b := range fleet.boxesAt(mv.From) { |