From 4b97a2ad30c1827e854f5aae91e30e1e2b89bc85 Mon Sep 17 00:00:00 2001 From: Aviv Carmi Date: Wed, 18 Jun 2025 17:18:02 +0300 Subject: [PATCH 1/5] Enhance ENVITE setup process with runtime information integration - Updated network and component logic to utilize runtime information for hostname and network latency. - Modified validation and update functions to reflect changes in the hosts file with dynamic hostname. - Improved user messages in setup files to include runtime-specific details for better clarity during initial setup. - Add wait time for Colima container start to allow network binding to kick in. --- docker/component.go | 19 +++-- docker/network.go | 43 +++++++----- docker/runtime.go | 142 ++++++++++++++++++++++++++++++++++++++ docker/setup-finished.txt | 2 +- docker/setup-needed.txt | 6 +- ui/src/ProductTour.tsx | 2 +- 6 files changed, 187 insertions(+), 27 deletions(-) create mode 100644 docker/runtime.go diff --git a/docker/component.go b/docker/component.go index 716ca3a..b543c80 100644 --- a/docker/component.go +++ b/docker/component.go @@ -9,6 +9,12 @@ import ( "context" "encoding/json" "fmt" + "math" + "strings" + "sync" + "sync/atomic" + "time" + "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" @@ -18,11 +24,6 @@ import ( "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/stdcopy" "github.com/perimeterx/envite" - "math" - "strings" - "sync" - "sync/atomic" - "time" ) // ComponentType is the type identifier for the Docker component. @@ -170,12 +171,20 @@ func (c *Component) pullImage(ctx context.Context) error { } func (c *Component) Start(ctx context.Context) error { + runtimeInfo, err := GetRuntimeInfo() + if err != nil { + return err + } + id, err := c.startContainer(ctx) if err != nil { return err } c.monitorStartingStatus(id, true) + if runtimeInfo.NetworkLatency > 0 { + time.Sleep(runtimeInfo.NetworkLatency) + } return nil } diff --git a/docker/network.go b/docker/network.go index 49a038b..c81015f 100644 --- a/docker/network.go +++ b/docker/network.go @@ -8,16 +8,17 @@ import ( "context" _ "embed" "fmt" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/client" - "github.com/docker/go-connections/nat" "os" "regexp" "runtime" "strings" "sync" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/client" + "github.com/docker/go-connections/nat" ) // Network represents a Docker network. @@ -109,7 +110,12 @@ func newOpenLinuxNetwork(cli *client.Client, envID string) (*Network, error) { } func newOpenNetwork(cli *client.Client, envID string) (*Network, error) { - err := validateHostsFile() + runtimeInfo, err := GetRuntimeInfo() + if err != nil { + return nil, err + } + + err = validateHostsFile(runtimeInfo) if err != nil { return nil, err } @@ -129,7 +135,7 @@ func newOpenNetwork(cli *client.Client, envID string) (*Network, error) { runConfig.networkingConfig = &network.NetworkingConfig{ EndpointsConfig: map[string]*network.EndpointSettings{id: {NetworkID: id}}, } - runConfig.hostname = "host.docker.internal" + runConfig.hostname = runtimeInfo.InternalHostname runConfig.containerConfig.ExposedPorts = nat.PortSet{} runConfig.hostConfig.PortBindings = nat.PortMap{} for _, port := range config.Ports { @@ -198,8 +204,8 @@ var setupNeeded string var setupFinished string // validateHostsFile installs necessary steps to /etc/hosts if needed -func validateHostsFile() error { - valid, err := isHostsFileValid() +func validateHostsFile(runtimeInfo *RuntimeInfo) error { + valid, err := isHostsFileValid(runtimeInfo) if err != nil { return err } @@ -208,28 +214,31 @@ func validateHostsFile() error { return nil } - err = updateHostsFile() + err = updateHostsFile(runtimeInfo) if err == nil { - fmt.Println(setupFinished) + fmt.Println(fmt.Sprintf(setupFinished, runtimeInfo.Runtime)) os.Exit(0) } if strings.Contains(err.Error(), "permission denied") { - fmt.Println(setupNeeded) + fmt.Println(fmt.Sprintf(setupNeeded, runtimeInfo.Runtime, runtimeInfo.InternalHostname)) os.Exit(1) } return err } -var hostsEntryRE = regexp.MustCompile(`^\s*127\.0\.0\.1\s+host\.docker\.internal\s*$`) - -func isHostsFileValid() (bool, error) { +func isHostsFileValid(runtimeInfo *RuntimeInfo) (bool, error) { data, err := os.ReadFile("/etc/hosts") if err != nil { return false, err } + hostsEntryRE, err := regexp.Compile(fmt.Sprintf(`^\s*127\.0\.0\.1\s+%s\s*$`, strings.ReplaceAll(runtimeInfo.InternalHostname, ".", "\\."))) + if err != nil { + return false, err + } + lines := strings.Split(string(data), "\n") for _, line := range lines { if hostsEntryRE.MatchString(strings.TrimSpace(line)) { @@ -240,13 +249,13 @@ func isHostsFileValid() (bool, error) { return false, nil } -func updateHostsFile() error { +func updateHostsFile(runtimeInfo *RuntimeInfo) error { f, err := os.OpenFile("/etc/hosts", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return err } - _, err = f.WriteString("\n# To allow envite to create open docker networks\n127.0.0.1 host.docker.internal\n# End of section\n") + _, err = f.WriteString(fmt.Sprintf("\n# To allow ENVITE to create open docker networks\n127.0.0.1 %s\n# End of section\n", runtimeInfo.InternalHostname)) if err != nil { _ = f.Close() return err diff --git a/docker/runtime.go b/docker/runtime.go new file mode 100644 index 0000000..c10b3a1 --- /dev/null +++ b/docker/runtime.go @@ -0,0 +1,142 @@ +package docker + +import ( + "fmt" + "os" + "strings" + "sync" + "time" + + "github.com/docker/docker/client" +) + +// runtimeInfos contains information about all known runtimes +var runtimeInfos = []*RuntimeInfo{ + { + Runtime: "Docker Desktop", + SocketPath: "/var/run/docker.sock", + InternalHostname: "host.docker.internal", + }, + { + Runtime: "Colima", + SocketPath: "~/.colima/default/docker.sock", + InternalHostname: "host.lima.internal", + NetworkLatency: time.Second * 3, + }, + { + Runtime: "Podman", + SocketPath: "~/.podman/podman.sock", + InternalHostname: "host.containers.internal", + }, + { + Runtime: "Rancher Desktop", + SocketPath: "~/.rd/docker.sock", + InternalHostname: "host.docker.internal", + }, + { + Runtime: "Lima", + SocketPath: "~/.lima/docker.sock", + InternalHostname: "host.lima.internal", + }, + { + Runtime: "OrbStack", + SocketPath: "~/.orbstack/run/docker.sock", + InternalHostname: "host.docker.internal", + }, + { + Runtime: "Minikube", + SocketPath: "~/.minikube/docker.sock", + InternalHostname: "host.minikube.internal", + }, + { + Runtime: "ContainerD", + SocketPath: "/run/containerd/containerd.sock", + InternalHostname: "host.docker.internal", + }, + { + Runtime: "Finch", + SocketPath: "~/.finch/docker.sock", + InternalHostname: "host.docker.internal", + }, +} + +// RuntimeInfo contains information about a runtime type +type RuntimeInfo struct { + // Runtime is the name of the runtime + Runtime string + // SocketPath is the path to the docker socket + SocketPath string + // InternalHostname is the hostname to use for the docker daemon + InternalHostname string + // NetworkLatency some runtimes have a slight latency before network is ready when starting a container + NetworkLatency time.Duration +} + +var ( + mu sync.Mutex + runtimeInfo *RuntimeInfo +) + +func GetRuntimeInfo() (*RuntimeInfo, error) { + mu.Lock() + defer mu.Unlock() + + if runtimeInfo == nil { + var err error + runtimeInfo, err = detectRuntime() + if err != nil { + return nil, err + } + fmt.Printf("ENVITE docker runtime detected: %s.\nIf you want to use another docker runtime, set the DOCKER_HOST environment variable.\n", runtimeInfo.Runtime) + } + return runtimeInfo, nil +} + +// detectRuntime detects the Docker daemon implementation +func detectRuntime() (*RuntimeInfo, error) { + home, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("failed to get user home directory: %w", err) + } + + for _, runtimeInfo := range runtimeInfos { + runtimeInfo.SocketPath = strings.ReplaceAll(runtimeInfo.SocketPath, "~/", home+"/") + } + + // Check DOCKER_HOST env var + dockerHost := os.Getenv("DOCKER_HOST") + if dockerHost != "" { + // Remove unix:// prefix if present + dockerHost = strings.TrimPrefix(dockerHost, "unix://") + + // Check if the socket path exists + for _, runtimeInfo := range runtimeInfos { + if runtimeInfo.SocketPath == dockerHost { + return runtimeInfo, nil + } + } + + // If no match, return fallback to Docker Desktop + return runtimeInfos[0], nil + } + + // Try all known runtimes + for _, runtimeInfo := range runtimeInfos { + // Check if the socket path exists + if _, err := os.Stat(runtimeInfo.SocketPath); err == nil { + // Create a new client with the socket path + cli, err := client.NewClientWithOpts(client.WithHost("unix://" + runtimeInfo.SocketPath)) + if err == nil { + // Close the client + err = cli.Close() + if err != nil { + return nil, fmt.Errorf("failed to close client: %w", err) + } + // Return the runtime info + return runtimeInfo, nil + } + } + } + + return nil, fmt.Errorf("could not detect a running Docker-compatible daemon") +} diff --git a/docker/setup-finished.txt b/docker/setup-finished.txt index 7b56756..f916a0c 100644 --- a/docker/setup-finished.txt +++ b/docker/setup-finished.txt @@ -1,3 +1,3 @@ Done ☑️ -Network features will now work properly for any envite environment. +Network features will now work properly for any ENVITE environment with %s. You can now rerun the process WITHOUT sudo and continue normally. \ No newline at end of file diff --git a/docker/setup-needed.txt b/docker/setup-needed.txt index 1c274d5..12da6b6 100644 --- a/docker/setup-needed.txt +++ b/docker/setup-needed.txt @@ -1,4 +1,4 @@ Hey there 👋 -It seems to be the first time you're running envite with docker. -We need to install a quick change. To do it, rerun the process using sudo. -If you prefer, you can manually add `127.0.0.1 host.docker.internal` to `/etc/hosts` and then rerun without sudo. \ No newline at end of file +It seems to be the first time you're running ENVITE with %s. +We need to install a quick change. To do it, rerun the process once using sudo. +If you prefer, you can manually add `127.0.0.1 %s` to `/etc/hosts` and then rerun without sudo. \ No newline at end of file diff --git a/ui/src/ProductTour.tsx b/ui/src/ProductTour.tsx index 10038ae..bb19ea6 100644 --- a/ui/src/ProductTour.tsx +++ b/ui/src/ProductTour.tsx @@ -28,7 +28,7 @@ function ProductTour(props: ProductTourProps) { friendly than manipulating components one by one.

- When you apply state, envite takes care of the dependencies + When you apply state, ENVITE takes care of the dependencies between components by loading them in the required order and monitoring their status. It will also stop and re-run components to your request. From fea29d5639e5b04b59045361bc71551dd2ce4bf5 Mon Sep 17 00:00:00 2001 From: Aviv Carmi Date: Thu, 19 Jun 2025 13:18:55 +0300 Subject: [PATCH 2/5] Add Colima runtime support in Docker configuration - Introduced a new runtime entry for Colima, including its socket path, internal hostname, and network latency. - Updated the fallback behavior in the runtime detection function to return a default 'Unknown' runtime when no matches are found. --- docker/runtime.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docker/runtime.go b/docker/runtime.go index c10b3a1..ecc05dc 100644 --- a/docker/runtime.go +++ b/docker/runtime.go @@ -23,6 +23,12 @@ var runtimeInfos = []*RuntimeInfo{ InternalHostname: "host.lima.internal", NetworkLatency: time.Second * 3, }, + { + Runtime: "Colima", + SocketPath: "~/.colima/docker.sock", + InternalHostname: "host.lima.internal", + NetworkLatency: time.Second * 3, + }, { Runtime: "Podman", SocketPath: "~/.podman/podman.sock", @@ -116,8 +122,12 @@ func detectRuntime() (*RuntimeInfo, error) { } } - // If no match, return fallback to Docker Desktop - return runtimeInfos[0], nil + // If no match, assume default behavior + return &RuntimeInfo{ + Runtime: "Unknown", + SocketPath: dockerHost, + InternalHostname: "host.docker.internal", + }, nil } // Try all known runtimes From 3c19e1aa546448707bac6f7ce689fa4662dea8f3 Mon Sep 17 00:00:00 2001 From: Aviv Carmi Date: Thu, 19 Jun 2025 14:50:01 +0300 Subject: [PATCH 3/5] Integrate RuntimeInfo into Component and Network structures - Added RuntimeInfo field to Component and Network types to enhance runtime-specific functionality. - Updated newComponent and NewNetwork functions to accept RuntimeInfo, improving the handling of network latency and internal hostnames. - Refactored Start method in Component to utilize the new RuntimeInfo field for network latency management. --- docker/component.go | 12 ++-- docker/network.go | 30 ++++---- docker/runtime.go | 172 +++++++++++++++----------------------------- 3 files changed, 79 insertions(+), 135 deletions(-) diff --git a/docker/component.go b/docker/component.go index b543c80..2e066b1 100644 --- a/docker/component.go +++ b/docker/component.go @@ -34,6 +34,7 @@ type Component struct { lock sync.Mutex envID string cli *client.Client + runtimeInfo *RuntimeInfo config Config runConfig *runConfig network *Network @@ -49,6 +50,7 @@ type Component struct { // docker components must be instantiated via a Network. func newComponent( cli *client.Client, + runtimeInfo *RuntimeInfo, envID string, network *Network, config Config, @@ -64,6 +66,7 @@ func newComponent( c := &Component{ cli: cli, + runtimeInfo: runtimeInfo, config: config, envID: envID, runConfig: runConf, @@ -171,19 +174,14 @@ func (c *Component) pullImage(ctx context.Context) error { } func (c *Component) Start(ctx context.Context) error { - runtimeInfo, err := GetRuntimeInfo() - if err != nil { - return err - } - id, err := c.startContainer(ctx) if err != nil { return err } c.monitorStartingStatus(id, true) - if runtimeInfo.NetworkLatency > 0 { - time.Sleep(runtimeInfo.NetworkLatency) + if c.runtimeInfo.NetworkLatency > 0 { + time.Sleep(c.runtimeInfo.NetworkLatency) } return nil } diff --git a/docker/network.go b/docker/network.go index c81015f..c8ef12a 100644 --- a/docker/network.go +++ b/docker/network.go @@ -29,6 +29,7 @@ import ( // component, err := network.NewComponent(dockerComponentConfig) type Network struct { client *client.Client + runtimeInfo *RuntimeInfo envID string ID string lock sync.Mutex @@ -45,13 +46,18 @@ type Network struct { // - On linux, it will create a network with mode "host" and attach new components to it. // - On other types of OS, it will create a network in mode "bridge" and expose ports for all components. func NewNetwork(cli *client.Client, networkID, envID string) (*Network, error) { + runtimeInfo, err := ExtractRuntimeInfo(context.Background(), cli) + if err != nil { + return nil, err + } + if networkID != "" { - return newClosedNetwork(cli, envID, networkID) + return newClosedNetwork(cli, envID, networkID, runtimeInfo) } if runtime.GOOS == "linux" { - return newOpenLinuxNetwork(cli, envID) + return newOpenLinuxNetwork(cli, envID, runtimeInfo) } - return newOpenNetwork(cli, envID) + return newOpenNetwork(cli, envID, runtimeInfo) } // NewComponent creates a new Docker component within the network. @@ -59,10 +65,10 @@ func (n *Network) NewComponent(config Config) (*Component, error) { if n.OnNewComponent != nil { n.OnNewComponent(&config) } - return newComponent(n.client, n.envID, n, config) + return newComponent(n.client, n.runtimeInfo, n.envID, n, config) } -func newClosedNetwork(cli *client.Client, envID, networkIdentifier string) (*Network, error) { +func newClosedNetwork(cli *client.Client, envID, networkIdentifier string, runtimeInfo *RuntimeInfo) (*Network, error) { networks, err := cli.NetworkList(context.Background(), types.NetworkListOptions{}) if err != nil { return nil, err @@ -75,6 +81,7 @@ func newClosedNetwork(cli *client.Client, envID, networkIdentifier string) (*Net return &Network{ client: cli, + runtimeInfo: runtimeInfo, envID: envID, shouldDelete: false, ID: nw.ID, @@ -88,7 +95,7 @@ func newClosedNetwork(cli *client.Client, envID, networkIdentifier string) (*Net }, nil } -func newOpenLinuxNetwork(cli *client.Client, envID string) (*Network, error) { +func newOpenLinuxNetwork(cli *client.Client, envID string, runtimeInfo *RuntimeInfo) (*Network, error) { id, err := createNetworkIfNotExist(cli, envID, "host") if err != nil { return nil, err @@ -96,6 +103,7 @@ func newOpenLinuxNetwork(cli *client.Client, envID string) (*Network, error) { return &Network{ client: cli, + runtimeInfo: runtimeInfo, envID: envID, shouldDelete: true, ID: id, @@ -109,13 +117,8 @@ func newOpenLinuxNetwork(cli *client.Client, envID string) (*Network, error) { }, nil } -func newOpenNetwork(cli *client.Client, envID string) (*Network, error) { - runtimeInfo, err := GetRuntimeInfo() - if err != nil { - return nil, err - } - - err = validateHostsFile(runtimeInfo) +func newOpenNetwork(cli *client.Client, envID string, runtimeInfo *RuntimeInfo) (*Network, error) { + err := validateHostsFile(runtimeInfo) if err != nil { return nil, err } @@ -127,6 +130,7 @@ func newOpenNetwork(cli *client.Client, envID string) (*Network, error) { return &Network{ client: cli, + runtimeInfo: runtimeInfo, envID: envID, shouldDelete: true, ID: id, diff --git a/docker/runtime.go b/docker/runtime.go index ecc05dc..9225d2f 100644 --- a/docker/runtime.go +++ b/docker/runtime.go @@ -1,152 +1,94 @@ package docker import ( - "fmt" - "os" + "context" "strings" - "sync" "time" "github.com/docker/docker/client" ) -// runtimeInfos contains information about all known runtimes -var runtimeInfos = []*RuntimeInfo{ - { - Runtime: "Docker Desktop", - SocketPath: "/var/run/docker.sock", - InternalHostname: "host.docker.internal", - }, - { - Runtime: "Colima", - SocketPath: "~/.colima/default/docker.sock", - InternalHostname: "host.lima.internal", - NetworkLatency: time.Second * 3, - }, - { - Runtime: "Colima", - SocketPath: "~/.colima/docker.sock", - InternalHostname: "host.lima.internal", - NetworkLatency: time.Second * 3, - }, - { - Runtime: "Podman", - SocketPath: "~/.podman/podman.sock", - InternalHostname: "host.containers.internal", - }, - { - Runtime: "Rancher Desktop", - SocketPath: "~/.rd/docker.sock", - InternalHostname: "host.docker.internal", - }, - { - Runtime: "Lima", - SocketPath: "~/.lima/docker.sock", - InternalHostname: "host.lima.internal", - }, - { - Runtime: "OrbStack", - SocketPath: "~/.orbstack/run/docker.sock", - InternalHostname: "host.docker.internal", - }, - { - Runtime: "Minikube", - SocketPath: "~/.minikube/docker.sock", - InternalHostname: "host.minikube.internal", - }, - { - Runtime: "ContainerD", - SocketPath: "/run/containerd/containerd.sock", - InternalHostname: "host.docker.internal", - }, - { - Runtime: "Finch", - SocketPath: "~/.finch/docker.sock", - InternalHostname: "host.docker.internal", - }, -} - // RuntimeInfo contains information about a runtime type type RuntimeInfo struct { // Runtime is the name of the runtime Runtime string - // SocketPath is the path to the docker socket - SocketPath string // InternalHostname is the hostname to use for the docker daemon InternalHostname string // NetworkLatency some runtimes have a slight latency before network is ready when starting a container NetworkLatency time.Duration } -var ( - mu sync.Mutex - runtimeInfo *RuntimeInfo -) +// ExtractRuntimeInfo detects the Docker daemon implementation for the given client +func ExtractRuntimeInfo(ctx context.Context, cli *client.Client) (*RuntimeInfo, error) { + info, err := cli.Info(ctx) + if err != nil { + return nil, err + } -func GetRuntimeInfo() (*RuntimeInfo, error) { - mu.Lock() - defer mu.Unlock() + name := strings.ToLower(info.Name) + serverVersion := strings.ToLower(info.ServerVersion) - if runtimeInfo == nil { - var err error - runtimeInfo, err = detectRuntime() - if err != nil { - return nil, err - } - fmt.Printf("ENVITE docker runtime detected: %s.\nIf you want to use another docker runtime, set the DOCKER_HOST environment variable.\n", runtimeInfo.Runtime) + // Check for specific runtime indicators + if strings.Contains(name, "colima") || strings.Contains(serverVersion, "colima") { + return &RuntimeInfo{ + Runtime: "Colima", + InternalHostname: "host.lima.internal", + NetworkLatency: time.Second * 3, + }, nil } - return runtimeInfo, nil -} -// detectRuntime detects the Docker daemon implementation -func detectRuntime() (*RuntimeInfo, error) { - home, err := os.UserHomeDir() - if err != nil { - return nil, fmt.Errorf("failed to get user home directory: %w", err) + if strings.Contains(name, "podman") || strings.Contains(serverVersion, "podman") { + return &RuntimeInfo{ + Runtime: "Podman", + InternalHostname: "host.containers.internal", + }, nil + } + + if strings.Contains(name, "rancher") || strings.Contains(serverVersion, "rancher") { + return &RuntimeInfo{ + Runtime: "Rancher Desktop", + InternalHostname: "host.docker.internal", + }, nil } - for _, runtimeInfo := range runtimeInfos { - runtimeInfo.SocketPath = strings.ReplaceAll(runtimeInfo.SocketPath, "~/", home+"/") + if strings.Contains(name, "lima") || strings.Contains(serverVersion, "lima") { + return &RuntimeInfo{ + Runtime: "Lima", + InternalHostname: "host.lima.internal", + }, nil } - // Check DOCKER_HOST env var - dockerHost := os.Getenv("DOCKER_HOST") - if dockerHost != "" { - // Remove unix:// prefix if present - dockerHost = strings.TrimPrefix(dockerHost, "unix://") + if strings.Contains(name, "orbstack") || strings.Contains(serverVersion, "orbstack") { + return &RuntimeInfo{ + Runtime: "OrbStack", + InternalHostname: "host.docker.internal", + }, nil + } - // Check if the socket path exists - for _, runtimeInfo := range runtimeInfos { - if runtimeInfo.SocketPath == dockerHost { - return runtimeInfo, nil - } - } + if strings.Contains(name, "minikube") || strings.Contains(serverVersion, "minikube") { + return &RuntimeInfo{ + Runtime: "Minikube", + InternalHostname: "host.minikube.internal", + }, nil + } - // If no match, assume default behavior + if strings.Contains(name, "containerd") || strings.Contains(serverVersion, "containerd") { return &RuntimeInfo{ - Runtime: "Unknown", - SocketPath: dockerHost, + Runtime: "ContainerD", InternalHostname: "host.docker.internal", }, nil } - // Try all known runtimes - for _, runtimeInfo := range runtimeInfos { - // Check if the socket path exists - if _, err := os.Stat(runtimeInfo.SocketPath); err == nil { - // Create a new client with the socket path - cli, err := client.NewClientWithOpts(client.WithHost("unix://" + runtimeInfo.SocketPath)) - if err == nil { - // Close the client - err = cli.Close() - if err != nil { - return nil, fmt.Errorf("failed to close client: %w", err) - } - // Return the runtime info - return runtimeInfo, nil - } - } + if strings.Contains(name, "finch") || strings.Contains(serverVersion, "finch") { + return &RuntimeInfo{ + Runtime: "Finch", + InternalHostname: "host.docker.internal", + }, nil } - return nil, fmt.Errorf("could not detect a running Docker-compatible daemon") + // Default to Docker Desktop for unknown runtimes + return &RuntimeInfo{ + Runtime: "Docker Desktop", + InternalHostname: "host.docker.internal", + }, nil } From 00014b857bc7968bbf7dd62183e48579d37990fd Mon Sep 17 00:00:00 2001 From: Aviv Carmi Date: Fri, 20 Jun 2025 17:17:40 +0300 Subject: [PATCH 4/5] Improve error handling in API and environment functions - Improved error handling with error wrapping to provide better error messages - Updated changeslog and readme --- CHANGELOG.md | 18 ++++++++++++++++++ README.md | 7 +++++++ api.go | 4 ++-- cmd/envite/environment.go | 2 +- docker/component.go | 20 ++++++++++---------- docker/config.go | 2 +- docker/network.go | 18 +++++++++--------- docker/runtime.go | 3 ++- docker/waiter.go | 4 ++-- environment.go | 4 ++-- seed/redis/component.go | 2 +- 11 files changed, 55 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdf43c4..0a9be49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.0.11](https://github.com/PerimeterX/envite/compare/v0.0.10...v0.0.11) + +### Added + +- Added docker runtime awareness with support for the following: + - Docker Desktop + - Colima (with 3-second network latency) + - Podman + - Rancher Desktop + - Lima + - OrbStack + - Minikube + - ContainerD + - Finch +- `ExtractRuntimeInfo` function to detect runtime type from Docker client info +- Runtime-specific internal hostname mapping (e.g., `host.docker.internal`, `host.lima.internal`) +- Network latency configuration for runtimes that require startup delays + ## [0.0.10](https://github.com/PerimeterX/envite/compare/v0.0.9...v0.0.10) ### Fixed diff --git a/README.md b/README.md index 951931e..504f6df 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ A framework to manage development and testing environments. - [Flags and Options](#flags-and-options) - [Adding Custom Components](#adding-custom-components) * [Key Elements of ENVITE](#key-elements-of-envite) +* [Runtime Awareness](#runtime-awareness) * [Local Development](#local-development) * [Contact and Contribute](#contact-and-contribute) * [ENVITE Logo](#envite-logo) @@ -334,6 +335,12 @@ functional environment. * `Component` Graph: Organizes components into layers and defines their relationships. * `Server`: Allow serving a UI to manage the environment. +## Runtime Awareness + +ENVITE automatically detects and adapts to different Docker-compatible runtimes (Docker Desktop, Colima, Podman, Rancher Desktop, Lima, OrbStack, Minikube, ContainerD, and Finch). This runtime awareness allows ENVITE to handle runtime-specific behaviors automatically. + +> Colima has some latency when attaching networking stack of new containers. This may lead to issue when adding log message based waiters. As a workaround, ENVITE adds a 3-second wait time after creating containers, to allow colima to finalize networking. This may not work perfectly as it depends on the time it takes colima to complete. + ## Local Development To locally work on ENVITE UI, cd into the `ui` dir and run react dev server using `npm start`. diff --git a/api.go b/api.go index 25f110e..f367d48 100644 --- a/api.go +++ b/api.go @@ -76,13 +76,13 @@ type GetStatusResponseComponent struct { func buildComponentInfo(c Component) (map[string]any, error) { data, err := json.Marshal(c.Config()) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to marshal component config: %w", err) } var result map[string]any err = json.Unmarshal(data, &result) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to unmarshal component config: %w", err) } if result == nil { diff --git a/cmd/envite/environment.go b/cmd/envite/environment.go index 4ec69f2..5f1e26d 100644 --- a/cmd/envite/environment.go +++ b/cmd/envite/environment.go @@ -162,7 +162,7 @@ func extractComponentType(err error, data []byte) (string, error) { } err = json.Unmarshal(data, &t) if err != nil { - return "", err + return "", fmt.Errorf("could not unmarshal component type: %w", err) } return t.Type, nil diff --git a/docker/component.go b/docker/component.go index 2e066b1..6ac07af 100644 --- a/docker/component.go +++ b/docker/component.go @@ -58,7 +58,7 @@ func newComponent( imageCloneTag := fmt.Sprintf("%s_%s", config.Image, envID) runConf, err := config.initialize(network, imageCloneTag) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to initialize component config: %w", err) } containerName := fmt.Sprintf("%s_%s", envID, config.Name) @@ -202,11 +202,11 @@ func (c *Component) startContainer(ctx context.Context) (string, error) { if err == nil { id = res.ID } else if !errdefs.IsConflict(err) { - return "", err + return "", fmt.Errorf("failed to create container: %w", err) } else { cont, err := c.findContainer(ctx) if err != nil { - return "", err + return "", fmt.Errorf("failed to find container: %w", err) } id = cont.ID @@ -214,7 +214,7 @@ func (c *Component) startContainer(ctx context.Context) (string, error) { err = c.cli.ContainerStart(context.Background(), id, container.StartOptions{}) if err != nil { - return "", err + return "", fmt.Errorf("failed to start container: %w", err) } go c.writeLogs(id) @@ -286,7 +286,7 @@ func (c *Component) Status(context.Context) (envite.ComponentStatus, error) { // check if container stopped cont, err := c.findContainer(context.Background()) if err != nil { - return "", err + return "", fmt.Errorf("failed to find container: %w", err) } if cont == nil || cont.State != "running" { @@ -323,7 +323,7 @@ func (c *Component) Config() any { func (c *Component) Exec(ctx context.Context, cmd []string) (int, error) { cont, err := c.findContainer(ctx) if err != nil { - return 0, err + return 0, fmt.Errorf("failed to find container: %w", err) } c.Writer().WriteString(c.Writer().Color.Cyan(fmt.Sprintf("executing: %s", strings.Join(cmd, " ")))) @@ -334,12 +334,12 @@ func (c *Component) Exec(ctx context.Context, cmd []string) (int, error) { AttachStderr: true, }) if err != nil { - return 0, err + return 0, fmt.Errorf("failed to create exec: %w", err) } hijack, err := c.cli.ContainerExecAttach(ctx, response.ID, types.ExecStartCheck{}) if err != nil { - return 0, err + return 0, fmt.Errorf("failed to attach exec: %w", err) } scanner := bufio.NewScanner(hijack.Reader) @@ -351,7 +351,7 @@ func (c *Component) Exec(ctx context.Context, cmd []string) (int, error) { execResp, err := c.cli.ContainerExecInspect(ctx, response.ID) if err != nil { - return 0, err + return 0, fmt.Errorf("failed to inspect exec: %w", err) } c.Writer().WriteString(c.Writer().Color.Cyan(fmt.Sprintf("exit code: %d", execResp.ExitCode))) @@ -364,7 +364,7 @@ func (c *Component) findContainer(ctx context.Context) (*types.Container, error) Filters: filters.NewArgs(filters.Arg("name", c.containerName)), }) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to list containers: %w", err) } for _, co := range containers { diff --git a/docker/config.go b/docker/config.go index 198bad7..7a8e6b7 100644 --- a/docker/config.go +++ b/docker/config.go @@ -652,7 +652,7 @@ func (c Config) imagePullOptions() (image.PullOptions, error) { var err error auth, err = c.ImagePullOptions.RegistryAuthFunc() if err != nil { - return image.PullOptions{}, err + return image.PullOptions{}, fmt.Errorf("failed to get registry auth: %w", err) } } else { auth = c.ImagePullOptions.RegistryAuth diff --git a/docker/network.go b/docker/network.go index c8ef12a..632acd1 100644 --- a/docker/network.go +++ b/docker/network.go @@ -48,7 +48,7 @@ type Network struct { func NewNetwork(cli *client.Client, networkID, envID string) (*Network, error) { runtimeInfo, err := ExtractRuntimeInfo(context.Background(), cli) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to extract runtime info: %w", err) } if networkID != "" { @@ -71,12 +71,12 @@ func (n *Network) NewComponent(config Config) (*Component, error) { func newClosedNetwork(cli *client.Client, envID, networkIdentifier string, runtimeInfo *RuntimeInfo) (*Network, error) { networks, err := cli.NetworkList(context.Background(), types.NetworkListOptions{}) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to list networks: %w", err) } nw, err := findNetwork(networks, networkIdentifier) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to find network: %w", err) } return &Network{ @@ -98,7 +98,7 @@ func newClosedNetwork(cli *client.Client, envID, networkIdentifier string, runti func newOpenLinuxNetwork(cli *client.Client, envID string, runtimeInfo *RuntimeInfo) (*Network, error) { id, err := createNetworkIfNotExist(cli, envID, "host") if err != nil { - return nil, err + return nil, fmt.Errorf("failed to create network: %w", err) } return &Network{ @@ -120,12 +120,12 @@ func newOpenLinuxNetwork(cli *client.Client, envID string, runtimeInfo *RuntimeI func newOpenNetwork(cli *client.Client, envID string, runtimeInfo *RuntimeInfo) (*Network, error) { err := validateHostsFile(runtimeInfo) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to validate hosts file: %w", err) } id, err := createNetworkIfNotExist(cli, envID, "bridge") if err != nil { - return nil, err + return nil, fmt.Errorf("failed to create network: %w", err) } return &Network{ @@ -183,7 +183,7 @@ func createNetworkIfNotExist(cli *client.Client, name, driver string) (string, e }) if err != nil { if !strings.Contains(err.Error(), "already exists") { - return "", err + return "", fmt.Errorf("failed to create network: %w", err) } return name, nil @@ -235,12 +235,12 @@ func validateHostsFile(runtimeInfo *RuntimeInfo) error { func isHostsFileValid(runtimeInfo *RuntimeInfo) (bool, error) { data, err := os.ReadFile("/etc/hosts") if err != nil { - return false, err + return false, fmt.Errorf("failed to read hosts file: %w", err) } hostsEntryRE, err := regexp.Compile(fmt.Sprintf(`^\s*127\.0\.0\.1\s+%s\s*$`, strings.ReplaceAll(runtimeInfo.InternalHostname, ".", "\\."))) if err != nil { - return false, err + return false, fmt.Errorf("failed to compile hosts entry regex: %w", err) } lines := strings.Split(string(data), "\n") diff --git a/docker/runtime.go b/docker/runtime.go index 9225d2f..2039070 100644 --- a/docker/runtime.go +++ b/docker/runtime.go @@ -2,6 +2,7 @@ package docker import ( "context" + "fmt" "strings" "time" @@ -22,7 +23,7 @@ type RuntimeInfo struct { func ExtractRuntimeInfo(ctx context.Context, cli *client.Client) (*RuntimeInfo, error) { info, err := cli.Info(ctx) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get docker info: %w", err) } name := strings.ToLower(info.Name) diff --git a/docker/waiter.go b/docker/waiter.go index b30a233..def3ae6 100644 --- a/docker/waiter.go +++ b/docker/waiter.go @@ -64,7 +64,7 @@ func validateWaiter(w Waiter) (waiterFunc, error) { case WaiterTypeRegex: re, err := regexp.Compile(w.Regex) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to compile regex: %w", err) } return func(ctx context.Context, cli *client.Client, containerID string, _ bool) error { @@ -86,7 +86,7 @@ func validateWaiter(w Waiter) (waiterFunc, error) { case WaiterTypeDuration: d, err := time.ParseDuration(w.Duration) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to parse duration: %w", err) } return func(_ context.Context, _ *client.Client, _ string, isNewContainer bool) error { diff --git a/environment.go b/environment.go index c82993c..19b0d85 100644 --- a/environment.go +++ b/environment.go @@ -60,7 +60,7 @@ func NewEnvironment(id string, componentGraph *ComponentGraph, options ...Option err := component.AttachEnvironment(context.Background(), b, om.writer(componentID)) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to attach environment to component %s: %w", componentID, err) } b.componentsByID[componentID] = component @@ -216,7 +216,7 @@ func (b *Environment) Status(ctx context.Context) (GetStatusResponse, error) { info, err := buildComponentInfo(component) if err != nil { - return GetStatusResponse{}, err + return GetStatusResponse{}, fmt.Errorf("failed to build component info for %s: %w", id, err) } components = append(components, GetStatusResponseComponent{ diff --git a/seed/redis/component.go b/seed/redis/component.go index b464221..9b17e00 100644 --- a/seed/redis/component.go +++ b/seed/redis/component.go @@ -141,7 +141,7 @@ func (r *SeedComponent) clientProvider() (*redis.Client, error) { options, err := redis.ParseURL(r.config.Address) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to parse redis address: %w", err) } return redis.NewClient(options), nil From fec2766dee2f4bc8552c72049db62aaebc4726ca Mon Sep 17 00:00:00 2001 From: Aviv Carmi Date: Fri, 20 Jun 2025 17:17:50 +0300 Subject: [PATCH 5/5] chagelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a9be49..6757e1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `ExtractRuntimeInfo` function to detect runtime type from Docker client info - Runtime-specific internal hostname mapping (e.g., `host.docker.internal`, `host.lima.internal`) - Network latency configuration for runtimes that require startup delays +- Improved error handling with error wrapping to provide better error messages ## [0.0.10](https://github.com/PerimeterX/envite/compare/v0.0.9...v0.0.10)