package console import ( "context" "errors" "os" "github.com/carlosframework/platform/internal/hostops" "github.com/carlosframework/platform/internal/relay" "github.com/carlosframework/platform/internal/release" ) // The relay's app gate. Every per-app relay walk (enable, disable, adopted, // reissue, converge, mark done) runs its whole body inside one app mutation // of that app, and every bucket write below it uses the permitted context. // The public handlers have already authenticated and authorized the caller; // these helpers only open or join the operation. With HostClaim off the // deployment bucket implements no release.AppMutationRunner, so each helper // runs its callback unchanged. // relayAppMutation runs fn as an app mutation of acct/app. A caller already // holding a live permit for the same app joins it. func (s *Server) relayAppMutation(ctx context.Context, acct, app, action string, fn func(context.Context) error) error { return release.WithAppMutation(ctx, s.cfg.Releases, acct, app, action, fn) } // relayGated reports whether the deployment bucket is the enabled boundary. func (s *Server) relayGated() bool { _, ok := s.cfg.Releases.(release.AppMutationRunner) return ok } // relayWithScopes acquires (or joins) an app mutation for every scope, in // the sorted, deduplicated order hostops.RelayAuthorityScopes returns, and // runs fn under the innermost context only after all of them are held. A // failed acquisition returns before fn, so nothing was issued and every // enclosing operation may release as unapplied. func (s *Server) relayWithScopes(ctx context.Context, scopes []hostops.AppScope, action string, fn func(context.Context) error) error { if len(scopes) == 0 { return fn(ctx) } first := scopes[0] return s.relayAppMutation(ctx, first.Account, first.App, action, func(ctx context.Context) error { return s.relayWithScopes(ctx, scopes[1:], action, fn) }) } // relayPublishGated is relayPublishOnce's write on the enabled boundary. It // reads the aggregate at the caller's expected etag, computes exactly which // app entries the new body adds, removes or changes, acquires a permit for // each, and then writes with that same etag. An aggregate that moved since // the caller read its etag is a conflict before any permit is taken. A body // whose every entry is identical needs no permit: the boundary classifies it // as the written_at-only rewrite it is. func (s *Server) relayPublishGated(ctx context.Context, etag string, body []byte) error { before, cur, err := s.cfg.Releases.Get(ctx, relay.AuthorityKey) switch { case errors.Is(err, os.ErrNotExist): before, cur = nil, "" case err != nil: return err } if cur != etag { return release.ErrCASConflict } scopes, err := hostops.RelayAuthorityScopes(before, body) if err != nil { return err } return s.relayWithScopes(ctx, scopes, "relay-aggregate", func(ctx context.Context) error { if err := s.requireRelayLease(); err != nil { return err } _, err := s.cfg.Releases.Put(ctx, relay.AuthorityKey, body, etag) return err }) }