package appconfig import ( "context" "encoding/json" "fmt" "github.com/carlosframework/platform/internal/release" ) // SwapPolicy is an app's swap policy (zero-downtime spec §4.1) with its // defaults filled in. It lives in the DEFAULT bundle beside exec_delivery, // which is where plan 5's boxes read it; a named environment never carries it. type SwapPolicy struct { Swap string // SwapOverlap or SwapHold Concurrency int // 1 or more } // PolicyOf reads m's swap fields with the defaults filled in, the way the // box's rollout.PolicyFromManifest does. nil is an app never configured. A // stored mode this binary does not know reads as hold: missing or // unverifiable policy never permits an overlap (spec §4.2). func PolicyOf(m *Manifest) SwapPolicy { p := SwapPolicy{Swap: SwapOverlap, Concurrency: 1} if m == nil { return p } if m.Swap != "" && m.Swap != SwapOverlap { p.Swap = SwapHold } if m.SwapConcurrency > 1 { p.Concurrency = m.SwapConcurrency } return p } // GetSwapPolicy reads the app's policy. No config.json is the defaults, not // an error (Get's own convention). func GetSwapPolicy(ctx context.Context, s release.Store, acct, app string) (SwapPolicy, error) { m, err := Get(ctx, s, acct, app) if err != nil { return SwapPolicy{}, err } return PolicyOf(m), nil } // SetSwapPolicy writes the policy into the default bundle by the CAS // read-modify-write SetExecDeliveryAt uses, and returns what is stored. swap // "" and concurrency 0 leave their half alone. // // Defaults are stored as ABSENT keys, so an app set back to overlap/1 has the // bytes it had before. A call that changes nothing writes nothing and touches // nothing: setting the defaults on an app with no config.json never creates // one. A first non-default policy does create one, holding no vars, and // config sync leaves an instance whose config file is absent as it is for // such a bundle (renderExecConfig's keepAbsent), so that first write // restarts nothing (Astra plan 6 R1 #1). func SetSwapPolicy(ctx context.Context, s release.Store, acct, app, swap string, concurrency int) (claimResult0 SwapPolicy, claimResult1 error) { claimResult1 = release.WithAppMutation(ctx, s, acct, app, "appconfig.SetSwapPolicy", func(claimContext context.Context) error { claimResult0, claimResult1 = claimRawSetSwapPolicy(claimContext, s, acct, app, swap, concurrency) return claimResult1 }) return } // claimRawSetSwapPolicy is the unwrapped implementation of SetSwapPolicy, // called from within the WithAppMutation wrapper. func claimRawSetSwapPolicy(ctx context.Context, s release.Store, acct, app, swap string, concurrency int) (SwapPolicy, error) { if !release.ValidAccount(acct) { return SwapPolicy{}, fmt.Errorf("config: invalid account %q", acct) } if !release.ValidApp(app) { return SwapPolicy{}, fmt.Errorf("config: invalid app %q", app) } if !ValidSwap(swap) { return SwapPolicy{}, fmt.Errorf("config: invalid swap %q, want %q or %q", swap, SwapOverlap, SwapHold) } if !ValidSwapConcurrency(concurrency) { return SwapPolicy{}, fmt.Errorf("config: invalid swap_concurrency %d, want 1 or more", concurrency) } key := Key(acct, app) m, etag, _, err := getForWriteAt(ctx, s, key, app) if err != nil { return SwapPolicy{}, err } next := m switch swap { case SwapOverlap: next.Swap = "" case SwapHold: next.Swap = SwapHold } switch { case concurrency == 1: next.SwapConcurrency = 0 case concurrency > 1: next.SwapConcurrency = concurrency } if next.Swap == m.Swap && next.SwapConcurrency == m.SwapConcurrency { return PolicyOf(&m), nil } next.Schema, next.App = 1, app raw, err := json.MarshalIndent(next, "", " ") if err != nil { return SwapPolicy{}, err } if _, err := s.Put(ctx, key, raw, etag); err != nil { return SwapPolicy{}, err } // A policy change is a config change for the fast loop (apply's doc // comment): best-effort, since the write above has landed. touchLogged(ctx, s, acct, app) return PolicyOf(&next), nil }