Documentation
¶
Overview ¶
Package state holds sqlc-generated queries and repository types.
This store is the sole record of how every app runs (R-020), which sets the bar: every mutation is audited, every object is exportable, nothing important lives only in memory. Several requirements are enforced by database constraints rather than by code — see the package CLAUDE.md before changing the schema.
Index ¶
- Constants
- Variables
- func CanCopyDatabase(ctx context.Context, ownerURL string) error
- func ConnectArchiver(ctx context.Context, ownerURL string, password secret.Value) (*pgxpool.Pool, error)
- func DefaultSCIMIdentityAttribute(kind string) string
- func DropSnapshot(ctx context.Context, ownerURL string) error
- func HeaderField(name string) string
- func Migrate(ctx context.Context, ownerURL string) error
- func Regrant(ctx context.Context, ownerURL string) error
- func RestoreSnapshot(ctx context.Context, ownerURL string) error
- func SignInAddrKey(addr string) string
- func SignInUsernameKey(username string) string
- func Snapshot(ctx context.Context, ownerURL string) error
- func SnapshotName(ownerURL string) (string, error)
- func SuspendedBySCIM(providerID string) string
- type AIAssignment
- type AIAssignments
- func (s *AIAssignments) Delete(ctx context.Context, function string) error
- func (s *AIAssignments) Get(ctx context.Context, function string) (AIAssignment, bool, error)
- func (s *AIAssignments) List(ctx context.Context) ([]AIAssignment, error)
- func (s *AIAssignments) Put(ctx context.Context, a AIAssignment) error
- type AcmeAccount
- type Activity
- func (a *Activity) IdleCandidates(ctx context.Context, d IdleDefaults, now time.Time) ([]IdleApp, error)
- func (a *Activity) IdleStatus(ctx context.Context, appID string) (IdleStatus, error)
- func (a *Activity) RecordActivity(ctx context.Context, seen map[string]time.Time) error
- func (a *Activity) SetIdleNotice(ctx context.Context, appID string, which IdleNotice, at *time.Time) error
- func (a *Activity) SetIdleSettings(ctx context.Context, appID string, s IdleSettings) error
- func (a *Activity) StopForIdle(ctx context.Context, appID string) (bool, error)
- type AdapterConfig
- type AdapterCredentials
- func NewAdapterCredentials(db *DB, adapter api.SecretsAdapter, adapterRef string) *AdapterCredentials
- func NewIdentityCredentials(db *DB, adapter api.SecretsAdapter, adapterRef string) *AdapterCredentials
- func NewRegistryCredentials(db *DB, adapter api.SecretsAdapter, adapterRef string) *AdapterCredentials
- func NewSourceAuthorizations(db *DB, adapter api.SecretsAdapter, adapterRef string) *AdapterCredentials
- func (c *AdapterCredentials) Delete(ctx context.Context, adapterID, field string) error
- func (c *AdapterCredentials) Fields(ctx context.Context) (map[string][]string, error)
- func (c *AdapterCredentials) Put(ctx context.Context, adapterID, field string, v secret.Value) error
- func (c *AdapterCredentials) Resolve(ctx context.Context, adapterID string) (map[string]secret.Value, error)
- type Adapters
- func (a *Adapters) DeleteInCategory(ctx context.Context, id, category string) error
- func (a *Adapters) List(ctx context.Context) ([]AdapterConfig, error)
- func (a *Adapters) Save(ctx context.Context, c AdapterConfig) (Replaced, error)
- func (a *Adapters) Upsert(ctx context.Context, c AdapterConfig) error
- type Allocations
- type AnonymousGrant
- type App
- type AppLimitReached
- type AppLimits
- func (l *AppLimits) Facts(ctx context.Context, userID string) (LimitFacts, error)
- func (l *AppLimits) GroupMaxApps(ctx context.Context, groupID string) (*int, error)
- func (l *AppLimits) SetGroupMaxApps(ctx context.Context, groupID string, maxApps *int) error
- func (l *AppLimits) SetUserMaxApps(ctx context.Context, userID string, maxApps *int) error
- type AppWithStorage
- type ApprovalDecision
- type ApprovalReason
- type Apps
- func (a *Apps) Archive(ctx context.Context, appID string) error
- func (a *Apps) AwaitingTeardown(ctx context.Context, limit int) ([]TeardownTarget, error)
- func (a *Apps) ByID(ctx context.Context, appID string) (App, bool, error)
- func (a *Apps) ByPath(ctx context.Context, requestPath string) (App, *spec.AppSpec, string, bool, error)
- func (a *Apps) ByRouting(ctx context.Context, by, value string) (App, *spec.AppSpec, bool, error)
- func (a *Apps) CheckAddress(ctx context.Context, appID string, r spec.Routing) error
- func (a *Apps) ClearIcon(ctx context.Context, appID string) error
- func (a *Apps) Create(ctx context.Context, name, slug, ownerUserID, createdBy string, ...) (App, error)
- func (a *Apps) CreateRevision(ctx context.Context, appID string, s *spec.AppSpec, origin spec.Origin, ...) (Revision, error)
- func (a *Apps) CreateSection(ctx context.Context, userID, name string) (Section, error)
- func (a *Apps) CreateWithin(ctx context.Context, name, slug, ownerUserID, createdBy string, ...) (App, error)
- func (a *Apps) DeleteSection(ctx context.Context, userID, sectionID string) (bool, error)
- func (a *Apps) DiscardStorage(ctx context.Context, appID string) error
- func (a *Apps) DueForBackup(ctx context.Context, takenSince, triedSince time.Time, limit int) ([]AppWithStorage, error)
- func (a *Apps) ForgetVolume(ctx context.Context, appID, volumeID string) error
- func (a *Apps) IconOf(ctx context.Context, appID string) (Icon, bool, error)
- func (a *Apps) Known(ctx context.Context, appID string) (bool, error)
- func (a *Apps) ListAll(ctx context.Context) ([]App, error)
- func (a *Apps) ListAllPage(ctx context.Context, page Page) ([]App, string, int, error)
- func (a *Apps) ListForPrincipal(ctx context.Context, p authz.Principal) ([]App, error)
- func (a *Apps) ListForPrincipalPage(ctx context.Context, p authz.Principal, page Page) ([]App, string, int, error)
- func (a *Apps) ListForUse(ctx context.Context, p authz.Principal, page Page, every bool) ([]App, string, error)
- func (a *Apps) ListRevisions(ctx context.Context, appID string) ([]Revision, error)
- func (a *Apps) Live(ctx context.Context, appID string) (bool, error)
- func (a *Apps) LiveApps(ctx context.Context, f planner.InventoryFilter) ([]planner.InventoryApp, error)
- func (a *Apps) MarkBundleDestroyed(ctx context.Context, appID string) error
- func (a *Apps) OrphanedVolumes(ctx context.Context, limit int) ([]OrphanedVolume, error)
- func (a *Apps) Pin(ctx context.Context, appID, specID, newState, pinnedBy string) error
- func (a *Apps) PlaceApp(ctx context.Context, userID, sectionID, appID string) (bool, error)
- func (a *Apps) PruneSpecRevisions(ctx context.Context) (int, error)
- func (a *Apps) Rename(ctx context.Context, appID, name string) error
- func (a *Apps) RenameSection(ctx context.Context, userID, sectionID, name string) (Section, bool, error)
- func (a *Apps) RevisionByID(ctx context.Context, specID string) (Revision, bool, error)
- func (a *Apps) RevisionByNumber(ctx context.Context, appID string, number int) (Revision, bool, error)
- func (a *Apps) Sections(ctx context.Context, userID string) ([]Section, error)
- func (a *Apps) SetDesiredState(ctx context.Context, appID, desired string) error
- func (a *Apps) SetFavorite(ctx context.Context, userID, appID string, favorite bool) error
- func (a *Apps) SetIcon(ctx context.Context, appID, contentType string, data []byte) error
- func (a *Apps) SetSource(ctx context.Context, appID string, src spec.Source) error
- func (a *Apps) SetState(ctx context.Context, appID, appState string) error
- func (a *Apps) SetStateIf(ctx context.Context, appID, from, to string) error
- func (a *Apps) UnplaceApp(ctx context.Context, userID, sectionID, appID string) error
- func (a *Apps) VolumeCount(ctx context.Context, appID string) (int, error)
- func (a *Apps) WithAutoDeploy(ctx context.Context) ([]App, error)
- func (a *Apps) WithStorage(ctx context.Context) ([]AppWithStorage, error)
- type AuditCursor
- type AuditSinkState
- type AuditSinks
- func (s *AuditSinks) Backlog(ctx context.Context, st AuditSinkState, limit int) (int, error)
- func (s *AuditSinks) Delivered(ctx context.Context, adapterID string, txid uint64, id int64, n int, ...) (bool, error)
- func (s *AuditSinks) Disable(ctx context.Context, adapterID, reason string, at time.Time) (bool, error)
- func (s *AuditSinks) Failed(ctx context.Context, adapterID, message string, at, next time.Time) (AuditSinkState, error)
- func (s *AuditSinks) Get(ctx context.Context, adapterID string, start *AuditCursor) (AuditSinkState, error)
- func (s *AuditSinks) Holds(ctx context.Context, st AuditSinkState, lo, hi time.Time) (bool, error)
- func (s *AuditSinks) List(ctx context.Context) ([]AuditSinkState, error)
- func (s *AuditSinks) Resume(ctx context.Context, adapterID string, at time.Time) (AuditSinkState, error)
- func (s *AuditSinks) Skipped(ctx context.Context, adapterID string, txid uint64, id int64, at time.Time) error
- type AuthzStore
- func (s *AuthzStore) AnonymousAccess(ctx context.Context, appID string) (granted, passcode bool, err error)
- func (s *AuthzStore) ControlGrantsFor(ctx context.Context, appID string, p authz.Principal) ([]authz.Grant, error)
- func (s *AuthzStore) ControlGrantsForApps(ctx context.Context, appIDs []string, p authz.Principal) (map[string][]authz.Grant, error)
- func (s *AuthzStore) DataFacts(ctx context.Context, appID string, p authz.Principal, passcodeToken string) (authz.DataFacts, error)
- func (s *AuthzStore) DeployApprovers(ctx context.Context, appID string) ([]string, error)
- func (s *AuthzStore) GroupsForUser(ctx context.Context, userID string) ([]string, error)
- func (s *AuthzStore) HasDataGrant(ctx context.Context, appID string, p authz.Principal) (bool, error)
- func (s *AuthzStore) InstallGrantsFor(ctx context.Context, p authz.Principal) ([]authz.Grant, error)
- func (s *AuthzStore) InstallVerbHolders(ctx context.Context, verb authz.Verb) ([]string, error)
- func (s *AuthzStore) InstallVerbsFor(ctx context.Context, p authz.Principal) ([]string, error)
- func (s *AuthzStore) IsOwner(ctx context.Context, appID, userID string) (bool, error)
- func (s *AuthzStore) PasscodeUnlocked(ctx context.Context, appID, token string) (bool, error)
- func (s *AuthzStore) Role(ctx context.Context, roleID string) (authz.Role, error)
- func (s *AuthzStore) UserStatus(ctx context.Context, userID string) (string, error)
- type AutoDeployCheck
- type AutoDeployChecks
- func (c *AutoDeployChecks) Attempted(ctx context.Context, appID, ref, commit, deploymentID, message string) error
- func (c *AutoDeployChecks) ByApp(ctx context.Context, appID string) (AutoDeployCheck, bool, error)
- func (c *AutoDeployChecks) Checked(ctx context.Context, appID, ref, commit, message string) error
- type AutoDeploySecrets
- func (k *AutoDeploySecrets) Delete(ctx context.Context, appID string) error
- func (k *AutoDeploySecrets) Get(ctx context.Context, appID string) (secret.Value, bool, error)
- func (k *AutoDeploySecrets) Has(ctx context.Context, appID string) (bool, error)
- func (k *AutoDeploySecrets) Put(ctx context.Context, appID string, v secret.Value) error
- type AwaitingApproval
- type AwaitingKey
- type Backup
- type BackupAttempt
- type Backups
- func (b *Backups) Attempts(ctx context.Context, appID string, limit int) ([]BackupAttempt, error)
- func (b *Backups) ByID(ctx context.Context, backupID string) (Backup, bool, error)
- func (b *Backups) Expired(ctx context.Context, now time.Time) ([]Backup, error)
- func (b *Backups) Forget(ctx context.Context, backupID string) error
- func (b *Backups) List(ctx context.Context, appID string, page Page) ([]Backup, string, error)
- func (b *Backups) NewID() string
- func (b *Backups) Record(ctx context.Context, rec Backup) error
- func (b *Backups) RecordAttempt(ctx context.Context, a BackupAttempt) error
- type BundleSource
- func (b *BundleSource) AdapterConfigs(ctx context.Context) ([]byte, error)
- func (b *BundleSource) Counts(ctx context.Context) (map[string]int, error)
- func (b *BundleSource) HostPolicy(ctx context.Context) ([]byte, error)
- func (b *BundleSource) ServicesToSnapshot(ctx context.Context) ([]backup.ServiceRef, error)
- func (b *BundleSource) VolumesForApp(ctx context.Context, appID string) ([]backup.VolumeRef, error)
- func (b *BundleSource) VolumesToSnapshot(ctx context.Context) ([]backup.VolumeRef, error)
- type ConnectOptions
- type DB
- type Deliveries
- func (s *Deliveries) Claim(ctx context.Context, now time.Time, lease time.Duration, limit int) ([]Delivery, error)
- func (s *Deliveries) Drop(ctx context.Context, deliveryID, reason string) error
- func (s *Deliveries) Get(ctx context.Context, deliveryID string) (Delivery, bool, error)
- func (s *Deliveries) List(ctx context.Context, subscriptionID, before string, limit int) ([]Delivery, error)
- func (s *Deliveries) Record(ctx context.Context, deliveryID string, a DeliveryAttempt, status string, ...) error
- func (s *Deliveries) Redeliver(ctx context.Context, deliveryID, by string) error
- type Delivery
- type DeliveryAttempt
- type Deployment
- type Deployments
- func (d *Deployments) ByID(ctx context.Context, deploymentID string) (Deployment, bool, error)
- func (d *Deployments) Claim(ctx context.Context, limit int) ([]Deployment, error)
- func (d *Deployments) Create(ctx context.Context, appID, specID, trigger, createdBy string) (Deployment, error)
- func (d *Deployments) CreateAwaiting(ctx context.Context, appID, specID, trigger, createdBy string, required int, ...) (Deployment, error)
- func (d *Deployments) Decide(ctx context.Context, deploymentID, principalID, decision, comment string) error
- func (d *Deployments) Decisions(ctx context.Context, deploymentIDs []string) (map[string][]ApprovalDecision, error)
- func (d *Deployments) EndAwaiting(ctx context.Context, deploymentID, status, errorCode, message string) (bool, error)
- func (d *Deployments) ExpireDue(ctx context.Context, now time.Time) ([]Deployment, error)
- func (d *Deployments) Finish(ctx context.Context, deploymentID, status, errorCode, message string) error
- func (d *Deployments) InFlight(ctx context.Context, appID string) (bool, error)
- func (d *Deployments) LastImage(ctx context.Context, appID string) (string, error)
- func (d *Deployments) ListAwaiting(ctx context.Context) ([]AwaitingApproval, error)
- func (d *Deployments) ListAwaitingAfter(ctx context.Context, after AwaitingKey, limit int) ([]AwaitingApproval, error)
- func (d *Deployments) ListForApp(ctx context.Context, appID string) ([]Deployment, error)
- func (d *Deployments) PriorState(ctx context.Context, deploymentID string) (string, error)
- func (d *Deployments) QueueDepth(ctx context.Context) (int, error)
- func (d *Deployments) QueuePosition(ctx context.Context, deploymentID string) (int, bool, error)
- func (d *Deployments) QueuePositions(ctx context.Context, deps []Deployment) error
- func (d *Deployments) RanSuccessfully(ctx context.Context, appID, specID string) (bool, error)
- func (d *Deployments) RecordPriorState(ctx context.Context, deploymentID, appState string) error
- func (d *Deployments) RecoverInFlight(ctx context.Context) (int64, error)
- func (d *Deployments) Release(ctx context.Context, deploymentIDs []string) (int64, error)
- func (d *Deployments) Runner(ctx context.Context, deploymentID string) (string, error)
- func (d *Deployments) RunningSpecID(ctx context.Context, appID string) (string, error)
- func (d *Deployments) SetEgressRules(ctx context.Context, deploymentID string, rules egress.Rules) error
- func (d *Deployments) SetImageRef(ctx context.Context, deploymentID, imageRef, digest string, ...) error
- func (d *Deployments) SetResultState(ctx context.Context, deploymentID, appState string) error
- func (d *Deployments) SetStatus(ctx context.Context, deploymentID, status string) error
- func (d *Deployments) StartApproved(ctx context.Context, deploymentID string) (bool, error)
- func (d *Deployments) SupersedeAwaiting(ctx context.Context, appID, exceptID string) ([]string, error)
- func (d *Deployments) Waiting(ctx context.Context, deploymentID string) (bool, error)
- type Detection
- type DetectionSummary
- type Detections
- func (d *Detections) Claim(ctx context.Context, limit int) ([]string, error)
- func (d *Detections) FailIfRunning(ctx context.Context, appID string, cause error) error
- func (d *Detections) Get(ctx context.Context, appID string) (Detection, error)
- func (d *Detections) Queue(ctx context.Context, appID string) (Detection, error)
- func (d *Detections) Rebuild(ctx context.Context, appID string, body any) (bool, error)
- func (d *Detections) RecoverRunning(ctx context.Context) (int64, error)
- func (d *Detections) Release(ctx context.Context, appIDs []string) (int64, error)
- func (d *Detections) Save(ctx context.Context, appID, status string, body any, commit string) error
- func (d *Detections) SaveAnswers(ctx context.Context, appID string, answers map[string]string) error
- func (d *Detections) Start(ctx context.Context, appID string) error
- func (d *Detections) Summaries(ctx context.Context, appIDs []string) (map[string]DetectionSummary, error)
- type Disk
- type DiskApp
- type DiskMark
- type EdgeCertificate
- type EdgeCertificates
- func (e *EdgeCertificates) Account(ctx context.Context, email, directory string) (AcmeAccount, bool, error)
- func (e *EdgeCertificates) Certificate(ctx context.Context, name string) (EdgeCertificate, bool, error)
- func (e *EdgeCertificates) Challenge(ctx context.Context, token string) (string, bool, error)
- func (e *EdgeCertificates) DeleteChallenge(ctx context.Context, token string) error
- func (e *EdgeCertificates) PutAccount(ctx context.Context, a AcmeAccount) error
- func (e *EdgeCertificates) PutCertificate(ctx context.Context, c EdgeCertificate) error
- func (e *EdgeCertificates) PutChallenge(ctx context.Context, token, keyAuthorization, domain string, expires time.Time) error
- func (e *EdgeCertificates) RecordFailure(ctx context.Context, name string, domains []string, message string, ...) error
- type ErrAssigned
- type Event
- type Events
- func (s *Events) Emit(ctx context.Context, e Event) (string, error)
- func (s *Events) EmitRouted(ctx context.Context, e Event, subscriptionID string) (Delivery, error)
- func (s *Events) ForApp(ctx context.Context, appID, before string, limit int) ([]Event, error)
- func (s *Events) Get(ctx context.Context, eventID string) (Event, bool, error)
- func (s *Events) Prune(ctx context.Context, before time.Time, limit int) (int64, error)
- func (s *Events) Route(ctx context.Context, limit int, before time.Time, ...) ([]Event, error)
- type Execer
- type FailedAttempts
- func (p *FailedAttempts) Clear(ctx context.Context, key string) error
- func (p *FailedAttempts) Prune(ctx context.Context, window time.Duration) error
- func (p *FailedAttempts) Recent(ctx context.Context, key string, window time.Duration) (int, error)
- func (p *FailedAttempts) Record(ctx context.Context, key string, window time.Duration) error
- func (p *FailedAttempts) Wait(ctx context.Context, key string, window time.Duration) (time.Duration, error)
- type GrantRow
- type Grants
- func (g *Grants) AnonymousGrantFor(ctx context.Context, appID string) (AnonymousGrant, bool, error)
- func (g *Grants) Create(ctx context.Context, appID, plane, kind, principalID, roleID, createdBy string) (GrantRow, error)
- func (g *Grants) Delete(ctx context.Context, appID, grantID string) error
- func (g *Grants) ForGroup(ctx context.Context, groupID string, page Page) ([]UserAppGrant, string, error)
- func (g *Grants) ForUser(ctx context.Context, userID string, page Page) ([]UserAppGrant, string, error)
- func (g *Grants) GrantInstall(ctx context.Context, kind, principalID, roleID, createdBy string) (GrantRow, error)
- func (g *Grants) InstallRoleOf(ctx context.Context, kind, principalID string) (string, bool, error)
- func (g *Grants) InstallRoles(ctx context.Context) ([]authz.Role, error)
- func (g *Grants) InstallRolesByPrincipal(ctx context.Context) (map[string]string, error)
- func (g *Grants) InstallRolesForUsers(ctx context.Context, userIDs []string) (map[string][]HeldInstallRole, error)
- func (g *Grants) ListForApp(ctx context.Context, appID string, page Page) ([]GrantRow, string, error)
- func (g *Grants) RevokeInstall(ctx context.Context, kind, principalID string) error
- func (g *Grants) SetPasscode(ctx context.Context, appID, grantID, passcodeHash string) error
- func (g *Grants) SetRole(ctx context.Context, appID, grantID, roleID string) error
- func (g *Grants) Unlock(ctx context.Context, appID, grantID string) (string, time.Time, error)
- type Group
- type GroupFilter
- type GroupLimit
- type Groups
- func (g *Groups) AddMember(ctx context.Context, groupID, userID string) error
- func (g *Groups) ByID(ctx context.Context, groupID string) (Group, bool, error)
- func (g *Groups) Create(ctx context.Context, name string) (Group, error)
- func (g *Groups) Delete(ctx context.Context, groupID string) error
- func (g *Groups) Exists(ctx context.Context, groupID string) (bool, error)
- func (g *Groups) Link(ctx context.Context, syncedGroupID, groupID, by string) error
- func (g *Groups) List(ctx context.Context) ([]Group, error)
- func (g *Groups) ListPage(ctx context.Context, page Page, f GroupFilter) ([]Group, string, int, error)
- func (g *Groups) MembersPage(ctx context.Context, groupID string, page Page) ([]User, string, int, error)
- func (g *Groups) RemoveMember(ctx context.Context, groupID, userID string) error
- func (g *Groups) Search(ctx context.Context, q string, limit int) ([]Group, error)
- func (g *Groups) SetMembers(ctx context.Context, groupID string, userIDs []string) error
- func (g *Groups) SyncMemberships(ctx context.Context, adapterID, userID string, names []string) error
- func (g *Groups) Unlink(ctx context.Context, syncedGroupID, groupID string) error
- type HeldInstallRole
- type Icon
- type Idempotency
- type Identities
- func (s *Identities) ByVerifiedEmail(ctx context.Context, email string) (User, bool, error)
- func (s *Identities) CreateExternal(ctx context.Context, n NewExternal) (User, error)
- func (s *Identities) ForUser(ctx context.Context, userID string) ([]Identity, error)
- func (s *Identities) Link(ctx context.Context, userID, adapterID, externalID, by string, replace bool) (string, error)
- func (s *Identities) RecordSignIn(ctx context.Context, adapterID, externalID, userID, email, displayName string) error
- func (s *Identities) Resolve(ctx context.Context, adapterID, externalID string) (User, bool, error)
- func (s *Identities) Unlink(ctx context.Context, userID, adapterID, externalID string) error
- type Identity
- type IdentityProvider
- type IdentityProviders
- func (s *IdentityProviders) AnyExternalEnabled(ctx context.Context) (bool, error)
- func (s *IdentityProviders) ByID(ctx context.Context, providerID string) (IdentityProvider, bool, error)
- func (s *IdentityProviders) BySCIMToken(ctx context.Context, digest string) (IdentityProvider, bool, error)
- func (s *IdentityProviders) Create(ctx context.Context, kind string, set ProviderSettings) (IdentityProvider, error)
- func (s *IdentityProviders) Delete(ctx context.Context, providerID string) error
- func (s *IdentityProviders) List(ctx context.Context) ([]IdentityProvider, error)
- func (s *IdentityProviders) SetSCIMToken(ctx context.Context, providerID, digest string) error
- func (s *IdentityProviders) Update(ctx context.Context, providerID string, set ProviderSettings) (IdentityProvider, error)
- type IdleApp
- type IdleDefaults
- type IdleNotice
- type IdleSettings
- type IdleStatus
- type InstallRoleVia
- type Issued
- type Leadership
- type Lease
- func (l *Lease) Claim(ctx context.Context, now, cutoff time.Time, limit int, lease time.Duration) ([]Reconcilable, error)
- func (l *Lease) Extend(ctx context.Context, lease time.Duration) (map[string]bool, error)
- func (l *Lease) Holder() string
- func (l *Lease) Release(ctx context.Context, appID string, visited *time.Time) error
- func (l *Lease) ReleaseSettled(ctx context.Context, appID string, visited time.Time, extra time.Duration) error
- type LimitFacts
- type NewExternal
- type Notification
- type NotificationPreference
- type NotificationPreferences
- type Notifications
- func (n *Notifications) ListForUser(ctx context.Context, userID string, unreadOnly bool, before string, limit int) ([]Notification, error)
- func (n *Notifications) MarkAllRead(ctx context.Context, userID string) error
- func (n *Notifications) MarkRead(ctx context.Context, userID, notificationID string) error
- func (n *Notifications) Record(ctx context.Context, msg api.Notification, retainFor time.Duration) error
- func (n *Notifications) Unread(ctx context.Context, userID string) (int, error)
- type Nudged
- type OrphanedVolume
- type Page
- type Passwords
- type Policy
- type Ports
- type Profile
- type ProviderSettings
- type Reconcilable
- type Reconciles
- func (r *Reconciles) CatchUp(ctx context.Context, runtimeRef string) (int64, error)
- func (r *Reconciles) ClearFailures(ctx context.Context, appID string) error
- func (r *Reconciles) ClearUnobservable(ctx context.Context, appID string) error
- func (r *Reconciles) Cutoff(ctx context.Context, revisit time.Duration) (time.Time, error)
- func (r *Reconciles) Lease() *Lease
- func (r *Reconciles) MarkUnobservable(ctx context.Context, appID, reason string) error
- func (r *Reconciles) Nudge(ctx context.Context, appID string) (Nudged, error)
- func (r *Reconciles) RecordFailure(ctx context.Context, appID, reason string, window time.Duration, ...) (int, error)
- func (r *Reconciles) RecoverCurrentDeploy(ctx context.Context, appID string) error
- func (r *Reconciles) SetAppliedEnvFingerprint(ctx context.Context, appID, fingerprint string) error
- type Replaced
- type Replayed
- type Replica
- type Replicas
- func (r *Replicas) ByID(ctx context.Context, replicaID string) (Replica, bool, error)
- func (r *Replicas) Heartbeat(ctx context.Context, replicaID string) (bool, error)
- func (r *Replicas) Live(ctx context.Context) ([]Replica, error)
- func (r *Replicas) Prune(ctx context.Context, olderThan time.Duration) (int64, error)
- func (r *Replicas) Register(ctx context.Context, rep Replica) error
- func (r *Replicas) RequestRestart(ctx context.Context) error
- func (r *Replicas) RestartPending(ctx context.Context, replicaID string) (bool, error)
- func (r *Replicas) Stop(ctx context.Context, replicaID string) error
- func (r *Replicas) StopSilent(ctx context.Context) (int64, error)
- func (r *Replicas) TryLead(ctx context.Context, check time.Duration) (*Leadership, bool, error)
- func (r *Replicas) VerificationKeys(ctx context.Context, validity time.Duration) ([]Replica, error)
- type Retention
- func (r *Retention) DeletedApps(ctx context.Context, before time.Time, limit int) (int64, error)
- func (r *Retention) Deployments(ctx context.Context, keepPerApp, limit int) (int64, error)
- func (r *Retention) IdempotencyKeys(ctx context.Context, before time.Time, limit int) (int64, error)
- func (r *Retention) Notifications(ctx context.Context, now time.Time, limit int) (int64, error)
- func (r *Retention) SSOFlows(ctx context.Context, before, now time.Time, limit int) (int64, error)
- func (r *Retention) Scans(ctx context.Context, before time.Time, limit int) (int64, error)
- func (r *Retention) Sessions(ctx context.Context, before time.Time, limit int) (int64, error)
- type Revision
- type RoleRow
- type Roles
- type SCIMGroup
- type SCIMGroups
- func (s *SCIMGroups) ByID(ctx context.Context, adapterID, groupID string, members bool) (SCIMGroup, bool, error)
- func (s *SCIMGroups) ChangeMembers(ctx context.Context, adapterID, groupID string, add, remove []string) error
- func (s *SCIMGroups) Create(ctx context.Context, adapterID, displayName, externalID string, ...) (string, error)
- func (s *SCIMGroups) List(ctx context.Context, adapterID, attr, value string, offset, limit int, ...) ([]SCIMGroup, int, error)
- func (s *SCIMGroups) Update(ctx context.Context, adapterID, groupID string, displayName *string, ...) error
- func (s *SCIMGroups) UserGroups(ctx context.Context, adapterID, userID string) ([]SCIMMember, error)
- type SCIMMember
- type SCIMUser
- type SCIMUserUpdate
- type SCIMUsers
- func (s *SCIMUsers) ByID(ctx context.Context, adapterID, userID string) (SCIMUser, bool, error)
- func (s *SCIMUsers) ByKey(ctx context.Context, adapterID, key string) (SCIMUser, bool, error)
- func (s *SCIMUsers) Forget(ctx context.Context, adapterID, userID string) error
- func (s *SCIMUsers) List(ctx context.Context, adapterID, attr, value string, offset, limit int) ([]SCIMUser, int, error)
- func (s *SCIMUsers) Reactivate(ctx context.Context, adapterID, userID string) (bool, error)
- func (s *SCIMUsers) Write(ctx context.Context, adapterID, userID string, w SCIMUserUpdate) error
- type SSOFlow
- type SSOFlows
- func (s *SSOFlows) Authenticated(ctx context.Context, flowID, userID, handoffHash string, ...) error
- func (s *SSOFlows) ByID(ctx context.Context, flowID string) (SSOFlow, bool, error)
- func (s *SSOFlows) Complete(ctx context.Context, handoffHash string) (SSOFlow, bool, error)
- func (s *SSOFlows) Create(ctx context.Context, f SSOFlow) error
- func (s *SSOFlows) CreateUnsolicited(ctx context.Context, f SSOFlow) error
- func (s *SSOFlows) Fail(ctx context.Context, flowID string, result json.RawMessage) error
- func (s *SSOFlows) TakeForCallback(ctx context.Context, flowID, adapterID string) (SSOFlow, bool, error)
- func (s *SSOFlows) UseOnce(ctx context.Context, adapterID, oneTimeID string, until time.Time) (bool, error)
- type Scan
- type Scans
- func (s *Scans) ClearInsecure(ctx context.Context, appID string) error
- func (s *Scans) ForSource(ctx context.Context, appID, source, image, scannerRef string) (Scan, bool, error)
- func (s *Scans) History(ctx context.Context, appID string, limit int) ([]Scan, error)
- func (s *Scans) Latest(ctx context.Context, appID, specID string) (Scan, bool, error)
- func (s *Scans) LiveSecurityState(ctx context.Context, t SecurityThreshold) ([]SecurityState, error)
- func (s *Scans) MarkInsecure(ctx context.Context, appID string, at time.Time) error
- func (s *Scans) Record(ctx context.Context, scan Scan) (Scan, error)
- func (s *Scans) Reuse(ctx context.Context, from Scan, specID string) (Scan, error)
- func (s *Scans) SecurityStateFor(ctx context.Context, appID string) (SecurityState, bool, error)
- func (s *Scans) SetStoppedForSecurity(ctx context.Context, appID string, stopped bool) error
- type SecretKey
- type Secrets
- func (s *Secrets) Delete(ctx context.Context, appID, key string) error
- func (s *Secrets) Get(ctx context.Context, appID, key string) (secret.Value, error)
- func (s *Secrets) Keys(ctx context.Context, appID string) ([]SecretKey, error)
- func (s *Secrets) Put(ctx context.Context, appID, key string, v secret.Value) error
- func (s *Secrets) Resolve(ctx context.Context, appID string) (map[secretKey]secret.Value, error)
- func (s *Secrets) VerifyKey(ctx context.Context) error
- func (s *Secrets) Versions(ctx context.Context, appID string) (map[string]int, error)
- type Section
- type SecurityState
- type SecurityThreshold
- type ServiceInstance
- type Services
- func (s *Services) ForApp(ctx context.Context, appID string) ([]ServiceInstance, error)
- func (s *Services) Get(ctx context.Context, appID, slotKey string) (ServiceInstance, bool, error)
- func (s *Services) NewID() string
- func (s *Services) Record(ctx context.Context, v ServiceInstance) error
- func (s *Services) Release(ctx context.Context, id string) error
- type Session
- type Sessions
- func (s *Sessions) Active(ctx context.Context, sessionID string) (Session, bool, error)
- func (s *Sessions) ActiveWithUser(ctx context.Context, sessionID string) (Session, User, []string, bool, error)
- func (s *Sessions) Create(ctx context.Context, userID, adapterID string, lifetime time.Duration, ...) (Session, error)
- func (s *Sessions) Revoke(ctx context.Context, sessionID string) error
- func (s *Sessions) RevokeAllForUser(ctx context.Context, userID string) error
- func (s *Sessions) RevokeOthersForUser(ctx context.Context, userID, keepSessionID string) error
- type Subscription
- type SubscriptionFilter
- type SubscriptionPatch
- type SubscriptionSecrets
- func (k *SubscriptionSecrets) All(ctx context.Context, subscriptionID string) (map[string]secret.Value, error)
- func (k *SubscriptionSecrets) DeleteHeaders(ctx context.Context, subscriptionID string) error
- func (k *SubscriptionSecrets) Put(ctx context.Context, subscriptionID, field string, v secret.Value) error
- type Subscriptions
- func (s *Subscriptions) Create(ctx context.Context, sub Subscription) (Subscription, error)
- func (s *Subscriptions) Delete(ctx context.Context, subscriptionID string) error
- func (s *Subscriptions) Disable(ctx context.Context, subscriptionID, reason string) (bool, error)
- func (s *Subscriptions) Enabled(ctx context.Context) ([]Subscription, time.Time, error)
- func (s *Subscriptions) Failed(ctx context.Context, subscriptionID string, at time.Time) (time.Time, int, error)
- func (s *Subscriptions) Get(ctx context.Context, subscriptionID string) (Subscription, bool, error)
- func (s *Subscriptions) List(ctx context.Context, f SubscriptionFilter) ([]Subscription, error)
- func (s *Subscriptions) ListPage(ctx context.Context, f SubscriptionFilter, page Page) ([]Subscription, string, error)
- func (s *Subscriptions) Succeeded(ctx context.Context, subscriptionID string) error
- func (s *Subscriptions) Update(ctx context.Context, subscriptionID string, p SubscriptionPatch) (Subscription, error)
- func (s *Subscriptions) Watch(ctx context.Context, changed func(), connected func(bool))
- type TeardownTarget
- type Token
- type Tokens
- func (t *Tokens) Active(ctx context.Context, tokenID string) (Token, bool, error)
- func (t *Tokens) Authenticate(ctx context.Context, presented secret.Value) (Token, error)
- func (t *Tokens) Create(ctx context.Context, kind, name, ownerUserID, createdBy string, ...) (Issued, error)
- func (t *Tokens) ListAccounts(ctx context.Context) ([]Token, error)
- func (t *Tokens) ListForUser(ctx context.Context, userID string) ([]Token, error)
- func (t *Tokens) Owner(ctx context.Context, tokenID string) (string, bool, error)
- func (t *Tokens) Revoke(ctx context.Context, tokenID string) error
- func (t *Tokens) VerifyKey(ctx context.Context) error
- type User
- type UserAppGrant
- type Users
- func (u *Users) ByExternalID(ctx context.Context, adapterID, externalID string) (User, bool, error)
- func (u *Users) ByID(ctx context.Context, userID string) (User, bool, error)
- func (u *Users) ByUsername(ctx context.Context, username string) (local.Record, bool, error)
- func (u *Users) ClaimFirst(ctx context.Context, setupToken secret.Value, ...) (User, string, error)
- func (u *Users) ClearSetupToken(ctx context.Context) error
- func (u *Users) Create(ctx context.Context, ...) (User, error)
- func (u *Users) Delete(ctx context.Context, userID string) error
- func (u *Users) EnsureLocalAdapter(ctx context.Context) error
- func (u *Users) EnsureSetupToken(ctx context.Context) (string, time.Time, error)
- func (u *Users) List(ctx context.Context) ([]User, error)
- func (u *Users) ListPage(ctx context.Context, page Page) ([]User, string, int, error)
- func (u *Users) NeedsSetup(ctx context.Context) (bool, error)
- func (u *Users) ReplaceSetupToken(ctx context.Context) (string, error)
- func (u *Users) ResetPassword(ctx context.Context, username, passwordHash string) (string, error)
- func (u *Users) Search(ctx context.Context, q string, limit int) ([]User, error)
- func (u *Users) SetPassword(ctx context.Context, userID, passwordHash string) error
- func (u *Users) SetPasswordFor(ctx context.Context, userID, passwordHash string, mustChange bool) (bool, error)
- func (u *Users) SetStatus(ctx context.Context, userID, status string) error
- func (u *Users) SetStatusBy(ctx context.Context, userID, status, by string) error
- func (u *Users) UpdateProfile(ctx context.Context, userID string, p Profile) error
- type VolumeRecord
- type Volumes
- func (v *Volumes) Create(ctx context.Context, appID, name, adapterRef string) (string, error)
- func (v *Volumes) DeleteForApp(ctx context.Context, appID string) error
- func (v *Volumes) Handles(ctx context.Context, appID string) (map[string]string, error)
- func (v *Volumes) RecordFromRuntime(ctx context.Context, appID, adapterRef string, observed []VolumeRecord) error
- type WorkloadImage
Constants ¶
const ( StateDraft = "draft" StateProposed = "proposed" StateDeploying = "deploying" StateRunning = "running" StateDegraded = "degraded" StateStopped = "stopped" StateFailed = "failed" StateArchived = "archived" )
App lifecycle states (design 05 §1.1).
const ( AttemptTaken = "taken" AttemptSkipped = "skipped" AttemptFailed = "failed" )
Backup attempt outcomes.
const ( DeployPending = "pending" DeployBuilding = "building" DeployApplying = "applying" DeploySucceeded = "succeeded" DeployFailed = "failed" DeploySuperseded = "superseded" // Deploy approval (R-154 – R-159). A deploy that needs approval is a // deployment that waits in awaiting_approval, tied to one spec revision // (R-156), and moves to pending — the ordinary path — once it has its // approvals. Rejected and expired are where a request that never ran // ends. None of the three is in flight: a request waiting for somebody // does not stop the app being deployed some other way. DeployAwaitingApproval = "awaiting_approval" DeployRejected = "rejected" DeployExpired = "expired" )
Deployment statuses.
const ( DecisionApprove = "approve" DecisionReject = "reject" )
Decisions on a deploy that needs approval.
const ( TriggerManual = "manual" TriggerRollback = "rollback" )
Deployment triggers.
const ( DetectionRunning = "running" DetectionReady = "ready" DetectionNeedsAnswers = "needs_answers" DetectionUnknown = "unknown" DetectionBlocked = "blocked" DetectionFailed = "failed" )
Detection statuses. The first is Pando's; the rest come from the auction.
const ( DestinationWebhook = "webhook" DestinationNotify = "notify" )
Subscription destinations.
const ( DeliveryPending = "pending" DeliverySucceeded = "succeeded" DeliveryFailed = "failed" )
Delivery statuses.
const ( DefaultPageSize = 100 MaxPageSize = 500 )
Page sizes. The default is a screenful; the maximum bounds what one request may read, whatever it asks for.
const ( HeartbeatInterval = 10 * time.Second ReplicaStale = 45 * time.Second )
HeartbeatInterval is how often a replica says it is alive, and ReplicaStale how long it may go without saying so before it is taken to have stopped.
Stale is several heartbeats, so one slow query is not a death; and short enough that a deploy left by a lost pod is recorded as interrupted within a minute rather than blocking its app's next deploy.
const ( FlowSignIn = "sign_in" FlowTest = "test" )
The two things a flow is for.
const ( TokenDelegated = "delegated" TokenAccount = "account" )
Token kinds (R-058, R-060).
const AppRole = "pando_app"
AppRole is the Postgres role Pando serves traffic as.
It is deliberately not the role that owns the schema, and the reason is more specific than it first appears. REVOKE does work against a table's owner — after REVOKE UPDATE, has_table_privilege reports false even for the owner. But an owner holds grant option implicitly, so it can hand the privilege straight back to itself:
GRANT UPDATE ON audit_events TO pando_app; -- succeeds, run as pando_app UPDATE audit_events SET action = 'something else';
Against a role that owns the table, the REVOKE is therefore a speed bump and not a boundary: one statement undoes it, and that statement is available to exactly the process an attacker would be running inside. Ownership is the property that has to be denied, not the privilege.
So the owner creates and migrates; this role reads and writes, owns nothing, and cannot grant itself anything (R-027).
const ArchiverRole = "pando_audit_archiver"
ArchiverRole is the Postgres role audit retention runs as (R-348).
Retention removes months of audit events, which the application role must never be able to do (R-027). So it is a third role: it owns nothing, reads the log, records archives, and may call the two functions that make and drop a month's partition. Those functions run as the owner and refuse, in the database, a month younger than the floor or one without an archive of every row — so this role cannot remove recent history either, and nothing else can remove any.
const FirstAccountLock int64 = 46046
FirstAccountLock serializes making the install's first account, whether a person submits the setup form (Users.ClaimFirst) or a starting replica makes it from PANDO_ADMIN_PASSWORD (bootstrap.Run). Session and transaction advisory locks on one key exclude each other, so both paths share it.
const LocalAdapterID = "idp_local"
LocalAdapterID is the identity adapter seeded on a fresh install (R-041).
const MaxAttempts = 3
MaxAttempts is how many times a deploy or a detection is started before a replica stopping under it is taken as the reason, rather than bad luck [P]. A build that takes its replica down with it (out of memory, say) would otherwise move from replica to replica until every one had fallen over.
const SigningKeyField = "signing_key"
SigningKeyField is the field a webhook's signing key is kept under.
const SnapshotSuffix = "_pre_upgrade"
SnapshotSuffix names the copy beside the database it copies.
const SubscriptionsChannel = "pando_subscriptions"
SubscriptionsChannel is the channel a change to what routing reads about a subscription is announced on, by trigger (migration 61): one made, removed, turned on or off, or given other events or another app.
const SuspendedByAdmin = "admin"
Who suspended an account (users.suspended_by).
const TotalCap = 10000
TotalCap is how far a list's total is counted exactly (O-53). Counting every match costs a read of every match, which at 100,000 accounts is the whole table on every page; a count that stops past the cap costs at most the cap. A list holding more reports TotalCap+1, which the API sends as TotalCap with `total_is_lower_bound` ("10,000+").
const UnlockLifetime = 24 * time.Hour
UnlockLifetime is how long an entered passcode lets a browser in before it is asked again. [P] A day: long enough that nobody re-enters it through a working session, short enough that a borrowed laptop stops working.
Variables ¶
var ErrAlreadySetUp = errs.New(errs.ValidInvalid, "This installation is already set up.").
WithRemedy("Sign in with an existing account. An administrator can create one for you.")
ErrAlreadySetUp is ClaimFirst's refusal once an installation has an account.
var ErrBadCursor = errs.New(errs.ValidInvalid,
"The cursor must be the next_cursor value from a previous page of the same list.").
WithRemedy("Leave the cursor out to start from the first page.")
ErrBadCursor refuses a cursor that did not come from a previous page.
var ErrIdentityTaken = errs.New(errs.ValidInvalid,
"That identity already signs in to a different account.").
WithRemedy("To move it here, link it again with replace_account set. The other account is kept, " +
"suspended, as an alias of this one — never deleted, because apps may hold data under its ID.")
ErrIdentityTaken is Link's refusal when the identity already reaches a different account and the caller did not ask to move it.
var ErrSCIMConflict = errs.New(errs.ValidInvalid, "A resource with that name already exists for this provider.")
ErrSCIMConflict is a SCIM uniqueness conflict (RFC 7644 §3.3).
var ErrSetupTokenWrong = errs.New(errs.AuthInvalid, "That setup token is not the one this installation is waiting for.").
WithRemedy("Pando printed the setup token to its log when it first started, as setup_token on the line " +
"\"this installation is not set up yet\". With Docker Compose: docker compose logs pando | grep setup_token. " +
"If the log no longer has it, run pando admin setup-token where Pando runs for a new one " +
"(with Docker Compose: docker compose exec pando pando admin setup-token).")
ErrSetupTokenWrong is ClaimFirst's refusal of a missing or mistaken token.
Functions ¶
func CanCopyDatabase ¶
CanCopyDatabase reports whether the account in ownerURL may copy Pando's database. The bundled Compose file's account is a superuser; an external database's may not be (design 00 §1.1).
func ConnectArchiver ¶
func ConnectArchiver(ctx context.Context, ownerURL string, password secret.Value) (*pgxpool.Pool, error)
ConnectArchiver opens a small pool held as ArchiverRole (R-348).
Two connections: retention is one loop doing one month at a time.
func DefaultSCIMIdentityAttribute ¶
DefaultSCIMIdentityAttribute is which SCIM attribute a provider of this kind signs people in with, when the administrator has not said.
func DropSnapshot ¶
DropSnapshot removes the copy, once the new version has been healthy long enough (R-359). Removing one that does not exist is not an error.
func HeaderField ¶
HeaderField is the field a custom header's value is kept under.
func Regrant ¶
Regrant applies the grant policy again and verifies it, as a start does.
For after a DR restore (R-212): pg_restore recreates every table, partition and function, and a recreated table arrives with the owner's default privileges — which hand the application role UPDATE and DELETE — until the next start re-applies the policy. This closes that window instead.
func RestoreSnapshot ¶
RestoreSnapshot replaces Pando's database with the copy and removes the copy. Anything still connected to the migrated database is disconnected.
func SignInAddrKey ¶
SignInAddrKey is the key wrong passwords from a client address are counted under (R-404).
func SignInUsernameKey ¶
SignInUsernameKey is the key wrong passwords for username are counted under (R-404): as typed, whether or not the account exists.
func Snapshot ¶
Snapshot copies Pando's database to SnapshotName, replacing an older copy. Pando must be stopped: a database cannot be copied while anything is connected to it, so this retries briefly while the last connections close.
func SnapshotName ¶
SnapshotName is the copy's name for the database ownerURL names.
func SuspendedBySCIM ¶
SuspendedBySCIM is the suspended_by value for a SCIM client acting for a provider.
Types ¶
type AIAssignment ¶
type AIAssignment struct {
Function string `json:"function"`
AdapterID string `json:"adapter_id"`
Model string `json:"model,omitempty"`
UpdatedBy string `json:"updated_by"`
UpdatedAt time.Time `json:"updated_at"`
}
AIAssignment is one stored assignment of an AI function to an adapter (R-259).
type AIAssignments ¶
type AIAssignments struct {
// contains filtered or unexported fields
}
AIAssignments reads and writes ai_assignments.
func NewAIAssignments ¶
func NewAIAssignments(db *DB) *AIAssignments
func (*AIAssignments) Delete ¶
func (s *AIAssignments) Delete(ctx context.Context, function string) error
Delete removes a function's assignment, turning the function off. Removing one that is not assigned is not an error: the function is off either way.
func (*AIAssignments) Get ¶
func (s *AIAssignments) Get(ctx context.Context, function string) (AIAssignment, bool, error)
Get returns one function's stored assignment.
func (*AIAssignments) List ¶
func (s *AIAssignments) List(ctx context.Context) ([]AIAssignment, error)
List returns every stored assignment, by function.
func (*AIAssignments) Put ¶
func (s *AIAssignments) Put(ctx context.Context, a AIAssignment) error
Put assigns a function to an adapter, or changes the model of an existing assignment to the same adapter.
A function another adapter already handles is refused, and the refusal is the database's: the primary key on function admits one row, and the conflict clause updates that row only when it names the same adapter. No row back means a different adapter holds it, which is ErrAssigned — never a silent move from one adapter to another.
type AcmeAccount ¶
type AcmeAccount struct {
Email string
Directory string
KeyPEM secret.Value
Registration json.RawMessage
}
AcmeAccount is the ACME account the edge's certificates are ordered with.
type Activity ¶
type Activity struct {
// contains filtered or unexported fields
}
Activity is the store half of idle apps.
func NewActivity ¶
func (*Activity) IdleCandidates ¶
func (a *Activity) IdleCandidates(ctx context.Context, d IdleDefaults, now time.Time) ([]IdleApp, error)
IdleCandidates lists the apps the idle pass could act on now: those idle long enough to be owed a notice for something in force, and those holding a notice, which may be stale. Every other app would be read and left alone.
func (*Activity) IdleStatus ¶
IdleStatus reads an app's idle status.
func (*Activity) RecordActivity ¶
RecordActivity writes when each app was last used, one statement for the batch. GREATEST, because two replicas flush independently and an older flush landing second must not move an app's clock backwards.
func (*Activity) SetIdleNotice ¶
func (a *Activity) SetIdleNotice(ctx context.Context, appID string, which IdleNotice, at *time.Time) error
SetIdleNotice records when the owner was told, or clears it with nil.
func (*Activity) SetIdleSettings ¶
SetIdleSettings replaces an app's own settings. Any notice already given is withdrawn, because it was given under the old settings; the next pass gives a new one if the new settings call for it.
func (*Activity) StopForIdle ¶
StopForIdle stops an app and records why, in one write: the proxy cache is emptied by the desired state changing, and the page a visitor sees must say why in the same moment (R-396). Only an app that is meant to be running, so an app its owner stopped since the pass read it is left as they left it.
type AdapterConfig ¶
type AdapterConfig struct {
ID string `json:"id"`
Category string `json:"category"`
Kind string `json:"kind"`
Name string `json:"name"`
Config json.RawMessage `json:"-"`
IsDefault bool `json:"is_default"`
Enabled bool `json:"enabled"`
// UpdatedAt is when the configuration last changed. Adapters are loaded
// at startup (R-253), so one changed after Pando started is not what is
// running.
UpdatedAt time.Time `json:"-"`
}
AdapterConfig is a configured adapter instance.
type AdapterCredentials ¶
type AdapterCredentials struct {
// contains filtered or unexported fields
}
AdapterCredentials stores the credentials an adapter needs, encrypted (R-190, R-194; O-20).
The same arrangement as Secrets, one scope up: the row holds ciphertext or an external reference that the install's secrets adapter produced, and there is no plaintext column. Core never encrypts. adapter_configs.config stays what it always was — non-secret settings — and the database refuses a `credentials` key in it, so this is the only place a credential can be kept.
func NewAdapterCredentials ¶
func NewAdapterCredentials(db *DB, adapter api.SecretsAdapter, adapterRef string) *AdapterCredentials
func NewIdentityCredentials ¶
func NewIdentityCredentials(db *DB, adapter api.SecretsAdapter, adapterRef string) *AdapterCredentials
NewIdentityCredentials stores identity providers' secrets — an OIDC client secret — the same way, in their own table (identity_adapters is not adapter_configs). A different scope in the sealed reference as well, so a provider's ciphertext cannot be replayed as an adapter's even if two IDs matched.
func NewRegistryCredentials ¶
func NewRegistryCredentials(db *DB, adapter api.SecretsAdapter, adapterRef string) *AdapterCredentials
NewRegistryCredentials stores the credential an app's image is pulled with (issue #41), keyed by the app. Its own table and scope rather than the app's secrets: an app secret can be named by an env entry and so reach the app, and the credential that pulls the app's image is never the app's to read. App-owned (O-30). A source credential is not: since issue #127 a source connection is the install's (O-3), stored in adapter_credentials.
func NewSourceAuthorizations ¶
func NewSourceAuthorizations(db *DB, adapter api.SecretsAdapter, adapterRef string) *AdapterCredentials
NewSourceAuthorizations stores an OAuth authorization of a source connection while it is in progress (issue #127): the device code or PKCE verifier, and the state a web callback presents. Sealed like a credential and under its own scope, so a pending flow cannot be replayed as the connection's token.
func (*AdapterCredentials) Delete ¶
func (c *AdapterCredentials) Delete(ctx context.Context, adapterID, field string) error
Delete removes one credential.
func (*AdapterCredentials) Fields ¶
Fields names the credentials each adapter has, never their values. It is what GET /adapters reports, so the console can say "an API key is set" without anything being able to read it back.
func (*AdapterCredentials) Put ¶
func (c *AdapterCredentials) Put(ctx context.Context, adapterID, field string, v secret.Value) error
Put stores or rotates one credential. An empty value removes it.
func (*AdapterCredentials) Resolve ¶
func (c *AdapterCredentials) Resolve(ctx context.Context, adapterID string) (map[string]secret.Value, error)
Resolve decrypts one adapter's credentials, for Configure at startup.
The only place they leave storage. The values stay secret.Value until the adapter reads them, so none of them can be logged on the way.
type Adapters ¶
type Adapters struct {
// contains filtered or unexported fields
}
Adapters reads configured adapter instances.
func NewAdapters ¶
func (*Adapters) DeleteInCategory ¶
DeleteInCategory removes an adapter configuration of one category, with its credentials (ON DELETE CASCADE). The category is part of the condition so a route for one kind of adapter cannot remove another kind's — disconnecting a source cannot remove the runtime.
func (*Adapters) List ¶
func (a *Adapters) List(ctx context.Context) ([]AdapterConfig, error)
List returns configured adapters.
Config is loaded but never serialized: an adapter's configuration can hold credentials, and GET /adapters is a list of what exists, not a dump of how it authenticates.
func (*Adapters) Save ¶ added in v0.6.0
Save stores an adapter configuration, and returns the adapter it replaced as its category's default, if any.
Saving one as the default is a handover (issue #220): the previous default is cleared and the new one set in one transaction, so there is never a moment with two defaults or none. The category's default is locked for the transaction, so two saves racing to be the default hand over one after the other rather than both clearing the same row.
type Allocations ¶
type Allocations struct {
// contains filtered or unexported fields
}
Allocations reports what apps already hold on a runtime adapter (R-242).
func NewAllocations ¶
func NewAllocations(db *DB) *Allocations
func (*Allocations) AllocatedOn ¶
func (a *Allocations) AllocatedOn(ctx context.Context, adapterRef, excludeAppID string) (planner.Allocation, error)
AllocatedOn sums the resources of apps pinned to a runtime adapter.
Excludes the app being planned: replanning must not count an app's own current allocation against itself, which would make any app that already fits look like it no longer does.
Only apps that are actually running or deploying count. A stopped app holds no CPU or memory, and counting it would refuse deploys to make room for something that is not there.
Read live on every call, never from a cache: this is the plan-time check of R-242, and a stale sum is a deploy let through onto a full host. What each app reserves is copied from its pinned revision onto the app by a trigger when the pin moves (migration 60), so the sum is an index-only scan of apps_allocation_idx rather than a JSON walk of every running app's revision (issue #72). Which apps count is still decided here, from state.
A deploy in flight counts too, from the moment it is created: what its revision asks for is reserved on its runtime until it ends (migration 62), so a deploy not yet pinned is not room for another. An app counts once, at the larger of what its pin holds and what its deploy reserves, resource by resource: a redeploy of a running app is not two apps.
func (*Allocations) Hold ¶
func (a *Allocations) Hold(ctx context.Context, runtimeRef string, fn func(context.Context) error) error
Hold runs fn with the runtime's capacity to itself (R-242): no other Hold on the same runtime, on any replica, runs until fn returns. A deploy's plan check and its creation run inside one, so the second of two deploys racing for the last room is planned against the first one's reservation rather than beside it.
A transaction-scoped advisory lock, released when the transaction ends and with it if the connection is lost, so a lock cannot outlive the call that took it. On a connection of its own, outside the pool: fn does its work on pooled connections, and callers waiting for the lock while each holding a pooled one could leave the holder none to finish with.
type AnonymousGrant ¶
AnonymousGrant is an app's grant to everyone, with its passcode digest if it has one.
type App ¶
type App struct {
ID string `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
OwnerUserID string `json:"owner_user_id"`
State string `json:"state"`
DesiredState string `json:"desired_state"`
PinnedSpecID string `json:"pinned_spec_id,omitempty"`
// StoppedForIdle says Pando stopped this app because nobody had used it
// (R-396), so a stopped app is not taken for a failed one, or for one its
// owner stopped. Starting it clears this (migration 69).
StoppedForIdle bool `json:"stopped_for_idle,omitempty"`
// StoppedForDisk says Pando stopped this app because it stayed over its
// disk limit (R-403). Starting it clears this (migration 71).
StoppedForDisk bool `json:"stopped_for_disk,omitempty"`
// Source is where the app comes from, recorded at creation so detection has
// something to clone before any spec exists to carry it.
Source spec.Source `json:"source"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt *time.Time `json:"deleted_at,omitempty"`
// Routing is the pinned spec's routing block, so a caller can say where
// the app is without fetching a whole spec revision to find out.
//
// Zero until an app has a pinned spec: before that it has no address at
// all, which is a different thing from having one nobody can reach.
Routing spec.Routing `json:"routing,omitzero"`
// Address is where to open this app, rendered for the request that asked.
// Filled in by the API layer, which is the only place that knows what host
// the caller reached Pando on. See spec.Address.
Address string `json:"address,omitempty"`
// SecurityScore is the newest score of the revision this app is running
// (R-310). Nil for an app that has never been scanned, which is not a score
// of zero (R-318) — the list shows the difference.
//
// Which of the two stored numbers this is — every finding, or only the ones
// with a fix — is host policy's choice, applied by the API layer.
SecurityScore *int `json:"security_score,omitempty"`
// SecurityScoreFixable is the same scan counting only fixable findings, so
// the API layer can answer either question without a second query.
SecurityScoreFixable *int `json:"-"`
// SecurityVerdict is that score placed against host policy: ok, insecure,
// unscanned or inert. Filled in by the API layer, because it depends on a
// policy document and on whether a scanner is configured, and neither is
// the store's to know.
SecurityVerdict string `json:"security_verdict,omitempty"`
// SecurityScanning says a scan of this app is running now, whoever started
// it. Filled in by the API layer from the security service, which is the
// one place that knows (R-310, R-261).
SecurityScanning bool `json:"security_scanning,omitempty"`
// SecurityAwaitingImage says the app's only scan read its source alone,
// before there was a revision: detection's, of a repository Pando builds.
// That number is the source's and not the app's (R-311), so
// SecurityScore is empty until a scan reads the image the app runs. The
// source scan's score and findings are at GET /apps/{id}/security.
SecurityAwaitingImage bool `json:"security_awaiting_image,omitempty"`
// IconUpdatedAt is when the app's tile image last changed, and nil when it
// has none (R-340). The time rather than a flag so a client can put it in
// the image's URL and a browser never shows yesterday's picture from cache.
// The bytes are at GET /apps/{id}/icon; no list carries them.
IconUpdatedAt *time.Time `json:"icon_updated_at,omitempty"`
// Favorite is whether the caller has marked this app as a favorite
// (R-341). Only GET /me/apps fills it in: it is a fact about the person
// asking, and no other list is answering a question about them.
Favorite bool `json:"favorite,omitempty"`
// SectionID is the caller's launcher section this app is filed under, and
// empty when it is under "Your apps" (R-342). Like Favorite, only GET
// /me/apps fills it in.
SectionID string `json:"section_id,omitempty"`
// CanManage, on the launcher list (GET /me/apps), is whether the caller can
// also administer the app: whether GET /apps lists it for them. The
// control plane's answer beside the data plane's, so the launcher offers
// "Manage" without asking about each tile (R-070, R-071).
CanManage bool `json:"can_manage,omitempty"`
// Detection is where the app's detection has got to — running and at
// which stage, or how it finished — so a draft says why it is still a
// draft (issue #80). Filled in by the API layer on GET /apps and GET
// /apps/{id}; nil for an app that has never been through detection.
Detection *DetectionSummary `json:"detection,omitempty"`
}
App is a deployable unit.
type AppLimitReached ¶
AppLimitReached is CreateWithin's refusal: the owner already owns Limit apps or more (R-244).
func (*AppLimitReached) Error ¶
func (e *AppLimitReached) Error() string
type AppLimits ¶
type AppLimits struct {
// contains filtered or unexported fields
}
AppLimits reads and writes users' and groups' own limits.
func NewAppLimits ¶
func (*AppLimits) GroupMaxApps ¶
GroupMaxApps reads a group's own limit, nil when not set.
func (*AppLimits) SetGroupMaxApps ¶
SetGroupMaxApps sets a group's own limit, or clears it with nil.
type AppWithStorage ¶
type AppWithStorage struct {
AppID string
Spec []byte
// Retain is how many daily copies to keep (R-211).
Retain int
}
AppWithStorage is an app that has data worth backing up (R-210).
type ApprovalDecision ¶
type ApprovalDecision struct {
PrincipalID string `json:"principal_id"`
PrincipalName string `json:"principal_name"`
Decision string `json:"decision"`
Comment string `json:"comment,omitempty"`
DecidedAt time.Time `json:"decided_at"`
}
ApprovalDecision is one person's answer to a deploy that needs approval.
type ApprovalReason ¶
ApprovalReason is why a deploy needed approval. The store holds the reason; the approval service, which owns the wording, fills in Message.
type Apps ¶
type Apps struct {
// ReservedHostnames lists the hostnames no app may be reached at, because
// Pando answers on them itself: its external URL's host and a routing
// adapter's console hostname.
// An app given one would receive the console's traffic on the console's
// origin (2026-10-16 assessment, finding 2). Nil reserves none.
ReservedHostnames func(ctx context.Context) []string
// contains filtered or unexported fields
}
func (*Apps) AwaitingTeardown ¶
AwaitingTeardown returns archived apps whose bundle is still standing.
The leak this closes: nothing ever called Destroy, so a deleted app's containers and its private network stayed up. Docker's default pool holds about thirty networks and Pando takes one per app, so an install that adds and removes apps eventually cannot start one.
Returns the routing adapter ref too, because the route outlives the app the same way — a Traefik file per deleted app, accumulating.
An app deleted before anything was accepted has no revision, and may still have an image: detection builds what an app would deploy (R-311). Its runtime and builder come from the proposal that built it, so the image and the build cache go with the app rather than staying on the host (R-224).
func (*Apps) ByPath ¶
func (a *Apps) ByPath(ctx context.Context, requestPath string) (App, *spec.AppSpec, string, bool, error)
ByPath resolves the path-mode app whose path is the longest prefix of the request's path, on whole segments, and returns that path.
Case-sensitive, as a URL's path is: paths are stored lowercase, and the proxy strips exactly the prefix that matched.
The candidates are built here and matched with = ANY, so the lookup is the unique index on address_path rather than a scan comparing the request's path with every app's. An app's path matches when it is the request's path, or the request's path continues past it with a slash — which is exactly when it is one of pathCandidates.
func (*Apps) ByRouting ¶
ByRouting resolves a running app from how it is addressed.
Used by the proxy on every request, so it reads the pinned spec in the same query rather than making a second round trip per request, and matches on indexed columns of apps rather than on the pinned spec's JSON: the address columns are written with the pin (migrations 35 and 46), so they say what the pinned spec says.
`by` is "hostname", "slug", "id" or "port".
func (*Apps) CheckAddress ¶
CheckAddress reports whether another live app already holds the address r would claim for appID, saying which address and why. nil means it is free.
Asked before a change is saved, so the person choosing hears it then, and again inside Pin, which is where it is decided: the unique indexes refuse an exact duplicate there however two writers race, and this refuses the rest — a path inside or around another app's, or one that takes another app's slug.
func (*Apps) ClearIcon ¶
ClearIcon removes an app's tile image. Clearing one that is not there is not an error: the outcome asked for is the outcome that holds.
func (*Apps) Create ¶
func (a *Apps) Create(ctx context.Context, name, slug, ownerUserID, createdBy string, src spec.Source) (App, error)
Create inserts an app and its two grants in one transaction.
R-073: app creation writes two grant rows, one per plane, independently revocable. They are written here rather than by the handler so that an app cannot exist without them — a control grant without a data grant would leave the owner able to manage an app they cannot open.
func (*Apps) CreateRevision ¶
func (a *Apps) CreateRevision(ctx context.Context, appID string, s *spec.AppSpec, origin spec.Origin, createdBy string) (Revision, error)
CreateRevision appends a spec revision.
The revision number is assigned inside the transaction from the current maximum, so two concurrent edits cannot both claim the same number — the UNIQUE (app_id, revision) constraint is the backstop, and this is what keeps it from being hit in normal operation.
func (*Apps) CreateSection ¶
CreateSection makes a new section for a person.
func (*Apps) CreateWithin ¶
func (a *Apps) CreateWithin(ctx context.Context, name, slug, ownerUserID, createdBy string, src spec.Source, limit int) (App, error)
CreateWithin is Create, refused with *AppLimitReached when the owner already owns limit apps or more; 0 is no limit (R-244).
Counted in the transaction that inserts, under a lock on the owner's row, so two creates at once cannot both see room for one more.
func (*Apps) DeleteSection ¶
DeleteSection removes one of a person's sections. Its apps go back to "Your apps" by the placements' cascade. False when they have no such section.
func (*Apps) DiscardStorage ¶
DiscardStorage records that a delete settled what becomes of the app's storage — discarded, or backed up first — so the teardown destroys the volumes with the bundle rather than leaving them on disk (R-204, R-224).
func (*Apps) DueForBackup ¶
func (a *Apps) DueForBackup(ctx context.Context, takenSince, triedSince time.Time, limit int) ([]AppWithStorage, error)
DueForBackup lists up to limit apps with storage that are due a rolling backup (R-211): none taken since takenSince, and no attempt of any outcome since triedSince, so a destination that is down is tried again on the retry interval rather than on every pass. The app whose last attempt is oldest comes first, and one never attempted before any.
Due-driven rather than a walk over every app with storage (issue #72): the job asks for what is due and gets a page of it, however many apps there are.
func (*Apps) ForgetVolume ¶
ForgetVolume removes a volume row whose storage has been destroyed.
By app and ID: a volume ID is unique only within its app.
func (*Apps) Known ¶
Archive soft-deletes an app.
Soft delete because the audit log references it, and an audit trail pointing at a row that no longer exists answers fewer questions than one that does. Volumes are ON DELETE RESTRICT and must be resolved first (R-204) — the caller handles the keep-or-discard decision. Known reports whether this installation ever created the app, deleted ones included. It is how startup tells this install's app networks from another install's on the same Docker host (issue #55).
func (*Apps) ListAll ¶
ListAll returns every app, for a principal whose install role reaches every app with app.view (install.apps.view, R-081). The caller decides that; this only reads.
Unpaginated, for the readers that need every app at once (the access assistant); the API pages with ListAllPage.
func (*Apps) ListAllPage ¶
ListAllPage is one page of ListAll, newest first. Page.Query matches the name or slug.
func (*Apps) ListForPrincipal ¶
ListForPrincipal returns apps the principal can see on the control plane.
Scoped by grant rather than returning everything and filtering afterward: a list endpoint that leaks the existence of apps is a smaller problem than one that leaks their contents, but it is still a leak.
Unpaginated; the API pages with ListForPrincipalPage.
func (*Apps) ListForPrincipalPage ¶
func (a *Apps) ListForPrincipalPage(ctx context.Context, p authz.Principal, page Page) ([]App, string, int, error)
ListForPrincipalPage is one page of ListForPrincipal, newest first, with the cursor for the next page and how many apps match in all. Page.Query matches the name or slug.
func (*Apps) ListForUse ¶
func (a *Apps) ListForUse(ctx context.Context, p authz.Principal, page Page, every bool) ([]App, string, error)
ListForUse returns one page of the apps the principal owns, or holds a DATA-plane grant on directly or through a group (R-264), with the cursor for the next page (empty after the last).
A grant to anonymous is not one of them. A public app can be opened by everyone, but being public alone never puts it on anyone's launcher: an app appears there because it was shared with you, not because it was shared with the world (R-264).
Deliberately a different query from ListForPrincipal. Two planes, two endpoints: the launcher shows what you can open, not what you can manage.
A UNION of indexed lookups — owned (apps_owner_user_idx), and a direct user or token grant and a group grant (grants_principal_idx) — rather than a join of every app's data grants under OR'd predicates, which read every grant in the install for each launcher load (issue #72).
Paged, because an app shared with a large group is on every member's launcher, and an install can hold 20,000 of them. Favorites come first, then apps filed in one of the person's sections, then the rest, each by name, so the first page holds what the person arranged. The page is chosen before anything else is read: routing, icons and CanManage are read for one page.
CanManage says whether the principal can also administer each app: what GET /apps would list for them (control-plane grants, or every app when every is set — install.apps.view, which the caller decides). Read here so the launcher need not ask GET /apps about each tile; it is the control plane's answer, reported beside the data plane's, never mixed into it (R-070).
Page.Query matches the name or slug.
func (*Apps) ListRevisions ¶
ListRevisions returns an app's revisions, newest first.
func (*Apps) Live ¶
Live reports whether an app still exists and has not been deleted. A question worth asking again just before acting on an app read a while ago.
func (*Apps) LiveApps ¶
func (a *Apps) LiveApps(ctx context.Context, f planner.InventoryFilter) ([]planner.InventoryApp, error)
LiveApps lists every app a candidate policy could block, for design 05 §3's preview.
Pinned revision, not newest: a preview answers "what would break", and what would break is what the app's next deploy would actually try to do. Draft and archived apps are excluded — a draft has never deployed and an archived one is not going to.
The anonymous grant comes along because a policy that forbids it (R-076) has no spec to detect it in, and an app that anyone on the internet can reach under a policy that says they may not is precisely the case an admin needs named before they save.
Only the apps f names (issue #72): the planner works out from the candidate policy which apps any of its checks could fail, and the rest are not read. Whether an app is shared with everyone comes from one pass over the anonymous grants rather than two lookups per app.
func (*Apps) MarkBundleDestroyed ¶
MarkBundleDestroyed records that the runtime confirmed the bundle is gone.
func (*Apps) OrphanedVolumes ¶
OrphanedVolumes lists storage that is safe to reclaim.
Narrow on purpose. A volume qualifies only when its app is deleted *and* a backup of that app exists — R-204 says volumes outlive the apps that mount them, and this does not weaken that. It stops the storage outliving the last thing that could ever want it, which is a different claim.
A delete made since apps.discard_storage exists removes the volume rows and records the decision on the app instead, and the teardown destroys the volumes with the bundle (Apps.DiscardStorage). This pass covers volumes whose rows a delete left behind before that.
func (*Apps) Pin ¶
Pin points the app at a revision.
Records the pinning as an event in the same transaction, because retention pruning must never remove a revision that was once live (R-152) and that fact has to survive the pointer moving on during a rollback. The event log doubles as the rollback history.
func (*Apps) PlaceApp ¶
PlaceApp puts an app into one of a person's sections, moving it out of any other. False when they have no such section.
func (*Apps) PruneSpecRevisions ¶
PruneSpecRevisions trims revision history to each app's retention setting.
R-152 keeps the last ten pinned specs so a rollback has something to roll back to. Two exemptions, and both matter more than the saving:
- **A revision that was ever pinned is never pruned.** Rollback is repointing at a revision that provably existed, and spec_pins is the append-only record of which those are. Pruning one would make a rollback target vanish from under a person who is looking at it.
- **The currently pinned revision is never pruned**, which the first rule already covers, but a deployment also references it by foreign key and would refuse.
- **A revision a scan describes is never pruned** (R-319). The scan is append-only and names its revision; the foreign key refuses the delete.
Batched (issue #72): a statement reads pruneAppsPerBatch apps, in ID order, and removes at most prunePerApp revisions from each, oldest first, so no statement's cost or lock grows with the install. The pass walks every app; an app with a longer backlog loses the rest on later passes.
Returns how many rows went.
func (*Apps) RenameSection ¶
func (a *Apps) RenameSection(ctx context.Context, userID, sectionID, name string) (Section, bool, error)
RenameSection renames one of a person's sections. False when they have no section with that ID.
func (*Apps) RevisionByID ¶
RevisionByID returns one revision.
func (*Apps) RevisionByNumber ¶
func (a *Apps) RevisionByNumber(ctx context.Context, appID string, number int) (Revision, bool, error)
RevisionByNumber returns an app's revision by its number.
func (*Apps) Sections ¶
Sections returns a person's sections, oldest first — the order they made them, which is the order they appear in.
func (*Apps) SetDesiredState ¶
SetDesiredState records what a human asked for.
Separate from state, which is what is true. The reconciler reads both: the gap between them is the work it has to do.
func (*Apps) SetFavorite ¶
SetFavorite marks or unmarks an app as one of a user's favorites (R-341). Both directions are idempotent: the outcome asked for is the one that holds.
func (*Apps) SetIcon ¶
SetIcon replaces an app's tile image. The caller has already checked the bytes are an image of an allowed type; the table's CHECK is the backstop.
func (*Apps) SetSource ¶
SetSource replaces an app's source.
For `pando deploy ./`, which turns an app into one fed by uploads. Recorded on the app rather than inferred at deploy time: R-020 makes the spec the sole record of how an app runs, and a source nobody wrote down is a deploy nobody can explain afterwards.
func (*Apps) SetStateIf ¶
SetStateIf moves an app to `to` only if it is in `from`, so a caller settling its own transition never overwrites one somebody else made since.
func (*Apps) UnplaceApp ¶
UnplaceApp takes an app out of a section, back to "Your apps". Taking out an app that is not in it is not an error.
func (*Apps) VolumeCount ¶
VolumeCount returns how many volumes an app holds, for the delete decision.
func (*Apps) WithAutoDeploy ¶
WithAutoDeploy returns apps that track a ref (R-141).
Off by default, so this is normally empty and the poll costs one indexed query (apps_auto_deploy_idx). Auto-deploy is a property of the spec someone reviewed and pinned: apps.auto_deploy is derived from the pinned revision by a trigger whenever the pin moves, and nothing else can set it (000056), so it cannot be changed without a revision.
func (*Apps) WithStorage ¶
func (a *Apps) WithStorage(ctx context.Context) ([]AppWithStorage, error)
WithStorage lists live apps that declare volumes, for rolling backups.
Only apps with storage. An app with none has nothing a rolling backup would hold that its spec revisions do not already, and taking one anyway would fill the destination with empty bundles nobody wants to page through.
The volumes test is jsonb_typeof and not a coalesce. `AppSpec.Volumes` is tagged without omitempty, so an app that declares no storage stores `"volumes": null` — and `->` on a key holding JSON null answers with that null, not SQL NULL, so the coalesce never fired and jsonb_array_length was handed a scalar. Postgres raises 22023, the reconciler logged "could not list apps for rolling backups", and no app on the installation was backed up (R-210).
A typeof guard beside it is not enough on its own: AND does not promise an evaluation order, and the planner is free to reach jsonb_array_length first — it did. So the emptiness test is a comparison, which is defined for every input this column can hold.
type AuditCursor ¶
AuditCursor is a position in the audit stream, as the state store keeps it: the stream's (txid, id) order (design 12 §2).
type AuditSinkState ¶
type AuditSinkState struct {
AdapterID string `json:"adapter_id"`
// HasCursor is false until the first delivery. CursorTxID and CursorID are
// the last event delivered, in the stream's (txid, id) order.
HasCursor bool `json:"-"`
CursorTxID uint64 `json:"-"`
CursorID int64 `json:"-"`
DeliveredAt *time.Time `json:"delivered_at,omitempty"`
CursorAt *time.Time `json:"cursor_at,omitempty"`
DeliveredCount int64 `json:"delivered_count"`
LastError string `json:"last_error,omitempty"`
LastErrorAt *time.Time `json:"last_error_at,omitempty"`
FailingSince *time.Time `json:"failing_since,omitempty"`
Attempts int `json:"attempts"`
NextAttemptAt *time.Time `json:"next_attempt_at,omitempty"`
DisabledAt *time.Time `json:"disabled_at,omitempty"`
DisabledReason string `json:"disabled_reason,omitempty"`
GapFrom *time.Time `json:"gap_from,omitempty"`
GapTo *time.Time `json:"gap_to,omitempty"`
}
AuditSinkState is where one audit sink has got to (design 12 §5.2).
type AuditSinks ¶
type AuditSinks struct {
// contains filtered or unexported fields
}
AuditSinks stores audit sink delivery state. It never touches audit_events except to read it (R-027).
func NewAuditSinks ¶
func NewAuditSinks(db *DB) *AuditSinks
func (*AuditSinks) Backlog ¶
func (s *AuditSinks) Backlog(ctx context.Context, st AuditSinkState, limit int) (int, error)
Backlog counts events past a cursor, at most limit: counting a large backlog exactly would cost what the backlog does.
func (*AuditSinks) Delivered ¶
func (s *AuditSinks) Delivered(ctx context.Context, adapterID string, txid uint64, id int64, n int, at time.Time) (bool, error)
Delivered moves a sink's cursor after a successful send and clears its error. It returns whether the sink had been failing, so the recovery is audited once (R-383).
func (*AuditSinks) Disable ¶
func (s *AuditSinks) Disable(ctx context.Context, adapterID, reason string, at time.Time) (bool, error)
Disable turns a failing sink off: its adapter row, so it stops holding archival and the adapters screen shows it off, and its state, so the reason is shown beside it (R-383, R-386). It reports whether this call changed anything, so the caller audits and notifies once. A sink declared in the configuration file has no row to turn off; its state alone records it.
func (*AuditSinks) Failed ¶
func (s *AuditSinks) Failed(ctx context.Context, adapterID, message string, at, next time.Time) (AuditSinkState, error)
Failed records a failed send and when to try again. It returns the state after, so the caller can tell a first failure from a repeated one.
func (*AuditSinks) Get ¶
func (s *AuditSinks) Get(ctx context.Context, adapterID string, start *AuditCursor) (AuditSinkState, error)
Get returns one sink's state, creating it when it has none: positioned at the start of the live log, or at start when given — a sink that starts now (R-382).
func (*AuditSinks) Holds ¶
func (s *AuditSinks) Holds(ctx context.Context, st AuditSinkState, lo, hi time.Time) (bool, error)
Holds reports whether a sink has events in [lo, hi) it has not been sent (R-386). A sink with no cursor yet starts at the oldest event, so it holds every month that has any.
func (*AuditSinks) List ¶
func (s *AuditSinks) List(ctx context.Context) ([]AuditSinkState, error)
List returns every sink's state.
func (*AuditSinks) Resume ¶
func (s *AuditSinks) Resume(ctx context.Context, adapterID string, at time.Time) (AuditSinkState, error)
Resume clears a sink Pando turned off and somebody turned back on, and records the range it missed (R-386): events after its cursor that the archiver removed while it did not hold. The archive covering them is the backfill.
type AuthzStore ¶
type AuthzStore struct {
// contains filtered or unexported fields
}
AuthzStore implements authz.Store against Postgres.
func NewAuthzStore ¶
func NewAuthzStore(db *DB) *AuthzStore
func (*AuthzStore) AnonymousAccess ¶
func (s *AuthzStore) AnonymousAccess(ctx context.Context, appID string) (granted, passcode bool, err error)
AnonymousAccess reports whether an app is shared with everyone, and whether that sharing asks for a passcode.
func (*AuthzStore) ControlGrantsFor ¶
func (s *AuthzStore) ControlGrantsFor(ctx context.Context, appID string, p authz.Principal) ([]authz.Grant, error)
ControlGrantsFor returns control-plane grants matching the principal.
Group membership is joined live (R-079) rather than read from the principal, so a grant made to a group the caller joined a moment ago is honored without waiting for a cache.
func (*AuthzStore) ControlGrantsForApps ¶
func (s *AuthzStore) ControlGrantsForApps(ctx context.Context, appIDs []string, p authz.Principal) (map[string][]authz.Grant, error)
ControlGrantsForApps is ControlGrantsFor for several apps in one query (authz.BatchStore), with an entry for every app asked about, so a list that shows what the caller may do on each row asks once rather than per row (issue #72).
func (*AuthzStore) DataFacts ¶
func (s *AuthzStore) DataFacts(ctx context.Context, appID string, p authz.Principal, passcodeToken string) (authz.DataFacts, error)
DataFacts reads every fact CheckData decides on, in one query: what IsOwner, HasDataGrant, AnonymousAccess and PasscodeUnlocked answer, by the same conditions. passcodeToken is the unlock this request brought for the app, or "" for none. It decides nothing; authz.CheckData does, in its fixed order, and R-029 holds there: nothing about the control plane is read here beyond the owner of record, R-072's one implication.
func (*AuthzStore) DeployApprovers ¶
DeployApprovers returns the active people who may approve a deploy of the app: holders of install.deploys.approve, and holders of app.deploy.approve on this app, directly or through a group (R-155). For telling them a request is waiting (R-159), not for deciding whether they may: that is the authorizer's, which also applies host policy.
func (*AuthzStore) GroupsForUser ¶
GroupsForUser resolves group membership live (R-079).
func (*AuthzStore) HasDataGrant ¶
func (s *AuthzStore) HasDataGrant(ctx context.Context, appID string, p authz.Principal) (bool, error)
HasDataGrant reports whether the principal may use the app.
func (*AuthzStore) InstallGrantsFor ¶
func (s *AuthzStore) InstallGrantsFor(ctx context.Context, p authz.Principal) ([]authz.Grant, error)
InstallGrantsFor returns the principal's installation-wide grants (O-17).
`app_id IS NULL` is the whole definition of install scope, and the schema guarantees such a row carries an install-scoped role: grants.role_scope is tied to roles.scope by a composite foreign key, and a CHECK ties role_scope to whether app_id is null. So this cannot return a grant carrying app verbs however the row was written.
Group membership counts, as it does per app. Token principals do too — an account token acts for its user (design 06 §2) — which means an install-wide administrator's token is administrative, and that is the point of R-262's "an agent holds what its user holds" rather than an exception to it.
func (*AuthzStore) InstallVerbHolders ¶
InstallVerbHolders returns the active people holding an install verb, directly or through a group. For telling them something — an update is available (R-362) — never for deciding what they may do: that is the authorizer's, which also applies host policy.
func (*AuthzStore) InstallVerbsFor ¶
InstallVerbsFor returns every installation-wide verb the principal holds.
For GET /me, so the console can decide whether to show the Admin entry and what to put in it (R-265) without guessing. The list is the server's answer, which is what keeps the console a client of the API rather than a second opinion about authorization (R-261).
func (*AuthzStore) PasscodeUnlocked ¶
PasscodeUnlocked reports whether token is a live unlock of the app. Live means unexpired and made under the app's current anonymous grant: the grant's own row is joined, so an unlock outlives neither the grant nor — since SetPasscode clears them — the passcode it was made with.
func (*AuthzStore) UserStatus ¶
UserStatus returns a user's status, or "deleted" if no such user.
A missing user resolving to "deleted" rather than an error matters for R-059: a delegated token whose owner was hard-removed must be orphaned, not produce a lookup failure that some caller might treat as transient and retry past.
type AutoDeployCheck ¶
type AutoDeployCheck struct {
AppID string `json:"app_id"`
CheckedAt time.Time `json:"checked_at"`
FoundRef string `json:"found_ref,omitempty"`
FoundCommit string `json:"found_commit,omitempty"`
// AttemptedCommit is tried once (O-58). Auto-deploy does not try it
// again, whatever came of it.
AttemptedCommit string `json:"attempted_commit,omitempty"`
AttemptedAt *time.Time `json:"attempted_at,omitempty"`
DeploymentID string `json:"deployment_id,omitempty"`
// Error is why the last check or attempt went nowhere, or empty.
Error string `json:"error,omitempty"`
}
AutoDeployCheck is the last time auto-deploy looked at an app (R-141): what the watched branch or release pointed at, and the last commit it tried.
type AutoDeployChecks ¶
type AutoDeployChecks struct {
// contains filtered or unexported fields
}
AutoDeployChecks stores one AutoDeployCheck per app.
func NewAutoDeployChecks ¶
func NewAutoDeployChecks(db *DB) *AutoDeployChecks
func (*AutoDeployChecks) Attempted ¶
func (c *AutoDeployChecks) Attempted(ctx context.Context, appID, ref, commit, deploymentID, message string) error
Attempted records that auto-deploy tried commit: the deployment it started, or why it could not start one.
func (*AutoDeployChecks) ByApp ¶
func (c *AutoDeployChecks) ByApp(ctx context.Context, appID string) (AutoDeployCheck, bool, error)
ByApp is the app's last check, and whether there has been one.
type AutoDeploySecrets ¶
type AutoDeploySecrets struct {
// contains filtered or unexported fields
}
AutoDeploySecrets stores each app's auto-deploy webhook secret, sealed by the secrets adapter (R-142, R-190, O-58): ciphertext or an external reference, never the value. The same arrangement as SubscriptionSecrets.
func NewAutoDeploySecrets ¶
func NewAutoDeploySecrets(db *DB, adapter api.SecretsAdapter, adapterRef string) *AutoDeploySecrets
func (*AutoDeploySecrets) Delete ¶
func (k *AutoDeploySecrets) Delete(ctx context.Context, appID string) error
Delete removes an app's webhook secret, which turns its webhook off.
type AwaitingApproval ¶
type AwaitingApproval struct {
Deployment
AppName string `json:"app_name"`
AppSlug string `json:"app_slug"`
}
AwaitingApproval is a deploy waiting for approval, with the app it is for: the list of everything waiting across apps (GET /approvals).
type AwaitingKey ¶
AwaitingKey is where a page of ListAwaitingAfter starts: after the request with this start time and ID. The zero key starts from the oldest.
func AwaitingKeyFrom ¶
func AwaitingKeyFrom(cursor string) (AwaitingKey, error)
AwaitingKeyFrom reads a cursor made by Cursor.
func (AwaitingKey) Cursor ¶
func (k AwaitingKey) Cursor() string
Cursor encodes the key for a next_cursor.
type Backup ¶
type Backup struct {
ID string `json:"id"`
AppID string `json:"app_id,omitempty"`
Kind string `json:"kind"`
AdapterRef string `json:"adapter_ref"`
ObjectName string `json:"object_name"`
SizeBytes int64 `json:"size_bytes"`
Manifest backup.Manifest `json:"manifest"`
// RetainUntil is nil when the backup is kept until explicitly discarded:
// always for on_delete (R-204), and for any kind whose destination owns
// retention itself (R-217).
RetainUntil *time.Time `json:"retain_until,omitempty"`
CreatedBy string `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
}
Backup is one stored backup.
type BackupAttempt ¶
type BackupAttempt struct {
AppID string `json:"app_id"`
// AppName is filled in when attempts are listed, for a screen that shows
// them. Not stored: an app's name can change.
AppName string `json:"app_name,omitempty"`
AttemptedAt time.Time `json:"attempted_at"`
Outcome string `json:"outcome"`
BackupID string `json:"backup_id,omitempty"`
Message string `json:"message,omitempty"`
Remedy string `json:"remedy,omitempty"`
}
BackupAttempt is the last scheduled rolling backup of one app and what came of it (issue #87).
A backup that did not happen used to leave nothing behind but a server log line, so an app that had not been backed up in a week looked exactly like one that had. Outcome is "taken", "skipped" or "failed"; Message and Remedy say why and what to do about it, to the R-105 standard, and are empty when the backup was taken.
type Backups ¶
type Backups struct {
// contains filtered or unexported fields
}
Backups records what has been backed up and where (design 02 §2.8).
func NewBackups ¶
func (*Backups) Attempts ¶
Attempts returns the last scheduled backup attempt of each live app, or of one app when appID is set. Newest first, and at most limit of them (backup_attempts_recent_idx): one row per app is 20,000 rows on a large install, and the newest are the ones to read (issue #72).
func (*Backups) Expired ¶
Expired returns backups whose retention has passed (R-211).
Never returns an on_delete row: those have no retain_until at all, and the schema refuses to give them one. The WHERE clause says so anyway, because a pruning query that relies on a constraint elsewhere is one migration away from deleting the backups R-204 promises to keep.
func (*Backups) Forget ¶
Forget removes the record of a backup whose object has been deleted.
The object goes first, then this. The other order leaves an object nothing references, which is invisible and accumulates.
func (*Backups) List ¶
List returns one page of backups, newest first, keyset-paged on (created_at, id), with the cursor for the next page (empty after the last). An empty appID lists every app's, and the install's bundles.
A page rather than every row: each carries its manifest, and an install that takes a rolling backup of 20,000 apps a day holds a great many (issue #72). backups_list_idx and backups_app_idx serve the order.
func (*Backups) NewID ¶
NewID returns an ID for a backup about to be written.
Allocated before the bundle is built, because the ID is the object name at the destination — so a crash mid-write leaves an object nothing references rather than a row pointing at nothing. The first is garbage; the second is a restore that cannot find its bundle.
func (*Backups) Record ¶
Record stores a backup that has been fully written.
Called after the destination's Close returns, never before. A row written first would claim a bundle exists during the window where it does not, and that window is exactly when a disaster is most likely to interrupt.
func (*Backups) RecordAttempt ¶
func (b *Backups) RecordAttempt(ctx context.Context, a BackupAttempt) error
RecordAttempt replaces the app's last attempt with this one.
type BundleSource ¶
type BundleSource struct {
// contains filtered or unexported fields
}
BundleSource implements backup.StateSource against Postgres.
Separate from Backups: one records what was backed up, the other supplies what goes in a bundle. Merging them would put the thing being backed up and the record of the backup behind one type, and the record has to survive the thing.
func NewBundleSource ¶
func NewBundleSource(db *DB) *BundleSource
func (*BundleSource) AdapterConfigs ¶
func (b *BundleSource) AdapterConfigs(ctx context.Context) ([]byte, error)
AdapterConfigs exports adapter configuration as readable JSON.
Also in the pg_dump, and deliberately duplicated: this copy is for the operator rebuilding an install by hand, which is exactly the situation where the database is the thing that will not load.
func (*BundleSource) Counts ¶
Counts are the object counts the manifest records (R-215).
Checksums prove the bytes arrived. Counts prove the bytes describe the install the operator thinks they are restoring, which is the question somebody staring at a restore prompt actually has.
func (*BundleSource) HostPolicy ¶
func (b *BundleSource) HostPolicy(ctx context.Context) ([]byte, error)
HostPolicy exports the policy document.
func (*BundleSource) ServicesToSnapshot ¶
func (b *BundleSource) ServicesToSnapshot(ctx context.Context) ([]backup.ServiceRef, error)
ServicesToSnapshot lists every provisioned service (R-131, R-212).
Every one, not only those the backup will call Snapshot on. The manifest records both counts, and a bundle that says "5 services, 0 snapshotted" because they all live in app volumes is very different from a bundle that says it because the provisioner was unreachable — but only if the first number is counted here rather than inferred from the second.
func (*BundleSource) VolumesForApp ¶
VolumesForApp lists one app's volumes, for a per-app backup (R-204).
Includes volumes whose app is already archived: R-204 keeps a volume after its app is gone, and the final backup is taken at exactly that moment.
func (*BundleSource) VolumesToSnapshot ¶
VolumesToSnapshot lists live volumes with the runtime that holds each.
Volumes belonging to deleted apps are included: R-204 keeps a volume after its app is gone, and a DR bundle that dropped exactly the data somebody is still deciding whether to keep would defeat the point of keeping it.
type ConnectOptions ¶
type ConnectOptions struct {
// OwnerURL is the connection string Pando is given. It must be able to run
// migrations and manage AppRole.
OwnerURL string
// ConnectTimeout bounds the retry loop. Compose starts Pando and Postgres
// together, so Postgres will not be accepting connections when Pando first
// dials — this is the standard Compose race and is handled rather than
// assumed away.
ConnectTimeout time.Duration
// SkipMigrate is for tests that manage schema themselves.
SkipMigrate bool
// MaxConns caps the application pool; zero is pgx's default
// (config.Database.MaxConns explains the choice).
MaxConns int32
}
ConnectOptions configures the bootstrap sequence.
type DB ¶
DB is a connection to the state store, held as the application role.
func Connect ¶
func Connect(ctx context.Context, opts ConnectOptions) (*DB, error)
Connect brings the state store up and returns a pool held as AppRole.
The sequence is: wait for Postgres, migrate as owner, provision the application role, apply the grant policy, verify it, then reconnect as the application role. Every step after the wait is idempotent and re-runs on each start, so a grant that drifts is corrected rather than discovered later.
func ConnectCopy ¶
ConnectCopy connects as AppRole to a copy of a database PrepareTemplate prepared, whose password it returned.
The audit log's protection is verified again on the copy rather than assumed from the template: it is the one property that must hold of every database Pando serves from (R-027).
func (*DB) Archiver ¶
Archiver is the pool held as ArchiverRole, or nil when this connection has none (R-348). Only audit retention uses it.
func (*DB) Exclusive ¶
Exclusive runs fn holding the session advisory lock key, waiting for it if another process holds it.
For work that must happen once however many replicas start at the same moment: the second waits, then finds it done.
func (*DB) Listen ¶
func (db *DB) Listen(ctx context.Context, channel string, changed func(payload string), connected func(bool))
Listen holds a connection of its own LISTENing on channel until ctx ends, calling changed for each notification. connected is told true once the LISTEN is in place and false whenever the connection is lost; a lost connection is opened again, with backoff, and told true again.
Its own connection rather than one from the pool: a pooled connection goes back to the pool still listening, and a listener holds its connection for as long as the process runs.
func (*DB) SchemaVersion ¶
SchemaVersion is the migration version this database is at.
type Deliveries ¶
type Deliveries struct {
// contains filtered or unexported fields
}
Deliveries stores deliveries and their attempts.
func NewDeliveries ¶
func NewDeliveries(db *DB) *Deliveries
func (*Deliveries) Claim ¶
func (s *Deliveries) Claim(ctx context.Context, now time.Time, lease time.Duration, limit int) ([]Delivery, error)
Claim takes up to limit deliveries that are due, on subscriptions that are on, and pushes each one's next attempt out by lease. A Pando that stops mid-send leaves the delivery to be retried when the lease runs out: at least once, never zero times (R-366).
func (*Deliveries) Drop ¶
func (s *Deliveries) Drop(ctx context.Context, deliveryID, reason string) error
Drop ends a delivery without an attempt — its owner can no longer see what it is about (R-368). Recorded, so the log says why nothing was sent.
func (*Deliveries) List ¶
func (s *Deliveries) List(ctx context.Context, subscriptionID, before string, limit int) ([]Delivery, error)
List returns one subscription's deliveries, newest first, before the delivery ID given (a cursor) when one is.
type Delivery ¶
type Delivery struct {
ID string `json:"id"`
SubscriptionID string `json:"subscription_id"`
EventID string `json:"event_id"`
EventName string `json:"event"`
Status string `json:"status"`
Attempts int `json:"attempts"`
NextAttemptAt *time.Time `json:"next_attempt_at,omitempty"`
LastAttemptAt *time.Time `json:"last_attempt_at,omitempty"`
LastStatusCode *int `json:"last_status_code,omitempty"`
LastError string `json:"last_error,omitempty"`
RedeliveredBy string `json:"redelivered_by,omitempty"`
RoundBase int `json:"-"`
CreatedAt time.Time `json:"created_at"`
AttemptLog []DeliveryAttempt `json:"attempt_log,omitempty"`
}
Delivery is one event to one subscription.
type DeliveryAttempt ¶
type DeliveryAttempt struct {
Attempt int `json:"attempt"`
AttemptedAt time.Time `json:"attempted_at"`
StatusCode *int `json:"status_code,omitempty"`
Error string `json:"error,omitempty"`
DurationMS int `json:"duration_ms"`
}
DeliveryAttempt is one try.
type Deployment ¶
type Deployment struct {
ID string `json:"id"`
AppID string `json:"app_id"`
SpecID string `json:"spec_id"`
Trigger string `json:"trigger"`
Status string `json:"status"`
// ResultState is what the app was doing when this deploy finished:
// "running", or "degraded" when it started and never reported healthy.
// Empty for a deploy that failed before it got that far.
//
// The deploy's own status answers "did Pando do the work"; this answers
// "did the app come up", which is what somebody reading a list of deploys
// is asking. Three rows reading "Deployed" for an app that had never
// served a request is a true answer to the wrong question.
ResultState string `json:"result_state,omitempty"`
ErrorCode string `json:"error_code,omitempty"`
ErrorDetail string `json:"error_detail,omitempty"`
StartedAt time.Time `json:"started_at"`
FinishedAt *time.Time `json:"finished_at,omitempty"`
CreatedBy string `json:"created_by"`
// SpecRevision is the revision number SpecID is, and RequestedByName who
// CreatedBy is, for showing: a person deciding whether to approve a
// deploy wants "revision 7, asked for by Ada", not two IDs.
SpecRevision int `json:"spec_revision,omitempty"`
RequestedByName string `json:"requested_by_name,omitempty"`
// Deploy approval (R-154 – R-159), on a deploy that needed it. The count
// and the expiry are fixed when the request is made, so a policy edit
// does not move a waiting request's goalposts.
ApprovalsRequired int `json:"approvals_required,omitempty"`
ApprovalExpiresAt *time.Time `json:"approval_expires_at,omitempty"`
ApprovalReasons []ApprovalReason `json:"approval_reasons,omitempty"`
Approvals []ApprovalDecision `json:"approvals,omitempty"`
// CanDecide is whether the caller may approve or reject this deploy. Set
// by the approval service, per caller, on a deploy that is waiting; the
// store never fills it in.
CanDecide bool `json:"can_decide,omitempty"`
// QueuePosition is how many queued deploys a queued one is behind: 0 for
// the next to be taken (issue #93). Absent on one that is not waiting in
// the queue. Set by the handlers from QueuePositions; the store's own
// reads never fill it in.
QueuePosition *int `json:"queue_position,omitempty"`
}
Deployment is one attempt to make an app match a spec.
Its own table, separate from the app, because a build failure fails the deployment and leaves the app untouched (R-146). Collapsing them would make that distinction impossible to express.
type Deployments ¶
type Deployments struct {
// contains filtered or unexported fields
}
Deployments stores deployment records.
func NewDeployments ¶
func NewDeployments(db *DB) *Deployments
func (*Deployments) ByID ¶
func (d *Deployments) ByID(ctx context.Context, deploymentID string) (Deployment, bool, error)
ByID returns one deployment.
func (*Deployments) Claim ¶
func (d *Deployments) Claim(ctx context.Context, limit int) ([]Deployment, error)
Claim takes up to limit queued deployments for this replica, oldest first, and returns them (issue #72, O-32).
FOR UPDATE SKIP LOCKED, so replicas claiming at once take different rows and none waits on another; claiming stamps replica_id, which is what makes a deployment this replica's to run, to fence (SetStatus, Finish) and to serve the live log of. The claimant's heartbeat is the lease: RecoverInFlight puts back what a stopped replica had claimed.
func (*Deployments) Create ¶
func (d *Deployments) Create(ctx context.Context, appID, specID, trigger, createdBy string) (Deployment, error)
Create records a new deployment, queued: pending with no replica running it. Any replica's deploy queue claims it (Claim) when it has room (issue #72, O-32).
func (*Deployments) CreateAwaiting ¶
func (d *Deployments) CreateAwaiting(ctx context.Context, appID, specID, trigger, createdBy string, required int, expiresAt *time.Time, reasons []string) (Deployment, error)
CreateAwaiting records a deploy that needs approval before it runs: tied to one spec revision (R-156), needing required approvals, waiting until expiresAt (nil waits until somebody answers), for the reasons given.
func (*Deployments) Decide ¶
func (d *Deployments) Decide(ctx context.Context, deploymentID, principalID, decision, comment string) error
Decide records one principal's decision on a deploy. A principal decides once per request (the table's primary key): deciding again replaces the earlier answer rather than counting twice, so a retried approval is harmless and two approvals from one person are one.
func (*Deployments) Decisions ¶
func (d *Deployments) Decisions(ctx context.Context, deploymentIDs []string) (map[string][]ApprovalDecision, error)
Decisions returns the decisions on each deployment, oldest first, with each decider's name for showing.
func (*Deployments) EndAwaiting ¶
func (d *Deployments) EndAwaiting(ctx context.Context, deploymentID, status, errorCode, message string) (bool, error)
EndAwaiting closes a deploy that is still waiting for approval — rejected, expired, superseded, or failed because its plan no longer passes — and reports whether it was still waiting. Conditional for the same reason as StartApproved: whichever of a rejection, an expiry and a final approval lands first decides.
func (*Deployments) ExpireDue ¶
func (d *Deployments) ExpireDue(ctx context.Context, now time.Time) ([]Deployment, error)
ExpireDue ends every request whose wait ran out at or before now, and returns them. A request with no expiry waits until somebody answers.
func (*Deployments) Finish ¶
func (d *Deployments) Finish(ctx context.Context, deploymentID, status, errorCode, message string) error
func (*Deployments) InFlight ¶
InFlight reports whether a deployment is already running for an app.
Used to refuse a concurrent deploy rather than queue one: two deploys racing on the same bundle is how an app ends up in a state neither of them intended.
func (*Deployments) LastImage ¶
LastImage returns the image the app's newest successful deploy shipped.
What a rescan looks at (R-312): the image that is running, not one derived from the app's name. Empty for an app that has never deployed, or one whose deploys never carried an image — an app that runs somebody else's published image has one, and an app that has never been built does not.
func (*Deployments) ListAwaiting ¶
func (d *Deployments) ListAwaiting(ctx context.Context) ([]AwaitingApproval, error)
ListAwaiting returns every deploy waiting for approval on an app that has not been deleted, oldest request first, with its decisions so far. Who may see each is the caller's to decide.
func (*Deployments) ListAwaitingAfter ¶
func (d *Deployments) ListAwaitingAfter(ctx context.Context, after AwaitingKey, limit int) ([]AwaitingApproval, error)
ListAwaitingAfter is ListAwaiting from after a key, at most limit of them (a negative limit reads them all).
func (*Deployments) ListForApp ¶
func (d *Deployments) ListForApp(ctx context.Context, appID string) ([]Deployment, error)
ListForApp returns an app's deployments, newest first, with the decisions on any that needed approval.
func (*Deployments) PriorState ¶
PriorState is what RecordPriorState stored, or "" if nothing was.
func (*Deployments) QueueDepth ¶
func (d *Deployments) QueueDepth(ctx context.Context) (int, error)
QueueDepth is how many deployments are waiting for a replica to take them.
func (*Deployments) QueuePosition ¶
QueuePosition is how many queued deploys are ahead of this one, in the order Claim takes them, and whether it is still waiting at all (issue #93).
func (*Deployments) QueuePositions ¶
func (d *Deployments) QueuePositions(ctx context.Context, deps []Deployment) error
QueuePositions sets QueuePosition on each of deps still waiting in the queue. One read of the whole queue, which holds at most one deploy per app, and none when no deploy in deps is pending.
func (*Deployments) RanSuccessfully ¶
RanSuccessfully reports whether a spec revision of an app has been deployed successfully before — what makes rolling back to it free of approval (R-157). A deploy that succeeded is the proof it ran; being pinned is not, since a revision can be pinned by hand without ever being deployed.
func (*Deployments) RecordPriorState ¶
func (d *Deployments) RecordPriorState(ctx context.Context, deploymentID, appState string) error
RecordPriorState stores the app's state as it was when this deploy was started: what a deploy that stops before touching the runtime puts back (R-146). Stored, because the deploy runs from the queue on whichever replica claims it.
func (*Deployments) RecoverInFlight ¶
func (d *Deployments) RecoverInFlight(ctx context.Context) (int64, error)
RecoverInFlight puts back in the queue every deployment claimed by a Pando process that is no longer running, and records as interrupted one that has been started MaxAttempts times. It returns how many it did either to.
Resume rather than record-interrupted (issue #72, O-32): every step a deploy takes before it commits is safe to take again — fetching the pinned commit and building it produce the same image, a scan of the same source is reused, provisioning finds the instance it made, and applying a bundle converges on the spec whatever state the last attempt left it in. So the deploy starts again from the top on whichever replica claims it next, and its user sees it finish rather than a failure that says "deploy again".
Only a stopped or silent process's work, never a live one's: a deploy belongs to the replica that claimed it until that replica stops heartbeating. Queued work nobody has claimed is nobody's to recover and is left in the queue. Called at startup and by the leader's sweeper.
func (*Deployments) Release ¶
Release puts deployments this replica claimed back in the queue, at a shutdown that stopped them part-way. Not counted as an attempt: a rolling restart is not the deploy's fault. One that finished first is left alone.
func (*Deployments) Runner ¶
Runner names the replica running a deployment, or "" when none is recorded. It is where the deploy's live log is (deploy.LogStore).
func (*Deployments) RunningSpecID ¶
RunningSpecID is the spec revision the app's newest successful deploy shipped: what it runs now. Empty for an app that has never deployed.
What deploy approval measures a deploy against (R-154): the app's own approval requirement is read from it as well as from the spec being deployed, and an egress loosening counts only when the running spec does not already carry it.
func (*Deployments) SetEgressRules ¶
func (d *Deployments) SetEgressRules(ctx context.Context, deploymentID string, rules egress.Rules) error
SetEgressRules records the egress rules a deployment runs with, so the reconciler can restore them rather than re-resolving policy under a running app (R-183, O-10).
func (*Deployments) SetImageRef ¶
func (d *Deployments) SetImageRef(ctx context.Context, deploymentID, imageRef, digest string, perWorkload map[string]WorkloadImage) error
SetImageRef records the image a deployment ran, and what the runtime resolved it to.
The reference is what the reconciler restores a missing workload with, rather than rebuilding: rebuilding to correct drift would turn "someone killed a container" into "ship whatever is on the branch now", a far larger action than the one being corrected (R-120).
The digest is what makes "a workload exists with the wrong image" detectable. A reference cannot be compared against a running container — the container reports a digest — and a tag can point somewhere new without changing.
func (*Deployments) SetResultState ¶
func (d *Deployments) SetResultState(ctx context.Context, deploymentID, appState string) error
Finish closes a deployment out. SetResultState records what the app was doing when the deploy finished.
func (*Deployments) SetStatus ¶
func (d *Deployments) SetStatus(ctx context.Context, deploymentID, status string) error
SetStatus advances a deployment.
Only this replica's deployment, or one nobody has claimed: a replica that was taken for dead and whose deploy another replica has since claimed must not move it under the one now running it (fenced, as Finish is).
func (*Deployments) StartApproved ¶
StartApproved moves a deploy that has its approvals from awaiting_approval to pending — queued, the ordinary path — and reports whether it did.
Conditional, so that two approvals arriving together start it once: the second finds it no longer waiting. And conditional on nothing else being in flight for the app, in the same statement, so an approval cannot start a deploy alongside one that began a moment earlier (the check-then-start a caller makes first has that gap). started_at becomes the time it started, so a list of deploys reads in the order they ran; when it was asked for is the deploy.request audit event.
func (*Deployments) SupersedeAwaiting ¶
func (d *Deployments) SupersedeAwaiting(ctx context.Context, appID, exceptID string) ([]string, error)
SupersedeAwaiting ends every request for an app still waiting for approval, except the one named, and returns the ones it ended. A newer request supersedes an older one (R-156): approving the older would deploy a revision nobody is asking for any more.
type Detection ¶
type Detection struct {
AppID string `json:"app_id"`
Status string `json:"status"`
Body json.RawMessage `json:"body"`
Answers map[string]string `json:"answers"`
Commit string `json:"commit,omitempty"`
StartedAt time.Time `json:"started_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Detection is an app's current proposal.
type DetectionSummary ¶
type DetectionSummary struct {
Status string `json:"status"`
// Stage is the step a running detection is on (detect.StageFetching and
// the rest). Empty once it has finished.
Stage string `json:"stage,omitempty"`
StartedAt time.Time `json:"started_at"`
UpdatedAt time.Time `json:"updated_at"`
}
DetectionSummary is where an app's detection has got to, without the proposal: enough for an app's own page and the app list to say why a draft is still a draft (issue #80).
type Detections ¶
type Detections struct {
// contains filtered or unexported fields
}
Detections stores detection proposals.
func (*Detections) Claim ¶
Claim takes up to limit queued detections for this replica, oldest first, and returns their apps' IDs. SKIP LOCKED, as Deployments.Claim.
func (*Detections) FailIfRunning ¶
FailIfRunning records a failure for a detection that is still marked running, and leaves one that has already recorded its own outcome alone.
func (*Detections) Queue ¶
Queue is Start, returning the detection as it wrote it.
In the same statement, so the row returned is the queued one: read back afterwards, a detection another replica claimed and failed at once in the meantime came back failed, and an API answering "queued" reported the outcome of work that had only just begun (issue #72).
func (*Detections) Rebuild ¶ added in v0.5.0
Rebuild marks a finished detection running again on this replica, with body as its progress, so the proposal's image can be built and scanned after an answer changed what is built (R-311). False when the detection is not finished as ready or needs_answers: one already running, failed or blocked has nothing to build.
Claimed for this replica in the same statement, so no queue claims it as a detection to run. A replica that stops part-way leaves it for RecoverRunning, which returns it to the queue: detection runs again, with the answers kept, and builds.
func (*Detections) RecoverRunning ¶
func (d *Detections) RecoverRunning(ctx context.Context) (int64, error)
RecoverRunning puts back in the queue every detection claimed by a Pando process that is no longer running, and records as interrupted one started MaxAttempts times (issue #72, as Deployments.RecoverInFlight). Detection writes nothing but its own row, so running it again from the start is always safe; answers given so far are kept, as on any re-run.
A detection that stayed "running" for good was a console spinning forever and a client polling for an answer that could not come (issue #55); this is what ends that, at startup and then periodically. Queued detections nobody has claimed are left in the queue.
func (*Detections) Release ¶
Release puts detections this replica claimed back in the queue, at a shutdown that stopped them part-way. Not counted as an attempt.
func (*Detections) SaveAnswers ¶
func (d *Detections) SaveAnswers(ctx context.Context, appID string, answers map[string]string) error
SaveAnswers merges answers into an app's detection.
Merged rather than replaced: the console posts answers as they are given, which is the workflow R-105 describes — one question at a time, pasted into an assistant and pasted back.
func (*Detections) Start ¶
func (d *Detections) Start(ctx context.Context, appID string) error
Start marks detection as running for an app, and queues it: running with no replica running it, until a replica's detection queue claims it (Claim, issue #72, O-32).
Answers are preserved across a re-run. Someone who answered "which service is primary" should not be asked again because detection was re-run for an unrelated reason (R-022 makes re-detection explicit, not free).
func (*Detections) Summaries ¶
func (d *Detections) Summaries(ctx context.Context, appIDs []string) (map[string]DetectionSummary, error)
Summaries returns the detection summary of each of apps that has been through detection, keyed by app ID. An app that never has — one created from a spec — is absent.
type Disk ¶
type Disk struct {
// contains filtered or unexported fields
}
Disk is the state the disk pass reads and writes (R-403, issue #130).
func (*Disk) DiskCandidates ¶
DiskCandidates lists every app meant to be running with a pinned spec. A failed app is left out: it is not running, and a failed app stays failed (R-151).
func (*Disk) SetDiskMark ¶
func (d *Disk) SetDiskMark(ctx context.Context, app DiskApp, which DiskMark, at *time.Time) (bool, error)
SetDiskMark records, or with nil clears, one of an app's disk marks. Only on the run of the app the pass read: false, and nothing written, when it has been stopped, deleted or restarted since (CodeRabbit on #180).
func (*Disk) StopForDisk ¶
StopForDisk stops an app and records why, in one write, as StopForIdle does: the visitor page says why in the same moment the app stops. Only the run of the app the pass read, still holding the over-limit mark the pass read, and not failed (R-151): one its owner stopped or restarted since is left as they left it, so a stop always follows two readings over in the same run (R-403, CodeRabbit on #180).
type DiskApp ¶
type DiskApp struct {
AppID string
Name string
OwnerUserID string
RuntimeRef string
DiskBytes int64
WarnedAt *time.Time
OverAt *time.Time
// StartedAt is when the app was last started. Every write the pass makes
// requires it unchanged, so nothing read before a restart lands after it.
StartedAt *time.Time
}
DiskApp is a running app the disk pass measures: its limit and runtime from its pinned spec, and what Pando has already said about it.
type EdgeCertificate ¶
type EdgeCertificate struct {
Name string
Domains []string
CertPEM []byte
KeyPEM secret.Value
NotAfter time.Time // zero until the first issuance succeeds
AttemptedAt time.Time
LastError string
}
EdgeCertificate is one certificate as stored.
type EdgeCertificates ¶
type EdgeCertificates struct {
// contains filtered or unexported fields
}
EdgeCertificates keeps the certificates Pando issues for the edge, its ACME account, and pending HTTP-01 challenges (migration 000052; R-169, R-174).
The account key and each certificate with its key are sealed by the install's secrets adapter, as adapter credentials are (R-190): the rows hold ciphertext or an external reference, never PEM.
func NewEdgeCertificates ¶
func NewEdgeCertificates(db *DB, adapter api.SecretsAdapter, adapterRef string) *EdgeCertificates
func (*EdgeCertificates) Account ¶
func (e *EdgeCertificates) Account(ctx context.Context, email, directory string) (AcmeAccount, bool, error)
Account reads the account for an email address at a CA.
func (*EdgeCertificates) Certificate ¶
func (e *EdgeCertificates) Certificate(ctx context.Context, name string) (EdgeCertificate, bool, error)
Certificate reads one certificate. A row with only a failure recorded is returned with no PEM.
func (*EdgeCertificates) Challenge ¶
Challenge is the key authorization for a pending token, unexpired.
func (*EdgeCertificates) DeleteChallenge ¶
func (e *EdgeCertificates) DeleteChallenge(ctx context.Context, token string) error
DeleteChallenge removes a challenge once the CA has checked it, and any that have expired.
func (*EdgeCertificates) PutAccount ¶
func (e *EdgeCertificates) PutAccount(ctx context.Context, a AcmeAccount) error
PutAccount stores the account key, sealed, and its registration.
func (*EdgeCertificates) PutCertificate ¶
func (e *EdgeCertificates) PutCertificate(ctx context.Context, c EdgeCertificate) error
PutCertificate stores a newly issued certificate, clearing any failure.
func (*EdgeCertificates) PutChallenge ¶
func (e *EdgeCertificates) PutChallenge(ctx context.Context, token, keyAuthorization, domain string, expires time.Time) error
PutChallenge records a pending HTTP-01 challenge for any replica to answer.
type ErrAssigned ¶
type ErrAssigned struct{ Held AIAssignment }
ErrAssigned is returned by Put when another adapter holds the function. Held names that adapter.
func (ErrAssigned) Error ¶
func (e ErrAssigned) Error() string
type Event ¶
type Event struct {
ID string `json:"id"`
Name string `json:"name"`
AppID string `json:"app_id,omitempty"`
ActorKind string `json:"actor_kind"`
ActorID string `json:"actor_id,omitempty"`
OnBehalfOf string `json:"on_behalf_of,omitempty"`
RequestID string `json:"request_id,omitempty"`
Data map[string]any `json:"data"`
OccurredAt time.Time `json:"occurred_at"`
}
Event is one row of the outbox (issue #50, R-366).
type Events ¶
type Events struct {
// contains filtered or unexported fields
}
Events is the outbox.
func (*Events) EmitRouted ¶
EmitRouted records an event that goes to one subscription and no other — a test delivery — and makes that delivery, in one transaction.
func (*Events) ForApp ¶
ForApp returns an app's recent events, newest first, older than the event ID before when one is given (R-378). As long as the outbox keeps them: this is a feed, and the audit log is the history.
func (*Events) Prune ¶
Prune removes up to limit events older than before that have nothing left to deliver, and their deliveries and delivery attempts with them, and returns how many it removed. The outbox is not a history: the audit log is (R-224, R-366).
A batch at a time, so a backlog of a month's events is removed in short transactions the retention job repeats rather than in one that holds locks across the whole outbox (issue #72). The cascade into event_deliveries is by event_id, which is indexed (migration 000050).
func (*Events) Route ¶
func (s *Events) Route(ctx context.Context, limit int, before time.Time, route func(context.Context, Event) ([]string, error)) ([]Event, error)
Route takes up to limit events nobody has routed yet that occurred before before (all of them, when it is zero), oldest first, asks route which subscriptions each one goes to, and queues a delivery for each, all in one transaction. Another Pando routing at the same time skips the rows this one holds, so an event is routed once.
An event route cannot decide about is left for the next pass rather than marked routed: a database hiccup must not lose an event (R-366).
The cutoff is for a router matching against a cached list of subscriptions: an event newer than the list could be meant for a subscription made after it, so it waits for the next list.
type Execer ¶
type Execer interface {
Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
}
Execer is a pool or a transaction.
type FailedAttempts ¶
type FailedAttempts struct {
// contains filtered or unexported fields
}
FailedAttempts counts wrong guesses at something secret, for the limits on guessing: a passcode, per app and client address (R-075a), and a password at sign-in, per username and per client address (R-404). A key says which.
In the database rather than in memory (issue #72): with N replicas behind a load balancer, a count kept per process gave an attacker N times the attempts, and one more set every time a replica restarted.
func NewFailedAttempts ¶
func NewFailedAttempts(db *DB) *FailedAttempts
func (*FailedAttempts) Clear ¶
func (p *FailedAttempts) Clear(ctx context.Context, key string) error
Clear forgets key's failures, after a right guess.
func (*FailedAttempts) Prune ¶
Prune forgets every failure older than window: keys nobody tried again.
func (*FailedAttempts) Record ¶
Record counts one failure for key, and forgets failures older than window for it, which nothing will count again.
func (*FailedAttempts) Wait ¶
func (p *FailedAttempts) Wait(ctx context.Context, key string, window time.Duration) (time.Duration, error)
Wait is how long until the oldest failure for key within window leaves it, which is when one more attempt is allowed once a limit is reached. Zero when there is none. Measured by the database's clock, which recorded them.
type GrantRow ¶
type GrantRow struct {
ID string `json:"id"`
AppID string `json:"app_id"`
Plane string `json:"plane"`
PrincipalKind string `json:"principal_kind"`
PrincipalID string `json:"principal_id,omitempty"`
RoleID string `json:"role_id,omitempty"`
// PrincipalName is who the principal is, for a screen a person reads.
//
// Carried here rather than joined by the caller because the caller cannot.
// This list is read with app.view, and resolving a name from /users needs
// install.view — so an app's owner, who is the person this screen exists
// for, would be refused the second request and left looking at identifiers.
// The API is the product (R-261); a surface that cannot render a name
// without a permission it has no business holding is an API gap.
//
// Empty for an anonymous grant, which is not a person and is rendered as a
// sentence rather than a name (R-077).
PrincipalName string `json:"principal_name,omitempty"`
// RoleName is what the role is called, for the same reason as above:
// resolving one from /roles needs install.view, which the app owner reading
// this screen does not hold. Empty on a data-plane grant, which carries no
// role at all (R-070).
RoleName string `json:"role_name,omitempty"`
// Passcode is whether the anonymous grant asks for one (R-075a). Never the
// passcode or its digest: nobody reads a passcode back, they set a new one.
Passcode bool `json:"passcode,omitempty"`
}
Grant is a stored grant.
type Grants ¶
type Grants struct {
// contains filtered or unexported fields
}
Grants records who may do what with an app.
func (*Grants) AnonymousGrantFor ¶
AnonymousGrantFor returns the app's anonymous grant, if it has one.
func (*Grants) Create ¶
func (g *Grants) Create(ctx context.Context, appID, plane, kind, principalID, roleID, createdBy string) (GrantRow, error)
Create adds a grant.
The shapes are enforced by the database — a data-plane grant carries no role (R-070), only an anonymous grant has a null principal (R-074), and the anonymous grant cannot be inserted twice. This method translates those refusals into something a person can act on rather than surfacing a constraint name.
func (*Grants) ForGroup ¶
func (g *Grants) ForGroup(ctx context.Context, groupID string, page Page) ([]UserAppGrant, string, error)
ForGroup returns a group's own app grants, what everyone in it gets, for one page of apps by name (the cursor is the last app's), with the cursor for the next page.
func (*Grants) ForUser ¶
func (g *Grants) ForUser(ctx context.Context, userID string, page Page) ([]UserAppGrant, string, error)
ForUser returns every app grant that reaches a person — their own, and their groups' (resolved live, R-079) — for one page of apps by name, with the cursor for the next page. Install grants are not app grants and are not here.
func (*Grants) GrantInstall ¶
func (g *Grants) GrantInstall(ctx context.Context, kind, principalID, roleID, createdBy string) (GrantRow, error)
GrantInstall gives a principal an installation-wide control-plane grant (O-17).
Install grants are deliberately not reachable through Create: an app-scoped grant and an install-scoped one are different powers, and a caller that can pass an empty app ID into the sharing path would be able to make an administrator out of a sharing request. They are separate methods so the callers are separate too — this one is reached from bootstrap, and will be reached by the accounts screen's promote/demote route when that exists; never from `POST /apps/{id}/grants`.
There is no anonymous install grant, and the schema is what says so: an anonymous grant has a NULL principal (R-074) and this refuses an empty one.
A principal already holding an install grant has its role replaced rather than a second row added — the schema allows exactly one control grant per principal install-wide, so "grant administrator to someone who is already a viewer" has only one sensible meaning. That also makes it idempotent, which is what the bootstrap path needs: it runs on every start.
func (*Grants) InstallRoleOf ¶ added in v0.8.0
InstallRoleOf returns the role of the installation-wide grant made to this principal itself, if there is one — for a group, what everyone in it holds through it (R-078), which is what changing its membership hands on (R-080a).
func (*Grants) InstallRoles ¶
InstallRoles returns the roles that may be granted installation-wide (R-082).
Read from the roles table rather than hardcoded, so a custom install-scoped role appears in the console the moment it exists without a second change here.
func (*Grants) InstallRolesByPrincipal ¶
InstallRolesByPrincipal maps each principal holding an install grant to its role ID — the grant made to that principal itself, for the groups list. An account's roles, through its groups too, are InstallRolesForUsers.
One query rather than one per account: the list is small (R-015 — one install, one organization) but a per-row lookup is the shape that stops being small quietly.
func (*Grants) InstallRolesForUsers ¶ added in v0.6.0
func (g *Grants) InstallRolesForUsers(ctx context.Context, userIDs []string) (map[string][]HeldInstallRole, error)
InstallRolesForUsers returns every installation role each of these accounts holds, directly and through groups — the same grants InstallGrantsFor reads for authorization, so the accounts screen and the authorizer give one answer (R-078, issue #221). Group membership is read through effective_group_members, so a provider group linked to a Pando group counts.
One query for a page of accounts rather than one per account. Each account's roles come direct first, then by group name; an account holding nothing is absent from the map.
func (*Grants) ListForApp ¶
func (g *Grants) ListForApp(ctx context.Context, appID string, page Page) ([]GrantRow, string, error)
ListForApp returns one page of an app's grants, with the cursor for the next page (empty after the last).
Keyset-paged by principal (grants_app_list_idx), so one principal's grants on both planes arrive together, and the grant to everyone — principal kind "anonymous", which sorts first — is on the first page. An app shared with every person by name is as many grants as people (issue #72).
func (*Grants) RevokeInstall ¶
RevokeInstall removes a principal's installation-wide grant.
Refuses to remove the last account that can manage accounts (R-088). Without that check the install becomes unadministrable in one click and the only way back is a psql prompt — which is the same lockout O-17 allowed by accident, now reachable on purpose. The check and the delete share a transaction, so two administrators removing each other at the same moment cannot both succeed.
The rule is stated in terms of `install.users.manage` rather than "the administrator role", because a custom role (R-082) holding that verb is just as much an administrator and a rule naming the built-in role would not see it.
func (*Grants) SetPasscode ¶
SetPasscode sets, changes or (with an empty digest) removes the passcode on an app's anonymous grant, and ends every unlock made under the old one — in one transaction, so there is no moment when the new passcode is set and the old visitors are still let in.
func (*Grants) SetRole ¶
Delete revokes a grant. SetRole changes the role a control-plane grant carries, in one statement — not a revoke and a new grant, which would leave the person with nothing if the second half failed. A data-plane grant carries no role (R-070) and is refused, and so is an installation role: the composite foreign key on (role_id, role_scope) refuses it, and that becomes a sentence here.
type Group ¶
type Group struct {
ID string `json:"id"`
Name string `json:"name"`
// Source is the identity provider that owns the group's membership, and
// SourceName its name; both empty for a group made in Pando. A synced
// group's members change at the provider, never here (R-079).
Source string `json:"source,omitempty"`
SourceName string `json:"source_name,omitempty"`
Members []string `json:"members,omitempty"`
// MemberCount is how many people are in the group directly. Set by
// ListPage, which leaves Members out: a page of groups carrying every
// member of each is a page that grows with the organization (issue #72).
// GET /groups/{id} has the members.
MemberCount *int `json:"member_count,omitempty"`
// LinkedFrom, on a Pando-made group, are the provider groups whose members
// count as its members (group_links). LinksTo, on a synced group, are the
// Pando groups it feeds.
LinkedFrom []string `json:"linked_from,omitempty"`
LinksTo []string `json:"links_to,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
Group is a stored group.
type GroupFilter ¶
type GroupFilter struct {
// Member keeps only the groups this user is directly in.
Member string
}
GroupFilter narrows ListPage.
type GroupLimit ¶
GroupLimit is a group's own limit, as one of a person's groups.
type Groups ¶
type Groups struct {
// contains filtered or unexported fields
}
Groups are collections of users (R-078).
Pando-native here. An IdP may also push groups (R-048), and when it does the membership arrives the same way — which is why membership is a table rather than a field, and why authorization reads it live (R-079) instead of denormalizing it into grants.
func (*Groups) AddMember ¶
AddMember puts one person in a group. Adding someone already in it is not an error: the outcome holds. Refused for a group synced from an identity provider, whose membership belongs to the provider (R-079).
func (*Groups) ByID ¶
ByID returns one group, with how many people are in it directly and its links, but not who: a group can hold the whole organization, and the members come a page at a time from MembersPage (issue #72).
func (*Groups) Delete ¶
Delete removes a group. Grants made to it go with it — the people in it lose whatever the group gave them, and keep anything given to them directly.
Refused when it would leave nobody who can manage accounts (R-088): a group can hold an administrator role like anyone else, and deleting it is the same lockout as revoking that grant directly.
func (*Groups) Exists ¶
Exists reports whether there is a group with this ID: one primary-key lookup, for a caller that needs nothing else about it.
func (*Groups) Link ¶
Link makes everyone in a provider's group count as a member of a Pando-made group, live (R-079).
func (*Groups) ListPage ¶
func (g *Groups) ListPage(ctx context.Context, page Page, f GroupFilter) ([]Group, string, int, error)
ListPage is one page of groups — Pando's own first, then providers', each by name — with the cursor for the next page and how many match in all. Each carries MemberCount rather than Members, and its links.
Page.Query matches the name.
func (*Groups) MembersPage ¶
func (g *Groups) MembersPage(ctx context.Context, groupID string, page Page) ([]User, string, int, error)
MembersPage is one page of the people directly in a group, oldest account first, with the cursor for the next page and how many match (O-53). In account-ID order because group_members' primary key is in that order, so a page reads a page of rows however large the group. Page.Query matches the username, display name or email; Page.IDs keeps only those accounts, which is how a client asks whether particular people are members.
func (*Groups) RemoveMember ¶
RemoveMember takes one person out of a group, and with them whatever the group gave them. Refused when it would leave nobody who can manage accounts (R-088) — the last member of a group holding the administrator role is as much the last administrator as a direct grant is.
func (*Groups) SetMembers ¶
SetMembers replaces a group's membership.
Replaces rather than merges, in one transaction. A merge cannot express "remove this person", and removing someone from a group is the operation that has to work — it is how access is revoked (R-079).
func (*Groups) SyncMemberships ¶
func (g *Groups) SyncMemberships(ctx context.Context, adapterID, userID string, names []string) error
SyncMemberships sets which of a provider's groups a person is in, from the group names a sign-in carried: groups not seen before are made, and the person leaves any of the provider's groups the sign-in did not name. Groups made in Pando, and other providers' groups, are untouched.
Refused, and nothing changed, when it would leave nobody who can manage accounts (R-088) — the same rule as removing someone from a group by hand.
type HeldInstallRole ¶ added in v0.6.0
type HeldInstallRole struct {
RoleID string `json:"role_id"`
// RoleName travels with the role so that whoever may read the account
// can read what it holds — your own account needs no install.view, and
// the roles list does (issue #221).
RoleName string `json:"role_name"`
Via InstallRoleVia `json:"via"`
}
HeldInstallRole is one installation role an account holds, and how.
type Idempotency ¶
type Idempotency struct {
// contains filtered or unexported fields
}
Idempotency replays the result of a request that has already been made.
For MCP above all (R-262): an agent retries on a timeout, and a retried deploy that deploys twice is the failure mode that makes agents dangerous to give infrastructure to. A deploy resolves a ref — which means cloning — and regularly outlasts a client's patience, so the retry is ordinary rather than exceptional.
func NewIdempotency ¶
func NewIdempotency(db *DB) *Idempotency
func (*Idempotency) Lookup ¶
func (i *Idempotency) Lookup(ctx context.Context, key, principalID, endpoint string) (Replayed, bool, error)
Lookup returns a previous response for this key, if there is one.
func (*Idempotency) Remember ¶
func (i *Idempotency) Remember(ctx context.Context, key, principalID, endpoint string, status int, body []byte) error
Remember stores a response for replay.
ON CONFLICT DO NOTHING: two concurrent requests with the same key both do the work — this does not prevent that, and claiming otherwise would be a lie about a race it cannot win without locking the endpoint. What it prevents is the far more common sequential retry, which is the case that actually happens.
type Identities ¶
type Identities struct {
// contains filtered or unexported fields
}
Identities reads and writes user_identities.
func NewIdentities ¶
func NewIdentities(db *DB) *Identities
func (*Identities) ByVerifiedEmail ¶
ByVerifiedEmail finds the one active account with this email, for linking by an email a provider vouches for. More than one match is no match: a guess between two people is how one signs in as the other. An email the account's holder set themselves is not matched at all: they could have typed anyone's (R-045).
func (*Identities) CreateExternal ¶
func (s *Identities) CreateExternal(ctx context.Context, n NewExternal) (User, error)
CreateExternal makes an account for an external identity, in one transaction with the identity that reaches it. Used by a first sign-in (just-in-time) and by a SCIM push.
func (*Identities) Link ¶
func (s *Identities) Link(ctx context.Context, userID, adapterID, externalID, by string, replace bool) (string, error)
Link attaches an external identity to an account (O-1). An alias is added; nothing merges and no users.id changes.
When the identity already reaches another account, it moves only if replace is set. The account it leaves becomes an alias of this one if that identity was its only way in — suspended, its sessions ended, and never deleted, because its ID may be an assertion subject an app holds data under (R-054).
Returns the account that became an alias, if any.
func (*Identities) RecordSignIn ¶
func (s *Identities) RecordSignIn(ctx context.Context, adapterID, externalID, userID, email, displayName string) error
RecordSignIn notes a sign-in, and refreshes the account's email and name from the provider when the account came from it. A linked account's profile is its own and is not overwritten by whichever provider signed it in.
func (*Identities) Resolve ¶
Resolve finds the account an external identity signs in to. Deleted and suspended accounts are returned as they are, for the caller to refuse: treating a deleted account's identity as unknown would let a first sign-in make a new account for someone an administrator removed.
type Identity ¶
type Identity struct {
AdapterID string `json:"adapter_id"`
AdapterName string `json:"adapter_name"`
AdapterKind string `json:"adapter_kind"`
ExternalID string `json:"external_id"`
UserID string `json:"user_id"`
Origin bool `json:"origin"`
SCIM bool `json:"scim"`
LastSignIn *time.Time `json:"last_sign_in_at,omitempty"`
CreatedBy string `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
}
Identity is one way into an account: an external provider's subject, bound to a users.id (O-1, design 02 §2.1).
type IdentityProvider ¶
type IdentityProvider struct {
ID string `json:"id"`
Kind string `json:"kind"`
Name string `json:"name"`
Config json.RawMessage `json:"config"`
Enabled bool `json:"enabled"`
// JITProvisioning makes an account at a first sign-in. Off unless an
// administrator turns it on; host policy can refuse it everywhere.
JITProvisioning bool `json:"jit_provisioning"`
// LinkByEmail matches a first sign-in to an existing account by an email
// the provider vouches for (O-1). Off unless turned on.
LinkByEmail bool `json:"link_by_email"`
// SCIM is on when a token is set. The token itself is shown once and
// kept only as a digest.
SCIMEnabled bool `json:"scim_enabled"`
SCIMTokenCreatedAt *time.Time `json:"scim_token_created_at,omitempty"`
SCIMIdentityAttribute string `json:"scim_identity_attribute"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
IdentityProvider is a configured identity adapter instance (R-040, R-045): the local one seeded on every install, and any an administrator adds.
type IdentityProviders ¶
type IdentityProviders struct {
// contains filtered or unexported fields
}
IdentityProviders reads and writes identity_adapters.
func NewIdentityProviders ¶
func NewIdentityProviders(db *DB) *IdentityProviders
func (*IdentityProviders) AnyExternalEnabled ¶
func (s *IdentityProviders) AnyExternalEnabled(ctx context.Context) (bool, error)
AnyExternalEnabled reports whether an external provider is on — the condition for turning password sign-in off.
func (*IdentityProviders) ByID ¶
func (s *IdentityProviders) ByID(ctx context.Context, providerID string) (IdentityProvider, bool, error)
ByID returns one provider.
func (*IdentityProviders) BySCIMToken ¶
func (s *IdentityProviders) BySCIMToken(ctx context.Context, digest string) (IdentityProvider, bool, error)
BySCIMToken finds the provider a SCIM bearer token belongs to, by digest.
func (*IdentityProviders) Create ¶
func (s *IdentityProviders) Create(ctx context.Context, kind string, set ProviderSettings) (IdentityProvider, error)
Create adds a provider.
func (*IdentityProviders) Delete ¶
func (s *IdentityProviders) Delete(ctx context.Context, providerID string) error
Delete removes a provider nobody has used. One that has signed anyone in is referenced by their account, their sessions and the audit log, and is disabled instead — deleting it would free its identities for reuse.
func (*IdentityProviders) List ¶
func (s *IdentityProviders) List(ctx context.Context) ([]IdentityProvider, error)
List returns every provider, the local one first.
func (*IdentityProviders) SetSCIMToken ¶
func (s *IdentityProviders) SetSCIMToken(ctx context.Context, providerID, digest string) error
SetSCIMToken turns SCIM on, or rotates its token, storing only the digest. An empty digest turns SCIM off.
func (*IdentityProviders) Update ¶
func (s *IdentityProviders) Update(ctx context.Context, providerID string, set ProviderSettings) (IdentityProvider, error)
Update changes a provider's settings.
type IdleApp ¶
type IdleApp struct {
AppID string
Name string
OwnerUserID string
State string
DesiredState string
StoppedForIdle bool
// StopDays and DeleteDays are in force for this app: its own, or the
// install's. Zero is off.
StopDays int
DeleteDays int
LastActivity time.Time
StopNoticedAt *time.Time
DeleteNoticedAt *time.Time
}
IdleApp is an app the idle pass may have something to do for.
type IdleDefaults ¶
IdleDefaults is host policy's part of the question: the install's days, and the notice every action waits out.
type IdleNotice ¶
type IdleNotice string
IdleNotice is which action a notice is for.
const ( IdleNoticeStop IdleNotice = "stop" IdleNoticeDelete IdleNotice = "delete" )
type IdleSettings ¶
IdleSettings is an app's own idle settings (R-397). Nil is the install's; zero is off.
type IdleStatus ¶
type IdleStatus struct {
IdleSettings
DesiredState string
State string
StoppedForIdle bool
LastActivity time.Time
StopNoticedAt *time.Time
DeleteNoticedAt *time.Time
}
IdleStatus is an app's idle settings and where its clock stands.
type InstallRoleVia ¶ added in v0.6.0
type InstallRoleVia struct {
Kind string `json:"kind"`
GroupID string `json:"group_id,omitempty"`
Name string `json:"name,omitempty"`
Source string `json:"source,omitempty"`
SourceName string `json:"source_name,omitempty"`
}
InstallRoleVia says where a held installation role comes from: a grant to the account itself ("direct"), or one to a group it is in ("group"), with that group's name and, for a group synced from an identity provider, the provider (as on a Group).
type Issued ¶
Issued is a freshly minted token. Secret is present exactly once, here (R-063) — it is hashed on the way into the database and cannot be recovered.
Secret is a secret.Value, so serializing this struct redacts it. A handler returning the plaintext to its one legitimate caller must reveal it deliberately, which is the point: there is no path where it leaks by forgetting.
type Leadership ¶
type Leadership struct {
// contains filtered or unexported fields
}
Leadership is the lock that makes one replica the one that runs the install-wide background jobs (issue #72): garbage collection, auto-deploy, audit retention, and the rest of what must happen once per install rather than once per process.
A session advisory lock on a connection held for as long as the replica leads. If the process dies the connection closes and Postgres releases the lock itself; there is no lease to expire and no clock to trust. A leader that loses its connection stops leading at once (Lost closes), and another replica takes over at its next attempt.
func (*Leadership) Lost ¶
func (l *Leadership) Lost() <-chan struct{}
Lost is closed when this replica stops leading without having resigned.
func (*Leadership) Resign ¶
func (l *Leadership) Resign(ctx context.Context)
Resign gives the lock up. Safe to call after Lost has closed.
type Lease ¶
type Lease struct {
// contains filtered or unexported fields
}
Lease is one reconciliation pass's hold on the apps it claimed.
Two passes must never work on one app at once, on one replica or across several. That used to be a session advisory lock, which held a pooled connection for the whole reconciliation — Apply included, which can take minutes — while the reconciliation took more connections of its own. A lease is a column, so holding one holds nothing (issue #72).
func (*Lease) Claim ¶
func (l *Lease) Claim(ctx context.Context, now, cutoff time.Time, limit int, lease time.Duration) ([]Reconcilable, error)
Claim leases up to limit apps the loop should reconcile now.
Due means: in a state the loop acts on, out of backoff, and neither held by another pass nor visited since cutoff. Least recently visited first, so every app gets its turn: ordering by updated_at, as this used to, visited the same 200 apps forever because a healthy pass changes nothing on the row. SKIP LOCKED hands each concurrent claimant — another replica, usually — a disjoint set rather than the same rows to contend for.
Apps in backoff are filtered here rather than skipped in the loop, so an app waiting five minutes costs one row in a WHERE clause instead of a goroutine per tick that wakes up and does nothing. Backoff is measured on the loop's clock (now), as RecordFailure sets it; the lease on the database's.
The last successful deployment's facts are read for the claimed rows only, each an index probe on deployments (app_id, started_at DESC), so a claim's cost follows its limit and not the number of apps on the install.
func (*Lease) Extend ¶
Extend pushes every lease this pass still holds lease further out, and returns the apps it still holds.
An app missing from the result is no longer this pass's — its lease ran out before it was extended, and another pass may have it — and its work must stop.
func (*Lease) Release ¶
Release lets go of one app.
visited is when this pass's visit to it started, which orders the app behind every app visited less recently. Nil for an app the pass claimed and never started, which goes back to the front.
Only if this pass still holds it: a lease that ran out belongs to whoever claimed it next.
func (*Lease) ReleaseSettled ¶
func (l *Lease) ReleaseSettled(ctx context.Context, appID string, visited time.Time, extra time.Duration) error
ReleaseSettled lets go of an app found as it should be, and leaves it until a slow sweep comes round to it again (O-52): it is next due revisit after visited, where an ordinary release is due MinRevisit after it.
The lease column carries this as it carries everything else about when an app is due: set to visited plus the difference, a claim with the ordinary cutoff passes over it until the time comes. Nothing is held — the holder is cleared as in Release — so a nudge (Nudge, dueAgain) brings it forward at any moment.
Unless the app changed while it was being looked at: a state, desired state or secret written during the visit bumps updated_at past the visit's start, and the app is released as due now instead, so what changed is not left for the sweep.
type LimitFacts ¶
type LimitFacts struct {
// UserMaxApps is the user's own value, nil when not set.
UserMaxApps *int
// Groups are the person's groups that set a value. Through
// effective_group_members, so a provider's group linked to a Pando group
// counts like a direct member.
Groups []GroupLimit
// Owned is how many apps they own now, in any state but deleted.
Owned int
}
LimitFacts is everything a person's limit is decided from, but host policy.
type NewExternal ¶
type NewExternal struct {
AdapterID string
ExternalID string
Email string
DisplayName string
CreatedBy string
// SCIM fields, when a SCIM client made it.
SCIMUserName string
SCIMExternalID string
SCIMResource []byte
Suspended bool
}
NewExternal describes an account made for an external identity.
type Notification ¶
type Notification struct {
ID string `json:"id"`
UserID string `json:"user_id"`
AppID string `json:"app_id,omitempty"`
AppName string `json:"app_name,omitempty"`
Link string `json:"link,omitempty"`
EventID string `json:"event_id,omitempty"`
Kind string `json:"kind"`
Subject string `json:"subject"`
Body string `json:"body,omitempty"`
ReadAt *time.Time `json:"read_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
Notification is one stored message.
type NotificationPreference ¶
type NotificationPreference struct {
Kind string `json:"kind"`
Channel string `json:"channel"`
Enabled bool `json:"enabled"`
}
NotificationPreference is one choice a person made: this kind of notification, on this channel, on or off (R-373).
type NotificationPreferences ¶
type NotificationPreferences struct {
// contains filtered or unexported fields
}
NotificationPreferences stores those choices.
func NewNotificationPreferences ¶
func NewNotificationPreferences(db *DB) *NotificationPreferences
func (*NotificationPreferences) List ¶
func (s *NotificationPreferences) List(ctx context.Context, userID string) ([]NotificationPreference, error)
List returns one person's choices. Anything absent is the default.
func (*NotificationPreferences) Set ¶
func (s *NotificationPreferences) Set(ctx context.Context, userID string, prefs []NotificationPreference) error
Set records choices, replacing any earlier choice for the same kind and channel.
type Notifications ¶
type Notifications struct {
// contains filtered or unexported fields
}
Notifications stores console notifications (R-231).
Implements the console notify adapter's Sink. The adapter holds no storage of its own, which is R-027: an adapter never touches state, so core hands it a writer rather than a database.
func NewNotifications ¶
func NewNotifications(db *DB) *Notifications
func (*Notifications) ListForUser ¶
func (n *Notifications) ListForUser(ctx context.Context, userID string, unreadOnly bool, before string, limit int) ([]Notification, error)
ListForUser returns a user's notifications, newest first: up to limit, older than the notification ID before when one is given (a cursor).
func (*Notifications) MarkAllRead ¶
func (n *Notifications) MarkAllRead(ctx context.Context, userID string) error
MarkAllRead marks every one of a user's notifications read.
func (*Notifications) MarkRead ¶
func (n *Notifications) MarkRead(ctx context.Context, userID, notificationID string) error
MarkRead marks one notification read, for its owner only.
The user ID is in the WHERE clause rather than checked first: a check and then an update is two statements with a gap between them, and the gap is where someone marks a notification that is not theirs.
func (*Notifications) Record ¶
func (n *Notifications) Record(ctx context.Context, msg api.Notification, retainFor time.Duration) error
Record writes one row per recipient.
Per recipient rather than one row with a list, so "mark as read" is a write to one row and an unread count is a count. A shared row would make both of those a join against a second table that does not exist yet.
type Nudged ¶
type Nudged struct {
// Due: the app will be claimed by the next pass on any replica.
Due bool
// Held: a pass holds the app now, and it was left alone. Ask again once
// that pass lets go, or the change may be one the pass already looked
// past.
Held bool
}
Nudged is what Nudge did.
type OrphanedVolume ¶
OrphanedVolume is storage whose app is gone and whose data is in a backup.
type Page ¶
type Page struct {
// Limit is the most rows to return. Zero means DefaultPageSize; anything
// over MaxPageSize is read as MaxPageSize.
Limit int
// Cursor is the next_cursor of the previous page, or empty for the first.
Cursor string
// Query narrows the list by a case-insensitive substring match, on
// whatever a person would recognize the row by (a name, an email).
Query string
// IDs, when set, narrows the list to these rows: how a client refreshes
// the few rows it is watching change without reading the whole list again.
// The app and account lists read it.
IDs []string
}
Page asks for one page of a list.
type Passwords ¶
Passwords are the ones provisionRoles gave Pando's two restricted roles.
func PrepareTemplate ¶
PrepareTemplate migrates, grants and verifies a database to be copied, and returns the passwords it gave AppRole and ArchiverRole.
Nothing may be connected to the database when it is copied, so the caller makes its copies after this returns and never connects to it again.
type Policy ¶
type Policy struct {
// contains filtered or unexported fields
}
Policy reads and writes the host policy singleton.
type Ports ¶
type Ports struct {
// contains filtered or unexported fields
}
Ports allocates the host ports that port-mode routing hands out.
[P] — unspecified in the requirements. R-166 prefers subdomain and falls back to path, and the loopback adapter that ships as the laptop default supports neither: it is port mode only. So an install using it has to get a port from somewhere, and nothing said where. See docs/plan/open-decisions.md O-15.
The rule is the lowest free port in a configured range. Not random, so an app keeps its port across a rebuild and a bookmark keeps working; reused rather than ever-increasing, so a deleted app's port comes back.
It is a row in a table rather than a number derived from existing specs. The derived version raced: detection runs in the background per app, and three apps added together had two of them compute the same "lowest free" port before either had written anything down. A unique constraint cannot be raced — one writer wins and the other is told — which is the same reason every other invariant here lives in the schema.
func (*Ports) Allocate ¶
Allocate reserves the lowest free port in [from, to] for an app.
Idempotent: an app that already holds a port on this adapter gets the same one back. Re-detection (R-022) must not consume a second port and leave every bookmark pointing at nothing.
func (*Ports) InUse ¶
InUse lists every port currently allocated to an app.
What the proxy opens listeners on (design 03 §4.2). Allocations rather than running apps, deliberately: a port belongs to an app from the moment it is allocated until the app is gone, and an app that is stopped or mid-deploy should answer "this app isn't running right now" at its own address rather than refuse the connection — the two are very different things to debug.
type Profile ¶
type Profile struct {
Username *string
DisplayName *string
Email *string
// EmailBySelf says the account's holder set Email, rather than an
// administrator. Linking by email ignores such an address (R-045).
EmailBySelf bool
}
Profile is the part of an account a person or an administrator edits. A nil field is left as it is.
type ProviderSettings ¶
type ProviderSettings struct {
Name *string
Config json.RawMessage
Enabled *bool
JITProvisioning *bool
LinkByEmail *bool
SCIMIdentityAttribute *string
}
ProviderSettings are the fields an administrator sets. A nil field is left as it is on update.
type Reconcilable ¶
type Reconcilable struct {
App
ConsecutiveFailures int
LastFailureAt *time.Time
NextAttemptAt *time.Time
UnobservableSince *time.Time
AppliedEnvHash string
// WorkloadImages is what each part of the app ran, for an app whose parts
// are built separately. Empty for a single-image app, where ImageRef says
// everything, and for a deployment from before it was recorded.
//
// The reconciler restores a missing workload from the recorded image, and
// with only ImageRef to go on it restored every workload from one image —
// which for a compose app meant replacing the application with a second
// copy of whichever service happened to be primary.
WorkloadImages map[string]WorkloadImage
// ImageRef is what the last successful deployment ran. The reconciler
// restores a missing workload with this rather than rebuilding: correcting
// drift must not become "ship whatever is on the branch now".
ImageRef string
// ImageDigest is what the runtime resolved that reference to. A running
// container reports a digest, not a reference, so this is the only thing
// that can be compared against one — and a tag can point somewhere new
// without the reference changing at all.
ImageDigest string
// EgressRules is what the last successful deployment ran with. Rules
// take effect at a deploy (R-183, O-10), so the reconciler restores these
// rather than resolving policy afresh and changing a running app.
EgressRules egress.Rules
// ClaimedAt is the database's time when this pass claimed the app. The
// visit is released with it, so it is when the app was last looked at.
ClaimedAt time.Time
}
Reconcilable is an app the loop should look at, with the bookkeeping it needs.
type Reconciles ¶
type Reconciles struct {
// contains filtered or unexported fields
}
Reconciles reads and writes the reconciler's view of an app.
func (*Reconciles) CatchUp ¶
CatchUp makes every app on one runtime that is waiting for the slow sweep due now, and returns how many (O-52).
Called when that runtime's event stream opens: whatever happened to those apps while nobody was listening — before Pando started, or while the stream was down — was not reported, so each is looked at once rather than left for the sweep. Apps not waiting are due within a pass anyway and are left in their place in the queue.
func (*Reconciles) ClearFailures ¶
func (r *Reconciles) ClearFailures(ctx context.Context, appID string) error
ClearFailures resets the counter and backoff.
Called only when an app is running with health passing. A flapping app that recovers between failures still accumulates toward the threshold, which is correct: flapping is a failure mode, not a recovery.
func (*Reconciles) ClearUnobservable ¶
func (r *Reconciles) ClearUnobservable(ctx context.Context, appID string) error
ClearUnobservable is called on the first successful Observe.
func (*Reconciles) Cutoff ¶
Cutoff is the database's time minus revisit: an app visited after it is not due again in the pass that asked.
A pass fixes this once, before its first claim. Every app it visits is released with the time its visit started, which is after the cutoff, so the pass cannot claim the same app twice and ends when nothing older is left. revisit spreads the same guarantee across replicas: an app another replica visited less than revisit ago is left for later rather than visited twice.
The database's clock and not the caller's, because replicas' clocks differ and the lease is compared against the database's.
func (*Reconciles) Lease ¶
func (r *Reconciles) Lease() *Lease
Lease starts a pass. Nothing is claimed until Claim.
func (*Reconciles) MarkUnobservable ¶
func (r *Reconciles) MarkUnobservable(ctx context.Context, appID, reason string) error
MarkUnobservable records that the adapter could not be reached.
It touches neither state nor the failure counter. An adapter being down is a platform problem, not app failure — otherwise restarting the Docker daemon marks every app on the host as failed (design 05 §2).
func (*Reconciles) Nudge ¶
Nudge makes one app due now, because its runtime reported something happening to it (O-52).
Only an app the loop acts on: a failed app is not in reconcilableStates, so a nudge for one changes nothing — R-151 holds here by the same absence it holds by everywhere else in the loop. Backoff still applies, since Claim filters on next_attempt_at; a nudge moves an app forward in the queue, not past R-149. A lease that has run out is nobody's, and is cleared with it.
func (*Reconciles) RecordFailure ¶
func (r *Reconciles) RecordFailure(ctx context.Context, appID, reason string, window time.Duration, next time.Time) (int, error)
RecordFailure increments the failure counter and returns the new count.
The window is an idle timeout measured from the last failure, not a fixed window from the first. Measured from the first it is unreachable: R-149's backoff caps at five minutes, so ten attempts span 31.3 minutes against a 30-minute window that resets at 30 — the app retries forever and is never given up on, which is what R-150 exists to prevent.
As an idle timeout it means what the requirement means. Consecutive failures always reach the threshold however long backoff stretches them out, and unrelated failures a day apart never accumulate.
func (*Reconciles) RecoverCurrentDeploy ¶ added in v0.6.0
func (r *Reconciles) RecoverCurrentDeploy(ctx context.Context, appID string) error
RecoverCurrentDeploy records that an app's current deploy came up after all.
A deploy waits a bounded time for health and finishes with result_state `degraded` when the app has not passed by then. An app that is only slow to start — a migration on boot, a long warm-up — passes later, and the history must not go on saying "Deployed, not healthy" about an app that is serving traffic (R-221, issue #233). Only the current deploy, the latest one that succeeded: an older row describes what happened then, and stays as it was.
func (*Reconciles) SetAppliedEnvFingerprint ¶
func (r *Reconciles) SetAppliedEnvFingerprint(ctx context.Context, appID, fingerprint string) error
SetAppliedEnvFingerprint records the environment an app was last applied with.
R-193's only mechanism. Observe returns no environment and deliberately should not — reading it back would require every runtime adapter to handle secret-bearing data, which is what the adapter interface works to avoid — so a rotated secret is invisible to observation and is detected by comparing this instead (design 02 §2.4).
type Replaced ¶ added in v0.6.0
Replaced is the adapter that stopped being its category's default when another was saved as the default. Zero when none did.
type Replayed ¶
type Replayed struct {
StatusCode int
Body json.RawMessage
}
Replayed is a stored response.
type Replica ¶
type Replica struct {
ID string
Hostname string
AdvertiseURL string
Version string
AssertionKID string
AssertionKey []byte // Ed25519 public key
StartedAt time.Time
HeartbeatAt time.Time
StoppedAt *time.Time
}
Replica is one row.
type Replicas ¶
type Replicas struct {
// contains filtered or unexported fields
}
Replicas are the Pando processes sharing this database (issue #72).
A replica is Pando's own process and never a place an app runs: nothing that plans or places a workload reads this table (R-010, R-256). It answers which of Pando's processes are alive, which three things need to know — whose in-flight work is abandoned, which signing keys are published, and where a deploy's live log is.
Every time here is the database's, never a replica's own clock: replicas on different machines disagree about the time, and the database is the one clock they share.
func NewReplicas ¶
func (*Replicas) Heartbeat ¶
Heartbeat records that a replica is alive, and reports whether its row still exists and is not marked stopped. A replica that finds itself gone was taken for dead — its work has been abandoned by now — and the honest thing for it to do is restart as a new replica rather than carry on as if not.
func (*Replicas) Live ¶
Live lists the replicas that have heartbeated within ReplicaStale, oldest first.
func (*Replicas) Prune ¶
Prune removes replicas gone for longer than olderThan. Their keys have long since stopped verifying anything and their work has long since been abandoned; the row is only history.
func (*Replicas) RequestRestart ¶
RequestRestart asks every replica running now to restart (POST /restart).
func (*Replicas) RestartPending ¶
RestartPending reports whether a restart was asked for after replicaID started.
func (*Replicas) Stop ¶
Stop records a replica as stopped, at shutdown. Its key stays published for as long as an assertion it signed can still be valid.
func (*Replicas) StopSilent ¶
StopSilent records as stopped every replica that has not heartbeated for ReplicaStale, before its claimed work is put back in the queue (issue #72, O-32). A replica that was only slow — a long pause, a partition — then finds at its next heartbeat that it was taken for dead, and restarts rather than carrying on with deploys another replica has already claimed again.
func (*Replicas) TryLead ¶
TryLead takes the leader lock if nobody holds it. ok is false, with no error, when another replica leads.
func (*Replicas) VerificationKeys ¶
VerificationKeys lists the replicas whose signing keys must still be published: every live one, and every one that stopped or went silent less than validity ago, since an assertion it signed may still be in use.
type Retention ¶
type Retention struct {
// contains filtered or unexported fields
}
Retention removes rows from the tables that otherwise only grow (issue #72, R-224): one batch per call, so the retention job's deletes are short transactions it repeats rather than one long one holding locks a request is waiting on.
What each never removes is the point of each, and is written into the query rather than left to the caller:
- Spec revisions are never deleted here (R-152). Their pruning is the GC's, under its own rules.
- The audit log is never touched (R-027); its retention is R-347's.
- A deploy in flight or waiting for approval is never deleted, nor the newest successful deploy of a revision the app ever pinned: that row is what makes rolling back to the revision free of approval (R-157) and what the reconciler restores its image from.
- The newest scan of each revision is never deleted, so a rollback still restores its score (R-319).
func NewRetention ¶
func (*Retention) DeletedApps ¶
DeletedApps removes the detection and the backup-attempt record of apps deleted before before. Apps soft-delete, so the cascade that would have removed them never fires; one row per app is still a row per app ever made.
func (*Retention) Deployments ¶
Deployments removes up to limit deployments beyond each app's newest keepPerApp, never one in flight, waiting for approval, or the newest successful deploy of a revision the app ever pinned.
App by app, each read from deployments_app_idx past its newest keepPerApp, and stopping once limit rows are found, rather than numbering every deployment in the install with a window function on every batch (issue #72).
func (*Retention) IdempotencyKeys ¶
func (r *Retention) IdempotencyKeys(ctx context.Context, before time.Time, limit int) (int64, error)
IdempotencyKeys removes up to limit keys recorded before before. A key reused after that is a new request, which is correct: nobody retries something from yesterday (migration 000011).
func (*Retention) Notifications ¶
Notifications removes up to limit console notifications past their own retain_until. One with none is kept.
func (*Retention) SSOFlows ¶
SSOFlows removes up to limit sign-in flows that expired before before — kept that long so a person arriving late at a failed sign-in can still be told why — and the one-time identifiers whose assertions have expired. Both used to be deleted on every sign-in, inside the request.
type Revision ¶
type Revision struct {
ID string `json:"id"`
AppID string `json:"app_id"`
Revision int `json:"revision"`
Origin spec.Origin `json:"origin"`
Body *spec.AppSpec `json:"body"`
CreatedBy string `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
// EverPinned is derived from spec_pins, not stored on the revision —
// spec_revisions is append-only and a mutable column on it would be a hole
// in that guarantee.
EverPinned bool `json:"ever_pinned"`
}
Revision is a stored spec revision.
type RoleRow ¶
type RoleRow struct {
ID string `json:"id"`
Name string `json:"name"`
Builtin bool `json:"builtin"`
Scope string `json:"scope"`
Verbs []string `json:"verbs"`
}
RoleRow is a role as stored.
type Roles ¶
type Roles struct {
// contains filtered or unexported fields
}
Roles reads and writes custom roles (R-082).
func (*Roles) CreateCustom ¶
func (r *Roles) CreateCustom(ctx context.Context, name, scope string, verbs []authz.Verb) (authz.Role, error)
CreateCustom adds a custom role composed from the verb catalog.
Custom roles are arbitrary subsets of the catalog, and validated against it: a role naming a verb Pando does not have would grant nothing and look like it granted something.
func (*Roles) DeleteCustom ¶
DeleteCustom removes a custom role, and every grant of it with it: anyone who held it — on an app, or across the installation — loses what it allowed, and keeps anything they hold another way. Built-ins are protected by trigger (R-081), so this reports that rather than surfacing a constraint name.
The grants have to go first: they reference the role by foreign key, so a role anyone held could not be deleted at all, and the error said only that something had gone wrong.
Refused when it would leave nobody who can manage accounts (R-088) — a custom role holding install.users.manage is as much an administrator as the built-in one.
type SCIMGroup ¶
type SCIMGroup struct {
ID string
DisplayName string
ExternalID string
Resource []byte
Members []SCIMMember
CreatedAt string
UpdatedAt string
}
SCIMGroup is a provider's group as a SCIM client sees it.
type SCIMGroups ¶
type SCIMGroups struct {
// contains filtered or unexported fields
}
SCIMGroups is the SCIM view of one provider's groups.
func NewSCIMGroups ¶
func NewSCIMGroups(db *DB) *SCIMGroups
func (*SCIMGroups) ByID ¶
func (s *SCIMGroups) ByID(ctx context.Context, adapterID, groupID string, members bool) (SCIMGroup, bool, error)
ByID returns one of a provider's groups.
func (*SCIMGroups) ChangeMembers ¶
func (s *SCIMGroups) ChangeMembers(ctx context.Context, adapterID, groupID string, add, remove []string) error
ChangeMembers adds or removes members, leaving the rest.
func (*SCIMGroups) Create ¶
func (s *SCIMGroups) Create(ctx context.Context, adapterID, displayName, externalID string, resource []byte, members []string) (string, error)
Create makes a group for a SCIM client. The external ID it is keyed by is the client's externalId when it sends one; otherwise the display name, which is also what a sign-in's groups claim would carry.
func (*SCIMGroups) List ¶
func (s *SCIMGroups) List(ctx context.Context, adapterID, attr, value string, offset, limit int, members bool) ([]SCIMGroup, int, error)
List returns a provider's groups, optionally filtered by display name or external ID, with the total before paging.
func (*SCIMGroups) Update ¶
func (s *SCIMGroups) Update(ctx context.Context, adapterID, groupID string, displayName *string, resource []byte, members []string, replaceMembers bool) error
Update renames a group, replaces what the client said about it, and — when members is non-nil — replaces its membership.
func (*SCIMGroups) UserGroups ¶
func (s *SCIMGroups) UserGroups(ctx context.Context, adapterID, userID string) ([]SCIMMember, error)
UserGroups lists the provider's groups a user is in, for the user resource.
type SCIMMember ¶
SCIMMember is one member of a SCIM group.
type SCIMUser ¶
type SCIMUser struct {
ID string // users.id, which is the SCIM resource id
Key string // the identity's external_id: what sign-in matches on
UserName string
ExternalID string
Resource []byte
Email string
DisplayName string
Status string
SuspendedBy string
CreatedAt time.Time
UpdatedAt time.Time
}
SCIMUser is an account as one provider's SCIM client sees it (R-048).
type SCIMUserUpdate ¶
type SCIMUserUpdate struct {
Key string
UserName string
ExternalID string
Resource []byte
Email string
DisplayName string
}
SCIMUserUpdate is a SCIM client's view of an account, written back.
type SCIMUsers ¶
type SCIMUsers struct {
// contains filtered or unexported fields
}
SCIMUsers is the SCIM view of one provider's accounts. A client sees the accounts its provider has an identity on, and nothing else.
func NewSCIMUsers ¶
func (*SCIMUsers) Forget ¶
Forget is a SCIM DELETE: the client's record of the account goes, and so does its membership in the provider's groups. The account and its identity stay — suspended by the caller, not deleted (R-049) — so the identity cannot be reused by someone else, and a later POST for the same person picks the account up again.
func (*SCIMUsers) List ¶
func (s *SCIMUsers) List(ctx context.Context, adapterID, attr, value string, offset, limit int) ([]SCIMUser, int, error)
List returns the accounts a provider's SCIM client provisioned, optionally filtered by one attribute, with the total before paging. Accounts deleted in Pando, and ones the client deleted (Forget), are left out: to the client they no longer exist.
func (*SCIMUsers) Reactivate ¶
Reactivate lifts a suspension this provider's SCIM client made, and only that one. An administrator's suspension stands however often a client sends active=true — Entra resends it on every cycle (R-049).
type SSOFlow ¶
type SSOFlow struct {
ID string
AdapterID string
Purpose string // sign_in | test
BindHash string
ReturnOrigin string
NextPath string
CallbackURL string
EntityID string
Flow []byte
InitiatedBy string
UserID string
Result json.RawMessage
Failed bool
CreatedAt time.Time
ExpiresAt time.Time
ConsumedAt *time.Time
}
SSOFlow is a redirect sign-in in progress (design 06 §3.2).
type SSOFlows ¶
type SSOFlows struct {
// contains filtered or unexported fields
}
SSOFlows stores flows.
func NewSSOFlows ¶
func (*SSOFlows) Authenticated ¶
func (s *SSOFlows) Authenticated(ctx context.Context, flowID, userID, handoffHash string, result json.RawMessage) error
Authenticated records who the callback signed in and the digest of the one-time code that finishes the flow in the browser that started it.
func (*SSOFlows) Create ¶
Create stores a new flow. Expired flows and spent one-time identifiers are removed by the retention job (Retention.SSOFlows), not here: two deletes on every sign-in were two full scans on the path everyone waits on (issue #72).
func (*SSOFlows) CreateUnsolicited ¶
CreateUnsolicited stores a flow the provider started (SAML IdP-initiated), already past its callback. It has no browser binding to check.
func (*SSOFlows) Fail ¶
Fail records why a flow did not sign anyone in. Kept for a day so the sign-in page can say so, and a test sign-in can show it.
func (*SSOFlows) TakeForCallback ¶
func (s *SSOFlows) TakeForCallback(ctx context.Context, flowID, adapterID string) (SSOFlow, bool, error)
TakeForCallback claims a live flow for its callback, exactly once: a replayed callback finds the flow already taken. The adapter's data is cleared as it is handed over.
type Scan ¶
type Scan struct {
ID string `json:"id"`
AppID string `json:"app_id"`
SpecID string `json:"spec_id,omitempty"`
// Commit names the source the scan read, when it read one: a git commit,
// an image digest, or an upload's archive digest. A deploy of that source
// uses this scan rather than scanning again (ForSource).
Commit string `json:"commit,omitempty"`
// Image names the image the scan read, by digest, when it read one. Empty
// for a scan of the source alone — detection's, before anything was built —
// which a deploy that built an image does not reuse (R-311).
Image string `json:"image,omitempty"`
ScannerRef string `json:"scanner_ref"`
Scanner string `json:"scanner,omitempty"`
Score *int `json:"score"`
// ScoreFixable counts only findings with a fix available. Which of the two
// an installation means is host policy's (R-313); both are stored because
// policy changes without rescanning.
ScoreFixable *int `json:"score_fixable,omitempty"`
Findings []api.Finding `json:"findings"`
Error string `json:"error,omitempty"`
RanAt time.Time `json:"ran_at"`
// ReusedFrom is the scan this one repeats, when a deploy of a new revision
// of an unchanged source reused it rather than scanning again (Reuse).
// RanAt is that scan's: the findings are as old as the scan that found
// them, not as the deploy that reused them.
ReusedFrom string `json:"reused_from,omitempty"`
}
Scan is one scanner's answer about one revision.
type Scans ¶
type Scans struct {
// contains filtered or unexported fields
}
Scans stores them.
func (*Scans) ClearInsecure ¶
ClearInsecure records that an app is back above the threshold.
func (*Scans) ForSource ¶
func (s *Scans) ForSource(ctx context.Context, appID, source, image, scannerRef string) (Scan, bool, error)
ForSource returns the newest scan of an app's source and image that ran, by the scanner named — a scanner that failed produced no finding worth reusing (R-318), and another scanner's findings are not this one's.
Both have to match, because a scan covers what it read (R-311): a scan of the source alone does not cover an image built from it, and a scan of one image does not cover another built from the same commit. Image is empty for a deploy that built none, and then only a scan that read none matches. Found is false for an empty source, which names nothing.
func (*Scans) History ¶
History returns an app's scans, newest first.
The scans that ran. A reuse (Reuse) is the same scan attached to another revision, and listing it would show one scan twice at the same time.
func (*Scans) Latest ¶
Latest returns the scan that describes what this app is running.
The newest scan of that revision, and failing that the newest scan that belongs to no revision — which is what detection produces, from the source, before there is a revision to attach it to. Never another revision's: a score for a spec the app is not running is not a score of what is deployed.
The fallback is the difference between "scanned at discovery" meaning something and meaning nothing: accepting a proposal pins a revision, and without this the scan taken of that very source a minute earlier disappeared behind "this app has not been scanned yet".
func (*Scans) LiveSecurityState ¶
func (s *Scans) LiveSecurityState(ctx context.Context, t SecurityThreshold) ([]SecurityState, error)
LiveSecurityState returns the apps the security pass has something to decide about under threshold t.
Archived apps are not in it, and neither are drafts: an app that has never been deployed cannot be running below a threshold. Of the rest, an app is in it when it is marked — insecure, or stopped for being insecure, since an app Pando stopped has to be looked at again to be started again — or when its score does not meet t, including when it has none. An unmarked app that meets the threshold is one the pass would look at and leave alone, and over twenty thousand apps that is nearly all of them (issue #72).
One set-based query: the pinned revision's newest scores are kept on the app by trigger (migration 60), so there is no lookup into app_scans per app. The verdict is still security.Evaluate's, in the pass; this only leaves out apps it could not act on.
func (*Scans) MarkInsecure ¶
MarkInsecure records when an app was first found below the threshold.
Idempotent on purpose: the pass runs every tick, and the grace period is measured from the first time it was true, not the most recent (R-316). A clock that restarted on every pass would be a grace period that never expired.
func (*Scans) Record ¶
Record writes a scan.
Every scan, including a failed one. A scanner that could not run is a fact somebody has to see — silence would leave an unscannable app looking clean (R-318).
func (*Scans) Reuse ¶
Reuse attaches an earlier scan to another revision, as a new row with the same findings, score and ran_at, naming the scan it repeats.
A revision's score is the newest scan of that revision (Latest). A deploy of a new revision of an unchanged source does not scan again, and without this the revision would have no scan: refused as never scanned where a threshold is set, and shown as "not scanned" everywhere else.
func (*Scans) SecurityStateFor ¶
SecurityStateFor returns one app's standing.
func (*Scans) SetStoppedForSecurity ¶
SetStoppedForSecurity records whether Pando is the reason this app is stopped.
The difference decides whether a recovered score starts it again: an app its owner stopped stays stopped (design 09 §4.2).
type SecretKey ¶
type SecretKey struct {
Key string `json:"key"`
Version int `json:"version"`
UpdatedAt time.Time `json:"updated_at"`
}
SecretKey is one of an app's secrets as a listing shows it: its key and version, never its value.
type Secrets ¶
type Secrets struct {
// contains filtered or unexported fields
}
Secrets stores an app's secret values through a secrets adapter.
The row holds ciphertext or an external reference; there is no plaintext column anywhere (R-190, R-191). Core never encrypts — the adapter does, and core stores what it is handed.
func NewSecrets ¶
func NewSecrets(db *DB, adapter api.SecretsAdapter, adapterRef string) *Secrets
func (*Secrets) Get ¶
Get returns one secret's value, for R-083's separable read.
One key rather than reusing Resolve, which decrypts every secret an app has: reading one value should not require decrypting the rest, and the audit event this backs names a single key.
func (*Secrets) Keys ¶
Keys lists an app's secret keys and versions. Never values — reading a value is a separate, audited endpoint (R-083).
func (*Secrets) Put ¶
Put stores or rotates a secret.
Rotation increments version, which is how the reconciler learns a restart is required (R-193). The value itself never appears in a log or an error, because it is a secret.Value the whole way through.
func (*Secrets) Resolve ¶
Resolve decrypts an app's secrets for a deploy.
Called once, at step 11 of the pipeline, to materialize WorkloadPlan.Env. It is the only place values leave storage on a deploy path.
func (*Secrets) VerifyKey ¶
VerifyKey proves the secrets adapter holds the install's key, and refuses to go on if it does not (R-190, issue #72).
The local adapter's key lives beside the process, not in the database — a key stored with the ciphertext it protects protects nothing. So several replicas must each be given the same key, and one given a different key, or one that generated its own because its volume was empty, used to start normally and then write secrets that no other replica could read. Nothing noticed until a deploy on another replica failed to decrypt them.
The first replica seals a random value and records its digest; every replica after opens it at start. An adapter that seals nothing locally — one that keeps secrets in an external store and hands back a reference — has no local key to differ, and is not checked.
func (*Secrets) Versions ¶
Versions returns each secret's version, without decrypting anything.
This is what the environment fingerprint is built from (R-193). Drift detection runs on every tick for every app, and it must never be a reason to decrypt a secret — the version answers "did this change", which is the only question being asked.
type Section ¶
type Section struct {
ID string `json:"id"`
Name string `json:"name"`
CreatedAt time.Time `json:"created_at"`
}
Section is one of a person's launcher groupings.
type SecurityState ¶
type SecurityState struct {
AppID string
InsecureSince *time.Time
StoppedForSecurity bool
DesiredState string
State string
PinnedSpecID string
OwnerUserID string
Name string
// Score and ScoreFixable are the pinned revision's newest scan's two
// numbers, as Scans.Latest would find it: what the security pass places
// against policy, read in the same query so the pass is one query rather
// than three per app (issue #72).
Score *int
ScoreFixable *int
}
SecurityState is an app's standing against the installation's threshold.
type SecurityThreshold ¶
SecurityThreshold is what the security pass places apps against: host policy's minimum score, and whether it reads the score of fixable findings.
type ServiceInstance ¶
type ServiceInstance struct {
ID string `json:"id"`
AppID string `json:"app_id"`
SlotKey string `json:"slot_key"`
SlotType spec.SlotType `json:"slot_type"`
AdapterRef string `json:"adapter_ref"`
Handle string `json:"handle"`
ConnectionRef string `json:"secret_key"`
}
ServiceInstance is one provisioned service.
type Services ¶
type Services struct {
// contains filtered or unexported fields
}
Services records which provisioned service fills which slot (R-131).
func NewServices ¶
func (*Services) NewID ¶
NewID returns an ID for a service about to be provisioned.
Minted before the adapter is called and passed into Provision, so a deploy that dies between the adapter answering and the row being written can retry with the same ID and get the same names back. Without that, the retry stands up a second database and the first becomes a volume nobody can name.
func (*Services) Record ¶
func (s *Services) Record(ctx context.Context, v ServiceInstance) error
Record stores a newly provisioned service.
ON CONFLICT DO NOTHING rather than DO UPDATE: if a row already fills this slot, that row names the database holding the app's data and the caller's newer instance is an empty one from a retry. Overwriting would point the app at the empty database and leave the real one unreferenced.
type Session ¶
type Session struct {
ID string `json:"id"`
UserID string `json:"user_id"`
AdapterID string `json:"adapter_id"`
ExpiresAt time.Time `json:"expires_at"`
}
Session is an active login.
type Sessions ¶
type Sessions struct {
// contains filtered or unexported fields
}
Sessions are server-side rows; the cookie carries only ses_… (design 02 §2.7).
func NewSessions ¶
func (*Sessions) Active ¶
Active returns a session if it exists, is unrevoked, and has not expired.
The API checks this on every request. The proxy reads it with the account and groups in one query (ActiveWithUser) and keeps the answer until a NOTIFY says something changed, never more than 30 seconds — a bound that is in design 06 §3.1's revocation window, which is a stated number rather than an accident (issue #93).
func (*Sessions) ActiveWithUser ¶
func (s *Sessions) ActiveWithUser(ctx context.Context, sessionID string) (Session, User, []string, bool, error)
ActiveWithUser is Sessions.Active, Users.ByID and the user's groups in one query: a session unrevoked and unexpired, the account it belongs to, and the groups that account is in through the effective_group_members view, read live as R-079 requires. found is false where Active would have found no session or ByID no user.
func (*Sessions) Create ¶
func (s *Sessions) Create(ctx context.Context, userID, adapterID string, lifetime time.Duration, userAgent, ip string) (Session, error)
Create issues a session.
func (*Sessions) RevokeAllForUser ¶
RevokeAllForUser ends every session a user holds. Used when an adapter pushes a revocation (R-048), which is what makes immediate revocation possible.
func (*Sessions) RevokeOthersForUser ¶
RevokeOthersForUser ends every session a user holds except one.
For a password change: the sessions someone did not know about should end, and the one they are typing in should not — being signed out by your own password change teaches people that changing it is a risky thing to do.
type Subscription ¶
type Subscription struct {
ID string `json:"id"`
// OwnerID is the person who owns it; OwnerTokenID the account token, for
// one an account token made (R-060). Exactly one is set.
OwnerID string `json:"owner_id,omitempty"`
OwnerTokenID string `json:"owner_token_id,omitempty"`
OwnerName string `json:"owner_name,omitempty"`
AppID string `json:"app_id,omitempty"`
AppName string `json:"app_name,omitempty"`
Events []string `json:"events"`
Destination string `json:"destination"`
URL string `json:"url,omitempty"`
AdapterID string `json:"adapter_id,omitempty"`
// How a webhook is sent (R-375). Header values are sealed in
// subscription_secrets; only their names are here.
Method string `json:"method,omitempty"`
ContentType string `json:"content_type,omitempty"`
PayloadTemplate string `json:"payload_template,omitempty"`
HeaderNames []string `json:"header_names,omitempty"`
Description string `json:"description"`
Enabled bool `json:"enabled"`
DisabledReason string `json:"disabled_reason,omitempty"`
FailingSince *time.Time `json:"failing_since,omitempty"`
ConsecutiveFailures int `json:"consecutive_failures"`
CreatedBy string `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Subscription is which events, about what, sent where (R-367).
type SubscriptionFilter ¶
type SubscriptionFilter struct {
// OwnerID is a person's ID or an account token's: both columns are
// matched, and the two kinds of ID cannot collide.
OwnerID string
AppID string
// InstallOnly keeps only subscriptions with no app.
InstallOnly bool
// EnabledOnly keeps only subscriptions that are on.
EnabledOnly bool
}
SubscriptionFilter narrows a list. Empty fields do not narrow.
type SubscriptionPatch ¶
type SubscriptionPatch struct {
Events *[]string
URL *string
AdapterID *string
Description *string
Enabled *bool
Method *string
ContentType *string
PayloadTemplate *string
HeaderNames *[]string
}
SubscriptionPatch is a partial update. Nil fields are left alone.
type SubscriptionSecrets ¶
type SubscriptionSecrets struct {
// contains filtered or unexported fields
}
SubscriptionSecrets stores a webhook's secrets — its signing key and the values of its custom headers — sealed by the secrets adapter (R-371, R-375, R-190). The same arrangement as AdapterCredentials: ciphertext or an external reference, never the value, and no column on the subscription a value could be put in.
func NewSubscriptionSecrets ¶
func NewSubscriptionSecrets(db *DB, adapter api.SecretsAdapter, adapterRef string) *SubscriptionSecrets
func (*SubscriptionSecrets) All ¶
func (k *SubscriptionSecrets) All(ctx context.Context, subscriptionID string) (map[string]secret.Value, error)
All opens every secret a subscription has, keyed by field, for one send.
func (*SubscriptionSecrets) DeleteHeaders ¶
func (k *SubscriptionSecrets) DeleteHeaders(ctx context.Context, subscriptionID string) error
DeleteHeaders removes every stored header value, before a new set is put.
type Subscriptions ¶
type Subscriptions struct {
// contains filtered or unexported fields
}
Subscriptions stores subscriptions.
func NewSubscriptions ¶
func NewSubscriptions(db *DB) *Subscriptions
func (*Subscriptions) Create ¶
func (s *Subscriptions) Create(ctx context.Context, sub Subscription) (Subscription, error)
Create stores a subscription. s.ID must be set.
func (*Subscriptions) Delete ¶
func (s *Subscriptions) Delete(ctx context.Context, subscriptionID string) error
Delete removes a subscription, its key and its deliveries.
func (*Subscriptions) Disable ¶
Disable turns a subscription off and says why. It reports whether this call is the one that did it, so the owner is told once.
func (*Subscriptions) Enabled ¶
func (s *Subscriptions) Enabled(ctx context.Context) ([]Subscription, time.Time, error)
Enabled returns every enabled subscription, and the database's time just before it read them: a subscription made before asOf is in the list, give or take a transaction still committing (issue #72, the dispatcher's cache).
func (*Subscriptions) Failed ¶
func (s *Subscriptions) Failed(ctx context.Context, subscriptionID string, at time.Time) (time.Time, int, error)
Failed counts one more failed attempt and returns when the run of failures began and how long it is.
func (*Subscriptions) Get ¶
func (s *Subscriptions) Get(ctx context.Context, subscriptionID string) (Subscription, bool, error)
Get reads one subscription.
func (*Subscriptions) List ¶
func (s *Subscriptions) List(ctx context.Context, f SubscriptionFilter) ([]Subscription, error)
List returns every subscription the filter matches, newest first.
func (*Subscriptions) ListPage ¶
func (s *Subscriptions) ListPage(ctx context.Context, f SubscriptionFilter, page Page) ([]Subscription, string, error)
ListPage is one page of List, keyset-paged on (created_at, id), with the cursor for the next page (empty after the last).
func (*Subscriptions) Succeeded ¶
func (s *Subscriptions) Succeeded(ctx context.Context, subscriptionID string) error
Succeeded clears a subscription's run of failures.
func (*Subscriptions) Update ¶
func (s *Subscriptions) Update(ctx context.Context, subscriptionID string, p SubscriptionPatch) (Subscription, error)
Update applies a patch. Turning a subscription on clears the record of its failures, so an endpoint somebody fixed starts with a clean slate.
type TeardownTarget ¶
type TeardownTarget struct {
AppID string
RuntimeRef string
RoutingRef string
// BuilderRef is the builder that holds the app's build cache, so the
// teardown can have it forgotten.
BuilderRef string
// DiscardStorage says the delete settled the app's storage — discarded,
// or backed up first — so its volumes go with the bundle.
DiscardStorage bool
}
TeardownTarget is a deleted app whose bundle still exists.
type Token ¶
type Token struct {
ID string `json:"id"`
Kind string `json:"kind"`
Name string `json:"name"`
OwnerUserID string `json:"owner_user_id,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
RevokedAt *time.Time `json:"revoked_at,omitempty"`
}
Token is a stored token, without its secret.
type Tokens ¶
type Tokens struct {
// contains filtered or unexported fields
}
Tokens stores API tokens.
func NewTokens ¶
NewTokens returns the token store. key is the install's API token key (tokenkey.LoadOrCreate), and must be tokenkey.Size bytes: a store without one could neither issue a token nor check one, and finding that out at the first request is later than at start.
func (*Tokens) Active ¶
Active returns a token that is neither revoked nor expired, by ID alone.
For authorizing work a token set going rather than a request it is making now — an account token's event subscription (R-368) — where there is no secret to present. Not a way to authenticate.
func (*Tokens) Authenticate ¶
Authenticate resolves a presented token string.
The string is "<id>.<secret>". Splitting on the ID lets the digest be looked up directly rather than compared against every row.
Every failure returns the same error. Distinguishing "no such token" from "wrong secret" would let a caller enumerate valid token IDs.
func (*Tokens) Create ¶
func (t *Tokens) Create(ctx context.Context, kind, name, ownerUserID, createdBy string, expiresAt *time.Time) (Issued, error)
Create mints a token.
A delegated token requires an owner and holds no grants of its own: it resolves through the owner on every request (R-058, R-059). An account token has no owner and is its own principal in grants (R-060). The database enforces both shapes, so a caller cannot produce a token that is neither.
func (*Tokens) ListAccounts ¶
ListAccounts lists account-level tokens (R-060).
Separate from ListForUser because these have no owner: an account token is its own principal, appears in grants under its own ID, and outlives whoever created it. "Every token in the install" is not a list anyone needs and is not offered — a delegated token belongs to the person who made it, and showing all of them to an administrator would make a screen out of other people's credentials.
func (*Tokens) ListForUser ¶
ListForUser returns a user's own tokens, newest first.
Delegated tokens only — an account token has no owner and belongs to whoever administers the install, not to a person (R-060). Revoked ones are included, because "this token was revoked" is what someone checking after an incident needs to see; hiding them would make the list agree with nothing.
func (*Tokens) VerifyKey ¶
VerifyKey proves this process holds the install's API token key, and refuses to go on if it does not (R-063, issue #72).
The key lives beside the process, not in the database — a key stored with the digests it protects protects nothing. So several replicas must each be given the same key file, and one given a different key, or one that generated its own because its volume was empty, would issue tokens no other replica accepts and reject every token the others issued.
The first replica records HMAC(key, tokenKeyLabel); every replica after compares its own. The same shape as the secrets key check (Secrets.VerifyKey).
type User ¶
type User struct {
ID string `json:"id"`
AdapterID string `json:"adapter_id"`
ExternalID string `json:"external_id"`
Email string `json:"email,omitempty"`
DisplayName string `json:"display_name,omitempty"`
Status string `json:"status"`
MustChangePassword bool `json:"must_change_password"`
// AliasOf is set on an account whose identities an administrator moved to
// another (O-1). It stays, suspended, because its ID may be an assertion
// subject some app holds data under (R-054).
AliasOf string `json:"alias_of,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
User is a stored principal.
Tagged because this type is serialized directly by the API, and every other type on the wire is lower_snake_case. An untagged struct would put Go field names in the API surface — where they would then be a compatibility promise.
type UserAppGrant ¶
type UserAppGrant struct {
AppID string `json:"app_id"`
AppName string `json:"app_name"`
AppOwner string `json:"-"`
GrantID string `json:"grant_id"`
Plane string `json:"plane"`
RoleID string `json:"role_id,omitempty"`
RoleName string `json:"role_name,omitempty"`
Via string `json:"via"` // user or group
GroupID string `json:"group_id,omitempty"`
GroupName string `json:"group_name,omitempty"`
}
UserAppGrant is one grant that gives a person something on an app, directly or through a group they are in.
type Users ¶
type Users struct {
// contains filtered or unexported fields
}
Users reads and writes user records.
func (*Users) ByExternalID ¶
ByExternalID resolves an adapter's subject to a Pando user.
func (*Users) ByUsername ¶
ByUsername implements local.Users. Username is the local adapter's external_id.
func (*Users) ClaimFirst ¶
func (u *Users) ClaimFirst(ctx context.Context, setupToken secret.Value, username, displayName, passwordHash, roleID string) (User, string, error)
ClaimFirst creates the installation's first account and makes it an administrator, if and only if there is no account yet (R-046).
One transaction, serialized by an advisory lock, so two people submitting the setup form at the same moment cannot both become the first administrator: the second waits, then finds an account and is refused.
The claimant must present the setup token Pando printed to its log (issue #130), checked and used up inside the same transaction.
func (*Users) ClearSetupToken ¶
ClearSetupToken removes the setup token. The caller holds FirstAccountLock and has found or made an account, so the installation is set up and the token would only be a way in that nothing needs.
func (*Users) Create ¶
func (u *Users) Create(ctx context.Context, adapterID, externalID, email, displayName, passwordHash string, mustChange bool) (User, error)
Create inserts a user and returns it.
func (*Users) Delete ¶
Delete soft-deletes a user, which is what fires R-282's destruction rules.
Soft, because the audit log references this ID and R-054 makes users.id the assertion subject apps key their own data on. A hard delete would orphan both — the audit trail would name an ID nothing explains, and an app would be holding rows for a person Pando can no longer describe.
func (*Users) EnsureLocalAdapter ¶
EnsureLocalAdapter seeds the local identity adapter on a fresh install.
func (*Users) EnsureSetupToken ¶
EnsureSetupToken makes the setup token if there is none, and returns it. When one already exists it returns "" and when that one was made: its value cannot be recovered, only replaced.
The caller holds FirstAccountLock and has found no account (bootstrap.Run), so replicas starting together make one token between them, and a token is never made for an installation that is already set up.
func (*Users) List ¶
List returns every account, newest first.
Includes suspended accounts and excludes deleted ones: suspension is not deletion (R-049), and an administrator managing accounts needs to see the suspended ones — they are the ones most likely to need attention.
Unpaginated, for the one reader that needs every account at once (the access assistant). The API pages with ListPage: an organization of 100,000 people is not a screenful (issue #72).
func (*Users) ListPage ¶
ListPage is one page of List, newest first, with the cursor for the next page (empty after the last) and how many accounts match in all.
Page.Query matches the username, display name or email, as Search does.
func (*Users) NeedsSetup ¶
NeedsSetup reports whether the installation has no account yet.
func (*Users) ReplaceSetupToken ¶
ReplaceSetupToken makes a new setup token in place of any there was, for an operator who no longer has the one Pando printed. Refused with ErrAlreadySetUp once any account exists.
Under FirstAccountLock, as ClaimFirst is, so a claim cannot land between the check for accounts and the write and leave a token behind a set-up install.
func (*Users) ResetPassword ¶
ResetPassword sets a local account's password from outside any session, and requires the holder to change it again at the next sign-in.
Separate from SetPassword, which clears must_change_password because the person changing it is the person who chose it. Here somebody else chose it: it was handed over out of band, so it is a way back in rather than a credential, and R-046's rule applies for the same reason it applies on first run.
Returns the user ID so the caller can end that account's sessions and record what it did. A reset that leaves a stolen session alive has not reset anything.
func (*Users) Search ¶
Search finds active and suspended accounts by username, name or email, case-insensitively, at most limit of them, ordered by username. An empty query lists the first few.
func (*Users) SetPassword ¶
SetPassword replaces a local account's password and clears the must-change-on-first-login flag (R-046).
The flag is cleared here rather than by a separate call because there is no state in which a person has chosen their own password and must still change it. Two statements would make that state reachable by forgetting one.
Local accounts only. An external identity provider owns its own credentials (R-044), and a password Pando could change would be a second copy of one.
func (*Users) SetPasswordFor ¶
func (u *Users) SetPasswordFor(ctx context.Context, userID, passwordHash string, mustChange bool) (bool, error)
SetPasswordFor sets a local account's password on an administrator's behalf, and whether its holder must change it at the next sign-in.
Returns false when there is no such local account: an external identity provider owns its own credentials (R-044).
func (*Users) SetStatus ¶
SetStatus changes a user's status.
Suspension is not deletion (R-049): this is the endpoint behind PATCH, and it must never trigger the destruction rules that DELETE does (R-282).
func (*Users) SetStatusBy ¶
SetStatusBy is SetStatus on someone's behalf: an administrator, or a SCIM client. The R-088 lockout rule applies to both.
func (*Users) UpdateProfile ¶
UpdateProfile changes an account's username, name or email.
Username and email only on a local account: an external identity provider owns those, and a change here would be overwritten at the next sign-in — or worse, would make the account stop matching its subject (R-054 is about users.id, which never changes, but the provider matches on external_id).
type VolumeRecord ¶
VolumeRecord is one volume as the runtime reports it.
type Volumes ¶
type Volumes struct {
// contains filtered or unexported fields
}
Volumes records storage attached to apps.
func NewVolumes ¶
func (*Volumes) DeleteForApp ¶
DeleteForApp removes an app's volume rows.
Only reached once the caller has resolved the keep-or-discard decision (R-204). The ON DELETE RESTRICT on volumes.app_id means an app cannot be archived while rows remain, so this is the explicit step that constraint forces — which is exactly why the constraint is there.
func (*Volumes) Handles ¶
Handles reports each of an app's recorded volumes and the runtime handle it was last seen under — empty for a volume that was never materialized.
A non-empty handle is the distinction R-203 turns on. A volume row with a handle was created in the runtime and may hold data; recreating it after it disappears produces an empty replacement and an app that comes up healthy having lost everything — the failure that looks exactly like success. A row without a handle has never existed anywhere, so creating it loses nothing.
func (*Volumes) RecordFromRuntime ¶
func (v *Volumes) RecordFromRuntime(ctx context.Context, appID, adapterRef string, observed []VolumeRecord) error
RecordFromRuntime writes the volume rows for an app's spec volumes, using the handles the runtime reports.
Nothing used to do this. The spec declared volumes, the planner planned them, the runtime created them — and Pando's own `volumes` table stayed empty. Three things silently did nothing as a result: `ON DELETE RESTRICT` protected no rows, so R-204's "volumes survive app deletion" was a constraint on an empty table; deleting an app never offered to keep a backup, because it counted zero volumes; and the DR bundle contained no app data at all, because the query that finds volumes to snapshot found none.
The ID is the spec's volume ID, not a generated one. A spec that says `vol_data` and a row that says `vol_01HQ8…` are two names for one thing, and a restore matching them up is a join nobody wrote.
Idempotent: every deploy re-records, and the handle is refreshed in case the runtime's own naming changed under it.
Keyed by app and ID together (migration 000036). The ID is only unique within an app — two apps may both keep a volume called "data" — and keyed by ID alone, the second app's deploy overwrote the first app's row and recorded nothing for its own, so it was never backed up (issue #87).
type WorkloadImage ¶
type WorkloadImage struct {
Ref string `json:"ref,omitempty"`
Digest string `json:"digest,omitempty"`
}
WorkloadImage is what one part of an app ran.
Source Files
¶
- adapter_credentials.go
- adapters.go
- addresses.go
- ai_assignments.go
- applimits.go
- apps.go
- audit_sinks.go
- authz.go
- autodeploy.go
- backups.go
- db.go
- deployments.go
- detections.go
- disk.go
- doc.go
- edge_certificates.go
- events.go
- failed_attempts.go
- groups.go
- idempotency.go
- identity.go
- identity_providers.go
- idle.go
- jsonb.go
- launcher.go
- listen.go
- notifications.go
- page.go
- passcode.go
- ports.go
- proxy_reads.go
- reconcile.go
- replicas.go
- retention.go
- rows.go
- running.go
- scans.go
- scim_users.go
- secrets.go
- secrets_canary.go
- services.go
- setup_token.go
- snapshot.go
- sso_flows.go
- synced_groups.go
- template.go
- token_key_check.go
- tokens.go
- user_identities.go