controller

package
v1.76.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 13, 2026 License: MIT Imports: 30 Imported by: 0

Documentation

Overview

Package controller is the Slack controller: for every workspace the policy declares, read what Slack holds and who holds the groups the channels are bound to, decide, act where the workspace is enabled, and report.

It has no listener. It reads the console's API with its own ServiceAccount token, writes to Slack with each workspace's bot token, writes its reports through the blob port, and records what it changed to the audit installation as its own workload. It holds nothing of the issuer's: no signing key, no session store, no directory credential.

Ticks

The unit of work is a tick of one workspace (docs/decisions/0029), run under a lease on it (Controller.RunTarget) and publishing its own report and no other. A pass is a sweep of every workspace, run on the interval and when a credential changed; an operator's request, or the console's write, ticks only its workspace.

One tick

  1. The inputs every workspace's tick shares are read once and kept, by the policy digest and what is mounted ([Controller.inputs]): the mounted credentials and the console's records (bot tokens, each installed workspace's bot user id, the shared channels' definitions and the console's ordinary channels, validated against the policy, the refused reported, and the operators' confirmations that are still current), the holders of every bound group, who is in every DIRECTORY group the console channels and the shared channels name (nested groups expanded; a record whose source is not a group of an allowed directory is refused and reported; a directory that cannot be read fails only the workspaces that depend on one), and the domains each directory serves.
  2. The workspace: observe Slack whole, derive, ask the directory to vouch for each address a removal or a leaver report rests on (one question per address however many ticks name it), decide.
  3. An enabled workspace (rails.Switch) carries the decision out and records what became of it; every other workspace is a dry run, which changes and records nothing.
  4. A workspace that hosts a managed Slack Connect channel probes the guest sides its bot does not list, reading the guests' reports ([Controller.probeGuestSides]), and asks the guests to tick when it invited one.
  5. The workspace's report is published. A workspace whose tick failed keeps its previous report with the failure on it, and no other workspace is affected.

Index

Constants

This section is empty.

Variables

View Source
var ErrUnknownTarget = errors.New("not a target of this controller")

ErrUnknownTarget is a target that is not a workspace the policy declares.

Functions

This section is empty.

Types

type Config

type Config struct {
	// Interval is how long between passes.
	Interval time.Duration
	// PolicyRetry is how soon a pass that met a console answering under
	// another policy is tried again; each further retry waits twice as
	// long. Zero is five seconds.
	PolicyRetry time.Duration
	// Enabled are the workspaces the controller acts in. Every other
	// declared workspace is derived and reported, and nothing is changed: a
	// workspace is born disabled.
	Enabled map[string]bool
	// CredentialsDir is where the workspaces' credentials are mounted, one
	// file per workspace.
	CredentialsDir string
	// RecordsDir is where the console's records are mounted: each
	// workspace's record, the shared channels' definitions and the
	// operators' confirmations. Empty, or absent, is no records.
	RecordsDir string
	// CredentialPoll is how often the mounted credentials are looked at for
	// a change, which runs a pass at once instead of at the next interval:
	// an install lands as a new bot token, and its result should not wait
	// for the full interval. Zero is thirty seconds; negative turns it off.
	CredentialPoll time.Duration
}

Config is what a deployment decides.

type Controller

type Controller struct {
	// contains filtered or unexported fields
}

Controller is the loop and what it remembers between passes.

func New

func New(cfg Config, deps Deps) *Controller

New returns a controller.

func (*Controller) Pass

func (c *Controller) Pass(ctx context.Context) (otherPolicy bool)

Pass is a sweep: every declared workspace once, each under its own lease and publishing its own report. A workspace another runner holds is that runner's. It starts from fresh inputs, which the ticks of the sweep share. It says whether any answer it was given came from a console under another policy — a pass worth trying again soon, because the difference is usually a rollout that has not finished.

The sweep is also what carries a Slack Connect share to its guest when the host's tick could not notify it (the notification reaches this process only, and the lease may be another's), until the share's own storage exists (docs/decisions/0029, B3).

func (*Controller) Run

func (c *Controller) Run(ctx context.Context) error

Run passes now and then every interval, until the context ends. A pass that met a console answering under another policy is tried again soon (see rails.Run).

Beside the interval, a notification for a workspace (the trigger) ticks that workspace, and the mounted credentials are watched (see [Controller.watchCredentials]): a changed bot token runs a pass, and an operator's request ticks its workspace, within the poll period.

func (*Controller) RunTarget

