From 17d64826df6cbe14818e7aeba131c9ff7156f577 Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 14 Jul 2023 15:21:18 +0200 Subject: [PATCH] StackWithResources: Update ComposeYML behaviour Previously, there was a function to manually read bytes for a docker-compose.yml. But this proved to be akward at runtime. Instead, this code automatically reads an existing docker-compose.yml, and takes care of marshalling and unmarshalling. --- go.mod | 2 +- go.sum | 4 +- internal/dis/component/binder/binder.go | 60 ++++------------ internal/dis/component/stack.go | 96 ++++++++++++++++--------- internal/dis/component/web/web.go | 22 +++--- 5 files changed, 94 insertions(+), 90 deletions(-) diff --git a/go.mod b/go.mod index 61a252a..9200173 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/pquerna/otp v1.4.0 github.com/rs/zerolog v1.29.1 github.com/tkw1536/goprogram v0.3.5 - github.com/tkw1536/pkglib v0.0.0-20230713130635-2bcbc40ecdd9 + github.com/tkw1536/pkglib v0.0.0-20230714124712-ccf0c3658789 github.com/yuin/goldmark v1.5.4 github.com/yuin/goldmark-meta v1.1.0 golang.org/x/crypto v0.8.0 diff --git a/go.sum b/go.sum index ef1b41b..1e73a1b 100644 --- a/go.sum +++ b/go.sum @@ -114,8 +114,8 @@ github.com/tdewolff/parse v2.3.4+incompatible/go.mod h1:8oBwCsVmUkgHO8M5iCzSIDtp github.com/tdewolff/test v1.0.7 h1:8Vs0142DmPFW/bQeHRP3MV19m1gvndjUb1sn8yy74LM= github.com/tkw1536/goprogram v0.3.5 h1:S0axKo3R/vGa4zhYqYDKAZEPhAfwUSSeMtVwnAu4sNY= github.com/tkw1536/goprogram v0.3.5/go.mod h1:pYr4dMHOSVurbPQ4KTR0ett8XWNISbsRS6zlh9Nsxa8= -github.com/tkw1536/pkglib v0.0.0-20230713130635-2bcbc40ecdd9 h1:l+xh6dn9PW+HNYrIXgwsOileuIhkdxZocOicQfBdB9M= -github.com/tkw1536/pkglib v0.0.0-20230713130635-2bcbc40ecdd9/go.mod h1:0A1B9Cc5+yJXR3eeB14CqD4dFSbEjjWRo5Pr9M3XYuI= +github.com/tkw1536/pkglib v0.0.0-20230714124712-ccf0c3658789 h1:S6pXBfW8SJsN/MZEyhSbziV3F59P4VoddviiE5oPFoU= +github.com/tkw1536/pkglib v0.0.0-20230714124712-ccf0c3658789/go.mod h1:0A1B9Cc5+yJXR3eeB14CqD4dFSbEjjWRo5Pr9M3XYuI= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= diff --git a/internal/dis/component/binder/binder.go b/internal/dis/component/binder/binder.go index c61881e..1c51359 100644 --- a/internal/dis/component/binder/binder.go +++ b/internal/dis/component/binder/binder.go @@ -1,15 +1,12 @@ package binder import ( - "bytes" - "io" + "embed" "path/filepath" "github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/tkw1536/pkglib/yamlx" "gopkg.in/yaml.v3" - - _ "embed" ) type Binder struct { @@ -29,52 +26,25 @@ func (binder *Binder) Context(parent component.InstallationContext) component.In } //go:embed docker-compose.yml -var composeTemplate []byte - -func (binder *Binder) buildYML() ([]byte, error) { - var dockerCompose yaml.Node - if err := yaml.Unmarshal(composeTemplate, &dockerCompose); err != nil { - return nil, err - } - - for dockerCompose.Kind == yaml.DocumentNode { - dockerCompose = *dockerCompose.Content[0] - } - - { - ports := binder.Config.Listen.ComposePorts("8000") - portsNode, err := yamlx.Marshal(ports) - if err != nil { - return nil, err - } - if err := yamlx.Replace(&dockerCompose, *portsNode, "services", "binder", "ports"); err != nil { - return nil, err - } - } - - { - command := binder.Config.HTTP.TCPMuxCommand("0.0.0.0:8000", "http:80", "http:443", "ssh:2222") - commandNode, err := yamlx.Marshal(command) - if err != nil { - return nil, err - } - if err := yamlx.Replace(&dockerCompose, *commandNode, "services", "binder", "command"); err != nil { - return nil, err - } - } - - // do the final marshal - return yaml.Marshal(dockerCompose) -} +var composeTemplate embed.FS func (binder *Binder) Stack() component.StackWithResources { return component.MakeStack(binder, component.StackWithResources{ - ReadComposeFile: func() (io.Reader, error) { - data, err := binder.buildYML() - if err != nil { + ContextPath: ".", + Resources: composeTemplate, + + ComposerYML: func(root *yaml.Node) (*yaml.Node, error) { + ports := binder.Config.Listen.ComposePorts("8000") + if err := yamlx.ReplaceWith(root, ports, "services", "binder", "ports"); err != nil { return nil, err } - return bytes.NewReader(data), nil + + command := binder.Config.HTTP.TCPMuxCommand("0.0.0.0:8000", "http:80", "http:443", "ssh:2222") + if err := yamlx.ReplaceWith(root, command, "services", "binder", "command"); err != nil { + return nil, err + } + + return root, nil }, EnvContext: map[string]string{ diff --git a/internal/dis/component/stack.go b/internal/dis/component/stack.go index 17f8425..4d80b17 100644 --- a/internal/dis/component/stack.go +++ b/internal/dis/component/stack.go @@ -15,6 +15,7 @@ import ( "github.com/pkg/errors" "github.com/tkw1536/pkglib/fsx/umaskfree" "github.com/tkw1536/pkglib/stream" + "gopkg.in/yaml.v3" ) // Stack represents a 'docker compose' stack living in a specific directory @@ -176,12 +177,10 @@ type StackWithResources struct { // These all refer to paths within the Resource filesystem. Resources fs.FS - ContextPath string // the 'docker compose' stack context. Can, but does not have to, contain 'docker-compose.yml' + ContextPath string // the 'docker compose' stack context. May or may not contain 'docker-compose.yml' - // TODO: Make this nicer to replace variables - ReadComposeFile func() (io.Reader, error) // read the 'docker-compose.yml' (if not contained in context) + ComposerYML func(*yaml.Node) (*yaml.Node, error) // update 'docker-compose.yml', if no 'docker-compose.yml' exists, the passed node is nil. - // EnvPath string // the '.env' template, will be installed using [unpack.InstallTemplate]. If empty, use new syntax EnvContext map[string]string // context when instantiating the '.env' template CopyContextFiles []string // Files to copy from the installation context @@ -219,36 +218,12 @@ func (is StackWithResources) Install(ctx context.Context, progress io.Writer, co } } - // write the docker-compose.yml file - if is.ReadComposeFile != nil { - err := (func() error { - // find the file to install! - dst := filepath.Join(is.Dir, "docker-compose.yml") - defer fmt.Fprintf(progress, "[install] %s\n", dst) + // update the docker compose file + if is.ComposerYML != nil { + dst := filepath.Join(is.Dir, "docker-compose.yml") + fmt.Fprintf(progress, "[install] %s\n", dst) - // create the file - yml, err := umaskfree.Create(dst, umaskfree.DefaultFilePerm) - if err != nil { - return err - } - defer yml.Close() - - // open the source file from the context - src, err := is.ReadComposeFile() - if err != nil { - return err - } - if srcc, ok := src.(io.Closer); ok { - defer srcc.Close() - } - - // copy it over! - { - _, err := io.Copy(yml, src) - return err - } - })() - if err != nil { + if err := doComposeFile(dst, is.ComposerYML); err != nil { return err } } @@ -301,7 +276,7 @@ func (is StackWithResources) Install(ctx context.Context, progress io.Writer, co dst := filepath.Join(is.Dir, name) fmt.Fprintf(progress, "[touch] %s\n", dst) - if err := umaskfree.Touch(dst, is.TouchFilesPerm); err != nil { + if err := umaskfree.Touch(dst, umaskfree.DefaultFilePerm); err != nil { return err } } @@ -318,6 +293,59 @@ func (is StackWithResources) Install(ctx context.Context, progress io.Writer, co return nil } +// doComposeFile updates the compose file using the update function. +// +// If the file at path already exists, calls update with the existing data. +// if the file at path does not exist, calls update(nil). +func doComposeFile(path string, update func(node *yaml.Node) (*yaml.Node, error)) error { + var mode fs.FileMode + var node *yaml.Node + + { + stat, err := os.Stat(path) + switch { + case err == nil: + // file exists => use the previous file mode + mode = stat.Mode() + + // read the yaml bytes + bytes, err := os.ReadFile(path) + if err != nil { + return errors.Wrap(err, "unable to read existing file") + } + + // unmarshal it into a node, or bail out! + node = new(yaml.Node) + if err := yaml.Unmarshal(bytes, node); err != nil { + return errors.Wrap(err, "unable to unmarshal existing file") + } + case errors.Is(err, fs.ErrNotExist): + // file does not exist => use default mode + mode = umaskfree.DefaultFilePerm + + // use a nil existing node + node = nil + default: + return err + } + } + + // update the node + node, err := update(node) + if err != nil { + return errors.Wrap(err, "update function failed") + } + + // re-encode the bytes + result, err := yaml.Marshal(node) + if err != nil { + return errors.Wrap(err, "failed to re-marshal") + } + + // write the bytes back! + return umaskfree.WriteFile(path, result, mode) +} + // writeEnvFile writes an environment file func writeEnvFile(path string, perm fs.FileMode, variables map[string]string) error { // create the environment file diff --git a/internal/dis/component/web/web.go b/internal/dis/component/web/web.go index 15c8e71..ffaf0e1 100644 --- a/internal/dis/component/web/web.go +++ b/internal/dis/component/web/web.go @@ -1,11 +1,10 @@ package web import ( - "bytes" - "io" "path/filepath" "github.com/FAU-CDI/wisski-distillery/internal/dis/component" + "gopkg.in/yaml.v3" _ "embed" ) @@ -44,16 +43,23 @@ func (web *Web) Stack() component.StackWithResources { } if web.Config.HTTP.HTTPSEnabled() { - stack.ReadComposeFile = func() (io.Reader, error) { - return bytes.NewReader(dockerComposeHTTPS), nil - } + stack.ComposerYML = readYaml(dockerComposeHTTPS) stack.TouchFilesPerm = 0600 stack.TouchFiles = []string{"acme.json"} } else { - stack.ReadComposeFile = func() (io.Reader, error) { - return bytes.NewReader(dockerComposeHTTP), nil - } + stack.ComposerYML = readYaml(dockerComposeHTTP) } return component.MakeStack(web, stack) } + +func readYaml(bytes []byte) func(*yaml.Node) (*yaml.Node, error) { + return func(n *yaml.Node) (*yaml.Node, error) { + var node yaml.Node + err := yaml.Unmarshal(bytes, &node) + if err != nil { + return nil, err + } + return &node, nil + } +}