envite

package module
v0.0.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 25, 2024 License: MIT Imports: 18 Imported by: 0

README

ENVITE

envite-logo

CodeQL Status Run Tests Dependency Review Go Report Card Go Reference Licence Latest Release Top Languages Issues Pull Requests Commits Contributor Covenant

A framework to manage development and testing environments.

Contents

Why ENVITE?

marshmallow-gopher

Why should I Choose ENVITE? Why not stick to what you have today?

For starters, you might want to do that. Let's see when you actually need ENVITE. Here are the popular alternatives and how they compare with ENVITE.

Kubernetes

Using Kubernetes for testing and development, in addition to production.

This method has a huge advantage: you only have to describe your environment once. This means you maintain only one description of your environments - using Kubernetes manifest files. But more importantly, the way your components are deployed and provisioned in production is identical to the way they are in development in CI.

Let's talk about some possible downsides:

  • Local development is not always intuitive. While actively working on one or more components, there are some issues to solve:
    • If you fully containerize everything, like you normally do in Kubernetes:
      • You need to solve how you debug running containers. Attaching a remote debugging session is not always easy.
      • How do you rebuild container images each time you perform a code change? This process can take several minutes every time you perform any code change.
      • How do you manage and override image tag values in your original manifest files? Does it mean you maintain separate manifest files for production and dev purposes? Do you have to manually override environment variables?
      • Can you provide hot reloading or similar tools in environments where this is desired?
    • If you choose to avoid developing components in containers, and simply run them outside the cluster:
      • How easy it is to configure and run a component outside the cluster?
      • Can components running outside the cluster communicate with components running inside it? This needs to be solved specifically for development purposes.
      • Can components running inside the cluster communicate with components running outside of it? This requires a different, probably more complex solution.
  • What about non-containerized steps? By that, I'm not referring to actual production components that are not containerized. I'm talking about steps that do not exist in production at all. For instance, creating seed data that must exist in a database for other components to boot successfully. This step usually involves writing some code or using some automation to create initial data. For each such requirement, you can either find a solution that prevents writing custom code or containerizing your code. Either way, you add complexity and time. But more importantly, it misses the original goal of dev and CI being identical to production. These are extra steps that can hide production issues, or create issues that aren't really there.
  • What about an environment that keeps some components outside Kubernetes in production? For instance, some companies do not run their databases inside a Kubernetes cluster. This also means you have to maintain manifest files specifically for dev and CI, and the environments are not identical to production.
Docker Compose

If you're not using container orchestration tools like Kubernetes in production, but need some integration between several components, this will probably be your first choice.

However, it does have all the possible downsides of Kubernetes mentioned above, on top of some other ones:

  • You manage your docker-compose manifest files specifically for dev and CI. This means you have a duplicate to maintain, but also, your dev and CI envs can potentially be different from production.
  • Managing dependencies between services is not always easy - if one service needs to be fully operational before another one starts, it can be a bit tricky.
Remote Staging/Dev Environments

Some cases have developers use a remote environment for dev and testing purposes. It can either be achieved using tools such as Kubernetes, or even simply connecting to remote components.

These solutions are a very good fit for use cases that require running a lot of components. I.e., if you need 50 components up and running to run your tests, running it all locally is not feasible. However, they can have downsides or complexities:

  • You need internet connectivity. It sounds quite funny because you have a connection everywhere these days, right? But think about the times that your internet goes down, and you can at least keep on debugging your if statement. Now you can't. Think about all the times that the speed goes down, this directly affects your ability to run and debug your local code.
  • What if something breaks? Connecting to remote components every time you want to do any kind of local development simply add issues that are more complex to understand and debug. You might need to debug your debugging sessions.
  • Is this environment shared? If so, this is obviously bad. Tests can suddenly stop passing because someone made a change that had unintended consequences.
  • If this environment is not shared, how much does it cost to have an entire duplicate of the production stack for each engineer in the organization?
TestContainers

This option is quite close to ENVITE. TestContainers and similar tools allow you to write custom code to describe your environment, so you have full control over what you can do. As with most other options, you must manage your test env separately from production since you don't use testcontainers in production. This means you have to maintain 2 copies, but also, production env can defer from your test env.