func (c *Controller) RunTarget(ctx context.Context, target string) (ran, otherPolicy bool, err error)

RunTarget ticks one workspace under its lease and says whether it ran: false, with no error, when another runner holds the lease. The tick's context ends if the lease is lost, so it stops before its next write.

func (*Controller) Targets

func (c *Controller) Targets() []string

Targets are what a pass ticks: every declared workspace by key.

func (*Controller) Tick

func (c *Controller) Tick(ctx context.Context, target string) (otherPolicy bool, err error)

Tick is one workspace's work, with no lease (see Controller.RunTarget): its pass, ending in its own report whatever happened, which it publishes. The bool says whether an answer came from a console under another policy.

type Deps

type Deps struct {
	Log    *slog.Logger
	Access directoryrosterv1connect.AccessServiceClient
	// Audit is where the controller records what it did: the audit
	// installation, with this workload's own identity. Nil records nothing.
	Audit  audit.Recorder
	Status StatusWriter
	// Policy is the declared policy the controller decides with, and Digest
	// the digest of it. The console's answers carry the digest of the policy
	// it computed them under, and a pass acts only on answers from the same
	// one: in a rollout the controller and the console restart at different
	// moments, and a group the new policy binds has, under the old one,
	// nobody in it.
	Policy policy.Policy
	Digest string
	// Slack makes the client that acts with a bot token. Nil is Slack's own
	// API.
	Slack func(token string) *slackapp.Client
	// Leases takes a lease on each workspace before its tick, from the State
	// port. Nil runs every tick without one: a single runner, which is what
	// a deployment with no shared State has (see docs/concepts/sluis/ports.md).
	Leases *rails.Leases
	// Trigger says that a workspace has work: a notification runs that
	// workspace's tick. Nil is an in-process trigger.
	Trigger port.Trigger
	// Records is where the workspaces' records and credentials are read from
	// when they are kept on the State port. Nil reads the mounted
	// directories of [Config].
	Records RecordSource
	// Handoff carries a Slack Connect share from the host's tick to the guest's
	// (see [Handoff]). Nil keeps the hint alone: the host asks the guest to
	// tick, and a lost hint is answered at the guest's next sweep.
	Handoff Handoff
	// Members remembers who a channel's member is across passes (see
	// [apply.MemberCache]). Nil asks Slack every pass.
	Members apply.MemberCache
	Now     func() time.Time
}

Deps are what the controller talks to.

type Handoff

type Handoff interface {
	// Offer records that host invited guest to the channel, and asks the
	// guest to tick.
	Offer(ctx context.Context, host, channel, channelID, guest, inviteID string, at time.Time) error
	// Accepted records that guest accepted the host's share.
	Accepted(ctx context.Context, host, channel, channelID, guest string, at time.Time) error
	// Waiting are the shares offered to guest that it has not accepted.
	Waiting(ctx context.Context, guest string) ([]connection.PendingShare, error)
}

Handoff is where a Slack Connect share is handed from the workspace that hosts the channel to the one it is shared with (internal/portstore.Handoff, on `share.<host>.<channel>`).

type RecordSource

type RecordSource interface {
	// Records are the connected workspaces' records.
	Records(ctx context.Context) ([]connection.Record, error)
	// Credential is one workspace's credential; found is false when it has none.
	Credential(ctx context.Context, workspace string) (credential connection.Credential, found bool, err error)
	// Shared and Channels are the console's Slack Connect and channel records.
	Shared(ctx context.Context) ([]connection.SharedRecord, error)
	Channels(ctx context.Context) ([]connection.ChannelRecord, error)
	// Confirmations are the operators' confirmations that still stand.
	Confirmations(ctx context.Context) (map[string]connection.Confirmation, error)
	// PassRequests are the operators' last requests for a pass now.
	PassRequests(ctx context.Context) (map[string]connection.PassRequest, error)
	// Digest summarises what a change to wakes a pass: the records and the
	// credentials, not the confirmations and the pass requests.
	Digest(ctx context.Context) ([sha256.Size]byte, error)
}

RecordSource is where a pass reads the workspaces' records and credentials from when they are not mounted files: the domain stores on the State port (internal/portstore.SlackSource). Without one the controller reads the two mounted directories, as it always has.

type StatusReader

type StatusReader = rails.Reader

StatusReader is a status store that can give back what the last pass wrote, so a restarted controller does not record again what the previous process recorded. It is the generic rails.Reader.

type StatusWriter

type StatusWriter = rails.Store

StatusWriter replaces the reports. It is the generic rails.Store.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL