schedules

package
v0.1.37 Latest Latest
Warning

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

Go to latest
Published: Sep 15, 2026 License: Apache-2.0 Imports: 25 Imported by: 0

Documentation

Index

Constants

View Source
const (
	FormatFacetHTML = "facet-html"
	FormatFacetPDF  = "facet-pdf"
)

Report formats. Everything except the two facet formats is already produced by the profile export path (query/render.go), so a scheduled report and a downloaded one are the same bytes; the facet formats render a TSX template through the facet service.

View Source
const Kind = "schedule"

Kind is the task kind every scheduled run carries, and the key the runner is registered under with clicky's scheduler.

Variables

View Source
var ErrNotFound = errors.New("not found")

ErrNotFound is returned when a schedule or run does not exist.

Functions

This section is empty.

Types

type Artifact

type Artifact struct {
	Path        string
	ContentType string
	Filename    string
	Content     []byte
}

Artifact is a rendered report on its way to storage and delivery.

type ArtifactStore

type ArtifactStore interface {
	Save(ctx context.Context, schedule, runID string, artifact Artifact) (Artifact, error)
}

ArtifactStore persists a rendered report where the run history and the delivery link can both reach it.

func NewArtifactStore

func NewArtifactStore(filesystem fs.FilesystemRW, root string) (ArtifactStore, error)

NewArtifactStore stores reports under root on the given filesystem.

type Deliverer

type Deliverer interface {
	Deliver(ctx dbcontext.Context, schedule Schedule, artifact Artifact) ([]DeliveryResult, error)
}

Deliverer sends a produced report to the schedule's channels.

func NewDeliverer

func NewDeliverer(options DelivererOptions) Deliverer

NewDeliverer returns the default report deliverer.

type DelivererOptions

type DelivererOptions struct {
	// BaseURL is the externally reachable root of this server, used to build
	// the artifact links non-SMTP channels receive.
	BaseURL string
}

DelivererOptions configure delivery.

type DeliveryResult

type DeliveryResult struct {
	Connection string    `json:"connection"`
	Channel    string    `json:"channel,omitempty"`
	Sent       bool      `json:"sent"`
	Attached   bool      `json:"attached,omitempty"`
	Error      string    `json:"error,omitempty"`
	At         time.Time `json:"at"`
}

DeliveryResult records what happened when one channel was sent to.

type DeliverySpec

type DeliverySpec struct {
	Connection string `json:"connection" yaml:"connection"`

	Title   string `json:"title,omitempty" yaml:"title,omitempty"`
	Message string `json:"message,omitempty" yaml:"message,omitempty"`

	// Attach embeds the rendered report in the message. Only SMTP carries
	// attachments; every other channel gets a link to the stored artifact, and
	// asking for an attachment on one is refused rather than silently dropped.
	Attach bool `json:"attach,omitempty" yaml:"attach,omitempty"`

	// Properties are per-channel overrides, using the same `channel.key` prefix
	// convention the notification senders read.
	Properties map[string]string `json:"properties,omitempty" yaml:"properties,omitempty"`
}

DeliverySpec sends the result to one channel, named by connection.

type Duration

type Duration time.Duration

Duration is a time.Duration that marshals as a human string ("30m") rather than a nanosecond count, because these values are typed by people.

func (Duration) MarshalJSON

func (d Duration) MarshalJSON() ([]byte, error)

func (*Duration) UnmarshalJSON

func (d *Duration) UnmarshalJSON(data []byte) error

type FacetSpec

type FacetSpec struct {
	Connection   string   `json:"connection,omitempty" yaml:"connection,omitempty"`
	URL          string   `json:"url,omitempty" yaml:"url,omitempty"`
	Timeout      Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"`
	Header       string   `json:"header,omitempty" yaml:"header,omitempty"`
	Footer       string   `json:"footer,omitempty" yaml:"footer,omitempty"`
	TimestampURL string   `json:"timestampUrl,omitempty" yaml:"timestampUrl,omitempty"`
}

FacetSpec points at the facet render service. Connection or URL selects it; when neither is set the local facet binary is used instead.

type Options

type Options struct {
	Store      StoreProvider
	Scheduler  *task.Scheduler
	DecodeBody profiles.BodyDecoder

	// Runner and Deliverer power the trigger and test-delivery actions. Both
	// are optional; without them those actions refuse rather than pretend.
	Runner    *Runner
	Deliverer Deliverer

	// Context supplies the database-backed context delivery needs to resolve
	// connections and their secrets.
	Context func() dbcontext.Context
}

Options wire the service to its store and the scheduler it drives.

type PauseFlags

type PauseFlags struct{}

PauseFlags carries no options; pausing is the whole instruction.

func (PauseFlags) ClickyActionFlags

func (PauseFlags) ClickyActionFlags()

type QuerySpec

type QuerySpec struct {
	Profile string            `json:"profile" yaml:"profile"`
	Params  map[string]string `json:"params,omitempty" yaml:"params,omitempty"`

	// Limit caps the rows read. Zero uses the profile's own export ceiling —
	// a scheduled report has no user watching to stop a runaway read.
	Limit int `json:"limit,omitempty" yaml:"limit,omitempty"`
}

QuerySpec runs one profile. Params are the profile's own filter params, the same key=value pairs `query profiles run --param` takes.

type ReconcileSpec

type ReconcileSpec struct {
	Profile                 string `json:"profile" yaml:"profile"`
	profiles.ReconcileFlags `yaml:",inline"`
}

ReconcileSpec joins two profiles. It embeds the same flags the manual `reconcile` action takes, so a scheduled reconciliation and a hand-run one are configured identically rather than by two drifting sets of fields.

type ReportSpec

type ReportSpec struct {
	Title  string `json:"title,omitempty" yaml:"title,omitempty"`
	Format string `json:"format" yaml:"format"`

	// Template selects the TSX entry file for the facet formats. Empty uses the
	// embedded default.
	Template  string            `json:"template,omitempty" yaml:"template,omitempty"`
	Variables map[string]string `json:"variables,omitempty" yaml:"variables,omitempty"`

	Facet *FacetSpec `json:"facet,omitempty" yaml:"facet,omitempty"`
}

ReportSpec renders the result. Formats are the ones the profile export already serves — csv, json, yaml, markdown, html, excel, pdf — plus facet-html and facet-pdf, which render a TSX template through facet.

type Reporter

type Reporter interface {
	Render(ctx dbcontext.Context, spec ReportSpec, result *query.Result, columns []query.ColumnDef) ([]byte, error)
}

Reporter renders a result to report bytes. It is an interface so the schedule runner does not depend on the facet renderer directly, and a deployment without one still runs its queries.

func NewReporter

func NewReporter() Reporter

NewReporter returns the default report renderer.

type Run

type Run struct {
	ID           string            `json:"id" pretty:"label=ID"`
	Schedule     string            `json:"schedule,omitempty" pretty:"label=Schedule"`
	Name         string            `json:"name" pretty:"label=Name"`
	Kind         string            `json:"kind,omitempty" pretty:"label=Kind"`
	Status       string            `json:"status" pretty:"label=Status"`
	Total        int               `json:"total" pretty:"label=Tasks"`
	Completed    int               `json:"completed" pretty:"label=Completed"`
	Failed       int               `json:"failed" pretty:"label=Failed"`
	Labels       map[string]string `json:"labels,omitempty"`
	Owner        string            `json:"owner,omitempty"`
	ArtifactPath string            `json:"artifactPath,omitempty" pretty:"label=Artifact"`
	Delivery     []DeliveryResult  `json:"delivery,omitempty"`
	StartedAt    *time.Time        `json:"startedAt,omitempty" pretty:"label=Started"`
	FinishedAt   *time.Time        `json:"finishedAt,omitempty" pretty:"label=Finished"`
}

Run is one row of a schedule's history, as the `schedule run` entity lists it.

func (Run) Columns

func (r Run) Columns() []api.ColumnDef

Columns is the run listing: identity, outcome, and the counts that say whether an outcome is trustworthy.

func (Run) GetID

func (r Run) GetID() string

func (Run) GetName

func (r Run) GetName() string

func (Run) Row

func (r Run) Row() map[string]any

type RunFilter

type RunFilter struct {
	Schedule string
	Kind     string
	Status   string
	Labels   map[string]string
	Limit    int
}

RunFilter narrows a run listing. Empty fields match everything.

type Runner

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

Runner executes one schedule inside a clicky task group.

func NewRunner

func NewRunner(options RunnerOptions) (*Runner, error)

func (*Runner) Register

func (r *Runner) Register()

Register wires the runner into clicky's scheduler for this package's kind.

Registering the same kind twice panics by design in clicky, so this is called once at serve time rather than from the constructor, which a test may build more than once in a process.

func (*Runner) Run

func (r *Runner) Run(ctx flanksourceContext.Context, timing task.Schedule, group *task.Group) error

Run is the task.Runner for a schedule. Each stage is its own task in the group, so a failure names the stage that failed rather than the schedule.

func (*Runner) SetArtifacts

func (r *Runner) SetArtifacts(store ArtifactStore)

SetArtifacts installs the artifact store.

func (*Runner) SetDeliverer

func (r *Runner) SetDeliverer(deliverer Deliverer)

SetDeliverer installs the notifier once the server knows its own base URL.

type RunnerOptions

type RunnerOptions struct {
	Store     StoreProvider
	Profiles  profiles.StoreProvider
	Context   func() dbcontext.Context
	Reconcile *profiles.Service
	Reporter  Reporter
	Deliverer Deliverer
	Artifacts ArtifactStore

	// BeforeExecute, when set, prepares a scheduled query's data before it is
	// read — the same hook the profile service runs, so a scheduled read and a
	// hand-run one see the same data. The reconcile mode runs it through
	// Reconcile, the profile service's own.
	BeforeExecute profiles.BeforeExecuteFunc
}

RunnerOptions wire the runner to everything it needs. Reporter, Deliverer and Artifacts are optional: without them a schedule still runs its query and records the run, it just produces nothing to send.

type Schedule

type Schedule struct {
	Name      string `json:"name" yaml:"name" clicky:"title=Name,order=1"`
	Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty" clicky:"title=Namespace,order=2"`

	// Cron is a five-field spec or a descriptor ("@daily", "@every 30m").
	Cron     string `json:"cron" yaml:"cron" clicky:"type=cron,title=Schedule,order=3"`
	Timezone string `json:"timezone,omitempty" yaml:"timezone,omitempty" clicky:"title=Timezone,order=4"`
	Enabled  bool   `json:"enabled" yaml:"enabled" clicky:"title=Enabled,order=5"`

	// Overlap and CatchUp are per-schedule because there is no answer that is
	// right for both a cheap probe and an expensive report.
	Overlap string   `json:"overlap,omitempty" yaml:"overlap,omitempty" clicky:"title=On overlap,order=6"`
	CatchUp string   `json:"catchUp,omitempty" yaml:"catchUp,omitempty" clicky:"title=On missed runs,order=7"`
	Timeout Duration `json:"timeout,omitempty" yaml:"timeout,omitempty" clicky:"title=Timeout,order=8"`

	// Exactly one of Query and Reconcile says what the schedule runs.
	Query     *QuerySpec     `json:"query,omitempty" yaml:"query,omitempty"`
	Reconcile *ReconcileSpec `json:"reconcile,omitempty" yaml:"reconcile,omitempty"`

	Report  *ReportSpec    `json:"report,omitempty" yaml:"report,omitempty"`
	Deliver []DeliverySpec `json:"deliver,omitempty" yaml:"deliver,omitempty"`

	Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
	Owner  string            `json:"owner,omitempty" yaml:"owner,omitempty"`

	LastRun *time.Time `json:"lastRun,omitempty" yaml:"-"`
	NextRun *time.Time `json:"nextRun,omitempty" yaml:"-"`
}

Schedule is a recurring query or reconciliation, optionally rendered to a report and delivered. It is the stored spec; clicky's task.Schedule is the timing half derived from it.

func (Schedule) Columns

func (s Schedule) Columns() []api.ColumnDef

Columns is the schedule listing. It deliberately omits the nested query, reconcile, report and delivery blocks: a listing answers "what is scheduled, is it on, and what does it do", and rendering four nested documents per row answers none of those.

func (Schedule) GetID

func (s Schedule) GetID() string

GetID and GetName make a Schedule a clicky EntityItem. The name is the id: schedules are addressed by the name their author gave them, like profiles.

func (Schedule) GetName

func (s Schedule) GetName() string

func (Schedule) Row

