| 1 | package 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 | |
| 8 | import ( |
| 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. |
| 24 | type updatePolicyReq map[string]any |
| 25 | |
| 26 | // apiUpdatePolicy is the door's echo: what the console stored. |
| 27 | type 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. |
| 37 | func 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. |
| 73 | func (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. |
| 91 | func 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 | |