-
Notifications
You must be signed in to change notification settings - Fork 219
/
webui.go
156 lines (130 loc) · 4.14 KB
/
webui.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
package launch
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"strconv"
"strings"
"time"
"github.com/azazeal/pause"
"github.com/briandowns/spinner"
"github.com/skratchdot/open-golang/open"
fly "github.com/superfly/fly-go"
"github.com/superfly/flyctl/helpers"
"github.com/superfly/flyctl/internal/command/launch/plan"
"github.com/superfly/flyctl/internal/logger"
state2 "github.com/superfly/flyctl/internal/state"
"github.com/superfly/flyctl/internal/tracing"
"github.com/superfly/flyctl/iostreams"
)
// EditInWebUi launches a web-based editor for the app plan
func (state *launchState) EditInWebUi(ctx context.Context) error {
ctx, span := tracing.GetTracer().Start(ctx, "state.edit_in_web_ui")
defer span.End()
session, err := fly.StartCLISession(fmt.Sprintf("%s: %s", state2.Hostname(ctx), state.Plan.AppName), map[string]any{
"target": "launch",
"metadata": state.Plan,
})
if err != nil {
return err
}
io := iostreams.FromContext(ctx)
if err := open.Run(session.URL); err != nil {
fmt.Fprintf(io.ErrOut,
"failed opening browser. Copy the url (%s) into a browser and continue\n",
session.URL,
)
} else {
colorize := io.ColorScheme()
fmt.Fprintf(io.Out, "Opening %s ...\n\n", colorize.Bold(session.URL))
}
logger := logger.FromContext(ctx)
finalSession, err := waitForCLISession(ctx, logger, io.ErrOut, session.ID)
switch {
case errors.Is(err, context.DeadlineExceeded):
return errors.New("session expired, please try again")
case err != nil:
return err
}
oldPlan := helpers.Clone(state.Plan)
state.Plan = &plan.LaunchPlan{}
// TODO(Ali): Remove me.
// Hack because somewhere from between UI and here, the numbers get converted to strings
if err := patchNumbers(finalSession.Metadata, "vm_cpus", "vm_memory"); err != nil {
return err
}
// Wasteful, but gets the job done without uprooting the session types.
// Just round-trip the map[string]interface{} back into json, so we can re-deserialize it into a complete type.
metaJson, err := json.Marshal(finalSession.Metadata)
if err != nil {
return err
}
err = json.Unmarshal(metaJson, &state.Plan)
if err != nil {
return err
}
// Patch in some fields that we keep in the plan that aren't persisted by the UI.
// Technically, we should probably just be persisting this, but there's
// no clear value to the UI having these fields currently.
if _, ok := finalSession.Metadata["ha"]; !ok {
state.Plan.HighAvailability = oldPlan.HighAvailability
}
// This should never be changed by the UI!!
state.Plan.ScannerFamily = oldPlan.ScannerFamily
return nil
}
// TODO: I'd like to just fix the round-trip issue here, instead of this bandage.
// This is mostly so I can get a presentation out before I have to leave :)
// patchNumbers is a hack to fix the round-trip issue with numbers being converted to strings
// It supports nested paths, such as "vm_cpus" or "some_struct.int_value"
func patchNumbers(obj map[string]any, labels ...string) error {
outer:
for _, label := range labels {
// Borrow down to the right element.
path := strings.Split(label, ".")
iface := obj
var ok bool
for _, p := range path[:len(path)-1] {
if iface, ok = iface[p].(map[string]any); ok {
continue outer
}
}
// Patch the element
name := path[len(path)-1]
val, ok := iface[name]
if !ok {
continue
}
if numStr, ok := val.(string); ok {
num, err := strconv.ParseInt(numStr, 10, 64)
if err != nil {
return err
}
iface[name] = num
}
}
return nil
}
// TODO: this does NOT break on interrupts
func waitForCLISession(parent context.Context, logger *logger.Logger, w io.Writer, id string) (session fly.CLISession, err error) {
ctx, cancel := context.WithTimeout(parent, 15*time.Minute)
defer cancel()
s := spinner.New(spinner.CharSets[11], 100*time.Millisecond)
s.Writer = w
s.Prefix = "Waiting for launch data..."
s.Start()
for ctx.Err() == nil {
if session, err = fly.GetCLISessionState(ctx, id); err != nil {
logger.Debugf("failed retrieving token: %v", err)
pause.For(ctx, time.Second)
continue
}
logger.Debug("retrieved launch data.")
s.FinalMSG = "Waiting for launch data... Done\n"
s.Stop()
break
}
return
}