func (s Schedule) Row() map[string]any

func (Schedule) Timing

func (s Schedule) Timing() task.Schedule

Timing projects the stored spec onto clicky's schedule, which owns cron parsing, timezone resolution and the overlap/catch-up policies.

func (Schedule) Validate

func (s Schedule) Validate() error

Validate reports the first reason the schedule could not run. It is enforced on save so a schedule that can never fire is refused at the point someone writes it, not discovered as silence.

type Service

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

Service is the CLI and HTTP surface over schedules and their run history.

func New

func New(options Options) (*Service, error)

func (*Service) Delete

func (s *Service) Delete(ctx context.Context, name string) error

Delete removes a schedule and stops firing it.

func (*Service) LoadScheduler

func (s *Service) LoadScheduler(ctx context.Context) error

LoadScheduler registers every stored schedule with the scheduler. It is called at startup, once the database is up.

func (*Service) Pause

func (s *Service) Pause(ctx context.Context, name string, _ PauseFlags) (Schedule, error)

Pause stops a schedule firing without deleting it or its history.

func (*Service) RegisterClicky

func (s *Service) RegisterClicky()

RegisterClicky registers the schedule entity and, nested under it, the run entity that is its history.

Runs are an entity rather than an action returning a blob, so they arrive with filtering, sorting and paging already built. clicky derives the REST path from the command nesting, so `query schedule run list` and GET /api/v1/schedule/run come from the same declaration.

The consequence is that the fire-now action cannot be called `run` — it would collide with the child command — so it is `trigger`.

func (*Service) Resume

func (s *Service) Resume(ctx context.Context, name string, _ PauseFlags) (Schedule, error)

Resume starts a paused schedule firing again.

func (*Service) Save

func (s *Service) Save(ctx context.Context, body map[string]any, id string) (Schedule, error)

Save decodes a schedule document and persists it, then re-registers it with the scheduler so an edit takes effect without a restart.

func (*Service) SetDeliverer

func (s *Service) SetDeliverer(deliverer Deliverer)

SetDeliverer installs the notifier. It is set at serve time rather than in the constructor because delivery needs the externally reachable base URL, which only the server knows.

func (*Service) TestDelivery

func (s *Service) TestDelivery(
	ctx context.Context,
	name string,
	options TestDeliveryFlags,
) ([]DeliveryResult, error)

TestDelivery sends a sample message to each of a schedule's channels, so a misconfigured connection is found deliberately rather than by a report going missing at 6am.

func (*Service) Trigger

func (s *Service) Trigger(ctx context.Context, name string, options TriggerFlags) (*TriggerResult, error)

Trigger runs a schedule now. It goes through the same runner the scheduler uses, so a manual run and a scheduled one are the same code path and produce the same history — a "run it now" that behaved differently would be worse than no button at all.

type SnapshotFlags

type SnapshotFlags struct{}

SnapshotFlags carries no options.

func (SnapshotFlags) ClickyActionFlags

func (SnapshotFlags) ClickyActionFlags()

type Store

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

Store is the durable home of schedules and their run history. It implements clicky's task.Store, so the task manager persists finished runs through it and reads evicted ones back out without knowing anything about this schema.

func NewStore

func NewStore(db *gorm.DB) (*Store, error)

func (*Store) Control

func (s *Store) Control(_ context.Context, id string, action task.ControlAction) error

Control is refused for stored runs. A run this process no longer holds has no goroutine to stop, and pretending otherwise would report success for an action that did nothing.

func (*Store) Delete

func (s *Store) Delete(ctx context.Context, name string) error

Delete removes a schedule. Its runs are left behind: history outlives the definition that produced it.

func (*Store) DeleteSchedule

func (s *Store) DeleteSchedule(ctx context.Context, name string) error

func (*Store) Get

func (s *Store) Get(ctx context.Context, name string) (Schedule, error)

Get returns one schedule by name.

func (*Store) GetRun

func (s *Store) GetRun(ctx context.Context, id string) (Run, error)

GetRun returns one run by its id.

func (*Store) List

func (s *Store) List(ctx context.Context) ([]Schedule, error)

List returns every stored schedule, by name.

func (*Store) ListRuns

func (s *Store) ListRuns(ctx context.Context, filter RunFilter) ([]Run, error)

