rfsb

package module
v0.0.0-...-f2bba62 Latest Latest
Warning

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

Go to latest
Published: Aug 10, 2018 License: MPL-2.0 Imports: 15 Imported by: 0

README

Really Foolish Server Bootstrapping

Because sometimes you don't want to reprovision every machine in the fleet to add a sysctl

The workflow of Ansible meets the feature set of cloud-init plus the ease of deployment of bash script.

Why: How do I deploy applications without tearing my eyes out

Let's compare deployment technologies on the market today:

Name Do you really like it? Is it really wicked?
Kube We're loving it like this We're loving it like that
everything else Doesn't even know what it takes to be a garage MC Can't see any hands in the air

Unfortunately, none of these quite hit the mark of "we're loving it loving it loving it". To hit the triple L, we need to break it down:

L - Loving it

I like my machines. Sometimes I log onto them. When I do, I like stuff to be available. RFSB lets me make things available without introducing overhead.

I add myself as a user.

addLCM := &rfsb.UserResource{
    User:  "lcm",
    UID:   1000,
    GID:   1000,
    Home:  "/home/lcm",
    Shell: "/bin/bash",
}
rfsb.Register("addLCM", addLCM)

I add my key.

addLCMSSHKey := &rfsb.FileResource{
  Path:     "/home/lcm/.ssh/authorized_keys",
  Mode:     0400,
  Contents: `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAL5YH0a+pKd8E8Be97+gN/kn+U71JCapIH8uysrecKB lcm@lcm-mbp`,
  UID:      addLCM.UID,
  GID:      addLCMGroup.GID,
}
rg.When(addLCM).Do("addLCM", addLCMSSHKey)

Now I can log onto my machines.

L - Still loving it.

Sometimes I lose my machines. In these cases I have to get new machines. When I do, I want to install stuff without complications.

I make the machine mine.

$ GOOS=linux go build  -o dist/bootstrap ./example/bootstrap
$ scp dist/bootstrap lcm@schutzenberger.generictestdomain.net:~/bootstrap
$ ssh lcm@schutzenberger.generictestdomain.net ~/bootstrap

The bond between me and my machine is now strong.

L - Loving it more than ever
  • But does this scale?

    • You don't scale
    • Does anything scale?
    • Fuck Puppet
    • Bittorrent
  • I want features

    • Everything is a file

Guiding Principles

RFSB is designed to be simple and self sufficent. RFSB binaries should be able to bootstrap systems where /usr/bin has gone walkabouts.

RFSB gets out of your way. By presenting a very thin abstraction, RFSB hopes you will immediately see the weaknesses in its core abstractions, and stop using it.

All features not contained in the current version of RFSB are misfeatures until I decided I need them.

RFSB has a DAG and feels guilty about it.

Cool.

Check out example/bootstrap/main.go for an example of 100% of the functionality that rfsb supports along with our documentation for more explanations and examples.

Documentation

Overview

Package rfsb provides the primitives to build configuration management systems.

A Basic Example

As a starting point, a very basic usage example:

package main

import (
    "os"

    "github.com/sirupsen/logrus"
    "github.com/lclarkmichalek/rfsb"
)

func main() {
    rfsb.SetupDefaultLogging()

    exampleFile := &rfsb.FileResource{
        Path: "/tmp/example",
        Contents: "bar",
        Mode: 0644,
        UID: uint32(os.Getuid()),
        GID: uint32(os.Getgid()),
    }
    rfsb.Register("exampleFile", exampleFile)

    err := rfsb.Materialize(context.Background())
    if err != nil {
        logrus.Fatalf("materialization failed: %v", err)
	}
}

This is a complete rfsb config management system that defines a resource to be created, and then creates it (materializes it).

Resources

The atomic units of computation in the RFSB model are types implementing the Resource interface. This is a pretty simple interface, basically being 'a thing that can be run (and has a name, and a logger)'. RFSB comes with most of the resources a standard configuration management system would need, but if other resources are required, the interface is simple to understand and implement.

Resources can be composed together via a ResourceGraph:

func defineExampleFiles() Resource {
     exampleFile := &rfsb.FileResource{
         Path: "/tmp/example",
         Contents: "bar",
         Mode: 0644,
         UID: uint32(os.Getuid()),
         GID: uint32(os.Getgid()),
     }
     secondExampleFile := &rfsb.FileResource{
         Path: "/tmp/example2",
         Contents: "boz",
         Mode: 0644,
         UID: uint32(os.Getuid()),
         GID: uint32(os.Getgid()),
     }
     rg := &rfsb.ResourceGraph{}
     rg.Register("example", exampleFile)
     rg.Register("example2", secondExampleFile)
     return rg
 }

