Documentation
¶
Overview ¶
Package approval is deploy approval (R-154 – R-159): whether a deploy needs somebody's sign-off, and the request, approval, rejection and expiry of one.
Index ¶
- func BlocksAutoDeploy(doc policy.Document, appID string, s *spec.AppSpec) bool
- type Approvers
- type Authorizer
- type Capacity
- type Notifier
- type Planner
- type PolicyLoader
- type Reason
- type Service
- func (s *Service) Approve(ctx context.Context, p authz.Principal, appID, depID, comment string) (state.Deployment, error)
- func (s *Service) AutoDeployPaused(ctx context.Context, appID string, pinned *spec.AppSpec) (bool, error)
- func (s *Service) Awaiting(ctx context.Context, p authz.Principal, page state.Page, decidable bool) ([]state.AwaitingApproval, string, error)
- func (s *Service) CanDecide(ctx context.Context, p authz.Principal, appID string) (bool, error)
- func (s *Service) Deploy(ctx context.Context, p authz.Principal, app state.App, rev state.Revision, ...) (state.Deployment, error)
- func (s *Service) Describe(ctx context.Context, p authz.Principal, deps []state.Deployment) error
- func (s *Service) ExpireDue(ctx context.Context) (int, error)
- func (s *Service) Needed(ctx context.Context, app state.App, rev state.Revision, trigger string) ([]Reason, error)
- func (s *Service) NeededFor(ctx context.Context, app state.App, next *spec.AppSpec) ([]Reason, error)
- func (s *Service) Reject(ctx context.Context, p authz.Principal, appID, depID, comment string) (state.Deployment, error)
- func (s *Service) RunExpiry(ctx context.Context, every time.Duration)
- type Starter
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func BlocksAutoDeploy ¶
BlocksAutoDeploy reports whether an app's deploys need approval whatever they change, which is what refuses auto-deploy (R-158). An egress loosening is not one of these reasons: it needs approval only for the deploy that introduces it, and an automatic deploy changes the commit and nothing else.
Types ¶
type Authorizer ¶
type Authorizer interface {
CheckControl(ctx context.Context, p authz.Principal, appID string, verb authz.Verb) error
Allows(ctx context.Context, p authz.Principal, appID string, verb authz.Verb) (bool, error)
AllowsInstall(ctx context.Context, p authz.Principal, verb authz.Verb) (bool, error)
}
Authorizer is the part of authz.Authorizer approval asks. Allows and AllowsInstall check without auditing a denial; CheckControl audits one.
type Capacity ¶
type Capacity interface {
Hold(ctx context.Context, runtimeRef string, fn func(context.Context) error) error
}
Capacity holds a runtime's capacity while fn runs (state.Allocations.Hold).
type Notifier ¶
type Notifier interface {
Notify(ctx context.Context, n api.Notification) error
}
Notifier tells people something through the notification adapters.
type PolicyLoader ¶
PolicyLoader reads host policy as it is now. Read per call, never cached (R-274).
type Reason ¶
type Reason string
Reason is why a deploy needs approval. More than one can apply, and the console and the audit log name each.
const ( // ReasonInstall: host policy requires approval for every app. ReasonInstall Reason = "install" // ReasonAppPolicy: host policy requires it for this app, and its owner // cannot turn that off. ReasonAppPolicy Reason = "app_policy" // ReasonAppSpec: the app requires it of itself, in the running spec or // the one being deployed. ReasonAppSpec Reason = "app_spec" // ReasonEgressLoosening: the deploy loosens the install's egress rules // and policy says loosening needs approval (R-183). ReasonEgressLoosening Reason = "egress_loosening" )
func Reasons ¶
Reasons is why deploying next over running needs approval, or nothing when it does not (R-154). running is the app's pinned spec, nil for an app that has never had one.
The app's own requirement is read from both specs, so that turning it off is itself a deploy that needs approval. An egress loosening counts only when it is new: one the running spec already carries was approved when it was deployed, and a later push under it is not asking for anything more.
type Service ¶
type Service struct {
Deployments *state.Deployments
Apps *state.Apps
Authz Authorizer
Policy PolicyLoader
Planner Planner
Deployer Starter
// Capacity serializes a deploy's plan-time capacity check with its
// creation, per runtime (R-242). Optional: without it the two are not
// serialized, which is right only where nothing deploys concurrently.
Capacity Capacity
// Audit writes an event. Failing to write one is logged by whoever
// supplies it, never turned into a failure of the action.
Audit func(ctx context.Context, e audit.Event)
// Notifier and Approvers are optional: without them nobody is told, and
// everything else works (R-159's notification is best effort).
Notifier Notifier
Approvers Approvers
Clock clock.Clock
Logger *zap.Logger
}
Service starts deploys, and asks for, records and acts on approval of the ones that need it (R-154 – R-159).
Every way a deploy starts goes through here — a person's deploy, a rollback, and the last approval of a request — so that "does this need approval" has one answer, and a deploy that needed it cannot start any other way. The HTTP handlers and, through them, the CLI and MCP tools are clients of it (R-261).
func (*Service) Approve ¶
func (s *Service) Approve(ctx context.Context, p authz.Principal, appID, depID, comment string) (state.Deployment, error)
Approve records p's approval of a deploy waiting for one, and starts it when that is the last approval it needs (R-156).
The plan runs again first, because policy may have changed while the request waited. A plan that now fails ends the request as failed with the plan's own error, rather than leaving it waiting: no number of further approvals can make it pass, and the fix — changing the spec — is a new revision and so a new request. A failure to plan at all (an adapter that cannot be reached, an internal error) is not an answer about the revision, so the request stays waiting and nothing is recorded.
An approval that would start the deploy while another deploy of the app is in flight is refused, and the request keeps waiting: two deploys racing on one bundle is how an app ends up in a state neither intended.
func (*Service) AutoDeployPaused ¶
func (s *Service) AutoDeployPaused(ctx context.Context, appID string, pinned *spec.AppSpec) (bool, error)
AutoDeployPaused reports whether an app's pinned spec asks for auto-deploy and approval now stops it (R-158): what the console says on the app.
func (*Service) Awaiting ¶
func (s *Service) Awaiting(ctx context.Context, p authz.Principal, page state.Page, decidable bool) ([]state.AwaitingApproval, string, error)
Awaiting lists one page of the deploys waiting for approval on every app p may view, oldest first, each saying whether p may decide it, and the cursor for the next page (empty after the last). A page may hold fewer than its limit and still have a cursor, when the requests read were on apps p cannot see.
decidable keeps only the requests p may approve or reject (R-155): the console's Approvals screen and its count, which list what is waiting on the caller. A request waiting on somebody else is still on its app.
func (*Service) CanDecide ¶
CanDecide reports whether p may approve or reject the app's deploys, without auditing anything: what the console shows, not an attempt.
func (*Service) Deploy ¶
func (s *Service) Deploy(ctx context.Context, p authz.Principal, app state.App, rev state.Revision, trigger string) (state.Deployment, error)
Deploy deploys rev, or, when the deploy needs approval, records a request for it and tells the people who can approve (R-154). The deployment comes back either way: pending and running, or awaiting_approval.
The plan runs first, so a deploy that cannot succeed is refused here rather than part-way through — and is never put in front of an approver.
func (*Service) Describe ¶
Describe fills in what the store leaves to the service: what each approval reason means, and whether p may decide each deploy that is waiting.
func (*Service) ExpireDue ¶
ExpireDue ends every request whose wait has run out, and records each (R-159). Run on a timer; Approve and Reject also expire the one they are asked about, so a request is never approved after its time.
func (*Service) Needed ¶
func (s *Service) Needed(ctx context.Context, app state.App, rev state.Revision, trigger string) ([]Reason, error)
Needed is whether deploying rev over what the app runs now needs approval, and why: what the plan endpoint shows before anyone deploys.
func (*Service) NeededFor ¶
func (s *Service) NeededFor(ctx context.Context, app state.App, next *spec.AppSpec) ([]Reason, error)
NeededFor is Needed for a spec that has not been saved: what deploying a draft would need, for a dry-run save to show before anything is written.