ListRuns returns run history, newest first.

func (*Store) ListSchedules

func (s *Store) ListSchedules(ctx context.Context) ([]task.Schedule, error)

func (*Store) RecordFire

func (s *Store) RecordFire(ctx context.Context, name string, fire task.Fire) error

RecordFire appends one firing decision, including the ones that produced no run. A schedule that keeps skipping is invisible without this.

func (*Store) RecordReport

func (s *Store) RecordReport(ctx context.Context, runID, artifactPath string, delivery []DeliveryResult) error

RecordReport attaches the produced artifact and delivery outcomes to a run.

This runs from inside the run's own task, which is necessarily before the run itself is persisted: a run is written when it reaches a terminal status, and it cannot be terminal while the task recording the report is still going. So this upserts rather than updates — a plain UPDATE would match no row and silently discard the artifact. The placeholder it may insert is completed by the SaveRun that follows, which deliberately leaves these two columns alone.

func (*Store) Runs

func (s *Store) Runs(ctx context.Context, filter task.RunFilter) ([]task.RunMeta, error)

Runs lists persisted runs matching filter, newest first.

func (*Store) Save

func (s *Store) Save(ctx context.Context, schedule Schedule) error

Save upserts a schedule by name, refusing one that could never run.

func (*Store) SaveRun

func (s *Store) SaveRun(ctx context.Context, groupID string, snapshots []task.TaskSnapshot) error

SaveRun upserts one run keyed by its clicky group id. It is idempotent by design: the terminal-transition write and the later eviction write are the same row, and the newest snapshot wins.

func (*Store) SaveSchedule

func (s *Store) SaveSchedule(ctx context.Context, schedule task.Schedule) error

SaveSchedule persists the timing half of a schedule. The spec itself is written by Save; this records what the scheduler learned by running it, which is what lets a restart tell that a scheduled time was missed.

func (*Store) SetEnabled

func (s *Store) SetEnabled(ctx context.Context, name string, enabled bool) error

SetEnabled toggles a schedule without rewriting its spec.

func (*Store) Snapshot

func (s *Store) Snapshot(ctx context.Context, id string) ([]task.TaskSnapshot, error)

Snapshot returns the stored snapshot slice for one run.

type StoreProvider

type StoreProvider func() (*Store, error)

StoreProvider resolves the store lazily, because the command tree is built before the database exists — a metadata-only invocation must not start PostgreSQL just to list its own subcommands.

func (StoreProvider) TaskScheduleStore

func (p StoreProvider) TaskScheduleStore() task.ScheduleStore

TaskScheduleStore adapts the lazy database provider to Clicky's scheduling persistence without opening PostgreSQL while the command tree is built.

type TestDeliveryFlags

type TestDeliveryFlags struct {
	Message string `flag:"message" help:"Body of the test message"`
}

TestDeliveryFlags are the flags of the `test-delivery` action.

func (TestDeliveryFlags) ClickyActionFlags

func (TestDeliveryFlags) ClickyActionFlags()

type TriggerFlags

type TriggerFlags struct {
	Wait bool `flag:"wait" help:"Block until the run finishes instead of returning its id"`
}

TriggerFlags are the flags of the schedule `trigger` action.

func (TriggerFlags) ClickyActionFlags

func (TriggerFlags) ClickyActionFlags()

type TriggerResult

type TriggerResult struct {
	Schedule string     `json:"schedule" pretty:"label=Schedule"`
	RunID    string     `json:"runId" pretty:"label=Run"`
	Status   string     `json:"status" pretty:"label=Status"`
	Rows     int        `json:"rows,omitempty" pretty:"label=Rows"`
	Artifact string     `json:"artifact,omitempty" pretty:"label=Artifact"`
	Error    string     `json:"error,omitempty" pretty:"label=Error"`
	Started  time.Time  `json:"startedAt" pretty:"label=Started"`
	Finished *time.Time `json:"finishedAt,omitempty" pretty:"label=Finished"`
}

TriggerResult names the run a trigger started.

func (TriggerResult) Pretty

func (r TriggerResult) Pretty() api.Text

Directories

Path Synopsis
Package report renders tabular results to HTML or PDF through facet.
Package report renders tabular results to HTML or PDF through facet.

Jump to

Keyboard shortcuts

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