github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/cli/demo.go (about) 1 package cli 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "path/filepath" 8 "strings" 9 "time" 10 11 "github.com/fatih/color" 12 "github.com/pkg/errors" 13 "github.com/spf13/cobra" 14 15 "github.com/tilt-dev/go-get" 16 17 "github.com/tilt-dev/tilt/internal/analytics" 18 "github.com/tilt-dev/tilt/internal/cli/demo" 19 "github.com/tilt-dev/tilt/pkg/logger" 20 "github.com/tilt-dev/tilt/pkg/model" 21 ) 22 23 const demoResourcesPrefix = "tilt-demo-" 24 const sampleProjPackage = "github.com/tilt-dev/tilt-avatars" 25 26 type demoCmd struct { 27 // legacy disables the web UI (this is only used for integration tests) 28 legacy bool 29 // teardown will clean up any leftover `tilt demo` clusters and exit 30 teardown bool 31 // tmpdir for cloned `tilt-avatars` resources 32 tmpdir string 33 // skipCreateCluster uses default kubeconfig context instead of creating 34 // an ephemeral cluster 35 skipCreateCluster bool 36 // projPackage is the `go get` style URL for the demo project 37 projPackage string 38 // tiltfilePath is a path to a Tiltfile to launch instead of cloning and 39 // running the `tilt-avatars` project 40 tiltfilePath string 41 } 42 43 func (c *demoCmd) name() model.TiltSubcommand { return "demo" } 44 45 func (c *demoCmd) register() *cobra.Command { 46 cmd := &cobra.Command{ 47 Use: "demo [flags]", 48 Short: "Creates a local, temporary Kubernetes cluster and runs a Tilt sample project", 49 Long: fmt.Sprintf(`Test out Tilt using an isolated, ephemeral local Kubernetes setup. 50 51 Tilt will create a temporary, local Kubernetes development cluster running in Docker. 52 The cluster will be removed when Tilt is exited with Ctrl-C. 53 54 A sample project (%s) will be cloned locally to a temporary directory using Git and launched. 55 `, sampleProjPackage), 56 } 57 58 cmd.Flags().BoolVarP(&c.teardown, "teardown", "", false, 59 "Removes any leftover tilt-demo Kubernetes clusters and exits") 60 61 // --legacy flag only exists for integration tests to disable web console 62 cmd.Flags().BoolVar(&c.legacy, "legacy", false, "If true, tilt will open in legacy HUD mode.") 63 cmd.Flags().Lookup("legacy").Hidden = true 64 65 // --tmpdir exists so that integration tests can inspect the output / use the Tiltfile 66 cmd.Flags().StringVarP(&c.tmpdir, "tmpdir", "", "", 67 "Temporary directory to clone sample project to") 68 cmd.Flags().Lookup("tmpdir").Hidden = true 69 70 cmd.Flags().BoolVar(&c.skipCreateCluster, "no-cluster", false, 71 "Skip ephemeral cluster creation (requires local K8s cluster to already be configured)") 72 73 cmd.Flags().StringVarP(&c.projPackage, "repo", "r", sampleProjPackage, 74 "Path to custom repo to use instead of Tiltfile") 75 76 // we don't use the `addTiltfileFlag()` because the default here should be empty 77 cmd.Flags().StringVarP(&c.tiltfilePath, "file", "f", "", 78 "Path to custom Tiltfile to use instead of sample project") 79 80 addStartServerFlags(cmd) 81 addDevServerFlags(cmd) 82 83 return cmd 84 } 85 86 func (c *demoCmd) run(ctx context.Context, args []string) error { 87 a := analytics.Get(ctx) 88 a.Incr("cmd.demo", map[string]string{}) 89 defer a.Flush(time.Second) 90 91 client, err := wireDockerLocalClient(ctx) 92 if err != nil { 93 return errors.Wrap(err, "Failed to init Docker client") 94 } 95 k3dCli := demo.NewK3dClient(client) 96 97 if c.teardown { 98 return c.cleanupClusters(ctx, k3dCli) 99 } 100 101 if c.projPackage != sampleProjPackage && c.tiltfilePath != "" { 102 return fmt.Errorf("cannot specify both a custom repo and Tiltfile path") 103 } 104 105 // 106 // 0. Prepare environment 107 // 108 logger.Get(ctx).Infof("\nHang tight while Tilt prepares your demo environment!") 109 c.tmpdir, err = os.MkdirTemp(c.tmpdir, demoResourcesPrefix) 110 if err != nil { 111 return fmt.Errorf("could not create temporary directory: %v", err) 112 } 113 114 if !c.skipCreateCluster { 115 err = client.CheckConnected() 116 if err != nil { 117 return fmt.Errorf("tilt demo requires Docker to be installed and running: %v", err) 118 } 119 if !isLocalDockerHost(client.Env().DaemonHost()) { 120 // properly supporting remote Docker connections is very tricky - either: 121 // 122 // the remote host will need more ports accessible (for K8s API + registry API) and we have to ensure 123 // that everything both listens on the public interface and references it in configs 124 // (such as "local-registry-hosting" ConfigMap) 125 // OR 126 // we need to tunnel everything (perhaps using Docker - this is the approach ctlptl takes!) 127 // 128 // for now, it's not supported as it's a pretty advanced setup to begin with, so we're not really targeting 129 // it with the `tilt demo` functionality 130 return fmt.Errorf("tilt demo requires a local Docker daemon to create a temporary Kubernetes cluster (current Docker host: %s)", client.Env().DaemonHost()) 131 } 132 133 // 134 // 1. Create a cluster that will be torn down in the background on exit (Ctrl-C) 135 // 136 clusterName := filepath.Base(c.tmpdir) 137 logger.Get(ctx).Infof("\tCreating %q local Kubernetes cluster...", clusterName) 138 if err := k3dCli.CreateCluster(ctx, clusterName); err != nil { 139 return fmt.Errorf("failed to create Kubernetes cluster: %v", err) 140 } 141 defer func() { 142 // N.B. use background context because the main context has already been canceled due to Ctrl-C 143 // but also don't block on execution (just fire request to Docker API and forget) because at this 144 // point we have < 2 secs before the signal handler forcibly exits the process 145 ctx := logger.WithLogger(context.Background(), logger.Get(ctx)) 146 logger.Get(ctx).Infof("\nDeleting %q local Kubernetes cluster...", clusterName) 147 if err = k3dCli.DeleteCluster(ctx, clusterName, false); err != nil { 148 logger.Get(ctx).Warnf("\tFailed to delete cluster %q: %v", clusterName, err) 149 } 150 }() 151 152 // 153 // 2. Use the new cluster's kubeconfig for this Tilt process 154 // 155 if kubeconfig, err := k3dCli.GenerateKubeconfig(ctx, clusterName); err != nil { 156 return fmt.Errorf("failed to generate kubeconfig: %v", err) 157 } else { 158 kubeconfigPath := filepath.Join(c.tmpdir, "kubeconfig") 159 if err := os.WriteFile(kubeconfigPath, kubeconfig, 0666); err != nil { 160 return fmt.Errorf("failed to write kubeconfig file: %v", err) 161 } 162 err = os.Setenv("KUBECONFIG", kubeconfigPath) 163 if err != nil { 164 return fmt.Errorf("failed to set KUBECONFIG env var: %v", err) 165 } 166 } 167 } 168 169 // 170 // 3. Download the sample project to a tmpdir 171 // 172 var projPath string 173 if c.tiltfilePath == "" { 174 logger.Get(ctx).Infof("\tFetching %q project...", c.projPackage) 175 dlr := get.NewDownloader(c.tmpdir) 176 projPath, err = dlr.Download(c.projPackage) 177 if err != nil { 178 return fmt.Errorf("failed to download sample project: %v", err) 179 } 180 c.tiltfilePath = filepath.Join(projPath, "Tiltfile") 181 } 182 183 logger.Get(ctx).Infof("\tDone!") 184 if projPath != "" { 185 logger.Get(ctx).Infof( 186 ` 187 ----------------------------------------------------- 188 Open the project directory in your preferred editor: 189 %s 190 ----------------------------------------------------- 191 `, color.BlueString("%s", projPath)) 192 } 193 194 // 195 // 4. Launch the `tilt up` command with the sample project 196 // (it will implicitly use our kubeconfig) 197 // 198 up := upCmd{ 199 fileName: c.tiltfilePath, 200 legacy: c.legacy, 201 stream: false, 202 } 203 return up.run(ctx, args) 204 } 205 206 func (c *demoCmd) cleanupClusters(ctx context.Context, k3dCli *demo.K3dClient) error { 207 clusterNames, err := k3dCli.ListClusters(ctx) 208 if err != nil { 209 return err 210 } 211 212 failed := false 213 for _, clusterName := range clusterNames { 214 if strings.HasPrefix(clusterName, demoResourcesPrefix) { 215 logger.Get(ctx).Infof("Removing cluster %q...", clusterName) 216 if err := k3dCli.DeleteCluster(ctx, clusterName, true); err != nil { 217 failed = true 218 logger.Get(ctx).Errorf("Failed to remove %q cluster: %v", clusterName, err) 219 } 220 } 221 } 222 if failed { 223 return errors.New("could not remove one or more tilt-demo K8s clusters") 224 } 225 return nil 226 } 227 228 // TODO(milas): this is copy-pasted from ctlptl, use it from a common place 229 func isLocalDockerHost(dockerHost string) bool { 230 return dockerHost == "" || 231 232 // Check all the "standard" docker localhosts. 233 // https://github.com/docker/cli/blob/a32cd16160f1b41c1c4ae7bee4dac929d1484e59/opts/hosts.go#L22 234 strings.HasPrefix(dockerHost, "tcp://localhost:") || 235 strings.HasPrefix(dockerHost, "tcp://127.0.0.1:") || 236 237 // https://github.com/moby/moby/blob/master/client/client_windows.go#L4 238 strings.HasPrefix(dockerHost, "npipe:") || 239 240 // https://github.com/moby/moby/blob/master/client/client_unix.go#L6 241 strings.HasPrefix(dockerHost, "unix:") 242 }