Which can be then composed further, building up higher level abstractions:

func provisionBaseFilesystem() Resource {
     rg := &rfsb.ResourceGraph{}
     files := defineExampleFiles()
     rg.Register("exampleFiles", files)
     sshd := &rfsb.FileResource{
         Path: "/etc/ssh/sshd_config",
         Mode: 0600,
         UID: 0,
         GID: 0,
         Contents: `UseDNS no`,
     }
     rg.Register("sshd", sshd)
     return rg
}

Resources (and ResourceGraphs) can also have dependencies between them:

func provisionAdminUsers() Resource {...}

func provision() Resource {
     rg := &rfsb.ResourceGraph{}
     filesystem := provisionBaseFilesystem()
     rg.Register("filesystem", filesystem)
     users := provisionAdminUsers()
     rg.When(filesystem).Do(users)
     return rg
}

The `When` and `Do` methods here set up a Dependency between the filesystem provisioning resource and the admin users provisioning resource. This ensures that admin users will not be provisioned before the filesystem has been set up.

When all of the Resources have been defined, and composed together into a single ResourceGraph, we can Materialize the graph:

func main() {
    root := provision()
    err := root.Materialize(context.Background())
    if err != nil {
        os.Stderr.Write(err.Error())
        os.Exit(1)
    }
}

And there we have it. Everything you need to know about rfsb.

Rationale

Puppet/Chef/Ansible (henceforth referred to collectively as CAP) all suck. For small deployments, the centralised server of CP is annoying, and for larger deployments, A is limiting, P devolves into a mess, and C, well I guess it can work if you devote an entire team to it. RFSB aims to grow gracefully from small to large, and give you the power to run it in any way you like. Let's take a look at some features that RFSB doesn't force you to provide.

A popular feature of many systems is periodic agent runs. RFSB doesn't have any built in support for running periodically, so we'll need to build it ourselves. Let's take a look at what that might look like:

func main() {
    root := provision()
    for range time.NewTicker(time.Minute * 15) {
        err := root.Materialize(context.Background())
        if err != nil {
            os.Stderr.Write(err.Error())
        }
    }
}

Here we've used the radical method of running our Materialize function in a loop. But what you don't see is all the complexity you just avoided. For example, Chef supports both 'interactive' and 'daemon' mode. You can trigger an interactive Chef run via "chefctl -i". I think this communicates with the "chef-client" process, which then actually performs the Chef run, and reports the results back to chefctl. I think. In RFSB, this complexity goes away. If I want to understand how your RFSB job runs, I open its main.go and read. If you don't ever use periodic runs, you don't ever need to think about them.

Another common feature is some kind of fact store. This allows you to see in one place all of the information about all of your servers. Or at least, all of the information that Puppet or Chef figured might be useful. In RFSB, we take the radical approach of letting you use a fact store if you want, or not if you don't want. For example, if you have a facts store with some Go bindings, you might use it to customize how you provision things:

func provision() Resource {
     rg := &rfsb.ResourceGraph{}
     filesystem := provisionBaseFilesystem()
     rg.Register("filesystem", filesystem)

     hostname, _ := os.Hostname()
     role := myFactsStore.GetRoleFromHostname(hostname)
     if role == "database" {
         rg.When(filesystem).Do("database", provisionDatabase())
     } else if role == "frontend" {
         rg.When(filesystem).Do("frontend", provisionFrontend())
     }
     return rg
}

Now, this might be the point where you cry out and say 'but I can't have a big if else statement for all of my roles!'. The reality is that you probably can, and if you can't you should cut down the number of unique configurations. But anyway, assuming you have a good reason, RFSB has ways to handle this. Mostly, it relies on an interesting property, known as 'being just Go'. For example, you could use an interesting abstraction known as the 'map':

var provisionRole := map[string]func() Resource{}{
    "database": provisionDatabase(),
    "frontend": provisionFrontend(),
}

func provision() Resource {
     rg := &rfsb.ResourceGraph{}
     filesystem := provisionBaseFilesystem()
     rg.Register("filesystem", filesystem)

     hostname, _ := os.Hostname()
     role := myFactsStore.GetRoleFromHostname(hostname)
     rg.When(filesystem).Do(role, provisionRole[role]())
     return rg
}