In addition, testcontainers have 2 more downsides:

  • You can only write in Java or Go.
  • testcontainers bring a LOT of dependencies.
How's ENVITE Different?

With either option you choose, the main friction you're about to encounter is debugging and local development. Suppose your environment contains 10 components, but you're currently working on one. You make changes that you want to quickly update, you debug and use breakpoints, you want hot reloading or other similar tools - either way, if you must use containers it's going to be harder. ENVITE is designed to make it simple.

ENVITE supports a Go SDK that resembles testcontainers and a YAML CLI tool that resembles docker-compose. However, containers are not a requirement. ENVITE is designed to allow components to run inside or outside containers. Furthermore, ENVITE components can be anything, as long as they implement a simple interface. Components like data seed steps do not require containerizing at all. This allows the simple creation of components and ease of debugging and local development. It connects everything fluently and provides the best tooling to manage and monitor the entire environment. You'll see it in action below.

Does ENVITE Meet My Need?

As with other options, it does mean your ENVITE description of the environment is separate from the definition of production environments. If you want to know what's the best option for you - If you're able to run testing and local dev using only production manifest files, and also able to easily debug and update components, and this solution is cost-effective - you might not need ENVITE. If this is not the case, ENVITE is probably worth checking out.

At some point, we plan to add support to read directly from Helm and Kustomize files to allow enjoying the goodies of ENVITE without having to maintain a duplicate of production manifests.

Another limitation of ENVITE (and most other options as well) - since it runs everything locally, there's a limit on the number of components it can run. If you need 50 components up and running to run your tests, running it all locally might not be feasible.

If this is your direction, another interesting alternative to check out is Raftt.

Usage

ENVITE offers flexibility in environment management through both a Go SDK and a CLI. Depending on your use case, you can choose the method that best fits your needs.

The Go SDK provides fine-grained control over environment configuration and components. For example, you can create conditions to determine what the environment looks like or create a special connection between assets, particularly in seed data. However, the Go SDK is exclusively applicable within a Go environment and is most suitable for organizations or individuals already using or open to incorporating it into their tech stack. Regardless of the programming languages employed, if you opt to write your tests in Go, the ENVITE Go SDK is likely a more powerful choice.

Otherwise, the CLI is an easy-to-install and intuitive alternative, independent of any tech stack, and resembles docker-compose in its setup and usage. However, it's more powerful than docker-compose in many use cases as mentioned above.

Go SDK Usage
package main

import (
	"fmt"
	"github.com/docker/docker/client"
	"github.com/perimeterx/envite"
	"github.com/perimeterx/envite/docker"
	"github.com/perimeterx/envite/seed/mongo"
)

func runTestEnv() error {
	dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
	if err != nil {
		return err
	}

	network, err := docker.NewNetwork(dockerClient, "docker-network-or-empty-to-create-one", "my-test-env")
	if err != nil {
		return err
	}

	persistence, err := network.NewComponent(docker.Config{
		Name:    "mongo",
		Image:   "mongo:7.0.5",
		Ports:   []docker.Port{{Port: "27017"}},
		Waiters: []docker.Waiter{docker.WaitForLog("Waiting for connections")},
	})
	if err != nil {
		return err
	}

	cache, err := network.NewComponent(docker.Config{
		Name:    "redis",
		Image:   "redis:7.2.4",
		Ports:   []docker.Port{{Port: "6379"}},
		Waiters: []docker.Waiter{docker.WaitForLog("Ready to accept connections tcp")},
	})
	if err != nil {
		return err
	}

	seed := mongo.NewSeedComponent(mongo.SeedConfig{
		URI: fmt.Sprintf("mongodb://%s:27017", persistence.Host()),
	})

	env, err := envite.NewEnvironment(
		"my-test-env",
		envite.NewComponentGraph().
			AddLayer(map[string]envite.Component{
				"persistence": persistence,
				"cache":       cache,
			}).
			AddLayer(map[string]envite.Component{
				"seed": seed,
			}),
	)
	if err != nil {
		return err
	}

	server := envite.NewServer("4005", env)
	return envite.Execute(server, envite.ExecutionModeDaemon)
}
CLI Usage
  1. Install ENVITE from the GitHub releases page.
  2. Create an envite.yml file:
