carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// Deployment-wide settings records: system/<name>.json.
2//
3// These are NOT features.json. That one is per-app and holds switches the
4// platform "stores and serves but never interprets" (apifeatures.go) — an
5// app's own vocabulary, passed through. These the platform reads and acts on,
6// so each gets a typed shape and a pinned schema.
7//
8// A typed record per feature rather than one generic settings map, for the
9// same reason: a map would be features.json by another name, uninterpreted and
10// impossible to schema-pin meaningfully. system/pdf.json later is a new type
11// and a new door, not a new magic string.
12package release
13
14import (
15 "context"
16 "encoding/json"
17 "errors"
18 "fmt"
19 "os"
20 "time"
21)
22
23// AntivirusSchema is pinned. An unknown schema refuses rather than guessing —
24// the rule objstore.Record and ScanSidecar already follow, and it matters more
25// here: this record decides whether the deployment reads tenant object bytes.
26const AntivirusSchema = 1
27
28// SystemKey is where a deployment-wide settings record lives.
29func SystemKey(name string) string { return "system/" + name + ".json" }
30
31// AntivirusKey is the object this package's typed helpers read and write.
32const AntivirusName = "antivirus"
33
34// AntivirusSettings is whether object scanning is on for this deployment.
35//
36// Policy, not wiring. Where clamd lives stays in the unit file, because those
37// are coordinates a console must not guess; whether scanning is ON lives here,
38// so turning a compliance control on does not mean editing a systemd unit and
39// restarting the control plane.
40type AntivirusSettings struct {
41 Schema int `json:"schema"`
42 Enabled bool `json:"enabled"`
43 Engine string `json:"engine"`
44 At time.Time `json:"at"`
45 By string `json:"by"`
46}
47
48// GetAntivirus reads the record. Absent is (nil, "", nil) — no record is a
49// state, not an error, the same idiom objstore.Get uses.
50//
51// Absent means NOT ENABLED. A deployment that has never run `carlos system
52// antivirus enable` does not scan, which is what makes the migration in
53// cmd/console worth its own seeding step rather than a default.
54func GetAntivirus(ctx context.Context, s Store) (*AntivirusSettings, string, error) {
55 raw, etag, err := s.Get(ctx, SystemKey(AntivirusName))
56 if err != nil {
57 if errors.Is(err, os.ErrNotExist) {
58 return nil, "", nil
59 }
60 return nil, "", err
61 }
62 var av AntivirusSettings
63 if err := json.Unmarshal(raw, &av); err != nil {
64 return nil, "", fmt.Errorf("release: %s: %w", SystemKey(AntivirusName), err)
65 }
66 if av.Schema != AntivirusSchema {
67 return nil, "", fmt.Errorf("release: %s: schema %d is not %d",
68 SystemKey(AntivirusName), av.Schema, AntivirusSchema)
69 }
70 return &av, etag, nil
71}
72
73// PutAntivirus writes with the caller's CAS position; etag "" is create-only.
74// It stamps the schema so a caller cannot write a body it would then refuse to
75// read back.
76func PutAntivirus(ctx context.Context, s Store, av *AntivirusSettings, etag string) (string, error) {
77 av.Schema = AntivirusSchema
78 body, err := json.MarshalIndent(av, "", " ")
79 if err != nil {
80 return "", err
81 }
82 return s.Put(ctx, SystemKey(AntivirusName), append(body, '\n'), etag)
83}
84
85// AntivirusEnabled folds the read into the one question every caller actually
86// asks. A read error is NOT "disabled": it is an error, and a caller that
87// treated it as disabled would silently stop scanning on a transient bucket
88// failure.
89func AntivirusEnabled(ctx context.Context, s Store) (bool, error) {
90 av, _, err := GetAntivirus(ctx, s)
91 if err != nil {
92 return false, err
93 }
94 return av != nil && av.Enabled, nil
95}
96