package host import ( "context" "encoding/base64" "encoding/json" "errors" "fmt" "net/http" "strings" "sync" "time" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostenv" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/status" ) // A box its region's group launched is referenced by its group reference // (census ruling region-ha-2b): hostactors.GroupBoxReference of its account // and instance, "g-" and 20 hex digits. Its host.env says so: // carlos system host-env write adds CARLOS_BOX_REFERENCE=group. Status // labels come from 1,600 values and identity records are never deleted, so // as a group replaces its boxes, labels collide and a box whose label // another record holds can never publish its own; group references don't // collide, and are a pseudonym, so the public status feed shows no // instance id. Every other box, fleet boxes included, keeps its label and // its status key exactly as before. // groupReferenced reports whether this box takes its group reference. func groupReferenced(getenv func(string) string) bool { return strings.TrimSpace(getenv(hostenv.BoxReference)) == hostenv.GroupReference } // groupIdentity reads this box's account and instance from its signed // identity document (IMDS); a seam for tests. var groupIdentity = func() (account, instance string, err error) { rec, err := readIdentityRecord(&http.Client{Timeout: 2 * time.Second}, imdsBase, "", time.Now()) if err != nil { return "", "", err } doc, err := base64.StdEncoding.DecodeString(rec.IdentityDocument) if err != nil { return "", "", err } var d struct { AccountID string `json:"accountId"` InstanceID string `json:"instanceId"` } if err := json.Unmarshal(doc, &d); err != nil { return "", "", err } if d.AccountID == "" || d.InstanceID == "" { return "", "", errors.New("the identity document names no account or instance") } return d.AccountID, d.InstanceID, nil } var groupRef struct { sync.Mutex ref string } // groupBoxReference is this box's group reference, read once and kept. "" // while IMDS has not answered: never a label in its place. func groupBoxReference() string { ref, _ := resolveGroupBoxReference() return ref } // resolveGroupBoxReference is groupBoxReference with the reason it has // none: the IMDS read's error, kept so the caller can say why. Only a // success is kept; a failure is asked again next time. func resolveGroupBoxReference() (string, error) { groupRef.Lock() defer groupRef.Unlock() if groupRef.ref != "" { return groupRef.ref, nil } account, instance, err := groupIdentity() if err != nil { return "", err } groupRef.ref = hostactors.GroupBoxReference(account, instance) return groupRef.ref, nil } // errNoGroupReference is a tick of a group box whose reference has not // resolved: it publishes nothing. var errNoGroupReference = errors.New("this group box's reference could not be read, so it publishes no status report this tick") // groupStatusTick is statusLoop's tick (census ruling region-ha-2b). A box // whose host.env carries the group marker publishes its status report // only under its group reference, the key the console derives, resolved // afresh on every tick until it resolves (groupBoxReference keeps only a // success). Until then it publishes nothing at all: never under // status.Label, which may be another box's key, since WriteBox replaces // whatever is there. That holds whatever the claim and region HA switches // say. Every other box's tick is statusTick's with statusLoop's label, // exactly as before. func groupStatusTick(ctx context.Context, s release.Store, reg *registry.Registry, p status.Probes, publish bool, label string, getenv func(string) string) error { if groupReferenced(getenv) { ref, err := resolveGroupBoxReference() if err != nil { return fmt.Errorf("%w: %v", errNoGroupReference, err) } label = ref } return statusTick(ctx, s, reg, p, publish, label) }