default_id: "my-test-env"
components:
  -
    persistence:
      type: docker component
      image: mongo:7.0.5
      name: mongo
      ports:
        - port: '27017'
      waiters:
        - string: Waiting for connections
          type: string
    cache:
      type: docker component
      image: redis:7.2.4
      name: redis
      ports:
        - port: '6379'
      waiters:
        - string: Ready to accept connections tcp
          type: string
  -
    seed:
      type: mongo seed
      uri: mongodb://{{ persistence }}:27017
      data:
        - db: data
          collection: users
          documents:
            - first_name: John
              last_name: Doe
  1. Run ENVITE: envite.

The full list of CLI supported components can be found here.

Demo

With either approach, the result is a UI served via the browser. It enables managing the environment, monitoring, initiating and halting components, conducting detailed inspections, debugging, and providing all essential tools for development and testing, as well as automated and CI/CD processes.

ENVITE Demo

Voilà! You now have a fully usable dev and testing environment.

Execution Modes

ENVITE supports three execution modes:

  • Daemon Mode (envite -mode start): Start execution mode, which starts all components in the environment, and then exits.
  • Start Mode (envite -mode stop): Stops all components in the environment, performs cleanup, and then exits.
  • Stop Mode (envite -mode daemon): Starts ENVITE as a daemon and serves a web UI.

Typically, the daemon mode will be used for local purposes, and a combination of start and stop modes will be used for Continuous Integration or other automated systems.

Flags and Options

All flags and options are described via envite -help command:

  mode
        Mode to operate in (default: daemon)
  -file value
        Path to an environment yaml file (default: `envite.yml`)
  -id value
        Override the environment ID provided by the environment yaml
  -network value
        Docker network identifier to be used. Used only if docker components exist in the environment file. If not provided, ENVITE will create a dedicated open docker network.
  -port value
        Web UI port to be used if mode is daemon (default: `4005`)
Adding Custom Components

Integrate your own components into the environment, either as Docker containers or by providing implementations of the envite.Component interface.

Key Elements of ENVITE

ENVITE contains several different elements:

  • Environment: Represents the entire configuration, containing components and controlling them to provide a fully functional environment.
  • Component: Represents a specific part of the environment, such as a Docker container or a custom component.
  • Component Graph: Organizes components into layers and defines their relationships.
  • Server: Allow serving a UI to manage the environment.

Local Development

To locally work on ENVITE UI, cd into the ui dir and run react dev server using npm start.

To build the UI into shipped static files run ./build-ui.sh.

Contact and Contribute

Reporting issues and requesting features may be done on our GitHub issues page. For any further questions or comments, you can reach us at open-source@humansecurity.com.

Any type of contribution is warmly welcome and appreciated ❤️ Please read our contribution guide for more info.

If you're looking for something to get started with, you can always follow our issues page and look for good first issue and help wanted labels.

ENVITE logo and assets by Adva Rom are licensed under a Creative Commons Attribution 4.0 International License.

Documentation

Overview

Package envite generated by go-bindata.// sources: ui/build/asset-manifest.json ui/build/favicon.ico ui/build/index.html ui/build/logo-large.svg ui/build/logo-small.svg ui/build/static/css/main.02ca4c04.css ui/build/static/js/27.3d9e48d0.chunk.js ui/build/static/js/main.71222bff.js ui/build/static/js/main.71222bff.js.LICENSE.txt

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrEmptyEnvID indicates that an empty environment ID was provided.
	ErrEmptyEnvID = errors.New("environment ID cannot be empty")

	// ErrNilGraph indicates that a nil component graph was provided.
	ErrNilGraph = errors.New("environment component graph cannot be nil")
)

Functions

func Asset

func Asset(name string) ([]byte, error)

Asset loads and returns the asset for the given name. It returns an error if the asset could not be found or could not be loaded.

func AssetDir

func AssetDir(name string) ([]string, error)

AssetDir returns the file names below a certain directory embedded in the file by go-bindata. For example if you run go-bindata on data/... and data contains the following hierarchy:

data/
  foo.txt
  img/
    a.png
    b.png

