103 lines
3.2 KiB
Go
103 lines
3.2 KiB
Go
|
|
// Package state reads and writes the resolved presence: what provisioning
|
||
|
|
// actually produced, as opposed to what was asked for.
|
||
|
|
//
|
||
|
|
// It holds no secrets and is committed. The bot token, the operator session and
|
||
|
|
// the redaction salt live in OpenBao; what is here is the mechanical facts a
|
||
|
|
// later run needs in order to recognise what it already did.
|
||
|
|
package state
|
||
|
|
|
||
|
|
import (
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"gopkg.in/yaml.v3"
|
||
|
|
)
|
||
|
|
|
||
|
|
type File struct {
|
||
|
|
Resolved Resolved `yaml:"resolved"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type Resolved struct {
|
||
|
|
Campaign string `yaml:"campaign"`
|
||
|
|
|
||
|
|
// SpecDigest is the digest of the spec this state was produced from. It is
|
||
|
|
// the link that makes drift detectable: if the spec changed and this did
|
||
|
|
// not, provisioning is behind. Without it a stale state is indistinguishable
|
||
|
|
// from a current one.
|
||
|
|
SpecDigest string `yaml:"spec_digest"`
|
||
|
|
|
||
|
|
ProvisionedAt time.Time `yaml:"provisioned_at"`
|
||
|
|
|
||
|
|
Bot Bot `yaml:"bot"`
|
||
|
|
Channels map[string]Channel `yaml:"channels"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type Bot struct {
|
||
|
|
Username string `yaml:"username"`
|
||
|
|
ID int64 `yaml:"id"`
|
||
|
|
|
||
|
|
// AvatarDigest is the digest of the image file that was uploaded, so a
|
||
|
|
// replaced file is what triggers an update rather than a timestamp.
|
||
|
|
AvatarDigest string `yaml:"avatar_digest,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type Channel struct {
|
||
|
|
ChatID int64 `yaml:"chat_id"`
|
||
|
|
Username string `yaml:"username,omitempty"`
|
||
|
|
AdminRights []string `yaml:"admin_rights"`
|
||
|
|
|
||
|
|
// TestPublicationAt records the first successful publication to this
|
||
|
|
// channel. apply refuses to touch the public channel until the test channel
|
||
|
|
// has one -- a gate that has to survive a restart, so it lives here rather
|
||
|
|
// than in memory.
|
||
|
|
TestPublicationAt *time.Time `yaml:"test_publication_at,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// Path is where a campaign's resolved state lives, relative to the repo root.
|
||
|
|
func Path(root, campaign string) string {
|
||
|
|
return filepath.Join(root, "presence", "resolved", campaign+".yaml")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Load returns the resolved state, or a zero value if none exists yet. A missing
|
||
|
|
// file is the ordinary first-run case, not an error.
|
||
|
|
func Load(path string) (*Resolved, error) {
|
||
|
|
raw, err := os.ReadFile(path)
|
||
|
|
if errors.Is(err, os.ErrNotExist) {
|
||
|
|
return &Resolved{}, nil
|
||
|
|
}
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("read resolved state: %w", err)
|
||
|
|
}
|
||
|
|
var f File
|
||
|
|
if err := yaml.Unmarshal(raw, &f); err != nil {
|
||
|
|
return nil, fmt.Errorf("parse resolved state: %w", err)
|
||
|
|
}
|
||
|
|
return &f.Resolved, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func Save(path string, r *Resolved) error {
|
||
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
out, err := yaml.Marshal(File{Resolved: *r})
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
header := "# Generated by `provision apply`. Committed, and free of secrets:\n" +
|
||
|
|
"# tokens, sessions and the redaction salt live in OpenBao.\n"
|
||
|
|
return os.WriteFile(path, append([]byte(header), out...), 0o644)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Provisioned reports whether anything has been created for this campaign yet.
|
||
|
|
func (r *Resolved) Provisioned() bool { return r.Bot.ID != 0 }
|
||
|
|
|
||
|
|
// TestChannelVerified reports whether a publication has reached the test
|
||
|
|
// channel. The public channel is not touched until it has.
|
||
|
|
func (r *Resolved) TestChannelVerified(testKey string) bool {
|
||
|
|
c, ok := r.Channels[testKey]
|
||
|
|
return ok && c.TestPublicationAt != nil
|
||
|
|
}
|