tensile

package module
v0.0.0-...-8460a57 Latest Latest
Warning

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

Go to latest
Published: Dec 31, 2023 License: BSD-3-Clause Imports: 10 Imported by: 0

README

tensile

tensile is a yak shaving exercise to write a config management ecosystem a la Puppet, Ansible, Chef etc.pp. in Go.

The existing config management solutions all have hefty pros and cons and require either infrastructure, hacks, wrappers and/or get very complicated the more complex the deployment becomes.

tensile isn't supposed to be a one-size-fits-all solution but rather a library with which it becomes easy to implement requirements - all with the added benefit of Go tooling.

Design

  1. Shape
  2. Node
  3. Queue
  4. Engine
Shape

A shape is an abstract name of a type of Node, e.g. Service for sysV/systemd/etc.pp. services, File for files or directories, etc.pp.

These are used together with a Nodes name to identify collisions.

Node

A Node is an element to manage a resource like a file or to execute a command, hence similar to resources in Puppet or modules in Ansible.

// A file should exist at path /an/ex/ample with the content "Hello,
// world!"
myFile := &nodes.File{
	Target: "/an/ex/ample",
	Content: "Hello, world!",
}
// A directory at path /an should exist.
myDir := &nodes.Dir{
	Target: "/an/ex",
}
Queue

The Queue is the only non-interchangeable part and used to queue and order nodes for execution.

Engine

Engines manage the execution and state of nodes.

Engine can be implemented differently for e.g. parallelisation or for used in clusters.

// The simple engine sequentally realizes all elements.
simple := engines.NewSimple()

// The nodes from the previous example are added.
// The order does not matter - the queue figures out the order as
// needed.
// If the validation of any of the passed elements fails those errors
// will be returned.
if err := simple.Queue.Add(myFile, myDir); err != nil {
        log.Fatal(err)
}

// All that's left is letting the engine run.
if err := simple.Run(context.Background()); err != nil {
        log.Fatal(err)
}

As engines work with interfaces nodes can be anything that satisfies the relevant interfaces - and since elements are written in Go no other tool is needed to manage dependencies.

Compliance

Golang is ideally suited to ensure compliance of e.g. CIS or internal standards of all kinds of systems.

A single library can be written that maintains the status quo of the compliance requirements and either included in the binaries deploying e.g. applications or in a binary that does nothing but ensure compliance.

simple := engines.NewSimple()

// Assuming a module or package tensilecis where Nodes() returns
// a slice of Nodes.
if err := simple.Add(tensilecis.Nodes()...); err != nil {
    log.Fatal(err)
}

// Add other nodes to deploy applications, agents, etc.pp.
if err := simple.Add(otherNodes...); err != nil {
    log.Fatal(err)
}

if err := simple.Run(context.Background()); err != nil {
    log.Fatal(err)
}

These binaries can be built once for the target CPU architecture and run on physical or virtual machines, in the lifecycle of OS images and containers - even on unixoid appliances.

No need to set up AWX or a Puppet Master, no need for dependency management or a separate host to run from.

Cluster-awareness

A flexible approach to engines allows to deploy clustered applications easily.

Cluster members in a tensile cluster-aware engine could authenticate with each other through a pre shared key and exchange certificates for communication - e.g. to exchange secrets for the applications and services to deploy - and even manage the execution of the entire cluster to ensure that e.g. one node is deployed first before other nodes are set up and connected to the initial node.

Examples for this could be glusterfs, kubernetes, vault etc.pp. - any clustered applications that requires synchronization and key exchange during setup.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrCyclicalDependencies = fmt.Errorf("tensile: reached iteration limit, nodes have cyclical dependencies")
)
View Source
var (
	ErrIsCollisionerNotImplemented = fmt.Errorf("nodes do not implement IsCollisioner interface")
)
View Source
var ErrNodeIsNil = errors.New("tensile: node in wrapper is nil")

Functions

func FormatIdentity

func FormatIdentity(shape Shape, identifier string) string

func SetDebugLog

func SetDebugLog()

SetDebugLog is called from tensiles packages during unit tests to enable debug logging

func Template

func Template(facts facts.Facts, input string, writer io.Writer, customData map[string]any) error

func TemplateString

func TemplateString(facts facts.Facts, input string, customData map[string]any) (string, error)

Types

type AfterNoder

