envite

package module
v0.0.1 Latest Latest
Warning

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

Go to latest
Published: Dec 19, 2023 License: MIT Imports: 17 Imported by: 0

README

ENVITE

ENVITE Logo

ENVITE helps define and provision environments for development, testing, and continuous integration.

It's designed to allow switching between local development needs and automated environments seamlessly and provide the best tooling to describe, provision, and monitor integrated components.

A demo screen capture can be found here.


Motivation

Any software that interacts with external components requires some solutions. In production environments you need these two components to be able to interact in a safe and secure fashion. Obviously, there are countless solutions for these needs such as cloud-managed products, container orchestration solutions such as Kubernetes, load balancers, service discovery solutions, service mesh products, and so on. ENVITE doesn't aim to help there.

Next, if you want to run non-production automation such as integration testing during a CI/CD pipeline, you're going to need to create an environment similar to production to be able to execute these tests.

Lastly, development processes require a similar environment to function properly.

ENVITE aims to help with the last 2 use cases - development, and non-production automation. It aims to make them completely similar to allow full reproducibility, on the one hand, and the best tooling for development needs, on the other hand.

Alternatives

So why not stick to what you use 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.

Using Kubernetes for production, CI, and development

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?
      • 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.
  • 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.
Using docker-compose for CI and development

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.
Using a remote staging/dev environment

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?
Using testcontainers or a similar library to write container management code for CI and development

This option is quite close to ENVITE. These 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.
Can ENVITE meet my needs?

ENVITE is quite close to testcontainers. It allows you to either write Go, or use YAML files to describe your env. It can be used as a Go library, or as a CLI tool directly without actually writing code.

ENVITE is designed around running seamlessly inside and outside docker containers, to allow simple debugging of components you currently work on, while running all the rest in containers, connecting everything fluently, and providing you with the best tooling to manage and monitor the entire environment.

At this point, you will have to manage ENVITE files/code separately from your production environment, but we do want to add support to read directly from Helm and Kustomize files to allow maintaining only one copy of production env.

One last thing to consider, since ENVITE runs everything locally, you can't run too many components on your machine. As mentioned earlier, if you need 50 components up and running to run your tests, running it all locally is not feasible. You will have to use multiple remote machines for that. If this is your use case, an interesting company that does it well is Raftt and I suggest reading more about what they do.

Local Development

To locally work on the 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.

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.2bc38110.js ui/build/static/js/main.2bc38110.js.LICENSE.txt

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrEmptyEnvID = errors.New("environment ID cannot be empty")
	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 Execute

func Execute(server *Server, executionMode ExecutionMode) error

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{}

func (AnsiColor) Blue

func (a AnsiColor) Blue(s string) string

func (AnsiColor) Cyan

func (a AnsiColor) Cyan(s string) string

func (AnsiColor) Green

func (a AnsiColor) Green(s string) string

func (AnsiColor) Magenta

func (a AnsiColor) Magenta(s string) string

func (AnsiColor) Red

func (a AnsiColor) Red(s string) string

func (AnsiColor) Yellow

func (a AnsiColor) Yellow(s string) string

type Component

type Component interface {
	ID() string
	Type() string
	AttachEnvironment(ctx context.Context, env *Environment, writer *Writer) error
	Prepare(ctx context.Context) error
	Start(ctx context.Context) error
	Stop(ctx context.Context) error
	Cleanup(ctx context.Context) error
	Status(ctx context.Context) (ComponentStatus, error)
	Config() any
	EnvVars() map[string]string
}

type ComponentGraph

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

func NewComponentGraph

func NewComponentGraph() *ComponentGraph

func (*ComponentGraph) AddLayer

func (c *ComponentGraph) AddLayer(layerComponents ...Component) *ComponentGraph

type ComponentStatus

type ComponentStatus string
const (
	ComponentStatusStopped  ComponentStatus = "stopped"
	ComponentStatusFailed   ComponentStatus = "failed"
	ComponentStatusStarting ComponentStatus = "starting"
	ComponentStatusRunning  ComponentStatus = "running"
	ComponentStatusFinished ComponentStatus = "finished"
)

type Environment

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

func NewEnvironment

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

func (*Environment) Apply

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

func (*Environment) Cleanup

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

func (*Environment) Components

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

func (*Environment) Output

func (b *Environment) Output() *Reader

func (*Environment) StartAll

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

func (*Environment) StartComponent

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

func (*Environment) Status

func (*Environment) StopAll

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

func (*Environment) StopComponent

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

type ErrInvalidComponentID

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

func (ErrInvalidComponentID) Error

func (e ErrInvalidComponentID) Error() string

type ErrInvalidExecutionMode

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

func (ErrInvalidExecutionMode) Error

func (e ErrInvalidExecutionMode) Error() string

type ExecutionMode

type ExecutionMode string
const (
	ExecutionModeStart  ExecutionMode = "start"
	ExecutionModeStop   ExecutionMode = "stop"
	ExecutionModeDaemon ExecutionMode = "daemon"
)

func ParseExecutionMode

func ParseExecutionMode(value string) (ExecutionMode, error)

type GetStatusResponse

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

type GetStatusResponseComponent

type GetStatusResponseComponent struct {
	ID      string            `json:"id"`
	Type    string            `json:"type"`
	Status  ComponentStatus   `json:"status"`
	Info    any               `json:"info"`
	EnvVars map[string]string `json:"env_vars"`
}

type LogLevel

type LogLevel uint8
const (
	LogLevelTrace LogLevel = iota
	LogLevelDebug
	LogLevelInfo
	LogLevelError
	LogLevelFatal
)

type Logger

type Logger func(level LogLevel, message string)

type Option

type Option func(*Environment)

func WithLogger

func WithLogger(logger Logger) Option

type Reader

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

func (*Reader) Chan

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

func (*Reader) Close

func (o *Reader) Close() error

type Server

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

func NewServer

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

func (*Server) Close

func (s *Server) Close() error

func (*Server) Start

func (s *Server) Start() error

type Writer

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

func (*Writer) Write

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

func (*Writer) WriteString

func (w *Writer) WriteString(message string)

func (*Writer) WriteStringWithTime

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

func (*Writer) WriteWithTime

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

Directories

Path Synopsis
seed

Jump to

Keyboard shortcuts

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