then AssetDir("data") would return []string{"foo.txt", "img"} AssetDir("data/img") would return []string{"a.png", "b.png"} AssetDir("foo.txt") and AssetDir("notexist") would return an error AssetDir("") will return []string{"data"}.

func AssetFile

func AssetFile() http.FileSystem

AssetFile return a http.FileSystem instance that data backend by asset

func AssetInfo

func AssetInfo(name string) (os.FileInfo, error)

AssetInfo loads and returns the asset info for the given name. It returns an error if the asset could not be found or could not be loaded.

func AssetNames

func AssetNames() []string

AssetNames returns the names of the assets.

func DescribeAvailableModes added in v0.0.3

func DescribeAvailableModes() string

DescribeAvailableModes returns a string describing all available execution modes.

func Execute

func Execute(server *Server, executionMode ExecutionMode) error

Execute performs the specified action based on the provided execution mode. It takes a Server instance and an ExecutionMode as parameters and executes the corresponding action. The available execution modes are ExecutionModeStart, ExecutionModeStop, and ExecutionModeDaemon.

func MustAsset

func MustAsset(name string) []byte

MustAsset is like Asset but panics when Asset would return an error. It simplifies safe initialization of global variables.

func RestoreAsset

func RestoreAsset(dir, name string) error

RestoreAsset restores an asset under the given directory

func RestoreAssets

func RestoreAssets(dir, name string) error

RestoreAssets restores an asset under the given directory recursively

Types

type AnsiColor

type AnsiColor struct{}

AnsiColor provides ANSI color codes for console output.

func (AnsiColor) Blue

func (a AnsiColor) Blue(s string) string

Blue applies blue color to the given string.

func (AnsiColor) Cyan

func (a AnsiColor) Cyan(s string) string

Cyan applies cyan color to the given string.

func (AnsiColor) Green

func (a AnsiColor) Green(s string) string

Green applies green color to the given string.

func (AnsiColor) Magenta

func (a AnsiColor) Magenta(s string) string

Magenta applies magenta color to the given string.

func (AnsiColor) Red

func (a AnsiColor) Red(s string) string

Red applies red color to the given string.

func (AnsiColor) Yellow

func (a AnsiColor) Yellow(s string) string

Yellow applies yellow color to the given string.

type Component

type Component interface {
	// Type returns the type of the component.
	Type() string

	// AttachEnvironment associates the component with an environment and output writer.
	// It allows the component to interact with its environment and handle output properly.
	AttachEnvironment(ctx context.Context, env *Environment, writer *Writer) error

	// Prepare readies the component for operation. This may involve pre-start configuration or checks.
	Prepare(ctx context.Context) error

	// Start initiates the component's operation.
	// It should return any errors if encountered during startup.
	Start(ctx context.Context) error

	// Stop halts the component's operation.
	// It should return any errors if encountered during stop.
	Stop(ctx context.Context) error

	// Cleanup performs any necessary cleanup operations for the component,
	// such as removing temporary files or releasing external resources.
	Cleanup(ctx context.Context) error

	// Status reports the current operational status of the component.
	Status(ctx context.Context) (ComponentStatus, error)

	// Config returns the configuration of the component.
	// The exact return type can vary between component types.
	Config() any
}

Component defines the interface for an environment component. It includes methods for lifecycle management, configuration, and status reporting.

type ComponentGraph

type ComponentGraph struct {
	// contains filtered or unexported fields
}

ComponentGraph represents a graph of components organized in layers. Each layer can contain one or more components that can depend on components from the previous layers. A layer is represented as a map, mapping from component ID to a component. Layer components are assumed to not depend on each other and can be operated on concurrently.

This structure is useful for initializing, starting, and stopping components in the correct order, ensuring that dependencies are correctly managed.

func NewComponentGraph

func NewComponentGraph() *ComponentGraph

NewComponentGraph creates a new instance of ComponentGraph. It initializes an empty graph with no components and returns a pointer to it. This function is the starting point for building a graph of components by adding layers.

Example:

 graph := NewComponentGraph().
 	.AddLayer({
		"component-a": componentA,
 	})
	.AddLayer({
		"component-b": componentB,
		"component-c": componentC,
 	})

