From d0bd7d7a102e9199d32e6910d8af3c9b61529506 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Sun, 24 Aug 2025 15:21:15 +1200 Subject: [PATCH 01/25] Add tools code-server --- cmd/options.go | 53 +++-- cmd/tools.go | 16 ++ cmd/tools_api_start.go | 11 - cmd/tools_code_server.go | 457 +++++++++++++++++++++++++++++++++++++++ runtime/docker/docker.go | 9 + runtime/ignite/ignite.go | 9 + runtime/runtime.go | 2 + 7 files changed, 529 insertions(+), 28 deletions(-) create mode 100644 cmd/tools_code_server.go diff --git a/cmd/options.go b/cmd/options.go index 57113b0a26..4405cbafe3 100644 --- a/cmd/options.go +++ b/cmd/options.go @@ -102,6 +102,13 @@ func GetOptions() *Options { SrcPort: 0, DeletionPrefix: "vx-", }, + ToolsCodeServer: &ToolsCodeServerOptions{ + Image: "ghcr.io/kaelemc/clab-code-server:latest", + Name: "clab-code-server", + Host: "localhost", + LogLevel: "debug", + OutputFormat: "table", + }, Version: &VersionOptions{ Short: false, JSON: false, @@ -113,23 +120,24 @@ func GetOptions() *Options { } type Options struct { - Global *GlobalOptions - Filter *FilterOptions - Deploy *DeployOptions - Destroy *DestroyOptions - Config *ConfigOptions - Exec *ExecOptions - Inspect *InspectOptions - Graph *GraphOptions - ToolsAPI *ToolsApiOptions - ToolsCert *ToolsCertOptions - ToolsTxOffload *ToolsDisableTxOffloadOptions - ToolsGoTTY *ToolsGoTTYOptions - ToolsNetem *ToolsNetemOptions - ToolsSSHX *ToolsSSHXOptions - ToolsVeth *ToolsVethOptions - ToolsVxlan *ToolsVxlanOptions - Version *VersionOptions + Global *GlobalOptions + Filter *FilterOptions + Deploy *DeployOptions + Destroy *DestroyOptions + Config *ConfigOptions + Exec *ExecOptions + Inspect *InspectOptions + Graph *GraphOptions + ToolsAPI *ToolsApiOptions + ToolsCert *ToolsCertOptions + ToolsTxOffload *ToolsDisableTxOffloadOptions + ToolsGoTTY *ToolsGoTTYOptions + ToolsNetem *ToolsNetemOptions + ToolsSSHX *ToolsSSHXOptions + ToolsVeth *ToolsVethOptions + ToolsVxlan *ToolsVxlanOptions + ToolsCodeServer *ToolsCodeServerOptions + Version *VersionOptions } func (o *Options) ToClabOptions() []clabcore.ClabOption { @@ -435,6 +443,17 @@ type ToolsVxlanOptions struct { DeletionPrefix string } +type ToolsCodeServerOptions struct { + Image string + Name string + Port uint + Host string + LogLevel string + OutputFormat string + LabsDirectory string + Owner string +} + type VersionOptions struct { Short bool JSON bool diff --git a/cmd/tools.go b/cmd/tools.go index c52a53856e..b41c16020f 100644 --- a/cmd/tools.go +++ b/cmd/tools.go @@ -5,6 +5,7 @@ package cmd import ( + "os" "path/filepath" "strings" @@ -22,6 +23,7 @@ func toolsSubcommandRegisterFuncs() []func(*Options) (*cobra.Command, error) { sshxCmd, vethCmd, vxlanCmd, + codeServerCmd, } } @@ -81,3 +83,17 @@ func createLabelsMap(topo, labName, containerName, owner, toolType string) map[s return labels } + +// getclabBinaryPath determine the binary path of the running executable. +func getclabBinaryPath() (string, error) { + exePath, err := os.Executable() + if err != nil { + return "", err + } + + absPath, err := filepath.EvalSymlinks(exePath) + if err != nil { + return "", err + } + return absPath, nil +} diff --git a/cmd/tools_api_start.go b/cmd/tools_api_start.go index f50f1abb5a..05b2a9daf7 100644 --- a/cmd/tools_api_start.go +++ b/cmd/tools_api_start.go @@ -7,7 +7,6 @@ package cmd import ( "fmt" "os" - "path/filepath" "github.com/charmbracelet/log" "github.com/spf13/cobra" @@ -93,16 +92,6 @@ func (*APIServerNode) GetEndpoints() []clablinks.Endpoint { return nil } -// getclabBinaryPath determine the binary path of the running executable. -func getclabBinaryPath() (string, error) { - exePath, err := os.Executable() - if err != nil { - return "", err - } - - return filepath.EvalSymlinks(exePath) -} - // createLabels creates container labels. func createAPIServerLabels( containerName, diff --git a/cmd/tools_code_server.go b/cmd/tools_code_server.go new file mode 100644 index 0000000000..b612619d2f --- /dev/null +++ b/cmd/tools_code_server.go @@ -0,0 +1,457 @@ +// Copyright 2025 +// Licensed under the BSD 3-Clause License. +// SPDX-License-Identifier: BSD-3-Clause + +package cmd + +import ( + "encoding/json" + "fmt" + "os" + "strconv" + "strings" + + "github.com/charmbracelet/log" + "github.com/docker/go-connections/nat" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/jedib0t/go-pretty/v6/text" + "github.com/spf13/cobra" + clabcore "github.com/srl-labs/containerlab/core" + clablabels "github.com/srl-labs/containerlab/labels" + clablinks "github.com/srl-labs/containerlab/links" + clabruntime "github.com/srl-labs/containerlab/runtime" + clabtypes "github.com/srl-labs/containerlab/types" + clabutils "github.com/srl-labs/containerlab/utils" +) + +func codeServerCmd(o *Options) (*cobra.Command, error) { + c := &cobra.Command{ + Use: "code-server", + Short: "Containerlab code-server server operations", + Long: "Start, stop, and manage Containerlab code-server containers", + } + + codeServerStartCmd := &cobra.Command{ + Use: "start", + Short: "start Containerlab code-server container", + PreRunE: func(_ *cobra.Command, _ []string) error { + return clabutils.CheckAndGetRootPrivs() + }, + RunE: func(cobraCmd *cobra.Command, _ []string) error { + return codeServerStart(cobraCmd, o) + }, + } + + c.AddCommand(codeServerStartCmd) + codeServerStartCmd.Flags().StringVarP(&o.ToolsCodeServer.Image, "image", "i", + o.ToolsCodeServer.Image, + "container image to use for code-server") + codeServerStartCmd.Flags().StringVarP(&o.ToolsCodeServer.Name, "name", "n", o.ToolsCodeServer.Name, + "name of the code-server container") + codeServerStartCmd.Flags().StringVarP(&o.ToolsCodeServer.LabsDirectory, "labs-dir", "l", o.ToolsCodeServer.LabsDirectory, + "directory to mount as shared labs directory") + codeServerStartCmd.Flags().UintVarP(&o.ToolsCodeServer.Port, "port", "p", o.ToolsCodeServer.Port, + "port to expose the code-server on") + codeServerStartCmd.Flags().StringVarP(&o.ToolsCodeServer.Owner, "owner", "o", o.ToolsCodeServer.Owner, + "owner name for the code-server container") + + codeServerStatusCmd := &cobra.Command{ + Use: "status", + Short: "show status of active Containerlab code-server containers", + PreRunE: func(_ *cobra.Command, _ []string) error { + return clabutils.CheckAndGetRootPrivs() + }, + RunE: func(cobraCmd *cobra.Command, _ []string) error { + return codeServerStatus(cobraCmd, o) + }, + } + c.AddCommand(codeServerStatusCmd) + codeServerStatusCmd.Flags().StringVarP(&o.ToolsCodeServer.OutputFormat, "format", "f", o.ToolsCodeServer.OutputFormat, + "output format for 'status' command (table, json)") + + codeServerStopCmd := &cobra.Command{ + Use: "stop", + Short: "stop Containerlab code-server container", + PreRunE: func(_ *cobra.Command, _ []string) error { + return clabutils.CheckAndGetRootPrivs() + }, + RunE: func(cobraCmd *cobra.Command, _ []string) error { + return codeServerStop(cobraCmd, o) + }, + } + c.AddCommand(codeServerStopCmd) + codeServerStopCmd.Flags().StringVarP(&o.ToolsCodeServer.Name, "name", "n", o.ToolsCodeServer.Name, + "name of the code-server container to stop") + + return c, nil +} + +func NewCodeServerNode(name, image, labsDir string, port uint, runtime clabruntime.ContainerRuntime, labels map[string]string, +) (*codeServerNode, error) { + log.With("name", name, "image", image, "labsDir", labsDir, "runtime", runtime).Debug("Creating new code-server node.") + + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("failed to get user home directory: %w", err) + } + + binds := clabtypes.Binds{ + clabtypes.NewBind(homeDir, "/home/coder/labs", ""), + clabtypes.NewBind("/etc/group", "/etc/group", "ro"), + } + + // get the runtime socket path + rtSocket, err := runtime.GetRuntimeSocket() + if err != nil { + return nil, err + } + + // build the bindmount for the socket, path sound be the same in the container as is on the host + // append the socket to the binds + binds = append(binds, clabtypes.NewBind(rtSocket, rtSocket, "")) + + // append the mounts required for container out of container operation + binds = append(binds, runtime.GetCooCBindMounts()...) + + // Find Docker binary and add bind mount if found + rtBinPath, err := runtime.GetRuntimeBinary() + if err != nil { + return nil, fmt.Errorf("could not find docker binary: %v. code-server might not function correctly if docker is not available", err) + } + // currently only docker is supported. + binds = append(binds, clabtypes.NewBind(rtBinPath, "/usr/bin/docker", "ro")) + + // Find containerlab binary and add bind mount if found + clabPath, err := getclabBinaryPath() + if err != nil { + return nil, fmt.Errorf("could not find containerlab binary: %v. code-server might not function correctly if containerlab is not in its PATH", err) + } + binds = append(binds, clabtypes.NewBind(clabPath, "/usr/bin/containerlab", "ro")) + + // Publish host random port -> ctr port 8080 + exposedPorts := make(nat.PortSet) + portBindings := make(nat.PortMap) + + containerPort, err := nat.NewPort("tcp", "8080") + if err != nil { + return nil, fmt.Errorf("failed to create container port: %w", err) + } + exposedPorts[containerPort] = struct{}{} + portBindings[containerPort] = []nat.PortBinding{ + { + HostIP: "0.0.0.0", + HostPort: "", + }, + } + + nodeConfig := &clabtypes.NodeConfig{ + LongName: name, + ShortName: name, + Image: image, + Binds: binds.ToStringSlice(), + Labels: labels, + PortSet: exposedPorts, + PortBindings: portBindings, + NetworkMode: "bridge", + User: "0", + } + + return &codeServerNode{ + config: nodeConfig, + }, nil +} + +func (n *codeServerNode) Config() *clabtypes.NodeConfig { + return n.config +} + +// GetEndpoints implementation for the Node interface. +func (*codeServerNode) GetEndpoints() []clablinks.Endpoint { + return nil +} + +// createLabels creates container labels. +func createCodeServerLabels(containerName, owner string, port uint, labsDir, host string) map[string]string { + labels := map[string]string{ + clablabels.NodeName: containerName, + clablabels.NodeKind: "linux", + clablabels.NodeType: "tool", + clablabels.ToolType: "code-server", + "clab-code-server-host": host, + "clab-code-server-port": fmt.Sprintf("%d", port), + "clab-labs-dir": labsDir, + } + + // Add owner label if available + if owner != "" { + labels[clablabels.Owner] = owner + } + + return labels +} + +func codeServerStart(cobraCmd *cobra.Command, o *Options) error { //nolint: funlen + ctx := cobraCmd.Context() + + log.With( + "name", o.ToolsCodeServer.Name, + "image", o.ToolsCodeServer.Image, + "labsDir", o.ToolsCodeServer.LabsDirectory, + "port", o.ToolsCodeServer.Port).Debug("code-server start called.") + + runtimeName := o.Global.Runtime + if runtimeName == "" { + runtimeName = "docker" + } + + // Initialize runtime + _, rinit, err := clabcore.RuntimeInitializer(runtimeName) + if err != nil { + return fmt.Errorf("failed to get runtime initializer for '%s': %w", runtimeName, err) + } + + rt := rinit() + err = rt.Init(clabruntime.WithConfig(&clabruntime.RuntimeConfig{Timeout: o.Global.Timeout})) + if err != nil { + return fmt.Errorf("failed to initialize runtime: %w", err) + } + + // Set management network to bridge for default Docker networking + rt.WithMgmtNet(&clabtypes.MgmtNet{Network: "bridge"}) + + // Check if container already exists + filter := []*clabtypes.GenericFilter{{FilterType: "name", Match: o.ToolsCodeServer.Name}} + containers, err := rt.ListContainers(ctx, filter) + if err != nil { + return fmt.Errorf("failed to list containers: %w", err) + } + if len(containers) > 0 { + return fmt.Errorf("container %s already exists", o.ToolsCodeServer.Name) + } + + // Pull the container image + log.Infof("Pulling image %s...", o.ToolsCodeServer.Image) + if err := rt.PullImage(ctx, o.ToolsCodeServer.Image, clabtypes.PullPolicyIfNotPresent); err != nil { + return fmt.Errorf("failed to pull image %s: %w", o.ToolsCodeServer.Image, err) + } + + // Create container labels + if o.ToolsCodeServer.LabsDirectory == "" { + o.ToolsCodeServer.LabsDirectory = "~/.clab" + } + + owner := getOwnerName(o) + labels := createCodeServerLabels(o.ToolsCodeServer.Name, owner, o.ToolsCodeServer.Port, + o.ToolsCodeServer.LabsDirectory, o.ToolsCodeServer.Host) + + // Create and start code server container + log.Info("Creating code server container", "name", o.ToolsCodeServer.Name) + codeServerNode, err := NewCodeServerNode(o.ToolsCodeServer.Name, o.ToolsCodeServer.Image, + o.ToolsCodeServer.LabsDirectory, o.ToolsCodeServer.Port, rt, labels) + if err != nil { + return err + } + + id, err := rt.CreateContainer(ctx, codeServerNode.Config()) + if err != nil { + return fmt.Errorf("failed to create code-server container: %w", err) + } + + if _, err := rt.StartContainer(ctx, id, codeServerNode); err != nil { + // Clean up on failure + rt.DeleteContainer(ctx, o.ToolsCodeServer.Name) + return fmt.Errorf("failed to start code-server container: %w", err) + } + + log.Infof("code-server container %s started successfully.", o.ToolsCodeServer.Name) + + // Get the actual assigned port from the container if using random port + if o.ToolsCodeServer.Port == 0 { + // Get container info to find the assigned port + containers, err := rt.ListContainers(ctx, []*clabtypes.GenericFilter{{ + FilterType: "name", Match: o.ToolsCodeServer.Name, + }}) + if err == nil && len(containers) > 0 && len(containers[0].Ports) > 0 { + for _, portMapping := range containers[0].Ports { + if portMapping.ContainerPort == 8080 { + log.Infof("code-server available at: http://%s:%d", + o.ToolsCodeServer.Host, portMapping.HostPort) + break + } + } + } else { + log.Infof("code-server container started. Check 'docker ps' for the assigned port.") + } + } else { + log.Infof("code-server available at: http://%s:%d", o.ToolsCodeServer.Host, o.ToolsCodeServer.Port) + } + + return nil +} + +// codeServerListItem defines the structure for API server container info in JSON output. +type codeServerListItem struct { + Name string `json:"name"` + State string `json:"state"` + Host string `json:"host"` + Port int `json:"port"` + LabsDir string `json:"labs_dir"` + Owner string `json:"owner"` +} + +func codeServerStatus(cobraCmd *cobra.Command, o *Options) error { //nolint: funlen + ctx := cobraCmd.Context() + + // Use common.Runtime for consistency with other commands + runtimeName := o.Global.Runtime + if runtimeName == "" { + runtimeName = "docker" + } + + // Initialize containerlab with runtime using the same approach as inspect command + opts := []clabcore.ClabOption{ + clabcore.WithTimeout(o.Global.Timeout), + clabcore.WithRuntime(runtimeName, + &clabruntime.RuntimeConfig{ + Debug: o.Global.DebugCount > 0, + Timeout: o.Global.Timeout, + GracefulShutdown: o.Destroy.GracefulShutdown, + }, + ), + clabcore.WithDebug(o.Global.DebugCount > 0), + } + + c, err := clabcore.NewContainerLab(opts...) + if err != nil { + return err + } + + // Check connectivity like inspect does + err = c.CheckConnectivity(ctx) + if err != nil { + return err + } + + containers, err := c.ListContainers(ctx, clabcore.WithListToolType("code-server")) + if err != nil { + return fmt.Errorf("failed to list containers: %w", err) + } + + if len(containers) == 0 { + if o.ToolsCodeServer.OutputFormat == "json" { + fmt.Println("[]") + } else { + fmt.Println("No active code-server containers found") + } + return nil + } + + // Process containers and format output + listItems := make([]codeServerListItem, 0, len(containers)) + for idx := range containers { + name := strings.TrimPrefix(containers[idx].Names[0], "/") + + // Get port from labels or use default + port := 8080 // default + if portStr, ok := containers[idx].Labels["code-server-port"]; ok { + if portVal, err := strconv.Atoi(portStr); err == nil { + port = portVal + } + } + + // Get host from labels or use default + host := "localhost" // default + if hostVal, ok := containers[idx].Labels["code-server-host"]; ok { + host = hostVal + } + + // Get labs dir from labels or use default + labsDir := "~/.clab" // default + if dirsVal, ok := containers[idx].Labels["clab-labs-dir"]; ok { + labsDir = dirsVal + } + + // Get owner from container labels + owner := "N/A" + if ownerVal, exists := containers[idx].Labels[clablabels.Owner]; exists && ownerVal != "" { + owner = ownerVal + } + + listItems = append(listItems, codeServerListItem{ + Name: name, + State: containers[idx].State, + Host: host, + Port: port, + LabsDir: labsDir, + Owner: owner, + }) + } + + if o.ToolsCodeServer.OutputFormat == "json" { + b, err := json.MarshalIndent(listItems, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal to JSON: %w", err) + } + fmt.Println(string(b)) + } else { + // Use go-pretty table + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.SetStyle(table.StyleRounded) + t.Style().Format.Header = text.FormatTitle + t.Style().Options.SeparateRows = true + + t.AppendHeader(table.Row{"NAME", "STATUS", "HOST", "PORT", "LABS DIR", "OWNER"}) + + for _, item := range listItems { + t.AppendRow(table.Row{ + item.Name, + item.State, + item.Host, + item.Port, + item.LabsDir, + item.Owner, + }) + } + t.Render() + } + + return nil +} + +// codeServerNode implements runtime.Node interface for code-server containers. +type codeServerNode struct { + config *clabtypes.NodeConfig +} + +func codeServerStop(cobraCmd *cobra.Command, o *Options) error { + ctx := cobraCmd.Context() + + log.Debugf("Container name for deletion: %s", o.ToolsCodeServer.Name) + + // Use common.Runtime if available, otherwise use the api-server flag + runtimeName := o.Global.Runtime + if runtimeName == "" { + runtimeName = "docker" + } + + // Initialize runtime + _, rinit, err := clabcore.RuntimeInitializer(runtimeName) + if err != nil { + return fmt.Errorf("failed to get runtime initializer: %w", err) + } + + rt := rinit() + err = rt.Init(clabruntime.WithConfig(&clabruntime.RuntimeConfig{Timeout: o.Global.Timeout})) + if err != nil { + return fmt.Errorf("failed to initialize runtime: %w", err) + } + + log.Info("Removing code-server container", "name", o.ToolsCodeServer.Name) + if err := rt.DeleteContainer(ctx, o.ToolsCodeServer.Name); err != nil { + return fmt.Errorf("failed to remove code-server container: %w", err) + } + + log.Info("code server container removed", "name", o.ToolsCodeServer.Name) + return nil +} diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go index 3f97ec01a1..b1cd9a6427 100644 --- a/runtime/docker/docker.go +++ b/runtime/docker/docker.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "os" + "os/exec" "path" "strconv" "strings" @@ -1330,3 +1331,11 @@ func (*DockerRuntime) GetCooCBindMounts() clabtypes.Binds { clabtypes.NewBind("/run/netns", "/run/netns", ""), } } + +func (*DockerRuntime) GetRuntimeBinary() (string, error) { + path, err := exec.LookPath("docker") + if err != nil { + return "", fmt.Errorf("failed to get docker runtime binary path: %w", err) + } + return path, nil +} diff --git a/runtime/ignite/ignite.go b/runtime/ignite/ignite.go index f3e8f394d5..c163f35ff1 100644 --- a/runtime/ignite/ignite.go +++ b/runtime/ignite/ignite.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "os/exec" "strings" "time" @@ -518,3 +519,11 @@ func (*IgniteRuntime) GetRuntimeSocket() (string, error) { func (*IgniteRuntime) GetCooCBindMounts() clabtypes.Binds { return nil } + +func (*IgniteRuntime) GetRuntimeBinary() (string, error) { + path, err := exec.LookPath("ignite") + if err != nil { + return "", fmt.Errorf("failed to get ignite runtime binary path: %w", err) + } + return path, nil +} diff --git a/runtime/runtime.go b/runtime/runtime.go index 38eddac3ac..7c6a2690b8 100644 --- a/runtime/runtime.go +++ b/runtime/runtime.go @@ -74,6 +74,8 @@ type ContainerRuntime interface { // Container-outside-of-Container (CooC - General case – container uses host container // runtime) does need to function properly GetCooCBindMounts() clabtypes.Binds + // GetRuntimeBinary returns the path to the binary of the runtime + GetRuntimeBinary() (string, error) } type ContainerStatus string From 7fef53e0054a98ee9b92d992b3e527b0ef477fdb Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Sun, 24 Aug 2025 15:35:48 +1200 Subject: [PATCH 02/25] Display host port in inspect --- cmd/tools_code_server.go | 36 ++++++++++-------------------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/cmd/tools_code_server.go b/cmd/tools_code_server.go index b612619d2f..839be5a26e 100644 --- a/cmd/tools_code_server.go +++ b/cmd/tools_code_server.go @@ -8,7 +8,6 @@ import ( "encoding/json" "fmt" "os" - "strconv" "strings" "github.com/charmbracelet/log" @@ -96,8 +95,8 @@ func NewCodeServerNode(name, image, labsDir string, port uint, runtime clabrunti } binds := clabtypes.Binds{ - clabtypes.NewBind(homeDir, "/home/coder/labs", ""), - clabtypes.NewBind("/etc/group", "/etc/group", "ro"), + clabtypes.NewBind(homeDir, "/labs", ""), + // clabtypes.NewBind("/etc/group", "/etc/group", "ro"), } // get the runtime socket path @@ -173,13 +172,11 @@ func (*codeServerNode) GetEndpoints() []clablinks.Endpoint { // createLabels creates container labels. func createCodeServerLabels(containerName, owner string, port uint, labsDir, host string) map[string]string { labels := map[string]string{ - clablabels.NodeName: containerName, - clablabels.NodeKind: "linux", - clablabels.NodeType: "tool", - clablabels.ToolType: "code-server", - "clab-code-server-host": host, - "clab-code-server-port": fmt.Sprintf("%d", port), - "clab-labs-dir": labsDir, + clablabels.NodeName: containerName, + clablabels.NodeKind: "linux", + clablabels.NodeType: "tool", + clablabels.ToolType: "code-server", + "clab-labs-dir": labsDir, } // Add owner label if available @@ -315,7 +312,7 @@ func codeServerStatus(cobraCmd *cobra.Command, o *Options) error { //nolint: fun &clabruntime.RuntimeConfig{ Debug: o.Global.DebugCount > 0, Timeout: o.Global.Timeout, - GracefulShutdown: o.Destroy.GracefulShutdown, + GracefulShutdown: o.Global.GracefulShutdown, }, ), clabcore.WithDebug(o.Global.DebugCount > 0), @@ -352,18 +349,7 @@ func codeServerStatus(cobraCmd *cobra.Command, o *Options) error { //nolint: fun name := strings.TrimPrefix(containers[idx].Names[0], "/") // Get port from labels or use default - port := 8080 // default - if portStr, ok := containers[idx].Labels["code-server-port"]; ok { - if portVal, err := strconv.Atoi(portStr); err == nil { - port = portVal - } - } - - // Get host from labels or use default - host := "localhost" // default - if hostVal, ok := containers[idx].Labels["code-server-host"]; ok { - host = hostVal - } + port := containers[idx].Ports[0].HostPort // Get labs dir from labels or use default labsDir := "~/.clab" // default @@ -380,7 +366,6 @@ func codeServerStatus(cobraCmd *cobra.Command, o *Options) error { //nolint: fun listItems = append(listItems, codeServerListItem{ Name: name, State: containers[idx].State, - Host: host, Port: port, LabsDir: labsDir, Owner: owner, @@ -401,13 +386,12 @@ func codeServerStatus(cobraCmd *cobra.Command, o *Options) error { //nolint: fun t.Style().Format.Header = text.FormatTitle t.Style().Options.SeparateRows = true - t.AppendHeader(table.Row{"NAME", "STATUS", "HOST", "PORT", "LABS DIR", "OWNER"}) + t.AppendHeader(table.Row{"NAME", "STATUS", "PORT", "LABS DIR", "OWNER"}) for _, item := range listItems { t.AppendRow(table.Row{ item.Name, item.State, - item.Host, item.Port, item.LabsDir, item.Owner, From fcaa84b343973932323d209203db8f80b174c3df Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Sun, 24 Aug 2025 15:37:31 +1200 Subject: [PATCH 03/25] Remove Host option --- cmd/options.go | 2 -- cmd/tools_code_server.go | 9 ++++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/cmd/options.go b/cmd/options.go index 4405cbafe3..a2a38f8b1b 100644 --- a/cmd/options.go +++ b/cmd/options.go @@ -105,7 +105,6 @@ func GetOptions() *Options { ToolsCodeServer: &ToolsCodeServerOptions{ Image: "ghcr.io/kaelemc/clab-code-server:latest", Name: "clab-code-server", - Host: "localhost", LogLevel: "debug", OutputFormat: "table", }, @@ -447,7 +446,6 @@ type ToolsCodeServerOptions struct { Image string Name string Port uint - Host string LogLevel string OutputFormat string LabsDirectory string diff --git a/cmd/tools_code_server.go b/cmd/tools_code_server.go index 839be5a26e..b9e436824a 100644 --- a/cmd/tools_code_server.go +++ b/cmd/tools_code_server.go @@ -170,7 +170,7 @@ func (*codeServerNode) GetEndpoints() []clablinks.Endpoint { } // createLabels creates container labels. -func createCodeServerLabels(containerName, owner string, port uint, labsDir, host string) map[string]string { +func createCodeServerLabels(containerName, owner string, port uint, labsDir string) map[string]string { labels := map[string]string{ clablabels.NodeName: containerName, clablabels.NodeKind: "linux", @@ -239,7 +239,7 @@ func codeServerStart(cobraCmd *cobra.Command, o *Options) error { //nolint: funl owner := getOwnerName(o) labels := createCodeServerLabels(o.ToolsCodeServer.Name, owner, o.ToolsCodeServer.Port, - o.ToolsCodeServer.LabsDirectory, o.ToolsCodeServer.Host) + o.ToolsCodeServer.LabsDirectory) // Create and start code server container log.Info("Creating code server container", "name", o.ToolsCodeServer.Name) @@ -271,8 +271,7 @@ func codeServerStart(cobraCmd *cobra.Command, o *Options) error { //nolint: funl if err == nil && len(containers) > 0 && len(containers[0].Ports) > 0 { for _, portMapping := range containers[0].Ports { if portMapping.ContainerPort == 8080 { - log.Infof("code-server available at: http://%s:%d", - o.ToolsCodeServer.Host, portMapping.HostPort) + log.Infof("code-server available at: http://0.0.0.0:%d", portMapping.HostPort) break } } @@ -280,7 +279,7 @@ func codeServerStart(cobraCmd *cobra.Command, o *Options) error { //nolint: funl log.Infof("code-server container started. Check 'docker ps' for the assigned port.") } } else { - log.Infof("code-server available at: http://%s:%d", o.ToolsCodeServer.Host, o.ToolsCodeServer.Port) + log.Infof("code-server available at: http://0.0.0.0:%d", o.ToolsCodeServer.Port) } return nil From 275973dad28d9e5893c8044d6df33583eb257620 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Sun, 24 Aug 2025 15:40:34 +1200 Subject: [PATCH 04/25] Make the port flag work --- cmd/tools_code_server.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/tools_code_server.go b/cmd/tools_code_server.go index b9e436824a..1ad1bef024 100644 --- a/cmd/tools_code_server.go +++ b/cmd/tools_code_server.go @@ -136,10 +136,16 @@ func NewCodeServerNode(name, image, labsDir string, port uint, runtime clabrunti return nil, fmt.Errorf("failed to create container port: %w", err) } exposedPorts[containerPort] = struct{}{} + + var hostPort uint = 0 + if port != 0 { + hostPort = port + } + portBindings[containerPort] = []nat.PortBinding{ { HostIP: "0.0.0.0", - HostPort: "", + HostPort: fmt.Sprintf("%d", hostPort), }, } From 6cc981d1d6c30ea9d9ffa62756c359fe4e8c4c03 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Tue, 2 Sep 2025 18:40:32 +1200 Subject: [PATCH 05/25] mock gen --- mocks/mockruntime/runtime.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/mocks/mockruntime/runtime.go b/mocks/mockruntime/runtime.go index bfae029b13..c1b90c9186 100644 --- a/mocks/mockruntime/runtime.go +++ b/mocks/mockruntime/runtime.go @@ -230,6 +230,21 @@ func (mr *MockContainerRuntimeMockRecorder) GetName() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetName", reflect.TypeOf((*MockContainerRuntime)(nil).GetName)) } +// GetRuntimeBinary mocks base method. +func (m *MockContainerRuntime) GetRuntimeBinary() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRuntimeBinary") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetRuntimeBinary indicates an expected call of GetRuntimeBinary. +func (mr *MockContainerRuntimeMockRecorder) GetRuntimeBinary() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRuntimeBinary", reflect.TypeOf((*MockContainerRuntime)(nil).GetRuntimeBinary)) +} + // GetRuntimeSocket mocks base method. func (m *MockContainerRuntime) GetRuntimeSocket() (string, error) { m.ctrl.T.Helper() From 13671d1a97d8130f0a0a822b670497b5b626d20e Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Wed, 3 Sep 2025 20:33:45 +1200 Subject: [PATCH 06/25] Implement podman runtime binary path getter --- runtime/docker/docker.go | 4 ++-- runtime/podman/podman.go | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go index b1cd9a6427..a007492058 100644 --- a/runtime/docker/docker.go +++ b/runtime/docker/docker.go @@ -1333,9 +1333,9 @@ func (*DockerRuntime) GetCooCBindMounts() clabtypes.Binds { } func (*DockerRuntime) GetRuntimeBinary() (string, error) { - path, err := exec.LookPath("docker") + runtimePath, err := exec.LookPath("docker") if err != nil { return "", fmt.Errorf("failed to get docker runtime binary path: %w", err) } - return path, nil + return runtimePath, nil } diff --git a/runtime/podman/podman.go b/runtime/podman/podman.go index 7ceff7c15d..9b19b57803 100644 --- a/runtime/podman/podman.go +++ b/runtime/podman/podman.go @@ -489,3 +489,11 @@ func (r *PodmanRuntime) GetRuntimeSocket() (string, error) { } return socket, nil } + +func (*PodmanRuntime) GetRuntimeBinary() (string, error) { + runtimePath, err := exec.LookPath("podman") + if err != nil { + return "", fmt.Errorf("failed to get podman runtime binary path: %w", err) + } + return runtimePath, nil +} From 0008645c7eff2c0313426a516b9509fb3e80bbbd Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Wed, 3 Sep 2025 20:50:07 +1200 Subject: [PATCH 07/25] Fix linter issue --- cmd/tools.go | 1 + cmd/tools_code_server.go | 70 +++++++++++++++++++++++++++++----------- 2 files changed, 52 insertions(+), 19 deletions(-) diff --git a/cmd/tools.go b/cmd/tools.go index b41c16020f..7df12505a6 100644 --- a/cmd/tools.go +++ b/cmd/tools.go @@ -95,5 +95,6 @@ func getclabBinaryPath() (string, error) { if err != nil { return "", err } + return absPath, nil } diff --git a/cmd/tools_code_server.go b/cmd/tools_code_server.go index 1ad1bef024..f505f7bf08 100644 --- a/cmd/tools_code_server.go +++ b/cmd/tools_code_server.go @@ -23,6 +23,15 @@ import ( clabutils "github.com/srl-labs/containerlab/utils" ) +const ( + codeServerPort = 8080 +) + +// codeServerNode implements runtime.Node interface for code-server containers. +type codeServerNode struct { + config *clabtypes.NodeConfig +} + func codeServerCmd(o *Options) (*cobra.Command, error) { c := &cobra.Command{ Use: "code-server", @@ -45,13 +54,17 @@ func codeServerCmd(o *Options) (*cobra.Command, error) { codeServerStartCmd.Flags().StringVarP(&o.ToolsCodeServer.Image, "image", "i", o.ToolsCodeServer.Image, "container image to use for code-server") - codeServerStartCmd.Flags().StringVarP(&o.ToolsCodeServer.Name, "name", "n", o.ToolsCodeServer.Name, + codeServerStartCmd.Flags().StringVarP(&o.ToolsCodeServer.Name, "name", "n", + o.ToolsCodeServer.Name, "name of the code-server container") - codeServerStartCmd.Flags().StringVarP(&o.ToolsCodeServer.LabsDirectory, "labs-dir", "l", o.ToolsCodeServer.LabsDirectory, + codeServerStartCmd.Flags().StringVarP(&o.ToolsCodeServer.LabsDirectory, "labs-dir", "l", + o.ToolsCodeServer.LabsDirectory, "directory to mount as shared labs directory") - codeServerStartCmd.Flags().UintVarP(&o.ToolsCodeServer.Port, "port", "p", o.ToolsCodeServer.Port, + codeServerStartCmd.Flags().UintVarP(&o.ToolsCodeServer.Port, "port", "p", + o.ToolsCodeServer.Port, "port to expose the code-server on") - codeServerStartCmd.Flags().StringVarP(&o.ToolsCodeServer.Owner, "owner", "o", o.ToolsCodeServer.Owner, + codeServerStartCmd.Flags().StringVarP(&o.ToolsCodeServer.Owner, "owner", "o", + o.ToolsCodeServer.Owner, "owner name for the code-server container") codeServerStatusCmd := &cobra.Command{ @@ -65,7 +78,8 @@ func codeServerCmd(o *Options) (*cobra.Command, error) { }, } c.AddCommand(codeServerStatusCmd) - codeServerStatusCmd.Flags().StringVarP(&o.ToolsCodeServer.OutputFormat, "format", "f", o.ToolsCodeServer.OutputFormat, + codeServerStatusCmd.Flags().StringVarP(&o.ToolsCodeServer.OutputFormat, "format", "f", + o.ToolsCodeServer.OutputFormat, "output format for 'status' command (table, json)") codeServerStopCmd := &cobra.Command{ @@ -85,9 +99,15 @@ func codeServerCmd(o *Options) (*cobra.Command, error) { return c, nil } -func NewCodeServerNode(name, image, labsDir string, port uint, runtime clabruntime.ContainerRuntime, labels map[string]string, +func NewCodeServerNode(name, image, labsDir string, + port uint, + runtime clabruntime.ContainerRuntime, + labels map[string]string, ) (*codeServerNode, error) { - log.With("name", name, "image", image, "labsDir", labsDir, "runtime", runtime).Debug("Creating new code-server node.") + log.With("name", name, + "image", image, + "labsDir", labsDir, + "runtime", runtime).Debug("Creating new code-server node.") homeDir, err := os.UserHomeDir() if err != nil { @@ -115,7 +135,8 @@ func NewCodeServerNode(name, image, labsDir string, port uint, runtime clabrunti // Find Docker binary and add bind mount if found rtBinPath, err := runtime.GetRuntimeBinary() if err != nil { - return nil, fmt.Errorf("could not find docker binary: %v. code-server might not function correctly if docker is not available", err) + return nil, fmt.Errorf("could not find docker binary: %v. "+ + "code-server might not function correctly if docker is not available", err) } // currently only docker is supported. binds = append(binds, clabtypes.NewBind(rtBinPath, "/usr/bin/docker", "ro")) @@ -123,8 +144,10 @@ func NewCodeServerNode(name, image, labsDir string, port uint, runtime clabrunti // Find containerlab binary and add bind mount if found clabPath, err := getclabBinaryPath() if err != nil { - return nil, fmt.Errorf("could not find containerlab binary: %v. code-server might not function correctly if containerlab is not in its PATH", err) + return nil, fmt.Errorf("could not find containerlab binary: %v. "+ + "code-server might not function correctly if containerlab is not in its PATH", err) } + binds = append(binds, clabtypes.NewBind(clabPath, "/usr/bin/containerlab", "ro")) // Publish host random port -> ctr port 8080 @@ -135,6 +158,7 @@ func NewCodeServerNode(name, image, labsDir string, port uint, runtime clabrunti if err != nil { return nil, fmt.Errorf("failed to create container port: %w", err) } + exposedPorts[containerPort] = struct{}{} var hostPort uint = 0 @@ -176,7 +200,7 @@ func (*codeServerNode) GetEndpoints() []clablinks.Endpoint { } // createLabels creates container labels. -func createCodeServerLabels(containerName, owner string, port uint, labsDir string) map[string]string { +func createCodeServerLabels(containerName, owner, labsDir string) map[string]string { labels := map[string]string{ clablabels.NodeName: containerName, clablabels.NodeKind: "linux", @@ -193,7 +217,7 @@ func createCodeServerLabels(containerName, owner string, port uint, labsDir stri return labels } -func codeServerStart(cobraCmd *cobra.Command, o *Options) error { //nolint: funlen +func codeServerStart(cobraCmd *cobra.Command, o *Options) error { ctx := cobraCmd.Context() log.With( @@ -214,6 +238,7 @@ func codeServerStart(cobraCmd *cobra.Command, o *Options) error { //nolint: funl } rt := rinit() + err = rt.Init(clabruntime.WithConfig(&clabruntime.RuntimeConfig{Timeout: o.Global.Timeout})) if err != nil { return fmt.Errorf("failed to initialize runtime: %w", err) @@ -224,16 +249,20 @@ func codeServerStart(cobraCmd *cobra.Command, o *Options) error { //nolint: funl // Check if container already exists filter := []*clabtypes.GenericFilter{{FilterType: "name", Match: o.ToolsCodeServer.Name}} + containers, err := rt.ListContainers(ctx, filter) if err != nil { return fmt.Errorf("failed to list containers: %w", err) } + if len(containers) > 0 { return fmt.Errorf("container %s already exists", o.ToolsCodeServer.Name) } // Pull the container image log.Infof("Pulling image %s...", o.ToolsCodeServer.Image) + + //nolint:lll if err := rt.PullImage(ctx, o.ToolsCodeServer.Image, clabtypes.PullPolicyIfNotPresent); err != nil { return fmt.Errorf("failed to pull image %s: %w", o.ToolsCodeServer.Image, err) } @@ -244,11 +273,12 @@ func codeServerStart(cobraCmd *cobra.Command, o *Options) error { //nolint: funl } owner := getOwnerName(o) - labels := createCodeServerLabels(o.ToolsCodeServer.Name, owner, o.ToolsCodeServer.Port, + labels := createCodeServerLabels(o.ToolsCodeServer.Name, owner, o.ToolsCodeServer.LabsDirectory) // Create and start code server container log.Info("Creating code server container", "name", o.ToolsCodeServer.Name) + codeServerNode, err := NewCodeServerNode(o.ToolsCodeServer.Name, o.ToolsCodeServer.Image, o.ToolsCodeServer.LabsDirectory, o.ToolsCodeServer.Port, rt, labels) if err != nil { @@ -276,7 +306,7 @@ func codeServerStart(cobraCmd *cobra.Command, o *Options) error { //nolint: funl }}) if err == nil && len(containers) > 0 && len(containers[0].Ports) > 0 { for _, portMapping := range containers[0].Ports { - if portMapping.ContainerPort == 8080 { + if portMapping.ContainerPort == codeServerPort { log.Infof("code-server available at: http://0.0.0.0:%d", portMapping.HostPort) break } @@ -301,7 +331,7 @@ type codeServerListItem struct { Owner string `json:"owner"` } -func codeServerStatus(cobraCmd *cobra.Command, o *Options) error { //nolint: funlen +func codeServerStatus(cobraCmd *cobra.Command, o *Options) error { ctx := cobraCmd.Context() // Use common.Runtime for consistency with other commands @@ -345,6 +375,7 @@ func codeServerStatus(cobraCmd *cobra.Command, o *Options) error { //nolint: fun } else { fmt.Println("No active code-server containers found") } + return nil } @@ -382,6 +413,7 @@ func codeServerStatus(cobraCmd *cobra.Command, o *Options) error { //nolint: fun if err != nil { return fmt.Errorf("failed to marshal to JSON: %w", err) } + fmt.Println(string(b)) } else { // Use go-pretty table @@ -402,17 +434,13 @@ func codeServerStatus(cobraCmd *cobra.Command, o *Options) error { //nolint: fun item.Owner, }) } + t.Render() } return nil } -// codeServerNode implements runtime.Node interface for code-server containers. -type codeServerNode struct { - config *clabtypes.NodeConfig -} - func codeServerStop(cobraCmd *cobra.Command, o *Options) error { ctx := cobraCmd.Context() @@ -420,6 +448,7 @@ func codeServerStop(cobraCmd *cobra.Command, o *Options) error { // Use common.Runtime if available, otherwise use the api-server flag runtimeName := o.Global.Runtime + if runtimeName == "" { runtimeName = "docker" } @@ -431,16 +460,19 @@ func codeServerStop(cobraCmd *cobra.Command, o *Options) error { } rt := rinit() + err = rt.Init(clabruntime.WithConfig(&clabruntime.RuntimeConfig{Timeout: o.Global.Timeout})) if err != nil { return fmt.Errorf("failed to initialize runtime: %w", err) } log.Info("Removing code-server container", "name", o.ToolsCodeServer.Name) + if err := rt.DeleteContainer(ctx, o.ToolsCodeServer.Name); err != nil { return fmt.Errorf("failed to remove code-server container: %w", err) } log.Info("code server container removed", "name", o.ToolsCodeServer.Name) + return nil } From 357cd70cac928749426c4e4e28415d5efa6d4be5 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Wed, 3 Sep 2025 21:33:19 +1200 Subject: [PATCH 08/25] Change default codeServer port to 443 --- cmd/tools_code_server.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/tools_code_server.go b/cmd/tools_code_server.go index f505f7bf08..c181b338c7 100644 --- a/cmd/tools_code_server.go +++ b/cmd/tools_code_server.go @@ -24,7 +24,7 @@ import ( ) const ( - codeServerPort = 8080 + codeServerPort = 443 ) // codeServerNode implements runtime.Node interface for code-server containers. @@ -307,6 +307,7 @@ func codeServerStart(cobraCmd *cobra.Command, o *Options) error { if err == nil && len(containers) > 0 && len(containers[0].Ports) > 0 { for _, portMapping := range containers[0].Ports { if portMapping.ContainerPort == codeServerPort { + // log the HOST PORT log.Infof("code-server available at: http://0.0.0.0:%d", portMapping.HostPort) break } From 210d178014bfa1f842a4afb4cdff3f33c47cc650 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Wed, 3 Sep 2025 22:10:26 +1200 Subject: [PATCH 09/25] Use dev image (interim) --- cmd/options.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/options.go b/cmd/options.go index a2a38f8b1b..3d6996ce4d 100644 --- a/cmd/options.go +++ b/cmd/options.go @@ -103,7 +103,7 @@ func GetOptions() *Options { DeletionPrefix: "vx-", }, ToolsCodeServer: &ToolsCodeServerOptions{ - Image: "ghcr.io/kaelemc/clab-code-server:latest", + Image: "ghcr.io/kaelemc/clab-code-server:main", Name: "clab-code-server", LogLevel: "debug", OutputFormat: "table", From ab9de12d8dc68a8c5964eeb6c64698cda7b8c280 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Wed, 3 Sep 2025 22:11:59 +1200 Subject: [PATCH 10/25] use correct port var --- cmd/tools_code_server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/tools_code_server.go b/cmd/tools_code_server.go index c181b338c7..b8069d1370 100644 --- a/cmd/tools_code_server.go +++ b/cmd/tools_code_server.go @@ -154,7 +154,7 @@ func NewCodeServerNode(name, image, labsDir string, exposedPorts := make(nat.PortSet) portBindings := make(nat.PortMap) - containerPort, err := nat.NewPort("tcp", "8080") + containerPort, err := nat.NewPort("tcp", fmt.Sprintf("%d", codeServerPort)) if err != nil { return nil, fmt.Errorf("failed to create container port: %w", err) } From 8b8b60d9db321d088d86fc67461ed760dac66d4c Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Wed, 3 Sep 2025 23:25:07 +1200 Subject: [PATCH 11/25] Port back to 8080 --- cmd/tools_code_server.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cmd/tools_code_server.go b/cmd/tools_code_server.go index b8069d1370..dcd5b702d2 100644 --- a/cmd/tools_code_server.go +++ b/cmd/tools_code_server.go @@ -24,7 +24,7 @@ import ( ) const ( - codeServerPort = 443 + codeServerPort = 8080 ) // codeServerNode implements runtime.Node interface for code-server containers. @@ -183,6 +183,7 @@ func NewCodeServerNode(name, image, labsDir string, PortBindings: portBindings, NetworkMode: "bridge", User: "0", + Cmd: "--config /config.yaml", } return &codeServerNode{ @@ -263,7 +264,7 @@ func codeServerStart(cobraCmd *cobra.Command, o *Options) error { log.Infof("Pulling image %s...", o.ToolsCodeServer.Image) //nolint:lll - if err := rt.PullImage(ctx, o.ToolsCodeServer.Image, clabtypes.PullPolicyIfNotPresent); err != nil { + if err := rt.PullImage(ctx, o.ToolsCodeServer.Image, clabtypes.PullPolicyAlways); err != nil { return fmt.Errorf("failed to pull image %s: %w", o.ToolsCodeServer.Image, err) } From 66a6138cdb7266634bebf330680a0f518aaf5240 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Thu, 4 Sep 2025 00:02:36 +1200 Subject: [PATCH 12/25] extension dir, http -> https --- cmd/tools_code_server.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/tools_code_server.go b/cmd/tools_code_server.go index dcd5b702d2..c476c39a23 100644 --- a/cmd/tools_code_server.go +++ b/cmd/tools_code_server.go @@ -183,7 +183,7 @@ func NewCodeServerNode(name, image, labsDir string, PortBindings: portBindings, NetworkMode: "bridge", User: "0", - Cmd: "--config /config.yaml", + Cmd: "--config /config.yaml --extensions-dir /extensions", } return &codeServerNode{ @@ -309,7 +309,7 @@ func codeServerStart(cobraCmd *cobra.Command, o *Options) error { for _, portMapping := range containers[0].Ports { if portMapping.ContainerPort == codeServerPort { // log the HOST PORT - log.Infof("code-server available at: http://0.0.0.0:%d", portMapping.HostPort) + log.Infof("code-server available at: https://0.0.0.0:%d", portMapping.HostPort) break } } @@ -317,7 +317,7 @@ func codeServerStart(cobraCmd *cobra.Command, o *Options) error { log.Infof("code-server container started. Check 'docker ps' for the assigned port.") } } else { - log.Infof("code-server available at: http://0.0.0.0:%d", o.ToolsCodeServer.Port) + log.Infof("code-server available at: https://0.0.0.0:%d", o.ToolsCodeServer.Port) } return nil From 4912f329017dd30a88ba6046d0ca85c1b317bfcd Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Thu, 4 Sep 2025 02:53:19 +1200 Subject: [PATCH 13/25] Links -> HTTP --- cmd/tools_code_server.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/tools_code_server.go b/cmd/tools_code_server.go index c476c39a23..536ec42df3 100644 --- a/cmd/tools_code_server.go +++ b/cmd/tools_code_server.go @@ -309,7 +309,7 @@ func codeServerStart(cobraCmd *cobra.Command, o *Options) error { for _, portMapping := range containers[0].Ports { if portMapping.ContainerPort == codeServerPort { // log the HOST PORT - log.Infof("code-server available at: https://0.0.0.0:%d", portMapping.HostPort) + log.Infof("code-server available at: http://0.0.0.0:%d", portMapping.HostPort) break } } @@ -317,7 +317,7 @@ func codeServerStart(cobraCmd *cobra.Command, o *Options) error { log.Infof("code-server container started. Check 'docker ps' for the assigned port.") } } else { - log.Infof("code-server available at: https://0.0.0.0:%d", o.ToolsCodeServer.Port) + log.Infof("code-server available at: http://0.0.0.0:%d", o.ToolsCodeServer.Port) } return nil From 256a905a86d195aab7a683d073cc289283c23397 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Thu, 4 Sep 2025 03:17:42 +1200 Subject: [PATCH 14/25] Implement podman runtime binary func --- runtime/podman/podman.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/runtime/podman/podman.go b/runtime/podman/podman.go index 9b19b57803..4a2e7408d9 100644 --- a/runtime/podman/podman.go +++ b/runtime/podman/podman.go @@ -491,9 +491,5 @@ func (r *PodmanRuntime) GetRuntimeSocket() (string, error) { } func (*PodmanRuntime) GetRuntimeBinary() (string, error) { - runtimePath, err := exec.LookPath("podman") - if err != nil { - return "", fmt.Errorf("failed to get podman runtime binary path: %w", err) - } - return runtimePath, nil + return "", fmt.Errorf("Podman runtime is currently unsupported") } From 33f68521ecc4e802d7f6fbcc73b00b4f7e324bd0 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Thu, 4 Sep 2025 03:18:21 +1200 Subject: [PATCH 15/25] Remove nolint directive --- cmd/tools_code_server.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/tools_code_server.go b/cmd/tools_code_server.go index 536ec42df3..56116b39a3 100644 --- a/cmd/tools_code_server.go +++ b/cmd/tools_code_server.go @@ -263,7 +263,6 @@ func codeServerStart(cobraCmd *cobra.Command, o *Options) error { // Pull the container image log.Infof("Pulling image %s...", o.ToolsCodeServer.Image) - //nolint:lll if err := rt.PullImage(ctx, o.ToolsCodeServer.Image, clabtypes.PullPolicyAlways); err != nil { return fmt.Errorf("failed to pull image %s: %w", o.ToolsCodeServer.Image, err) } From c72a715c499cdc9cf8eedb2ce9567cb1b0402876 Mon Sep 17 00:00:00 2001 From: Flosch62 Date: Thu, 4 Sep 2025 11:08:48 +0200 Subject: [PATCH 16/25] persistent folders --- cmd/tools_code_server.go | 55 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/cmd/tools_code_server.go b/cmd/tools_code_server.go index 56116b39a3..ea5251100c 100644 --- a/cmd/tools_code_server.go +++ b/cmd/tools_code_server.go @@ -114,8 +114,50 @@ func NewCodeServerNode(name, image, labsDir string, return nil, fmt.Errorf("failed to get user home directory: %w", err) } + // Create persistent directories for code-server config and data + codeServerDataDir := fmt.Sprintf("%s/.clab/code-server/%s/data", homeDir, name) + codeServerConfigDir := fmt.Sprintf("%s/.clab/code-server/%s/config", homeDir, name) + codeServerExtensionsDir := fmt.Sprintf("%s/.clab/code-server/%s/extensions", homeDir, name) + + // Create directories if they don't exist + if err := os.MkdirAll(codeServerDataDir, 0755); err != nil { + return nil, fmt.Errorf("failed to create code-server data directory: %w", err) + } + if err := os.MkdirAll(codeServerConfigDir, 0755); err != nil { + return nil, fmt.Errorf("failed to create code-server config directory: %w", err) + } + if err := os.MkdirAll(codeServerExtensionsDir, 0755); err != nil { + return nil, fmt.Errorf("failed to create code-server extensions directory: %w", err) + } + + // Check if this is first run (marker file doesn't exist) + // On first run only, we'll copy pre-installed extensions + markerFile := fmt.Sprintf("%s/.initialized", codeServerExtensionsDir) + isFirstRun := false + if _, err := os.Stat(markerFile); os.IsNotExist(err) { + isFirstRun = true + // Create marker file immediately to avoid re-copying + os.WriteFile(markerFile, []byte("initialized"), 0644) + } + + // Create config.yaml file with password authentication + configFile := fmt.Sprintf("%s/config.yaml", codeServerConfigDir) + configContent := `bind-addr: 0.0.0.0:8080 +auth: password +password: clab +cert: false +` + if err := os.WriteFile(configFile, []byte(configContent), 0644); err != nil { + return nil, fmt.Errorf("failed to create code-server config file: %w", err) + } + binds := clabtypes.Binds{ clabtypes.NewBind(homeDir, "/labs", ""), + clabtypes.NewBind("/home", "/home", ""), + // Mount persistent directories for code-server + clabtypes.NewBind(codeServerDataDir, "/root/.local/share/code-server", ""), + clabtypes.NewBind(codeServerConfigDir, "/root/.config/code-server", ""), + clabtypes.NewBind(codeServerExtensionsDir, "/persistent-extensions", ""), // clabtypes.NewBind("/etc/group", "/etc/group", "ro"), } @@ -173,6 +215,16 @@ func NewCodeServerNode(name, image, labsDir string, }, } + // Build command based on whether it's first run + var cmd string + if isFirstRun { + // On first run, copy extensions then start + cmd = "-c \"cp -r /extensions/* /persistent-extensions/ 2>/dev/null || true; code-server --config /root/.config/code-server/config.yaml --extensions-dir /persistent-extensions\"" + } else { + // On subsequent runs, just start directly + cmd = "-c \"code-server --config /root/.config/code-server/config.yaml --extensions-dir /persistent-extensions\"" + } + nodeConfig := &clabtypes.NodeConfig{ LongName: name, ShortName: name, @@ -183,7 +235,8 @@ func NewCodeServerNode(name, image, labsDir string, PortBindings: portBindings, NetworkMode: "bridge", User: "0", - Cmd: "--config /config.yaml --extensions-dir /extensions", + Entrypoint: "/bin/sh", + Cmd: cmd, } return &codeServerNode{ From 7c6b20ff42b69d25338a8c456959c60fd76f2a29 Mon Sep 17 00:00:00 2001 From: Flosch62 Date: Thu, 4 Sep 2025 11:20:59 +0200 Subject: [PATCH 17/25] persistent user data --- cmd/tools_code_server.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cmd/tools_code_server.go b/cmd/tools_code_server.go index ea5251100c..c9f27e8c9d 100644 --- a/cmd/tools_code_server.go +++ b/cmd/tools_code_server.go @@ -118,6 +118,7 @@ func NewCodeServerNode(name, image, labsDir string, codeServerDataDir := fmt.Sprintf("%s/.clab/code-server/%s/data", homeDir, name) codeServerConfigDir := fmt.Sprintf("%s/.clab/code-server/%s/config", homeDir, name) codeServerExtensionsDir := fmt.Sprintf("%s/.clab/code-server/%s/extensions", homeDir, name) + codeServerUserDataDir := fmt.Sprintf("%s/.clab/code-server/%s/user-data", homeDir, name) // Create directories if they don't exist if err := os.MkdirAll(codeServerDataDir, 0755); err != nil { @@ -129,6 +130,9 @@ func NewCodeServerNode(name, image, labsDir string, if err := os.MkdirAll(codeServerExtensionsDir, 0755); err != nil { return nil, fmt.Errorf("failed to create code-server extensions directory: %w", err) } + if err := os.MkdirAll(codeServerUserDataDir, 0755); err != nil { + return nil, fmt.Errorf("failed to create code-server user-data directory: %w", err) + } // Check if this is first run (marker file doesn't exist) // On first run only, we'll copy pre-installed extensions @@ -158,6 +162,7 @@ cert: false clabtypes.NewBind(codeServerDataDir, "/root/.local/share/code-server", ""), clabtypes.NewBind(codeServerConfigDir, "/root/.config/code-server", ""), clabtypes.NewBind(codeServerExtensionsDir, "/persistent-extensions", ""), + clabtypes.NewBind(codeServerUserDataDir, "/persistent-user-data", ""), // clabtypes.NewBind("/etc/group", "/etc/group", "ro"), } @@ -219,10 +224,10 @@ cert: false var cmd string if isFirstRun { // On first run, copy extensions then start - cmd = "-c \"cp -r /extensions/* /persistent-extensions/ 2>/dev/null || true; code-server --config /root/.config/code-server/config.yaml --extensions-dir /persistent-extensions\"" + cmd = "-c \"cp -r /extensions/* /persistent-extensions/ 2>/dev/null || true; code-server --config /root/.config/code-server/config.yaml --extensions-dir /persistent-extensions --user-data-dir /persistent-user-data\"" } else { // On subsequent runs, just start directly - cmd = "-c \"code-server --config /root/.config/code-server/config.yaml --extensions-dir /persistent-extensions\"" + cmd = "-c \"code-server --config /root/.config/code-server/config.yaml --extensions-dir /persistent-extensions --user-data-dir /persistent-user-data\"" } nodeConfig := &clabtypes.NodeConfig{ From a87cd7e2bfb2bb7cee46f589d3a555df132d311f Mon Sep 17 00:00:00 2001 From: Flosch62 Date: Thu, 18 Sep 2025 16:38:37 +0200 Subject: [PATCH 18/25] tests for code-server --- tests/01-smoke/25-tools-code-server.robot | 117 ++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 tests/01-smoke/25-tools-code-server.robot diff --git a/tests/01-smoke/25-tools-code-server.robot b/tests/01-smoke/25-tools-code-server.robot new file mode 100644 index 0000000000..38c2f10645 --- /dev/null +++ b/tests/01-smoke/25-tools-code-server.robot @@ -0,0 +1,117 @@ +*** Comments *** +This test suite verifies the functionality of the Containerlab code-server tool operations: +- Starting a code-server container with default settings +- Checking code-server status in table and JSON formats +- Stopping a code-server container +- Starting a code-server container with a custom port +- Verifying cleanup behaviour when no code-server containers are running + +*** Settings *** +Library OperatingSystem +Library String +Resource ../common.robot + +Suite Teardown Run Keyword Cleanup Code Server Containers + +*** Variables *** +${runtime} docker +${code_server_name} clab-code-server +${code_server_image} ghcr.io/kaelemc/clab-code-server:main +${custom_port} 10080 + +*** Test Cases *** +Start Code Server With Default Settings + [Documentation] Test starting code-server with default parameters + ${rc} ${output}= Run And Return Rc And Output + ... ${CLAB_BIN} --runtime ${runtime} tools code-server start + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} code-server container ${code_server_name} started successfully + Should Contain ${output} code-server available at: http://0.0.0.0: + +Check Code Server Status + [Documentation] Verify code-server status is reported in table format + ${rc} ${output}= Run And Return Rc And Output + ... ${CLAB_BIN} --runtime ${runtime} tools code-server status + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} ${code_server_name} + Should Contain ${output} running + Should Contain ${output} ~/.clab + +Check Code Server Status JSON Format + [Documentation] Verify code-server status is reported in JSON format + ${rc} ${output}= Run And Return Rc And Output + ... ${CLAB_BIN} --runtime ${runtime} tools code-server status --format json + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} "${code_server_name}" + Should Contain ${output} "running" + Should Contain ${output} "labs_dir": "~/.clab" + +Stop Code Server + [Documentation] Test stopping the default code-server container + ${rc} ${output}= Run And Return Rc And Output + ... ${CLAB_BIN} --runtime ${runtime} tools code-server stop + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} Removing code-server container + Should Contain ${output} name=${code_server_name} + Should Contain ${output} code server container removed + + # Verify container is removed + ${rc} ${output}= Run And Return Rc And Output + ... ${runtime} ps -a | grep ${code_server_name} || true + Log ${output} + Should Not Contain ${output} ${code_server_name} + +Start Code Server With Custom Port + [Documentation] Test starting code-server with a custom host port + ${rc} ${output}= Run And Return Rc And Output + ... ${CLAB_BIN} --runtime ${runtime} tools code-server start --port ${custom_port} + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} code-server container ${code_server_name} started successfully + Should Contain ${output} code-server available at: http://0.0.0.0:${custom_port} + +Verify Code Server Status With Custom Port + [Documentation] Verify code-server status reflects the custom port value + ${rc} ${output}= Run And Return Rc And Output + ... ${CLAB_BIN} --runtime ${runtime} tools code-server status + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} ${code_server_name} + Should Contain ${output} running + Should Contain ${output} ${custom_port} + +Stop Code Server Custom Port + [Documentation] Stop the code-server container started with custom port + ${rc} ${output}= Run And Return Rc And Output + ... ${CLAB_BIN} --runtime ${runtime} tools code-server stop + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} Removing code-server container + Should Contain ${output} name=${code_server_name} + Should Contain ${output} code server container removed + +Verify Empty Code Server List + [Documentation] Verify status command reports no code-server containers running + ${rc} ${output}= Run And Return Rc And Output + ... ${CLAB_BIN} --runtime ${runtime} tools code-server status + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} No active code-server containers found + +Verify Empty Code Server List JSON Format + [Documentation] Verify JSON status is empty when no code-server containers exist + ${rc} ${output}= Run And Return Rc And Output + ... ${CLAB_BIN} --runtime ${runtime} tools code-server status --format json + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Be Equal ${output} [] + +*** Keywords *** +Cleanup Code Server Containers + [Documentation] Cleanup all code-server containers + Run Keyword And Ignore Error Run ${CLAB_BIN} --runtime ${runtime} tools code-server stop --name ${code_server_name} + Run Keyword And Ignore Error Run ${runtime} rm -f ${code_server_name} From ec1e2151ab9e408a4bf1b0e7ebabdf223a2e83a4 Mon Sep 17 00:00:00 2001 From: Flosch62 Date: Thu, 18 Sep 2025 16:58:04 +0200 Subject: [PATCH 19/25] docs for code-server --- docs/cmd/tools/code-server/start.md | 64 ++++++++++++++++++++++++++++ docs/cmd/tools/code-server/status.md | 48 +++++++++++++++++++++ docs/cmd/tools/code-server/stop.md | 39 +++++++++++++++++ mkdocs.yml | 4 ++ 4 files changed, 155 insertions(+) create mode 100644 docs/cmd/tools/code-server/start.md create mode 100644 docs/cmd/tools/code-server/status.md create mode 100644 docs/cmd/tools/code-server/stop.md diff --git a/docs/cmd/tools/code-server/start.md b/docs/cmd/tools/code-server/start.md new file mode 100644 index 0000000000..b2291f8e7b --- /dev/null +++ b/docs/cmd/tools/code-server/start.md @@ -0,0 +1,64 @@ +# code-server start + +## Description + +The `start` sub-command under the `tools code-server` command launches a dedicated [code-server](https://github.com/coder/code-server) container that is pre-configured with the VSCode Containerlab extension. The container exposes a VS Code compatible web UI in your browser and mounts both the host lab directory and user home so you can browse, edit, and run labs. + +On first start the command creates persistent directories under `~/.clab/code-server//` for configuration, extensions, and user data. It also seeds the extensions directory with the pre-baked extensions that ship in the container image and writes a default configuration (`password: clab`). + +## Usage + +``` +containerlab tools code-server start [flags] +``` + +## Flags + +### --image | -i + +Container image to use for the code-server instance. Defaults to `ghcr.io/kaelemc/clab-code-server:main`. + +### --name | -n + +Container name to create. Defaults to `clab-code-server`. + +### --labs-dir | -l + +Host directory that will be mounted inside the container at `/labs`. Defaults to `~/.clab` when not provided. + +### --port | -p + +Host TCP port that will be forwarded to the container's port `8080`. Defaults to `0`, which lets the container runtime pick a random available port. + +### --owner | -o + +Label value stored on the container to record the creator/owner. If omitted, Containerlab derives the value from `SUDO_USER` or `USER`. + +## Examples + +Start with default settings and let Docker assign a random host port: + +```bash +❯ containerlab tools code-server start +16:41:50 INFO Pulling image ghcr.io/kaelemc/clab-code-server:main... +16:41:50 INFO Pulling image image=ghcr.io/kaelemc/clab-code-server:main +main: Pulling from kaelemc/clab-code-server +Digest: sha256:5d3b80127db6f74b556f1df1ad8c339f8bbd9694616e8325ea7e9b9fe6065fe9 +Status: Image is up to date for ghcr.io/kaelemc/clab-code-server:main +16:41:50 INFO Done pulling image image=ghcr.io/kaelemc/clab-code-server:main +16:41:50 INFO Creating code server container name=clab-code-server +16:41:50 INFO Creating container name=clab-code-server +16:41:50 INFO code-server container clab-code-server started successfully. +16:41:50 INFO code-server available at: http://0.0.0.0:32779 +``` + +Expose the service on a specific host port with a custom labs directory: + +```bash +❯ containerlab tools code-server start --port 10080 --labs-dir /srv/containerlab/labs +...[snip]... +INFO code-server container clab-code-server started successfully. +INFO code-server available at: http://0.0.0.0:10080 +``` + +After the container starts you can browse to the reported URL and log in with username `clab` / password `clab`. diff --git a/docs/cmd/tools/code-server/status.md b/docs/cmd/tools/code-server/status.md new file mode 100644 index 0000000000..b4bd8ec3ff --- /dev/null +++ b/docs/cmd/tools/code-server/status.md @@ -0,0 +1,48 @@ +# code-server status + +## Description + +The `status` sub-command under the `tools code-server` command inspects the active code-server containers that were launched via `containerlab`. It reports each container's runtime state, exposed port, mounted labs directory, and owner label so that you can quickly find connection details or verify cleanup. + +## Usage + +``` +containerlab tools code-server status [flags] +``` + +## Flags + +### --format | -f + +Output format for the listing. Accepts `table` (default) or `json`. + +## Examples + +List all running code-server containers in table form: + +```bash +❯ containerlab tools code-server status +╭──────────────────┬─────────┬───────┬──────────┬───────╮ +│ NAME │ STATUS │ PORT │ LABS DIR │ OWNER │ +├──────────────────┼─────────┼───────┼──────────┼───────┤ +│ clab-code-server │ running │ 32779 │ ~/.clab │ clab │ +╰──────────────────┴─────────┴───────┴──────────┴───────╯ +``` + +Show the same information in JSON format (useful for scripting): + +```bash +❯ containerlab tools code-server status --format json +[ + { + "name": "clab-code-server", + "state": "running", + "host": "", + "port": 32779, + "labs_dir": "~/.clab", + "owner": "clab" + } +] +``` + +When no containers are active the command prints `No active code-server containers found` (or an empty JSON array when `--format json` is used). diff --git a/docs/cmd/tools/code-server/stop.md b/docs/cmd/tools/code-server/stop.md new file mode 100644 index 0000000000..a28a33fab8 --- /dev/null +++ b/docs/cmd/tools/code-server/stop.md @@ -0,0 +1,39 @@ +# code-server stop + +## Description + +The `stop` sub-command under the `tools code-server` command removes a running code-server container. Use it to tear down the VS Code web terminal once you are done editing lab files. The command deletes the container but leaves the persistent configuration, extension, and user-data directories on disk so that the next start is nearly instant. + +## Usage + +``` +containerlab tools code-server stop [flags] +``` + +## Flags + +### --name | -n + +Name of the code-server container to remove. Defaults to `clab-code-server`. + +## Examples + +Stop the default helper container: + +```bash +❯ containerlab tools code-server stop +16:42:13 INFO Removing code-server container name=clab-code-server +16:42:13 INFO Removed container name=clab-code-server +16:42:13 INFO code server container removed name=clab-code-server +``` + +Target a specific container name: + +```bash +❯ containerlab tools code-server stop --name dev-code-server +16:45:01 INFO Removing code-server container name=dev-code-server +16:45:01 INFO Removed container name=dev-code-server +16:45:01 INFO code server container removed name=dev-code-server +``` + +If the named container is not found the command returns an error from the underlying runtime (for example `container "dev-code-server" not found`). diff --git a/mkdocs.yml b/mkdocs.yml index 2f812cd430..24fb13b6af 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -121,6 +121,10 @@ nav: - start: cmd/tools/api-server/start.md - stop: cmd/tools/api-server/stop.md - status: cmd/tools/api-server/status.md + - code-server: + - start: cmd/tools/code-server/start.md + - stop: cmd/tools/code-server/stop.md + - status: cmd/tools/code-server/status.md - sshx: - attach: cmd/tools/sshx/attach.md - detach: cmd/tools/sshx/detach.md From 57fa84e2f28e3983021085b8db5a43fa1c710a8e Mon Sep 17 00:00:00 2001 From: Flosch62 Date: Thu, 18 Sep 2025 17:10:21 +0200 Subject: [PATCH 20/25] use clabconstants instead of clablabels --- cmd/tools_code_server.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cmd/tools_code_server.go b/cmd/tools_code_server.go index c9f27e8c9d..421e68bde1 100644 --- a/cmd/tools_code_server.go +++ b/cmd/tools_code_server.go @@ -15,8 +15,8 @@ import ( "github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/text" "github.com/spf13/cobra" + clabconstants "github.com/srl-labs/containerlab/constants" clabcore "github.com/srl-labs/containerlab/core" - clablabels "github.com/srl-labs/containerlab/labels" clablinks "github.com/srl-labs/containerlab/links" clabruntime "github.com/srl-labs/containerlab/runtime" clabtypes "github.com/srl-labs/containerlab/types" @@ -261,16 +261,16 @@ func (*codeServerNode) GetEndpoints() []clablinks.Endpoint { // createLabels creates container labels. func createCodeServerLabels(containerName, owner, labsDir string) map[string]string { labels := map[string]string{ - clablabels.NodeName: containerName, - clablabels.NodeKind: "linux", - clablabels.NodeType: "tool", - clablabels.ToolType: "code-server", - "clab-labs-dir": labsDir, + clabconstants.NodeName: containerName, + clabconstants.NodeKind: "linux", + clabconstants.NodeType: "tool", + clabconstants.ToolType: "code-server", + "clab-labs-dir": labsDir, } // Add owner label if available if owner != "" { - labels[clablabels.Owner] = owner + labels[clabconstants.Owner] = owner } return labels @@ -454,7 +454,7 @@ func codeServerStatus(cobraCmd *cobra.Command, o *Options) error { // Get owner from container labels owner := "N/A" - if ownerVal, exists := containers[idx].Labels[clablabels.Owner]; exists && ownerVal != "" { + if ownerVal, exists := containers[idx].Labels[clabconstants.Owner]; exists && ownerVal != "" { owner = ownerVal } From 9d08c134f386465e31b88ee210a09b294891dc38 Mon Sep 17 00:00:00 2001 From: Flosch62 Date: Thu, 18 Sep 2025 18:27:08 +0200 Subject: [PATCH 21/25] make lint happy --- cmd/tools_code_server.go | 238 ++++++++++++++++++++++++--------------- 1 file changed, 149 insertions(+), 89 deletions(-) diff --git a/cmd/tools_code_server.go b/cmd/tools_code_server.go index 421e68bde1..b9d617bc19 100644 --- a/cmd/tools_code_server.go +++ b/cmd/tools_code_server.go @@ -19,14 +19,154 @@ import ( clabcore "github.com/srl-labs/containerlab/core" clablinks "github.com/srl-labs/containerlab/links" clabruntime "github.com/srl-labs/containerlab/runtime" + clabruntimedocker "github.com/srl-labs/containerlab/runtime/docker" clabtypes "github.com/srl-labs/containerlab/types" clabutils "github.com/srl-labs/containerlab/utils" ) const ( - codeServerPort = 8080 + codeServerPort = 8080 + codeServerDirPerm = 0o755 + codeServerConfigPerm = 0o644 + codeServerMarkerName = ".initialized" ) +type codeServerPaths struct { + dataDir string + configDir string + extensionsDir string + userDataDir string + markerFile string + configFile string +} + +func newCodeServerPaths(homeDir, name string) codeServerPaths { + basePath := fmt.Sprintf("%s/.clab/code-server/%s", homeDir, name) + + return codeServerPaths{ + dataDir: fmt.Sprintf("%s/data", basePath), + configDir: fmt.Sprintf("%s/config", basePath), + extensionsDir: fmt.Sprintf("%s/extensions", basePath), + userDataDir: fmt.Sprintf("%s/user-data", basePath), + markerFile: fmt.Sprintf("%s/extensions/%s", basePath, codeServerMarkerName), + configFile: fmt.Sprintf("%s/config/config.yaml", basePath), + } +} + +func prepareCodeServerPersistence(paths *codeServerPaths) (bool, error) { + directories := []string{ + paths.dataDir, + paths.configDir, + paths.extensionsDir, + paths.userDataDir, + } + + for _, dir := range directories { + if err := os.MkdirAll(dir, codeServerDirPerm); err != nil { + return false, fmt.Errorf("failed to create %s directory: %w", dir, err) + } + } + + isFirstRun, err := ensureExtensionsInitialized(paths.markerFile) + if err != nil { + return false, err + } + + if err := writeCodeServerConfig(paths.configFile); err != nil { + return false, err + } + + return isFirstRun, nil +} + +func ensureExtensionsInitialized(markerFile string) (bool, error) { + if _, err := os.Stat(markerFile); err == nil { + return false, nil + } else if !os.IsNotExist(err) { + return false, fmt.Errorf("failed to check code-server marker file: %w", err) + } + + if err := os.WriteFile(markerFile, []byte("initialized"), codeServerConfigPerm); err != nil { + return false, fmt.Errorf("failed to create code-server marker file: %w", err) + } + + return true, nil +} + +func writeCodeServerConfig(configFile string) error { + const configContent = `bind-addr: 0.0.0.0:8080 +auth: password +password: clab +cert: false +` + + if err := os.WriteFile(configFile, []byte(configContent), codeServerConfigPerm); err != nil { + return fmt.Errorf("failed to create code-server config file: %w", err) + } + + return nil +} + +func buildCodeServerBinds( + homeDir string, + runtime clabruntime.ContainerRuntime, + paths *codeServerPaths, +) (clabtypes.Binds, error) { + binds := clabtypes.Binds{ + clabtypes.NewBind(homeDir, "/labs", ""), + clabtypes.NewBind("/home", "/home", ""), + clabtypes.NewBind(paths.dataDir, "/root/.local/share/code-server", ""), + clabtypes.NewBind(paths.configDir, "/root/.config/code-server", ""), + clabtypes.NewBind(paths.extensionsDir, "/persistent-extensions", ""), + clabtypes.NewBind(paths.userDataDir, "/persistent-user-data", ""), + } + + rtSocket, err := runtime.GetRuntimeSocket() + if err != nil { + return nil, err + } + + binds = append(binds, clabtypes.NewBind(rtSocket, rtSocket, "")) + binds = append(binds, runtime.GetCooCBindMounts()...) + + rtBinPath, err := runtime.GetRuntimeBinary() + if err != nil { + return nil, fmt.Errorf("could not find docker binary: %v. "+ + "code-server might not function correctly if docker is not available", err) + } + + binds = append(binds, clabtypes.NewBind(rtBinPath, "/usr/bin/docker", "ro")) + + clabPath, err := getclabBinaryPath() + if err != nil { + return nil, fmt.Errorf("could not find containerlab binary: %v. "+ + "code-server might not function correctly if containerlab is not in its PATH", err) + } + + binds = append(binds, clabtypes.NewBind(clabPath, "/usr/bin/containerlab", "ro")) + + return binds, nil +} + +func buildCodeServerCommand(isFirstRun bool) string { + baseCommand := strings.Join([]string{ + "code-server --config /root/.config/code-server/config.yaml", + "--extensions-dir /persistent-extensions", + "--user-data-dir /persistent-user-data", + }, " ") + + if !isFirstRun { + return fmt.Sprintf("-c %q", baseCommand) + } + + copyExtensionsCommand := "cp -r /extensions/* /persistent-extensions/" + + " 2>/dev/null || true" + + firstRunCommand := copyExtensionsCommand + "; " + baseCommand + + return fmt.Sprintf("-c %q", firstRunCommand) +} + // codeServerNode implements runtime.Node interface for code-server containers. type codeServerNode struct { config *clabtypes.NodeConfig @@ -114,90 +254,18 @@ func NewCodeServerNode(name, image, labsDir string, return nil, fmt.Errorf("failed to get user home directory: %w", err) } - // Create persistent directories for code-server config and data - codeServerDataDir := fmt.Sprintf("%s/.clab/code-server/%s/data", homeDir, name) - codeServerConfigDir := fmt.Sprintf("%s/.clab/code-server/%s/config", homeDir, name) - codeServerExtensionsDir := fmt.Sprintf("%s/.clab/code-server/%s/extensions", homeDir, name) - codeServerUserDataDir := fmt.Sprintf("%s/.clab/code-server/%s/user-data", homeDir, name) - - // Create directories if they don't exist - if err := os.MkdirAll(codeServerDataDir, 0755); err != nil { - return nil, fmt.Errorf("failed to create code-server data directory: %w", err) - } - if err := os.MkdirAll(codeServerConfigDir, 0755); err != nil { - return nil, fmt.Errorf("failed to create code-server config directory: %w", err) - } - if err := os.MkdirAll(codeServerExtensionsDir, 0755); err != nil { - return nil, fmt.Errorf("failed to create code-server extensions directory: %w", err) - } - if err := os.MkdirAll(codeServerUserDataDir, 0755); err != nil { - return nil, fmt.Errorf("failed to create code-server user-data directory: %w", err) - } - - // Check if this is first run (marker file doesn't exist) - // On first run only, we'll copy pre-installed extensions - markerFile := fmt.Sprintf("%s/.initialized", codeServerExtensionsDir) - isFirstRun := false - if _, err := os.Stat(markerFile); os.IsNotExist(err) { - isFirstRun = true - // Create marker file immediately to avoid re-copying - os.WriteFile(markerFile, []byte("initialized"), 0644) - } + paths := newCodeServerPaths(homeDir, name) - // Create config.yaml file with password authentication - configFile := fmt.Sprintf("%s/config.yaml", codeServerConfigDir) - configContent := `bind-addr: 0.0.0.0:8080 -auth: password -password: clab -cert: false -` - if err := os.WriteFile(configFile, []byte(configContent), 0644); err != nil { - return nil, fmt.Errorf("failed to create code-server config file: %w", err) - } - - binds := clabtypes.Binds{ - clabtypes.NewBind(homeDir, "/labs", ""), - clabtypes.NewBind("/home", "/home", ""), - // Mount persistent directories for code-server - clabtypes.NewBind(codeServerDataDir, "/root/.local/share/code-server", ""), - clabtypes.NewBind(codeServerConfigDir, "/root/.config/code-server", ""), - clabtypes.NewBind(codeServerExtensionsDir, "/persistent-extensions", ""), - clabtypes.NewBind(codeServerUserDataDir, "/persistent-user-data", ""), - // clabtypes.NewBind("/etc/group", "/etc/group", "ro"), - } - - // get the runtime socket path - rtSocket, err := runtime.GetRuntimeSocket() + isFirstRun, err := prepareCodeServerPersistence(&paths) if err != nil { return nil, err } - // build the bindmount for the socket, path sound be the same in the container as is on the host - // append the socket to the binds - binds = append(binds, clabtypes.NewBind(rtSocket, rtSocket, "")) - - // append the mounts required for container out of container operation - binds = append(binds, runtime.GetCooCBindMounts()...) - - // Find Docker binary and add bind mount if found - rtBinPath, err := runtime.GetRuntimeBinary() + binds, err := buildCodeServerBinds(homeDir, runtime, &paths) if err != nil { - return nil, fmt.Errorf("could not find docker binary: %v. "+ - "code-server might not function correctly if docker is not available", err) - } - // currently only docker is supported. - binds = append(binds, clabtypes.NewBind(rtBinPath, "/usr/bin/docker", "ro")) - - // Find containerlab binary and add bind mount if found - clabPath, err := getclabBinaryPath() - if err != nil { - return nil, fmt.Errorf("could not find containerlab binary: %v. "+ - "code-server might not function correctly if containerlab is not in its PATH", err) + return nil, err } - binds = append(binds, clabtypes.NewBind(clabPath, "/usr/bin/containerlab", "ro")) - - // Publish host random port -> ctr port 8080 exposedPorts := make(nat.PortSet) portBindings := make(nat.PortMap) @@ -220,15 +288,7 @@ cert: false }, } - // Build command based on whether it's first run - var cmd string - if isFirstRun { - // On first run, copy extensions then start - cmd = "-c \"cp -r /extensions/* /persistent-extensions/ 2>/dev/null || true; code-server --config /root/.config/code-server/config.yaml --extensions-dir /persistent-extensions --user-data-dir /persistent-user-data\"" - } else { - // On subsequent runs, just start directly - cmd = "-c \"code-server --config /root/.config/code-server/config.yaml --extensions-dir /persistent-extensions --user-data-dir /persistent-user-data\"" - } + cmd := buildCodeServerCommand(isFirstRun) nodeConfig := &clabtypes.NodeConfig{ LongName: name, @@ -287,7 +347,7 @@ func codeServerStart(cobraCmd *cobra.Command, o *Options) error { runtimeName := o.Global.Runtime if runtimeName == "" { - runtimeName = "docker" + runtimeName = clabruntimedocker.RuntimeName } // Initialize runtime @@ -396,7 +456,7 @@ func codeServerStatus(cobraCmd *cobra.Command, o *Options) error { // Use common.Runtime for consistency with other commands runtimeName := o.Global.Runtime if runtimeName == "" { - runtimeName = "docker" + runtimeName = clabruntimedocker.RuntimeName } // Initialize containerlab with runtime using the same approach as inspect command @@ -509,7 +569,7 @@ func codeServerStop(cobraCmd *cobra.Command, o *Options) error { runtimeName := o.Global.Runtime if runtimeName == "" { - runtimeName = "docker" + runtimeName = clabruntimedocker.RuntimeName } // Initialize runtime From e1af3480f05301bfc1dcddabee419407616cb988 Mon Sep 17 00:00:00 2001 From: Flosch62 Date: Thu, 18 Sep 2025 18:33:27 +0200 Subject: [PATCH 22/25] replace hardcoded labs directory with defaultLabsDir constant --- cmd/tools_api_start.go | 2 +- cmd/tools_api_status.go | 2 +- cmd/tools_code_server.go | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cmd/tools_api_start.go b/cmd/tools_api_start.go index 05b2a9daf7..99a274e209 100644 --- a/cmd/tools_api_start.go +++ b/cmd/tools_api_start.go @@ -228,7 +228,7 @@ func apiServerStart(cobraCmd *cobra.Command, o *Options) error { //nolint: funle // Create container labels if o.ToolsAPI.LabsDirectory == "" { - o.ToolsAPI.LabsDirectory = "~/.clab" + o.ToolsAPI.LabsDirectory = defaultLabsDir } owner := getOwnerName(o) diff --git a/cmd/tools_api_status.go b/cmd/tools_api_status.go index a1152d2225..3bbb70d14c 100644 --- a/cmd/tools_api_status.go +++ b/cmd/tools_api_status.go @@ -79,7 +79,7 @@ func apiServerStatus(cobraCmd *cobra.Command, o *Options) error { } // Get labs dir from labels or use default - labsDir := "~/.clab" // default + labsDir := defaultLabsDir // default if dirsVal, ok := containers[idx].Labels["clab-labs-dir"]; ok { labsDir = dirsVal } diff --git a/cmd/tools_code_server.go b/cmd/tools_code_server.go index b9d617bc19..bb9e41fce4 100644 --- a/cmd/tools_code_server.go +++ b/cmd/tools_code_server.go @@ -29,6 +29,7 @@ const ( codeServerDirPerm = 0o755 codeServerConfigPerm = 0o644 codeServerMarkerName = ".initialized" + defaultLabsDir = "~/.clab" ) type codeServerPaths struct { @@ -387,7 +388,7 @@ func codeServerStart(cobraCmd *cobra.Command, o *Options) error { // Create container labels if o.ToolsCodeServer.LabsDirectory == "" { - o.ToolsCodeServer.LabsDirectory = "~/.clab" + o.ToolsCodeServer.LabsDirectory = defaultLabsDir } owner := getOwnerName(o) @@ -507,7 +508,7 @@ func codeServerStatus(cobraCmd *cobra.Command, o *Options) error { port := containers[idx].Ports[0].HostPort // Get labs dir from labels or use default - labsDir := "~/.clab" // default + labsDir := defaultLabsDir // default if dirsVal, ok := containers[idx].Labels["clab-labs-dir"]; ok { labsDir = dirsVal } From b1139f28f425db5785646ed143e9e9b3fa28bf11 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Sun, 5 Oct 2025 20:29:43 +1300 Subject: [PATCH 23/25] open in the homedir by default --- cmd/tools_code_server.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/tools_code_server.go b/cmd/tools_code_server.go index bb9e41fce4..235b7b32ff 100644 --- a/cmd/tools_code_server.go +++ b/cmd/tools_code_server.go @@ -149,11 +149,11 @@ func buildCodeServerBinds( return binds, nil } -func buildCodeServerCommand(isFirstRun bool) string { +func buildCodeServerCommand(isFirstRun bool, defaultDir string) string { baseCommand := strings.Join([]string{ "code-server --config /root/.config/code-server/config.yaml", "--extensions-dir /persistent-extensions", - "--user-data-dir /persistent-user-data", + "--user-data-dir /persistent-user-data ", defaultDir, }, " ") if !isFirstRun { @@ -289,7 +289,7 @@ func NewCodeServerNode(name, image, labsDir string, }, } - cmd := buildCodeServerCommand(isFirstRun) + cmd := buildCodeServerCommand(isFirstRun, homeDir) nodeConfig := &clabtypes.NodeConfig{ LongName: name, From 78ea28d7df15cc39c70dbaa42b086c0af340b88e Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Sun, 5 Oct 2025 20:34:34 +1300 Subject: [PATCH 24/25] linter fix --- cmd/tools_code_server.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cmd/tools_code_server.go b/cmd/tools_code_server.go index 235b7b32ff..5325bfbc3e 100644 --- a/cmd/tools_code_server.go +++ b/cmd/tools_code_server.go @@ -234,7 +234,8 @@ func codeServerCmd(o *Options) (*cobra.Command, error) { }, } c.AddCommand(codeServerStopCmd) - codeServerStopCmd.Flags().StringVarP(&o.ToolsCodeServer.Name, "name", "n", o.ToolsCodeServer.Name, + codeServerStopCmd.Flags().StringVarP(&o.ToolsCodeServer.Name, + "name", "n", o.ToolsCodeServer.Name, "name of the code-server container to stop") return c, nil @@ -515,7 +516,8 @@ func codeServerStatus(cobraCmd *cobra.Command, o *Options) error { // Get owner from container labels owner := "N/A" - if ownerVal, exists := containers[idx].Labels[clabconstants.Owner]; exists && ownerVal != "" { + if ownerVal, exists := containers[idx].Labels[clabconstants.Owner]; exists && + ownerVal != "" { owner = ownerVal } From 3c8ae442804ba19cc93041ade679ef543e4d432d Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Sun, 5 Oct 2025 20:44:47 +1300 Subject: [PATCH 25/25] add `clab` as a bind as well --- cmd/tools_code_server.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/tools_code_server.go b/cmd/tools_code_server.go index 5325bfbc3e..fd131ed307 100644 --- a/cmd/tools_code_server.go +++ b/cmd/tools_code_server.go @@ -145,6 +145,7 @@ func buildCodeServerBinds( } binds = append(binds, clabtypes.NewBind(clabPath, "/usr/bin/containerlab", "ro")) + binds = append(binds, clabtypes.NewBind(clabPath, "/usr/bin/clab", "ro")) return binds, nil }