-
Notifications
You must be signed in to change notification settings - Fork 0
feat(lab): backend-agnostic ephemeral env API (Docker driver, TTL reaper, hot/cold tiers, audit) #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,282 @@ | ||||||
| package cmd | ||||||
|
|
||||||
| import ( | ||||||
| "context" | ||||||
| "encoding/json" | ||||||
| "fmt" | ||||||
| "os" | ||||||
| "strings" | ||||||
| "text/tabwriter" | ||||||
| "time" | ||||||
|
|
||||||
| "github.com/spf13/cobra" | ||||||
|
|
||||||
| "github.com/Gradient-Linux/concave/internal/lab" | ||||||
| "github.com/Gradient-Linux/concave/internal/ui" | ||||||
| ) | ||||||
|
|
||||||
| var ( | ||||||
| labLaunchImage string | ||||||
| labLaunchDisplayName string | ||||||
| labLaunchTTL time.Duration | ||||||
| labLaunchGPUs int | ||||||
| labLaunchCPU string | ||||||
| labLaunchMem string | ||||||
| labLaunchDriver string | ||||||
| labLaunchOwner string | ||||||
|
|
||||||
| labExtendBy time.Duration | ||||||
| labStorageHot string | ||||||
| labStorageCold string | ||||||
| labActiveDriver string | ||||||
| ) | ||||||
|
|
||||||
| var labEnvsCmd = &cobra.Command{ | ||||||
| Use: "envs", | ||||||
| Short: "Manage ephemeral JupyterLab environments", | ||||||
| } | ||||||
|
|
||||||
| var labEnvsListCmd = &cobra.Command{ | ||||||
| Use: "list", | ||||||
| Short: "List ephemeral lab environments", | ||||||
| RunE: runLabEnvsList, | ||||||
| } | ||||||
|
|
||||||
| var labEnvsLaunchCmd = &cobra.Command{ | ||||||
| Use: "launch", | ||||||
| Short: "Launch an ephemeral lab environment", | ||||||
| RunE: runLabEnvsLaunch, | ||||||
| } | ||||||
|
|
||||||
| var labEnvsExtendCmd = &cobra.Command{ | ||||||
| Use: "extend <env-id>", | ||||||
| Short: "Extend the TTL of an ephemeral lab environment", | ||||||
| Args: cobra.ExactArgs(1), | ||||||
| RunE: runLabEnvsExtend, | ||||||
| } | ||||||
|
|
||||||
| var labEnvsArchiveCmd = &cobra.Command{ | ||||||
| Use: "archive <env-id>", | ||||||
| Short: "Archive an ephemeral lab environment to the cold tier and destroy it", | ||||||
| Args: cobra.ExactArgs(1), | ||||||
| RunE: runLabEnvsArchive, | ||||||
| } | ||||||
|
|
||||||
| var labStorageCmd = &cobra.Command{ | ||||||
| Use: "storage", | ||||||
| Short: "Inspect or update lab storage tiers", | ||||||
| } | ||||||
|
|
||||||
| var labStorageShowCmd = &cobra.Command{ | ||||||
| Use: "show", | ||||||
| Short: "Show current lab storage configuration", | ||||||
| RunE: runLabStorageShow, | ||||||
| } | ||||||
|
|
||||||
| var labStorageSetCmd = &cobra.Command{ | ||||||
| Use: "set", | ||||||
| Short: "Set hot and cold tier paths", | ||||||
| RunE: runLabStorageSet, | ||||||
| } | ||||||
|
|
||||||
| var labDriverCmd = &cobra.Command{ | ||||||
| Use: "driver", | ||||||
| Short: "Inspect or select the active lab driver", | ||||||
| } | ||||||
|
|
||||||
| var labDriverShowCmd = &cobra.Command{ | ||||||
| Use: "show", | ||||||
| Short: "Show registered lab drivers and the active one", | ||||||
| RunE: runLabDriverShow, | ||||||
| } | ||||||
|
|
||||||
| var labDriverSetCmd = &cobra.Command{ | ||||||
| Use: "set <driver>", | ||||||
| Short: "Set the active lab driver (docker | slurm | proxmox)", | ||||||
| Args: cobra.ExactArgs(1), | ||||||
| RunE: runLabDriverSet, | ||||||
| } | ||||||
|
|
||||||
| func runLabEnvsList(_ *cobra.Command, _ []string) error { | ||||||
| mgr, err := newLabManager() | ||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
| envs := mgr.List() | ||||||
| tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) | ||||||
| fmt.Fprintln(tw, "ID\tOWNER\tDRIVER\tSTATUS\tIMAGE\tEXPIRES\tURL") | ||||||
| now := time.Now() | ||||||
| for _, env := range envs { | ||||||
| expires := "-" | ||||||
| if !env.ExpiresAt.IsZero() { | ||||||
| expires = env.Remaining(now).Round(time.Second).String() | ||||||
| } | ||||||
| fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", | ||||||
| env.ID, env.Owner, env.Driver, env.Status, env.Image, expires, env.JupyterURL) | ||||||
| } | ||||||
| return tw.Flush() | ||||||
| } | ||||||
|
|
||||||
| func runLabEnvsLaunch(cmd *cobra.Command, _ []string) error { | ||||||
| if labLaunchImage == "" { | ||||||
| return fmt.Errorf("--image is required") | ||||||
| } | ||||||
| if labLaunchTTL <= 0 { | ||||||
| return fmt.Errorf("--ttl must be positive (e.g. 2h)") | ||||||
| } | ||||||
| owner := labLaunchOwner | ||||||
| if owner == "" { | ||||||
| if u := os.Getenv("USER"); u != "" { | ||||||
| owner = u | ||||||
| } else { | ||||||
| owner = "local" | ||||||
| } | ||||||
| } | ||||||
| mgr, err := newLabManager() | ||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
| ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Minute) | ||||||
| defer cancel() | ||||||
| env, err := mgr.Launch(ctx, lab.EnvSpec{ | ||||||
| Owner: owner, | ||||||
| Image: labLaunchImage, | ||||||
| DisplayName: labLaunchDisplayName, | ||||||
| GPUs: labLaunchGPUs, | ||||||
| CPURequest: labLaunchCPU, | ||||||
| MemRequest: labLaunchMem, | ||||||
| TTL: labLaunchTTL, | ||||||
| Driver: labLaunchDriver, | ||||||
| }) | ||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
| ui.Info("lab", fmt.Sprintf("launched %s (%s) expiring in %s", env.ID, env.Driver, labLaunchTTL)) | ||||||
| if env.JupyterURL != "" { | ||||||
| ui.Info("jupyter", env.JupyterURL) | ||||||
| } | ||||||
| return nil | ||||||
| } | ||||||
|
|
||||||
| func runLabEnvsExtend(cmd *cobra.Command, args []string) error { | ||||||
| if labExtendBy <= 0 { | ||||||
| return fmt.Errorf("--by must be positive (e.g. 1h)") | ||||||
| } | ||||||
| mgr, err := newLabManager() | ||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
| env, err := mgr.ExtendTTL(cmd.Context(), args[0], labExtendBy) | ||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
| ui.Info("lab", fmt.Sprintf("extended %s; new expiry %s", env.ID, env.ExpiresAt.Format(time.RFC3339))) | ||||||
| return nil | ||||||
| } | ||||||
|
|
||||||
| func runLabEnvsArchive(cmd *cobra.Command, args []string) error { | ||||||
| mgr, err := newLabManager() | ||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
| env, err := mgr.ArchiveAndDestroy(cmd.Context(), args[0]) | ||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
| ui.Info("lab", fmt.Sprintf("archived %s -> %s", env.ID, env.ArchiveRef)) | ||||||
| return nil | ||||||
| } | ||||||
|
|
||||||
| func runLabStorageShow(_ *cobra.Command, _ []string) error { | ||||||
| storage, err := lab.LoadStorage() | ||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
| out, _ := json.MarshalIndent(storage, "", " ") | ||||||
| fmt.Println(string(out)) | ||||||
| return nil | ||||||
| } | ||||||
|
|
||||||
| func runLabStorageSet(_ *cobra.Command, _ []string) error { | ||||||
| current, err := lab.LoadStorage() | ||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
| if strings.TrimSpace(labStorageHot) != "" { | ||||||
| current.HotTier = labStorageHot | ||||||
| } | ||||||
| if strings.TrimSpace(labStorageCold) != "" { | ||||||
| current.ColdTier = labStorageCold | ||||||
| } | ||||||
| if err := lab.SaveStorage(current); err != nil { | ||||||
| return err | ||||||
| } | ||||||
| if err := lab.EnsureTierDirs(current); err != nil { | ||||||
| return err | ||||||
| } | ||||||
| ui.Info("lab", fmt.Sprintf("hot=%s cold=%s", current.HotTier, current.ColdTier)) | ||||||
| return nil | ||||||
| } | ||||||
|
|
||||||
| func runLabDriverShow(_ *cobra.Command, _ []string) error { | ||||||
| mgr, err := newLabManager() | ||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
| fmt.Println("active:", mgr.Registry().Active()) | ||||||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CONTRIBUTING.md violation:
Suggested change
Was this helpful? React with 👍 or 👎 to provide feedback. |
||||||
| fmt.Println("drivers:", strings.Join(mgr.Registry().Names(), ", ")) | ||||||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CONTRIBUTING.md violation:
Suggested change
Was this helpful? React with 👍 or 👎 to provide feedback. |
||||||
| return nil | ||||||
| } | ||||||
|
|
||||||
| func runLabDriverSet(_ *cobra.Command, args []string) error { | ||||||
| mgr, err := newLabManager() | ||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
| if err := mgr.Registry().SetActive(args[0]); err != nil { | ||||||
| return err | ||||||
| } | ||||||
| ui.Info("lab", "active driver: "+args[0]) | ||||||
| return nil | ||||||
| } | ||||||
|
|
||||||
| // newLabManager builds a local-only lab.Manager suitable for CLI usage. The | ||||||
| // API server builds its own instance. | ||||||
| func newLabManager() (*lab.Manager, error) { | ||||||
| registry := lab.NewRegistry() | ||||||
| registry.Register(lab.NewDockerDriver(lab.DockerDriverConfig{})) | ||||||
| store, err := lab.NewStore() | ||||||
| if err != nil { | ||||||
| return nil, err | ||||||
| } | ||||||
| storage, err := lab.LoadStorage() | ||||||
| if err != nil { | ||||||
| return nil, err | ||||||
| } | ||||||
| if err := lab.EnsureTierDirs(storage); err != nil { | ||||||
| return nil, err | ||||||
| } | ||||||
| return lab.NewManager(registry, store, storage), nil | ||||||
| } | ||||||
|
|
||||||
| func init() { | ||||||
| labEnvsLaunchCmd.Flags().StringVar(&labLaunchImage, "image", "", "docker image for the env (e.g. jupyter/datascience-notebook:latest)") | ||||||
| labEnvsLaunchCmd.Flags().StringVar(&labLaunchDisplayName, "name", "", "display name") | ||||||
| labEnvsLaunchCmd.Flags().DurationVar(&labLaunchTTL, "ttl", 2*time.Hour, "time-to-live before the env is archived") | ||||||
| labEnvsLaunchCmd.Flags().IntVar(&labLaunchGPUs, "gpus", 0, "number of GPUs to attach") | ||||||
| labEnvsLaunchCmd.Flags().StringVar(&labLaunchCPU, "cpu", "", "CPU request (docker --cpus)") | ||||||
| labEnvsLaunchCmd.Flags().StringVar(&labLaunchMem, "mem", "", "memory request (docker --memory, e.g. 8g)") | ||||||
| labEnvsLaunchCmd.Flags().StringVar(&labLaunchDriver, "driver", "", "driver to use (defaults to active)") | ||||||
| labEnvsLaunchCmd.Flags().StringVar(&labLaunchOwner, "owner", "", "override owner (defaults to $USER)") | ||||||
|
|
||||||
| labEnvsExtendCmd.Flags().DurationVar(&labExtendBy, "by", time.Hour, "extension duration") | ||||||
|
|
||||||
| labStorageSetCmd.Flags().StringVar(&labStorageHot, "hot", "", "absolute path for the hot tier") | ||||||
| labStorageSetCmd.Flags().StringVar(&labStorageCold, "cold", "", "absolute path for the cold tier") | ||||||
| _ = labActiveDriver // reserved for future use | ||||||
|
|
||||||
| labEnvsCmd.AddCommand(labEnvsListCmd, labEnvsLaunchCmd, labEnvsExtendCmd, labEnvsArchiveCmd) | ||||||
| labStorageCmd.AddCommand(labStorageShowCmd, labStorageSetCmd) | ||||||
| labDriverCmd.AddCommand(labDriverShowCmd, labDriverSetCmd) | ||||||
| labCmd.AddCommand(labEnvsCmd, labStorageCmd, labDriverCmd) | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 CONTRIBUTING.md violation:
fmt.Printlnused incmd/(runLabStorageShow)fmt.Println(string(out))atcmd/lab_envs.go:196directly writes to stdout, violating the mandatory CONTRIBUTING.md rule: "All terminal output incmd/must go throughinternal/ui/printer.go. Do not usefmt.Printlnorlog.Printfincmd/." No othercmd/file usesfmt.Println— the convention is consistently followed elsewhere.Was this helpful? React with 👍 or 👎 to provide feedback.