This example creates a new component graph and adds two layers to it.

func (*ComponentGraph) AddLayer

func (c *ComponentGraph) AddLayer(components map[string]Component) *ComponentGraph

AddLayer adds a new layer of components to the ComponentGraph. Each call to AddLayer represents a new level in the graph, with the given components being added as a single layer. Components within the same layer are considered to have no dependencies on each other, but depend on components from all previous layers.

Parameters:

components map[string]Component: A mapping from component ID to a component implementation.

Example:

 graph := NewComponentGraph().
 	.AddLayer({
		"component-a": componentA,
 	})
	.AddLayer({
		"component-b": componentB,
		"component-c": componentC,
 	})

This example creates a new component graph and adds two layers to it.

type ComponentStatus

type ComponentStatus string

ComponentStatus represents the operational status of a component within the environment.

const (
	// ComponentStatusStopped indicates that the component is not currently running.
	ComponentStatusStopped ComponentStatus = "stopped"

	// ComponentStatusFailed indicates that the component has encountered an error and cannot continue operation.
	ComponentStatusFailed ComponentStatus = "failed"

	// ComponentStatusStarting indicates that the component is in the process of starting up but is not yet fully operational.
	ComponentStatusStarting ComponentStatus = "starting"

	// ComponentStatusRunning indicates that the component is currently operational and running as expected.
	ComponentStatusRunning ComponentStatus = "running"

	// ComponentStatusFinished indicates that the component has completed its operation successfully and has stopped running.
	ComponentStatusFinished ComponentStatus = "finished"
)

type Environment

type Environment struct {
	Logger Logger
	// contains filtered or unexported fields
}

Environment represents a collection of components that can be managed together. Components within an environment can be started, stopped, and configured collectively or individually.

func NewEnvironment

func NewEnvironment(id string, componentGraph *ComponentGraph, options ...Option) (*Environment, error)

NewEnvironment creates and initializes a new Environment with the specified id and component graph. It returns an error if the id is empty, the graph is nil, or if any components are misconfigured.

func (*Environment) Apply

func (b *Environment) Apply(ctx context.Context, enabledComponentIDs []string) error

Apply applies the specified configuration to the environment, enabling only the components with IDs in enabledComponentIDs. It returns an error if applying the configuration fails.

func (*Environment) Cleanup

func (b *Environment) Cleanup(ctx context.Context) error

Cleanup performs cleanup operations for all components within the environment. It returns an error if cleaning up any component fails.

func (*Environment) Components

func (b *Environment) Components() []Component

Components returns a slice of all components within the environment.

func (*Environment) Output

func (b *Environment) Output() *Reader

Output returns a reader for the environment's combined output from all components.

func (*Environment) StartAll

func (b *Environment) StartAll(ctx context.Context) error

StartAll starts all components in the environment concurrently. It returns an error if starting any component fails.

func (*Environment) StartComponent

func (b *Environment) StartComponent(ctx context.Context, componentID string) error

StartComponent starts a single component identified by componentID. It does nothing if the component is already running. Returns an error if the component fails to start.

func (*Environment) Status

Status returns the current status of all components within the environment.

func (*Environment) StopAll

func (b *Environment) StopAll(ctx context.Context) error

StopAll stops all components in the environment in reverse order of their startup. It returns an error if stopping any component fails.

func (*Environment) StopComponent

func (b *Environment) StopComponent(ctx context.Context, componentID string) error

StopComponent stops a single component identified by componentID. Returns an error if the component fails to stop.

type ErrInvalidComponentID

type ErrInvalidComponentID struct {
	// contains filtered or unexported fields
}

ErrInvalidComponentID represents an error when a component ID is invalid.

func (ErrInvalidComponentID) Error

func (e ErrInvalidComponentID) Error() string

type ErrInvalidExecutionMode

type ErrInvalidExecutionMode struct {
	// contains filtered or unexported fields
}

ErrInvalidExecutionMode is an error type representing an invalid execution mode. It is returned when attempting to parse an unrecognized execution mode.

func (ErrInvalidExecutionMode) Error

func (e ErrInvalidExecutionMode) Error() string

type ExecutionMode

type ExecutionMode string