type AfterNoder interface {
	// AfterNodes lists nodes after which this node must be executed if they exist.
	AfterNodes() []string
}

type BeforeNoder

type BeforeNoder interface {
	// BeforeNodes lists nodes before which this node must be executed if they exist.
	BeforeNodes() []string
}

type Context

type Context interface {
	Context() context.Context
	Logger() *slog.Logger
	Result(Shape, string) (any, bool, error)
	Facts() facts.Facts
}

TODO context should be initialized by the engine, then passed to node wrappers to fill out with e.g. the correct logger

type Executor

type Executor interface {
	Execute(Context) (any, error)
}

type IsCollisioner

type IsCollisioner interface {
	// IsCollision receives another node and should return an error if they
	// are colliding.
	// See the Package node for an example.
	IsCollision(other Node) error
}

IsCollisioner is used to detect wether two nodes with the same identity cause a collision.

type NeedsExecutioner

type NeedsExecutioner interface {
	NeedsExecution(Context) (bool, error)
}

type Node

type Node interface {
	Shape() Shape
	Identifier() string
}

type NodeGenerator

type NodeGenerator interface {
	Nodes() ([]Node, error)
}

NodeGenerator can return more nodes for a Queue to collect.

This is primarily useful to have a single Noop node that dynamically generates more nodes based on configuration.

type NodeWrapper

type NodeWrapper struct {
	Node Node

	Before, After []string
}

NodeWrapper wraps around nodes to provide some common functionality.

func NodeWrap

func NodeWrap(node Node) NodeWrapper

func (NodeWrapper) AfterNodes

func (nw NodeWrapper) AfterNodes() []string

func (NodeWrapper) BeforeNodes

func (nw NodeWrapper) BeforeNodes() []string

func (NodeWrapper) Execute

func (nw NodeWrapper) Execute(ctx Context) (any, error)

func (NodeWrapper) Identifier

func (nw NodeWrapper) Identifier() string

func (NodeWrapper) Identity

func (nw NodeWrapper) Identity() (Shape, string)

func (NodeWrapper) IsCollision

func (nw NodeWrapper) IsCollision(other NodeWrapper) error

func (NodeWrapper) NeedsExecution

func (nw NodeWrapper) NeedsExecution(ctx Context) (bool, error)

func (NodeWrapper) Shape

func (nw NodeWrapper) Shape() Shape

func (NodeWrapper) String

func (nw NodeWrapper) String() string

func (NodeWrapper) Validate

func (nw NodeWrapper) Validate() error

type Queue

type Queue struct {
	QueueChannelLength int
	// contains filtered or unexported fields
}

func NewQueue

func NewQueue() *Queue

func (*Queue) Add

func (queue *Queue) Add(nodes ...Node) error

func (Queue) Channel

func (queue Queue) Channel(ctx context.Context) (chan NodeWrapper, chan error)

type Shape

type Shape string

Shape is - loosely defined - the type of resource modified by an element. Together with the string returned by a Identitier it defines the identity of an element.

This is meant to prevent clashes when multiple elements modify the same resource.

E.g. consider two elements:

templated := &tensile.Template{
  Path: "/path/to/file",
  ...
}

lineInFile := &tensile.LineInFile{
  Path: "/path/to/target",
}

Both would have the identity path[/path/to/target] and would create an error when adding to a queue.

const (
	Noop    Shape = "noop"
	Path    Shape = "path"
	Package Shape = "package"
	Service Shape = "service"
)

type TContext

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

func NewContext

func NewContext(ctx context.Context, logger *slog.Logger, facts facts.Facts) (*TContext, error)

func (TContext) Context

func (ec TContext) Context() context.Context

func (TContext) Facts

func (ec TContext) Facts() facts.Facts

func (TContext) Logger

func (ec TContext) Logger() *slog.Logger

func (TContext) Result

func (ec TContext) Result(shape Shape, name string) (any, bool, error)

type TemplateData

type TemplateData struct {
	Facts  facts.Facts
	Custom map[string]any
}

type Validator

type Validator interface {
	Validate() error
}

Validator validates an element when adding it to a queue. The error will be returned by the queue if validation fails.

This can be used to e.g. validate that all required options are set or to set internal values.

Directories

Path Synopsis
cmd
examples

Jump to

Keyboard shortcuts

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