Merge internal/stack => component

This commit is contained in:
Tom Wiesing 2022-09-11 16:03:13 +02:00
parent 7b2f79bea1
commit 91a088a56a
No known key found for this signature in database
16 changed files with 40 additions and 72 deletions

View file

@ -3,7 +3,6 @@ package component
import (
"github.com/FAU-CDI/wisski-distillery/internal/config"
"github.com/FAU-CDI/wisski-distillery/internal/stack"
)
// Component represents a logical subsystem of the distillery.
@ -30,11 +29,11 @@ type Component interface {
// Stack can be used to gain access to the "docker compose" stack.
//
// This should internally call
Stack() stack.Installable
Stack() Installable
// Context returns a new InstallationContext to be used during installation from the command line.
// Typically this should just pass through the parent, but might perform other tasks.
Context(parent stack.InstallationContext) stack.InstallationContext
Context(parent InstallationContext) InstallationContext
}
// ComponentBase implements base functionality for a component
@ -50,12 +49,12 @@ func (cb ComponentBase) Path() string {
}
// Context passes through the parent context
func (ComponentBase) Context(parent stack.InstallationContext) stack.InstallationContext {
func (ComponentBase) Context(parent InstallationContext) InstallationContext {
return parent
}
// MakeStack registers the Installable as a stack
func (cb ComponentBase) MakeStack(stack stack.Installable) stack.Installable {
func (cb ComponentBase) MakeStack(stack Installable) Installable {
stack.Dir = cb.Dir
return stack
}

View file

@ -5,7 +5,6 @@ import (
"github.com/FAU-CDI/wisski-distillery/component"
"github.com/FAU-CDI/wisski-distillery/core"
"github.com/FAU-CDI/wisski-distillery/internal/stack"
)
type Dis struct {
@ -23,8 +22,8 @@ func (dis Dis) Name() string {
//go:embed all:stack dis.env
var resources embed.FS
func (dis Dis) Stack() stack.Installable {
return dis.ComponentBase.MakeStack(stack.Installable{
func (dis Dis) Stack() component.Installable {
return dis.ComponentBase.MakeStack(component.Installable{
Resources: resources,
ContextPath: "stack",
EnvPath: "dis.env",
@ -44,8 +43,8 @@ func (dis Dis) Stack() stack.Installable {
})
}
func (dis Dis) Context(parent stack.InstallationContext) stack.InstallationContext {
return stack.InstallationContext{
func (dis Dis) Context(parent component.InstallationContext) component.InstallationContext {
return component.InstallationContext{
core.Executable: dis.Executable,
}
}

118
component/installable.go Normal file
View file

@ -0,0 +1,118 @@
package component
import (
"io/fs"
"os"
"path/filepath"
"github.com/FAU-CDI/wisski-distillery/internal/fsx"
"github.com/FAU-CDI/wisski-distillery/internal/unpack"
"github.com/pkg/errors"
"github.com/tkw1536/goprogram/stream"
)
// TODO: Move this package into components
// Installable represents a Stack that can be automatically installed from a set of resources
// See the [Install] method.
type Installable struct {
Stack
// Installable enabled installing several resources from a (potentially embedded) filesystem.
//
// The Resources holds these, with appropriate resources specified below.
// These all refer to paths within the Resource filesystem.
Resources fs.FS
ContextPath string // the 'docker compose' stack context, containing e.g. 'docker-compose.yml'.
EnvPath string // the '.env' template, will be installed using [unpack.InstallTemplate].
EnvContext map[string]string // context when instantiating the '.env' template
CopyContextFiles []string // Files to copy from the installation context
MakeDirsPerm fs.FileMode // permission for diretories, defaults to fs.ModeDir
MakeDirs []string // directories to ensure that exist
TouchFiles []string // Files to 'touch', i.e. ensure that exist; guaranteed to be run after MakeDirs
}
// InstallationContext is a context to install data in
type InstallationContext map[string]string
// Install installs or updates this stack into the directory specified by stack.Stack().
//
// Installation is non-interactive, but will provide debugging output onto io.
// InstallationContext
func (is Installable) Install(io stream.IOStream, context InstallationContext) error {
if is.ContextPath != "" {
// setup the base files
if err := unpack.InstallResource(
is.Dir,
is.ContextPath,
is.Resources,
func(dst, src string) {
io.Printf("[install] %s\n", dst)
},
); err != nil {
return err
}
}
// configure .env
envDest := filepath.Join(is.Dir, ".env")
if is.EnvPath != "" && is.EnvContext != nil {
io.Printf("[config] %s\n", envDest)
if err := unpack.InstallTemplate(
envDest,
is.EnvContext,
is.EnvPath,
is.Resources,
); err != nil {
return err
}
}
// make sure that certain dirs exist
for _, name := range is.MakeDirs {
// find the destination!
dst := filepath.Join(is.Dir, name)
io.Printf("[make] %s\n", dst)
if is.MakeDirsPerm == fs.FileMode(0) {
is.MakeDirsPerm = fs.ModeDir
}
if err := os.MkdirAll(dst, is.MakeDirsPerm); err != nil {
return err
}
}
// copy files from the context!
for _, name := range is.CopyContextFiles {
// find the source!
src, ok := context[name]
if !ok {
return errors.Errorf("Missing file from context: %s", src)
}
// find the destination!
dst := filepath.Join(is.Dir, name)
// copy over file from context
io.Printf("[copy] %s (from %s)\n", dst, src)
if err := fsx.CopyFile(dst, src); err != nil {
return errors.Wrapf(err, "Unable to copy file %s", src)
}
}
// make sure that certain files exist
for _, name := range is.TouchFiles {
// find the destination!
dst := filepath.Join(is.Dir, name)
io.Printf("[touch] %s\n", dst)
if err := fsx.Touch(dst); err != nil {
return err
}
}
return nil
}

View file

@ -11,7 +11,6 @@ import (
"github.com/FAU-CDI/wdresolve/resolvers"
"github.com/FAU-CDI/wisski-distillery/component"
"github.com/FAU-CDI/wisski-distillery/core"
"github.com/FAU-CDI/wisski-distillery/internal/stack"
"github.com/tkw1536/goprogram/stream"
)
@ -35,8 +34,8 @@ func (resolver Resolver) ConfigPath() string {
//go:embed all:stack resolver.env
var resources embed.FS
func (resolver Resolver) Stack() stack.Installable {
return resolver.ComponentBase.MakeStack(stack.Installable{
func (resolver Resolver) Stack() component.Installable {
return resolver.ComponentBase.MakeStack(component.Installable{
Resources: resources,
ContextPath: "stack",
EnvPath: "resolver.env",
@ -58,8 +57,8 @@ func (resolver Resolver) Stack() stack.Installable {
})
}
func (resolver Resolver) Context(parent stack.InstallationContext) stack.InstallationContext {
return stack.InstallationContext{
func (resolver Resolver) Context(parent component.InstallationContext) component.InstallationContext {
return component.InstallationContext{
core.Executable: resolver.Executable,
}
}

View file

@ -4,7 +4,6 @@ import (
"embed"
"github.com/FAU-CDI/wisski-distillery/component"
"github.com/FAU-CDI/wisski-distillery/internal/stack"
)
type Self struct {
@ -19,14 +18,14 @@ func (Self) Name() string {
//go:embed self.env
var resources embed.FS
func (self Self) Stack() stack.Installable {
func (self Self) Stack() component.Installable {
// TODO: Move me into config!
TARGET := "https://github.com/FAU-CDI/wisski-distillery"
if self.Config.SelfRedirect != nil { // TODO: move to config!
TARGET = self.Config.SelfRedirect.String()
}
return self.ComponentBase.MakeStack(stack.Installable{
return self.ComponentBase.MakeStack(component.Installable{
Resources: resources,
ContextPath: "stack",

View file

@ -7,7 +7,6 @@ import (
"time"
"github.com/FAU-CDI/wisski-distillery/component"
"github.com/FAU-CDI/wisski-distillery/internal/stack"
)
type SQL struct {
@ -26,8 +25,8 @@ func (SQL) Name() string {
//go:embed all:stack
var resources embed.FS
func (ssh SQL) Stack() stack.Installable {
return ssh.ComponentBase.MakeStack(stack.Installable{
func (ssh SQL) Stack() component.Installable {
return ssh.ComponentBase.MakeStack(component.Installable{
Resources: resources,
ContextPath: "stack",

View file

@ -4,7 +4,6 @@ import (
"embed"
"github.com/FAU-CDI/wisski-distillery/component"
"github.com/FAU-CDI/wisski-distillery/internal/stack"
)
type SSH struct {
@ -18,8 +17,8 @@ func (SSH) Name() string {
//go:embed all:stack
var resources embed.FS
func (ssh SSH) Stack() stack.Installable {
return ssh.ComponentBase.MakeStack(stack.Installable{
func (ssh SSH) Stack() component.Installable {
return ssh.ComponentBase.MakeStack(component.Installable{
Resources: resources,
ContextPath: "stack",
})

150
component/stack.go Normal file
View file

@ -0,0 +1,150 @@
// Package stack implements a docker compose stack
package component
import (
"errors"
"github.com/FAU-CDI/wisski-distillery/internal/execx"
"github.com/tkw1536/goprogram/stream"
)
// Stack represents a 'docker compose' stack living in a specific directory
//
// NOTE(twiesing): In the current implementation this requires a 'docker' executable on the system.
// This executable must be capable of the 'docker compose' command.
// In the future the idea is to replace this with a native docker compose client.
type Stack struct {
Dir string // Directory this Stack is located in
DockerExecutable string // Path to the native docker executable to use
}
var errStackUpdatePull = errors.New("Stack.Update: Pull returned non-zero exit code")
var errStackUpdateBuild = errors.New("Stack.Update: Build returned non-zero exit code")
// Update pulls, builds, and then optionally starts this stack.
// This does not have a direct 'docker compose' shell equivalent.
//
// See also Up.
func (ds Stack) Update(io stream.IOStream, start bool) error {
{
code, err := ds.compose(io, "pull")
if err != nil {
return err
}
if code != 0 {
return errStackUpdatePull
}
}
{
code, err := ds.compose(io, "build", "--pull")
if err != nil {
return err
}
if code != 0 {
return errStackUpdateBuild
}
}
if start {
return ds.Up(io)
}
return nil
}
var errStackUp = errors.New("Stack.Up: Up returned non-zero exit code")
// Up creates and starts the containers in this Stack.
// It is equivalent to 'docker compose up --remove-orphans --detach' on the shell.
func (ds Stack) Up(io stream.IOStream) error {
code, err := ds.compose(io, "up", "--remove-orphans", "--detach")
if err != nil {
return err
}
if code != 0 {
return errStackUp
}
return nil
}
// Exec executes an executable in the provided running service.
// It is equivalent to 'docker compose exec $service $executable $args...'.
//
// It returns the exit code of the process.
func (ds Stack) Exec(io stream.IOStream, service, executable string, args ...string) (int, error) {
compose := []string{"exec"}
if io.StdinIsATerminal() {
compose = append(compose, "-ti")
}
compose = append(compose, service)
compose = append(compose, executable)
compose = append(compose, args...)
return ds.compose(io, compose...)
}
// Run executes the provided service with the given executable.
// It is equivalent to 'docker compose run [--rm] $service $executable $args...'.
//
// It returns the exit code of the process.
func (ds Stack) Run(io stream.IOStream, autoRemove bool, service, command string, args ...string) (int, error) {
compose := []string{"run"}
if autoRemove {
compose = append(compose, "--rm")
}
if !io.StdinIsATerminal() {
compose = append(compose, "-T")
}
compose = append(compose, service, command)
compose = append(compose, args...)
code, err := ds.compose(io, compose...)
if err != nil {
return execx.ExecCommandError, nil
}
return code, nil
}
var errStackRestart = errors.New("Stack.Restart: Restart returned non-zero exit code")
// Restart restarts all containers in this Stack.
// It is equivalent to 'docker compose restart' on the shell.
func (ds Stack) Restart(io stream.IOStream) error {
code, err := ds.compose(io, "restart")
if err != nil {
return err
}
if code != 0 {
return errStackRestart
}
return nil
}
var errStackDown = errors.New("Stack.Down: Down returned non-zero exit code")
// Down stops and removes all containers in this Stack.
// It is equivalent to 'docker compose down -v' on the shell.
func (ds Stack) Down(io stream.IOStream) error {
code, err := ds.compose(io, "down", "-v")
if err != nil {
return err
}
if code != 0 {
return errStackDown
}
return nil
}
// compose executes a 'docker compose' command on this stack.
//
// NOTE(twiesing): Check if this can be replaced by an internal call to libcompose.
// But probably not.
func (ds Stack) compose(io stream.IOStream, args ...string) (int, error) {
if ds.DockerExecutable == "" {
var err error
ds.DockerExecutable, err = execx.LookPathAbs("docker")
if err != nil {
return execx.ExecCommandError, err
}
}
return execx.Exec(io, ds.Dir, ds.DockerExecutable, append([]string{"compose"}, args...)...), nil
}

View file

@ -8,7 +8,6 @@ import (
"time"
"github.com/FAU-CDI/wisski-distillery/component"
"github.com/FAU-CDI/wisski-distillery/internal/stack"
)
type Triplestore struct {
@ -27,8 +26,8 @@ func (Triplestore) Name() string {
//go:embed all:stack
var resources embed.FS
func (ts Triplestore) Stack() stack.Installable {
return ts.ComponentBase.MakeStack(stack.Installable{
func (ts Triplestore) Stack() component.Installable {
return ts.ComponentBase.MakeStack(component.Installable{
Resources: resources,
ContextPath: "stack",

View file

@ -4,7 +4,6 @@ import (
"embed"
"github.com/FAU-CDI/wisski-distillery/component"
"github.com/FAU-CDI/wisski-distillery/internal/stack"
)
// Web implements the web component
@ -20,13 +19,13 @@ func (Web) Name() string {
//go:embed web.env
var resources embed.FS
func (web Web) Stack() stack.Installable {
func (web Web) Stack() component.Installable {
HTTPS_METHOD := "nohttp"
if web.Config.HTTPSEnabled() {
HTTPS_METHOD = "redirect"
}
return web.MakeStack(stack.Installable{
return web.MakeStack(component.Installable{
Resources: resources,
ContextPath: "stack",
EnvPath: "web.env",