ExecutionMode represents different execution modes for ENVITE. It can be used to specify the behavior when executing ENVITE commands.

const (
	// ExecutionModeStart indicates the start execution mode, which starts all components in the environment,
	// and then exits.
	ExecutionModeStart ExecutionMode = "start"

	// ExecutionModeStop indicates the stop execution mode, which stops all components in the environment,
	// performs cleanup, and then exits.
	ExecutionModeStop ExecutionMode = "stop"

	// ExecutionModeDaemon indicates the daemon execution mode, which starts ENVITE as a daemon and serving a web UI.
	ExecutionModeDaemon ExecutionMode = "daemon"
)

func ParseExecutionMode

func ParseExecutionMode(value string) (ExecutionMode, error)

ParseExecutionMode parses the provided string value into an ExecutionMode. It returns the parsed ExecutionMode or an error if the value is not a valid execution mode.

type GetStatusResponse

type GetStatusResponse struct {
	ID         string                         `json:"id"`
	Components [][]GetStatusResponseComponent `json:"components"`
}

GetStatusResponse defines the structure of the response for a status request. It includes details such as component ID, type, status, additional information, and environment variables.

type GetStatusResponseComponent

type GetStatusResponseComponent struct {
	ID     string          `json:"id"`
	Type   string          `json:"type"`
	Status ComponentStatus `json:"status"`
	Config map[string]any  `json:"config"`
}

GetStatusResponseComponent represents a single component's status within the environment. It provides detailed information about the component, including its ID, type, current status, additional information specific to the component type, and environment variables associated with it.

Fields: - ID: A unique identifier for the component. - Type: The type of the component, indicating its role or function within the environment. - Status: The current status of the component, such as running, stopped, etc. - Config: The component config.

type LogLevel

type LogLevel uint8

LogLevel represents the severity level of a log message.

const (
	// LogLevelTrace represents the trace log level.
	LogLevelTrace LogLevel = iota
	// LogLevelDebug represents the debug log level.
	LogLevelDebug
	// LogLevelInfo represents the info log level.
	LogLevelInfo
	// LogLevelError represents the error log level.
	LogLevelError
	// LogLevelFatal represents the fatal log level.
	LogLevelFatal
)

func (LogLevel) String added in v0.0.3

func (l LogLevel) String() string

String converts a LogLevel value to a string.

type Logger

type Logger func(level LogLevel, message string)

Logger is a function type for logging messages with different log levels.

type Option

type Option func(*Environment)

Option is a function type for configuring the Environment during initialization.

func WithLogger

func WithLogger(logger Logger) Option

WithLogger is an Option function that sets the logger for the Environment.

type Reader

type Reader struct {
	// contains filtered or unexported fields
}

Reader represents a reader for log messages.

func (*Reader) Chan

func (o *Reader) Chan() chan []byte

Chan returns the channel for receiving log messages.

func (*Reader) Close

func (o *Reader) Close() error

Close closes the log message reader.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is an HTTP server, serving UI and API requests to manage the Environment when running in ExecutionModeDaemon.

func NewServer

func NewServer(port string, env *Environment) *Server

NewServer creates a new Server instance for the given Environment.

func (*Server) Close

func (s *Server) Close() error

Close gracefully shuts down the HTTP server.

func (*Server) Start

func (s *Server) Start() error

Start starts the HTTP server.

type Writer

type Writer struct {
	Color AnsiColor
	// contains filtered or unexported fields
}

Writer represents a writer for log messages. Example:

writer.WriteString(writer.Color.Red("warning!"))

this writes a red messages to the console with the text "warning!"

func (*Writer) Write

func (w *Writer) Write(message []byte)

Write writes a log message with the current timestamp.

func (*Writer) WriteString

func (w *Writer) WriteString(message string)

WriteString writes a log message with the current timestamp.

func (*Writer) WriteStringWithTime

func (w *Writer) WriteStringWithTime(t time.Time, message string)

WriteStringWithTime writes a log message with a specified timestamp.

func (*Writer) WriteWithTime

func (w *Writer) WriteWithTime(t time.Time, message []byte)

WriteWithTime writes a log message with a specified timestamp.

Directories

Path Synopsis
cmd
seed

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL