From f52213861a955f3af4d98e315e86db9d47c18fce Mon Sep 17 00:00:00 2001 From: Tyler Fong Date: Thu, 16 Apr 2026 16:38:03 -0700 Subject: [PATCH 1/5] boiler for launchable create flow --- pkg/cmd/cmd.go | 2 + pkg/cmd/launchable/create.go | 782 ++++++++++++++++++++++++++++++ pkg/cmd/launchable/create_test.go | 133 +++++ pkg/cmd/launchable/launchable.go | 29 ++ pkg/store/launchable.go | 92 ++++ 5 files changed, 1038 insertions(+) create mode 100644 pkg/cmd/launchable/create.go create mode 100644 pkg/cmd/launchable/create_test.go create mode 100644 pkg/cmd/launchable/launchable.go create mode 100644 pkg/store/launchable.go diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 6912df3a..cfbe9d98 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -27,6 +27,7 @@ import ( "github.com/brevdev/brev-cli/pkg/cmd/importideconfig" "github.com/brevdev/brev-cli/pkg/cmd/initfile" "github.com/brevdev/brev-cli/pkg/cmd/invite" + "github.com/brevdev/brev-cli/pkg/cmd/launchable" "github.com/brevdev/brev-cli/pkg/cmd/login" "github.com/brevdev/brev-cli/pkg/cmd/logout" "github.com/brevdev/brev-cli/pkg/cmd/ls" @@ -322,6 +323,7 @@ func createCmdTree(cmd *cobra.Command, t *terminal.Terminal, loginCmdStore *stor cmd.AddCommand(scale.NewCmdScale(t, noLoginCmdStore)) cmd.AddCommand(gpusearch.NewCmdGPUSearch(t, noLoginCmdStore)) cmd.AddCommand(gpucreate.NewCmdGPUCreate(t, loginCmdStore)) + cmd.AddCommand(launchable.NewCmdLaunchable(t, loginCmdStore)) cmd.AddCommand(configureenvvars.NewCmdConfigureEnvVars(t, loginCmdStore)) cmd.AddCommand(importideconfig.NewCmdImportIDEConfig(t, noLoginCmdStore)) cmd.AddCommand(shell.NewCmdShell(t, loginCmdStore, noLoginCmdStore)) diff --git a/pkg/cmd/launchable/create.go b/pkg/cmd/launchable/create.go new file mode 100644 index 00000000..275bc964 --- /dev/null +++ b/pkg/cmd/launchable/create.go @@ -0,0 +1,782 @@ +package launchable + +import ( + "encoding/json" + "fmt" + "net/url" + "os" + "regexp" + "strconv" + "strings" + + "github.com/brevdev/brev-cli/pkg/cmd/cmderrors" + "github.com/brevdev/brev-cli/pkg/cmd/gpusearch" + "github.com/brevdev/brev-cli/pkg/config" + breverrors "github.com/brevdev/brev-cli/pkg/errors" + "github.com/brevdev/brev-cli/pkg/store" + "github.com/brevdev/brev-cli/pkg/terminal" + "github.com/spf13/cobra" +) + +const ( + defaultLaunchableViewAccess = "public" + defaultLaunchableMode = "vm" + defaultFlexibleStorageGiB = 256 + maxLifecycleScriptBytes = 16 * 1024 + maxCTAServiceCount = 2 +) + +type launchableCreateOptions struct { + Name string + Description string + ViewAccess string + Mode string + InstanceType string + Storage string + FileURL string + Jupyter bool + LifecycleScript string + LifecycleFilePath string + ComposeURL string + ComposeFilePath string + ComposeYAML string + SecureLinks []string + FirewallRules []string + DryRun bool +} + +type secureLinkSpec struct { + Name string + Port int + CTA bool + CTALabel string +} + +type firewallRuleSpec struct { + Ports string + AllowedIPs string + StartPort int + EndPort int +} + +var ( + githubDirectoryURLPattern = regexp.MustCompile(`^https://github\.com/[A-Za-z0-9_-]+/[A-Za-z0-9_.-]+/tree/[A-Za-z0-9_.-]+(?:/[\S]*)?$`) + gitlabDirectoryURLPattern = regexp.MustCompile(`^https://gitlab\.com/(?:[A-Za-z0-9_-]+/)*[A-Za-z0-9_.-]+/-/tree/[A-Za-z0-9_.-]+(?:/[\S]*)?$`) + sizePattern = regexp.MustCompile(`(?i)^([0-9]+(?:\.[0-9]+)?)([a-z]+)$`) +) + +func NewCmdLaunchableCreate(t *terminal.Terminal, launchableStore LaunchableCmdStore) *cobra.Command { //nolint:funlen // easier to read with flags close to command + var opts launchableCreateOptions + + cmd := &cobra.Command{ + Use: "create [name]", + DisableFlagsInUseLine: true, + Short: "Create a launchable", + Long: `Create a Brev launchable using VM or Docker Compose configuration. + +Every supported setting in the current launchable create flow is exposed as flags, +including view access, storage, file source, networking, lifecycle scripts, and +Docker Compose source selection.`, + Example: ` + brev launchable create demo-vm --mode vm --instance-type n2-standard-4 \ + --description "My VM launchable" --jupyter \ + --lifecycle-script-file ./setup.sh \ + --secure-link name=notebook,port=8888,cta=true,cta-label=OpenNotebook + + brev launchable create demo-compose --mode compose --instance-type g5.xlarge \ + --compose-file ./docker-compose.yml --file-url https://github.com/acme/demo \ + --secure-link name=web,port=3000,cta=true \ + --firewall-rule ports=8000-8100,allowed-ips=user-ip +`, + Args: cmderrors.TransformToValidationError(cobra.MaximumNArgs(1)), + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 && opts.Name == "" { + opts.Name = args[0] + } + + err := runLaunchableCreate(t, launchableStore, opts) + if err != nil { + return breverrors.WrapAndTrace(err) + } + return nil + }, + } + + cmd.Flags().StringVarP(&opts.Name, "name", "n", "", "Launchable name (or pass as first argument)") + cmd.Flags().StringVar(&opts.Description, "description", "", "Launchable description") + cmd.Flags().StringVar(&opts.ViewAccess, "view-access", defaultLaunchableViewAccess, "Launchable visibility: public, organization, or published") + cmd.Flags().StringVarP(&opts.Mode, "mode", "m", defaultLaunchableMode, "Launchable build mode: vm or compose") + cmd.Flags().StringVarP(&opts.InstanceType, "instance-type", "t", "", "Instance type to use for this launchable") + cmd.Flags().StringVar(&opts.Storage, "storage", "", "Root disk size in GiB for instance types with flexible storage") + cmd.Flags().StringVar(&opts.FileURL, "file-url", "", "Public notebook, markdown, or git repo URL to preload into the launchable") + cmd.Flags().BoolVar(&opts.Jupyter, "jupyter", true, "Install Jupyter on the host (applies to vm and compose modes)") + cmd.Flags().StringVar(&opts.LifecycleScript, "lifecycle-script", "", "Inline lifecycle script for VM mode") + cmd.Flags().StringVar(&opts.LifecycleFilePath, "lifecycle-script-file", "", "Path to a lifecycle script file for VM mode") + cmd.Flags().StringVar(&opts.ComposeURL, "compose-url", "", "Public Docker Compose URL") + cmd.Flags().StringVar(&opts.ComposeFilePath, "compose-file", "", "Path to a local Docker Compose file") + cmd.Flags().StringVar(&opts.ComposeYAML, "compose-yaml", "", "Inline Docker Compose YAML") + cmd.Flags().StringArrayVar(&opts.SecureLinks, "secure-link", nil, "Expose a secure link: name=,port=[,cta=true][,cta-label=