Now you might say "but that's basically just a switch statement in map form". And you'd be correct. But you get the point. You have all the power of Go at your disposal. Go build the abstractions that make sense to you. I'm not going to tell you how to build your software; the person with the context needed to solve your problems is you.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// DefaultRegistry is an instance of the ResourceGraph that a number of convenience functions act on.
	DefaultRegistry = ResourceGraph{}
)

Functions

func Materialize

func Materialize(ctx context.Context) error

Materialize materializes the resources registered with the DefaultRegistry

Materialize is a shortcut for DefaultRegistry.Materialize. See there for more details

func Register

func Register(name string, r Resource)

Register registers the resource on the DefaultRegistry with the passed name

Register is a shortcut for DefaultRegistry.Register. See there for more details

func SetupDefaultLogging

func SetupDefaultLogging()

SetupDefaultLogging sets up a sane logrus logging config. Feel free not to use this; it's just for convenience

Types

type CmdResource

type CmdResource struct {
	ResourceMeta

	Command     string
	Arguments   []string
	CWD         string
	Environment map[string]string
}

CmdResource execs the specified command when materialized. It will not invoke a shell.

For the ability to set UID and GID, see sudo

func (*CmdResource) Materialize

func (cr *CmdResource) Materialize(ctx context.Context) error

Materialize runs the specified command

type DependencySetter

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

DependencySetter is a helper to improve the ergonomics of creating dependencies between resources

func When

func When(source Resource, signals ...Signal) *DependencySetter

When creates a DependencySetter using the DefaultRegistry

When is a shortcut for DefaultRegistry.When. See there for more details

func (*DependencySetter) And

func (ds *DependencySetter) And(source Resource, signals ...Signal) *DependencySetter

And adds an additional resource that must be completed before the Resource passed to Do will be run

func (*DependencySetter) Do

func (ds *DependencySetter) Do(name string, target Resource) *DependencySetter

Do registers the passed Resource as being dependent on the Resources passed to When and And

type FileResource

type FileResource struct {
	ResourceMeta
	Path     string
	Mode     os.FileMode
	UID      uint32
	GID      uint32
	Contents string
}

FileResource ensures the file at the given path has the given content, mode and owner.

It does not create directories. For that, see DirectoryResource

func (*FileResource) Materialize

func (fr *FileResource) Materialize(ctx context.Context) error

Materialize writes the file out and sets the owners correctly

func (*FileResource) ShouldSkip

func (fr *FileResource) ShouldSkip(context.Context) (bool, error)

ShouldSkip stats and reads the file to see if any modifications are required.

type GroupMembershipResource

type GroupMembershipResource struct {
	ResourceMeta
	GID  uint32
	User string
}

GroupMembershipResource ensures that a user belongs to a group

func (*GroupMembershipResource) Materialize

func (gmr *GroupMembershipResource) Materialize(context.Context) error

Materialize adds the user to the group

func (*GroupMembershipResource) ShouldSkip

func (gmr *GroupMembershipResource) ShouldSkip(context.Context) (bool, error)

ShouldSkip tests that the user belongs to the group

type GroupResource

type GroupResource struct {
	ResourceMeta
	Group string
	GID   uint32
}

GroupResource ensures that the given group exists

func (*GroupResource) Materialize

func (gr *GroupResource) Materialize(context.Context) error

Materialize creates the group

func (*GroupResource) ShouldSkip

func (gr *GroupResource) ShouldSkip(context.Context) (bool, error)

ShouldSkip tests that the group exists and has the correct name

type Resource

type Resource interface {
	Materialize(context.Context) error

	// Best to implement these by embedding ResourceMeta
	Name() string
	SetName(string)
	Logger() *logrus.Entry
}

Resource is a resource that can be materialized by rfsb

type ResourceGraph

type ResourceGraph struct {
	ResourceMeta
	// contains filtered or unexported fields
}

ResourceGraph is a container for Resources and dependencies between them.

func (*ResourceGraph) Materialize

func (rg *ResourceGraph) Materialize(ctx context.Context) error

Materialize executes all of the resources in the resource graph. Resources will be materialized in parallel, while not violating constraints introduced by RegisterDependency

func (*ResourceGraph) Register

func (rg *ResourceGraph) Register(name string, r Resource)

Register adds the Resource to the graph.

The Resource will have its Initialize method called with the passed name.

func (*ResourceGraph) RegisterDependency

func (rg *ResourceGraph) RegisterDependency(from Resource, signal Signal, to Resource)

RegisterDependency adds a dependency between two resources. This ensures the second resource (`to`) will not be Materialized before the first resource (`from`) has finished Materializing

