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 ¶
- Variables
- func Materialize(ctx context.Context) error
- func Register(name string, r Resource)
- func SetupDefaultLogging()
- type CmdResource
- type DependencySetter
- type FileResource
- type GroupMembershipResource
- type GroupResource
- type Resource
- type ResourceGraph
- func (rg *ResourceGraph) Materialize(ctx context.Context) error
- func (rg *ResourceGraph) Register(name string, r Resource)
- func (rg *ResourceGraph) RegisterDependency(from Resource, signal Signal, to Resource)
- func (rg *ResourceGraph) SetName(name string)
- func (rg *ResourceGraph) String() string
- func (rg *ResourceGraph) When(resource Resource, signals ...Signal) *DependencySetter
- type ResourceMeta
- type Signal
- type SkippableResource
- type SkippingWrapper
- type UserResource
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // DefaultRegistry is an instance of the ResourceGraph that a number of convenience functions act on. DefaultRegistry = ResourceGraph{} )
Functions ¶
func Materialize ¶
Materialize materializes the resources registered with the DefaultRegistry
Materialize is a shortcut for DefaultRegistry.Materialize. See there for more details
func Register ¶
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 )
type SkippableResource ¶
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 ¶
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
Source Files ¶
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. |