Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ 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
- 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)

### Fixed
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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`.
Expand Down
4 changes: 2 additions & 2 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion cmd/envite/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 22 additions & 15 deletions docker/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
Expand All @@ -33,6 +34,7 @@ type Component struct {
lock sync.Mutex
envID string
cli *client.Client
runtimeInfo *RuntimeInfo
config Config
runConfig *runConfig
network *Network
Expand All @@ -48,21 +50,23 @@ type Component struct {
// docker components must be instantiated via a Network.
func newComponent(
cli *client.Client,
runtimeInfo *RuntimeInfo,
envID string,
network *Network,
config Config,
) (*Component, error) {
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)
network.configure(config, runConf, containerName)

c := &Component{
cli: cli,
runtimeInfo: runtimeInfo,
config: config,
envID: envID,
runConfig: runConf,
Expand Down Expand Up @@ -176,6 +180,9 @@ func (c *Component) Start(ctx context.Context) error {
}

c.monitorStartingStatus(id, true)
if c.runtimeInfo.NetworkLatency > 0 {
time.Sleep(c.runtimeInfo.NetworkLatency)
}
return nil
}

Expand All @@ -195,19 +202,19 @@ 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
}

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)
Expand Down Expand Up @@ -279,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" {
Expand Down Expand Up @@ -316,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, " "))))
Expand All @@ -327,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)
Expand All @@ -344,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)))
Expand All @@ -357,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 {
Expand Down
2 changes: 1 addition & 1 deletion docker/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading