carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3// `carlos apps set`'s instance update policy flags (instance update settings
4// spec §3.2, §5): --instance-control, --default-updates, --default-channel and
5// --default-major, sent to the console's PUT /api/cli/apps/{account}/{app}/updates
6// door (handleAPIUpdatesPolicy).
7
8import (
9 "context"
10 "encoding/json"
11 "errors"
12 "flag"
13 "fmt"
14 "net/http"
15 "strconv"
16 "strings"
17
18 "github.com/carlosframework/platform/internal/release"
19 "github.com/carlosframework/platform/internal/updatewords"
20)
21
22// updatePolicyReq is the door's body: a key not present is left as it is, and
23// a key present with null or "" clears it.
24type updatePolicyReq map[string]any
25
26// apiUpdatePolicy is the door's echo: what the console stored.
27type apiUpdatePolicy struct {
28 InstanceControl []string `json:"instance_control"`
29 DefaultUpdates string `json:"default_updates"`
30 DefaultChannel string `json:"default_channel"`
31 DefaultMajor *int `json:"default_major"`
32}
33
34// updatePolicyRequest builds the door's body from the flags given: a flag not
35// given is not sent, and an empty value clears. It returns nil when none of
36// the four was given. A bad --default-major costs no round trip.
37func updatePolicyRequest(fs *flag.FlagSet, control, updates, channel, major string) (updatePolicyReq, error) {
38 req := updatePolicyReq{}
39 if flagWasSet(fs, "instance-control") {
40 var names []string // nil marshals as null, which the console reads as a clear
41 for _, n := range strings.Split(control, ",") {
42 if n = strings.TrimSpace(n); n != "" {
43 names = append(names, n)
44 }
45 }
46 req["instance_control"] = names
47 }
48 if flagWasSet(fs, "default-updates") {
49 req["default_updates"] = strings.TrimSpace(updates)
50 }
51 if flagWasSet(fs, "default-channel") {
52 req["default_channel"] = strings.TrimSpace(channel)
53 }
54 if flagWasSet(fs, "default-major") {
55 if m := strings.TrimSpace(major); m == "" {
56 req["default_major"] = nil
57 } else {
58 n, err := strconv.Atoi(m)
59 if err != nil || n < 0 {
60 return nil, errors.New(updatewords.Text("settings.cli.bad-major-flag"))
61 }
62 req["default_major"] = n
63 }
64 }
65 if len(req) == 0 {
66 return nil, nil
67 }
68 return req, nil
69}
70
71// setUpdatePolicy is the update policy half of `carlos apps set`. The console
72// refuses with its own words, and they are returned as they came.
73func (s *apiSession) setUpdatePolicy(ctx context.Context, a apiAcct, app string, req updatePolicyReq) (apiUpdatePolicy, error) {
74 status, body, err := s.putJSON(ctx, apiPath(a, app, "/updates"), req)
75 if err != nil {
76 return apiUpdatePolicy{}, err
77 }
78 if status != http.StatusOK {
79 return apiUpdatePolicy{}, apiErrorFromBody(status, body)
80 }
81 var out apiUpdatePolicy
82 if err := json.Unmarshal(body, &out); err != nil {
83 return apiUpdatePolicy{}, fmt.Errorf(appsSetUnreadable, err)
84 }
85 return out, nil
86}
87
88// appsSetRun is `carlos apps set` once its flags are checked and its session
89// resolved: the swap door when a swap flag was given, then the update policy
90// door when a policy flag was, one line for each. The first failure stops it.
91func appsSetRun(ctx context.Context, sess *apiSession, acct apiAcct, app string, swap *swapPolicyReq, policy updatePolicyReq) error {
92 if swap == nil && policy == nil {
93 return errors.New(appsSetNothing)
94 }
95 if swap != nil {
96 got, err := sess.setSwapPolicy(ctx, acct, app, *swap)
97 if err != nil {
98 return fmt.Errorf(appsSetError, err)
99 }
100 if err := appsSetStuck(*swap, got); err != nil {
101 return err
102 }
103 fmt.Fprintf(stdout, appsSetDone+"\n", app, got.Swap, got.Concurrency)
104 if got.Swap == "hold" {
105 mutedf(stdout, appsSetHoldNote)
106 } else {
107 mutedf(stdout, appsSetOverlapNote)
108 }
109 }
110 if policy != nil {
111 got, err := sess.setUpdatePolicy(ctx, acct, app, policy)
112 if err != nil {
113 return fmt.Errorf(appsSetError, err)
114 }
115 none := updatewords.Text("settings.cli.none")
116 or := func(s string) string {
117 if s == "" {
118 return none
119 }
120 return s
121 }
122 // An unset default is auto: the setting's own value word, not copy.
123 updates := got.DefaultUpdates
124 if updates == "" {
125 updates = release.UpdatesAuto
126 }
127 major := none
128 if got.DefaultMajor != nil {
129 major = strconv.Itoa(*got.DefaultMajor)
130 }
131 fmt.Fprintf(stdout, updatewords.Text("settings.cli.apps-set-policy-done")+"\n", app,
132 or(strings.Join(got.InstanceControl, ", ")), updates, or(got.DefaultChannel), major)
133 }
134 return nil
135}
136