| 1 | package appconfig |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/sha256" |
| 6 | "encoding/hex" |
| 7 | "errors" |
| 8 | "fmt" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/hostwrite" |
| 11 | "github.com/carlosframework/platform/internal/registry" |
| 12 | ) |
| 13 | |
| 14 | var errHostWriteOptions = errors.New("appconfig: more than one hostwrite configuration") |
| 15 | |
| 16 | func hostWriteOption(options []hostwrite.Config) (hostwrite.Config, error) { |
| 17 | if len(options) > 1 { |
| 18 | return hostwrite.Config{}, errHostWriteOptions |
| 19 | } |
| 20 | if len(options) == 1 { |
| 21 | return options[0], nil |
| 22 | } |
| 23 | return hostwrite.Config{}, nil |
| 24 | } |
| 25 | |
| 26 | func renderedIdentity(rendered []byte, defaultPresent, environmentPresent, instancePresent bool) hostwrite.Identity { |
| 27 | sum := sha256.Sum256(rendered) |
| 28 | return hostwrite.Identity{Config: fmt.Sprintf("sha256:%s;default=%t;environment=%t;instance=%t", hex.EncodeToString(sum[:]), defaultPresent, environmentPresent, instancePresent)} |
| 29 | } |
| 30 | |
| 31 | func guarded(ctx context.Context, work hostwrite.Work, id hostwrite.Identity, fn func() error) error { |
| 32 | if ctx == nil || work == nil { |
| 33 | return hostwrite.ErrUnavailable |
| 34 | } |
| 35 | return work.With(ctx, id, fn) |
| 36 | } |
| 37 | |
| 38 | type legacyHostWork struct{} |
| 39 | |
| 40 | func (legacyHostWork) With(_ context.Context, _ hostwrite.Identity, fn func() error) error { |
| 41 | return fn() |
| 42 | } |
| 43 | |
| 44 | // Callbacks keeps legacy handlers for flag-off passes. Enabled passes require |
| 45 | // the selected handlers, which must retain the selection until their final |
| 46 | // effect. They run outside the config writer's flock. |
| 47 | type Callbacks struct { |
| 48 | Restart func(string) error |
| 49 | Ack func(ConfigAck) |
| 50 | SelectedRestart hostwrite.Callback |
| 51 | SelectedAck func(context.Context, hostwrite.Selection, ConfigAck) error |
| 52 | // Delivered, when set, is told the revision of a route's config file |
| 53 | // each time this pass writes or removes it, before the restart callback |
| 54 | // runs: the zero-downtime scheduler's "delivered" revision (spec §8.1). |
| 55 | // It is never called for a shape whose process the platform does not |
| 56 | // restart (the TCP-addr shape), for a sidecar Managed does not name, or |
| 57 | // for a socket route Managed says no to. Nil keeps today's behaviour. |
| 58 | Delivered func(host, rev string) |
| 59 | // Managed, when set, names the routes the release scheduler moves itself |
| 60 | // (rollout.Managed, handed in because this package sits below the |
| 61 | // scheduler). It splits the pass the way the scheduler does (final |
| 62 | // review F7): a socket route it does not manage is restarted by |
| 63 | // Unmanaged, as config sync restarts it with the flag off, and is not |
| 64 | // reported to Delivered; a native sidecar it manages is reported to |
| 65 | // Delivered though config sync never restarts a sidecar, because the |
| 66 | // scheduler's config attempt is that sidecar's restart. Nil: Restart and |
| 67 | // Delivered serve every route, as before. |
| 68 | Managed func(rt registry.Route) bool |
| 69 | // Unmanaged is the restart a socket route outside Managed gets in |
| 70 | // Restart's place. Ignored while Managed is nil. |
| 71 | Unmanaged func(host string) error |
| 72 | enabled bool |
| 73 | } |
| 74 | |
| 75 | // ErrDeliveredOnWake is a restart callback's answer for an instance that is |
| 76 | // asleep and reads the new file when it next wakes: nothing was restarted |
| 77 | // and nothing failed. Its ack carries Hibernating (delivered on wake), the |
| 78 | // marker the unit pass sets for a hibernated unit (sync.go), with Restarted |
| 79 | // false and no RestartError. |
| 80 | var ErrDeliveredOnWake = errors.New("asleep: config delivered on wake") |
| 81 | |
| 82 | func (c Callbacks) restart(ctx context.Context, selected hostwrite.Selection) error { |
| 83 | if c.enabled { |
| 84 | if c.SelectedRestart == nil { |
| 85 | return hostwrite.ErrUnavailable |
| 86 | } |
| 87 | return c.SelectedRestart(ctx, selected) |
| 88 | } |
| 89 | if c.Restart == nil { |
| 90 | return nil |
| 91 | } |
| 92 | return c.Restart(selected.Host) |
| 93 | } |
| 94 | |
| 95 | func (c Callbacks) acknowledge(ctx context.Context, selected hostwrite.Selection, a ConfigAck) error { |
| 96 | if c.enabled { |
| 97 | if c.SelectedAck == nil { |
| 98 | return hostwrite.ErrUnavailable |
| 99 | } |
| 100 | return c.SelectedAck(ctx, selected, a) |
| 101 | } |
| 102 | if c.Ack != nil { |
| 103 | c.Ack(a) |
| 104 | } |
| 105 | return nil |
| 106 | } |
| 107 | |
| 108 | func configCallbacks(work hostwrite.Work, restart func(string) error, ack func(ConfigAck), options []Callbacks) Callbacks { |
| 109 | if len(options) != 0 { |
| 110 | return options[0] |
| 111 | } |
| 112 | _, legacy := work.(legacyHostWork) |
| 113 | return Callbacks{Restart: restart, Ack: ack, enabled: !legacy} |
| 114 | } |
| 115 | |