-
Notifications
You must be signed in to change notification settings - Fork 293
/
demo.go
242 lines (207 loc) · 8.14 KB
/
demo.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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
package cli
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/fatih/color"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/tilt-dev/go-get"
"github.com/tilt-dev/tilt/internal/analytics"
"github.com/tilt-dev/tilt/internal/cli/demo"
"github.com/tilt-dev/tilt/pkg/logger"
"github.com/tilt-dev/tilt/pkg/model"
)
const demoResourcesPrefix = "tilt-demo-"
const sampleProjPackage = "github.com/tilt-dev/tilt-avatars"
type demoCmd struct {
// legacy disables the web UI (this is only used for integration tests)
legacy bool
// teardown will clean up any leftover `tilt demo` clusters and exit
teardown bool
// tmpdir for cloned `tilt-avatars` resources
tmpdir string
// skipCreateCluster uses default kubeconfig context instead of creating
// an ephemeral cluster
skipCreateCluster bool
// projPackage is the `go get` style URL for the demo project
projPackage string
// tiltfilePath is a path to a Tiltfile to launch instead of cloning and
// running the `tilt-avatars` project
tiltfilePath string
}
func (c *demoCmd) name() model.TiltSubcommand { return "demo" }
func (c *demoCmd) register() *cobra.Command {
cmd := &cobra.Command{
Use: "demo [flags]",
Short: "Creates a local, temporary Kubernetes cluster and runs a Tilt sample project",
Long: fmt.Sprintf(`Test out Tilt using an isolated, ephemeral local Kubernetes setup.
Tilt will create a temporary, local Kubernetes development cluster running in Docker.
The cluster will be removed when Tilt is exited with Ctrl-C.
A sample project (%s) will be cloned locally to a temporary directory using Git and launched.
`, sampleProjPackage),
}
cmd.Flags().BoolVarP(&c.teardown, "teardown", "", false,
"Removes any leftover tilt-demo Kubernetes clusters and exits")
// --legacy flag only exists for integration tests to disable web console
cmd.Flags().BoolVar(&c.legacy, "legacy", false, "If true, tilt will open in legacy HUD mode.")
cmd.Flags().Lookup("legacy").Hidden = true
// --tmpdir exists so that integration tests can inspect the output / use the Tiltfile
cmd.Flags().StringVarP(&c.tmpdir, "tmpdir", "", "",
"Temporary directory to clone sample project to")
cmd.Flags().Lookup("tmpdir").Hidden = true
cmd.Flags().BoolVar(&c.skipCreateCluster, "no-cluster", false,
"Skip ephemeral cluster creation (requires local K8s cluster to already be configured)")
cmd.Flags().StringVarP(&c.projPackage, "repo", "r", sampleProjPackage,
"Path to custom repo to use instead of Tiltfile")
// we don't use the `addTiltfileFlag()` because the default here should be empty
cmd.Flags().StringVarP(&c.tiltfilePath, "file", "f", "",
"Path to custom Tiltfile to use instead of sample project")
addStartServerFlags(cmd)
addDevServerFlags(cmd)
return cmd
}
func (c *demoCmd) run(ctx context.Context, args []string) error {
a := analytics.Get(ctx)
a.Incr("cmd.demo", map[string]string{})
defer a.Flush(time.Second)
client, err := wireDockerLocalClient(ctx)
if err != nil {
return errors.Wrap(err, "Failed to init Docker client")
}
k3dCli := demo.NewK3dClient(client)
if c.teardown {
return c.cleanupClusters(ctx, k3dCli)
}
if c.projPackage != sampleProjPackage && c.tiltfilePath != "" {
return fmt.Errorf("cannot specify both a custom repo and Tiltfile path")
}
//
// 0. Prepare environment
//
logger.Get(ctx).Infof("\nHang tight while Tilt prepares your demo environment!")
c.tmpdir, err = os.MkdirTemp(c.tmpdir, demoResourcesPrefix)
if err != nil {
return fmt.Errorf("could not create temporary directory: %v", err)
}
if !c.skipCreateCluster {
err = client.CheckConnected()
if err != nil {
return fmt.Errorf("tilt demo requires Docker to be installed and running: %v", err)
}
if !isLocalDockerHost(client.Env().DaemonHost()) {
// properly supporting remote Docker connections is very tricky - either:
//
// the remote host will need more ports accessible (for K8s API + registry API) and we have to ensure
// that everything both listens on the public interface and references it in configs
// (such as "local-registry-hosting" ConfigMap)
// OR
// we need to tunnel everything (perhaps using Docker - this is the approach ctlptl takes!)
//
// for now, it's not supported as it's a pretty advanced setup to begin with, so we're not really targeting
// it with the `tilt demo` functionality
return fmt.Errorf("tilt demo requires a local Docker daemon to create a temporary Kubernetes cluster (current Docker host: %s)", client.Env().DaemonHost())
}
//
// 1. Create a cluster that will be torn down in the background on exit (Ctrl-C)
//
clusterName := filepath.Base(c.tmpdir)
logger.Get(ctx).Infof("\tCreating %q local Kubernetes cluster...", clusterName)
if err := k3dCli.CreateCluster(ctx, clusterName); err != nil {
return fmt.Errorf("failed to create Kubernetes cluster: %v", err)
}
defer func() {
// N.B. use background context because the main context has already been canceled due to Ctrl-C
// but also don't block on execution (just fire request to Docker API and forget) because at this
// point we have < 2 secs before the signal handler forcibly exits the process
ctx := logger.WithLogger(context.Background(), logger.Get(ctx))
logger.Get(ctx).Infof("\nDeleting %q local Kubernetes cluster...", clusterName)
if err = k3dCli.DeleteCluster(ctx, clusterName, false); err != nil {
logger.Get(ctx).Warnf("\tFailed to delete cluster %q: %v", clusterName, err)
}
}()
//
// 2. Use the new cluster's kubeconfig for this Tilt process
//
if kubeconfig, err := k3dCli.GenerateKubeconfig(ctx, clusterName); err != nil {
return fmt.Errorf("failed to generate kubeconfig: %v", err)
} else {
kubeconfigPath := filepath.Join(c.tmpdir, "kubeconfig")
if err := os.WriteFile(kubeconfigPath, kubeconfig, 0666); err != nil {
return fmt.Errorf("failed to write kubeconfig file: %v", err)
}
err = os.Setenv("KUBECONFIG", kubeconfigPath)
if err != nil {
return fmt.Errorf("failed to set KUBECONFIG env var: %v", err)
}
}
}
//
// 3. Download the sample project to a tmpdir
//
var projPath string
if c.tiltfilePath == "" {
logger.Get(ctx).Infof("\tFetching %q project...", c.projPackage)
dlr := get.NewDownloader(c.tmpdir)
projPath, err = dlr.Download(c.projPackage)
if err != nil {
return fmt.Errorf("failed to download sample project: %v", err)
}
c.tiltfilePath = filepath.Join(projPath, "Tiltfile")
}
logger.Get(ctx).Infof("\tDone!")
if projPath != "" {
logger.Get(ctx).Infof(
`
-----------------------------------------------------
Open the project directory in your preferred editor:
%s
-----------------------------------------------------
`, color.BlueString("%s", projPath))
}
//
// 4. Launch the `tilt up` command with the sample project
// (it will implicitly use our kubeconfig)
//
up := upCmd{
fileName: c.tiltfilePath,
legacy: c.legacy,
stream: false,
}
return up.run(ctx, args)
}
func (c *demoCmd) cleanupClusters(ctx context.Context, k3dCli *demo.K3dClient) error {
clusterNames, err := k3dCli.ListClusters(ctx)
if err != nil {
return err
}
failed := false
for _, clusterName := range clusterNames {
if strings.HasPrefix(clusterName, demoResourcesPrefix) {
logger.Get(ctx).Infof("Removing cluster %q...", clusterName)
if err := k3dCli.DeleteCluster(ctx, clusterName, true); err != nil {
failed = true
logger.Get(ctx).Errorf("Failed to remove %q cluster: %v", clusterName, err)
}
}
}
if failed {
return errors.New("could not remove one or more tilt-demo K8s clusters")
}
return nil
}
// TODO(milas): this is copy-pasted from ctlptl, use it from a common place
func isLocalDockerHost(dockerHost string) bool {
return dockerHost == "" ||
// Check all the "standard" docker localhosts.
// https://github.com/docker/cli/blob/a32cd16160f1b41c1c4ae7bee4dac929d1484e59/opts/hosts.go#L22
strings.HasPrefix(dockerHost, "tcp://localhost:") ||
strings.HasPrefix(dockerHost, "tcp://127.0.0.1:") ||
// https://github.com/moby/moby/blob/master/client/client_windows.go#L4
strings.HasPrefix(dockerHost, "npipe:") ||
// https://github.com/moby/moby/blob/master/client/client_unix.go#L6
strings.HasPrefix(dockerHost, "unix:")
}