func (*ResourceGraph) SetName

func (rg *ResourceGraph) SetName(name string)

SetName sets the name for the resource, but also prepends the resource name to any registered resources, ensuring that the resource hierachy is reflected in the logger naming

func (*ResourceGraph) String

func (rg *ResourceGraph) String() string

func (*ResourceGraph) When

func (rg *ResourceGraph) When(resource Resource, signals ...Signal) *DependencySetter

When returns a new DependencySetter for the ResourceGraph.

When is the main API that should be used to register dependencies. It's most basic use is just simple chaining of dependencies:

Example
rg := &ResourceGraph{}

firstFile := &FileResource{
	Path:     "/tmp/first_file",
	Contents: "hello",
	Mode:     0644,
	UID:      uint32(os.Getuid()),
	GID:      uint32(os.Getgid()),
}
rg.Register("firstFile", firstFile)

secondFile := &FileResource{
	Path:     "/tmp/second_file",
	Contents: "world",
	Mode:     0644,
	UID:      uint32(os.Getuid()),
	GID:      uint32(os.Getgid()),
}
rg.When(firstFile).Do("firstFile", secondFile)
Output:

type ResourceMeta

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

ResourceMeta is struct that should be embedded by all Resource implementers, so as to simplify the accounting side of things

func (*ResourceMeta) Logger

func (rm *ResourceMeta) Logger() *logrus.Entry

Logger returns the Resource's logger

func (*ResourceMeta) Name

func (rm *ResourceMeta) Name() string

Name returns the name of the Resource

func (*ResourceMeta) SetName

func (rm *ResourceMeta) SetName(name string)

SetName is called when the resource is registered with the registry

type Signal

type Signal byte

Signal is a wrapper for a string. Define your own if you feel the need

const (
	// Finished is emitted by ResourceGraph when a Resource finishes being evaluated. Regardless of what the evaluation
	// of the resource entailed, the Finished signal is involved. This includes:
	// 1. The resource's dependencies finished, but did not emit the signals the resource depended on.
	// 2. The resource's dependencies were met, and the resource was skipped
	// 3. The resource's dependencies were met, and the resource was materialized
	Finished Signal = iota
	// Unevaluated is emitted by ResourceGraph when a Resource's dependencies emit the Finished signal, and the resource
	// was dependent on a different signal.
	Unevaluated
	// Evaluated is emitted by ResourceGraph when a resource is Skipped or Materialized
	Evaluated
	// Skipped is emitted by ResourceGraph when a Resource's ShouldSkip function is called, and returns true, causing
	// the Materialization to be skipped. It is not called when a resource is not run due to missing dependencies (see
	// Evaluated)
	Skipped
	// Materialized is emitted by ResourceGraph when a Resource's Materialize function is called.
	Materialized
)

func (Signal) String

func (s Signal) String() string

type SkippableResource

type SkippableResource interface {
	Resource
	ShouldSkip(context.Context) (bool, error)
}

SkippableResource should be implemented by resources that can check to see if they need to be materialized, and skip materialisation if not needed.

type SkippingWrapper

type SkippingWrapper struct {
	Resource
	SkipFunc func(context.Context) (bool, error)
}

SkippingWrapper wraps a resource, allowing the user to provide a custom ShouldSkip function

func (*SkippingWrapper) ShouldSkip

func (sw *SkippingWrapper) ShouldSkip(ctx context.Context) (bool, error)

ShouldSkip calls the provided SkipFunc. If the underlying resource is a SkippableResource, its ShouldSkip will also be called, making the SkipFunc additive.

type UserResource

type UserResource struct {
	ResourceMeta
	User  string
	UID   uint32
	GID   uint32
	Home  string
	Shell string
}

UserResource ensures that the given user exists

func (*UserResource) Materialize

func (ur *UserResource) Materialize(context.Context) error

Materialize creates the user

func (*UserResource) ShouldSkip

func (ur *UserResource) ShouldSkip(context.Context) (bool, error)

ShouldSkip tests that the user exists, and has the correct properties. If it does, the resource is already materialized and will not be rerun

Directories

Path Synopsis
example
bootstrap
This example shows how I provision my pet server.
This example shows how I provision my pet server.
custom_resource
An example showing how to set up a custom resource.
An example showing how to set up a custom resource.
Package systemd implements some resources that make dealing with systemd a little easier.
Package systemd implements some resources that make dealing with systemd a little easier.

Jump to

Keyboard shortcuts

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