diff --git a/internal/dis/component/docker/compose.go b/internal/dis/component/docker/compose.go index 08eed2c..d7d30cf 100644 --- a/internal/dis/component/docker/compose.go +++ b/internal/dis/component/docker/compose.go @@ -2,55 +2,18 @@ package docker import ( "context" - "path/filepath" - "github.com/compose-spec/compose-go/cli" - "github.com/compose-spec/compose-go/loader" - ctypes "github.com/compose-spec/compose-go/types" "golang.org/x/exp/slices" + "github.com/FAU-CDI/wisski-distillery/pkg/compose" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" ) -// ComposeProject represents a compose project -type ComposeProject = *ctypes.Project - -// LoadComposeProject loads a docker compose project from the given path. -// The returned name indicates the name, as would be found by the 'docker compose' executable. -// If the project could not be found, an appropriate error is returned. -// -// NOTE: This intentionally omits using any kind of api for docker compose. -// This saves a *a lot* of dependencies./ -func (docker *Docker) LoadComposeProject(path string) (project ComposeProject, err error) { - ppath, err := filepath.Abs(path) - if err != nil { - return nil, err - } - - opts, err := cli.NewProjectOptions( - /* configs = */ nil, - cli.WithWorkingDirectory(ppath), - cli.WithDefaultConfigPath, - cli.WithName(loader.NormalizeProjectName(filepath.Base(ppath))), - cli.WithDotEnv, - ) - if err != nil { - return nil, err - } - - proj, err := cli.ProjectFromOptions(opts) - if err != nil { - return nil, err - } - - return proj, nil -} - // Containers loads the compose project at path, connects to the docker daemon, and then lists all containers belonging to the given services. // If services is empty, all containers belonging to any service are returned. func (docker *Docker) Containers(ctx context.Context, path string, services ...string) (containers []types.Container, err error) { - proj, err := docker.LoadComposeProject(path) + proj, err := compose.Open(path) if err != nil { return nil, err } @@ -76,7 +39,7 @@ const ( // // services optionally filters the returned containers by the services they belong to. // If services is empty, all containers are returned, else containers belonging to any of the services included. -func (*Docker) containers(ctx context.Context, project ComposeProject, client DockerClient, all bool, services ...string) ([]types.Container, error) { +func (*Docker) containers(ctx context.Context, project compose.Project, client DockerClient, all bool, services ...string) ([]types.Container, error) { // build filters f := filters.NewArgs( filters.Arg("label", projectLabel+"="+project.Name), diff --git a/internal/dis/component/stack.go b/internal/dis/component/stack.go index 4e9c753..8ad0ac5 100644 --- a/internal/dis/component/stack.go +++ b/internal/dis/component/stack.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" + "github.com/FAU-CDI/wisski-distillery/pkg/compose" "github.com/FAU-CDI/wisski-distillery/pkg/execx" "github.com/FAU-CDI/wisski-distillery/pkg/fsx" "github.com/FAU-CDI/wisski-distillery/pkg/logging" @@ -154,10 +155,13 @@ type StackWithResources struct { // // 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 + Resources fs.FS + + ContextPath string // the 'docker compose' stack context. Can, but does not have to, contain 'docker-compose.yml' + ReadComposeFile func() (io.Reader, error) // read the 'docker-compose.yml' (if not contained in context) + + 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 @@ -190,6 +194,40 @@ 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 logging.ProgressF(progress, ctx, "[install] %s\n", dst) + + // create the file + yml, err := fsx.Create(dst, fsx.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 { + return err + } + } + // configure .env envDest := filepath.Join(is.Dir, ".env") if is.EnvPath != "" && is.EnvContext != nil { @@ -247,5 +285,14 @@ func (is StackWithResources) Install(ctx context.Context, progress io.Writer, co } } + // check that the stack can be loaded + { + logging.ProgressF(progress, ctx, "[checking]") + _, err := compose.Open(is.Dir) + if err != nil { + return err + } + } + return nil } diff --git a/internal/dis/component/web/web-http/docker-compose.yml b/internal/dis/component/web/docker-compose-http.yml similarity index 100% rename from internal/dis/component/web/web-http/docker-compose.yml rename to internal/dis/component/web/docker-compose-http.yml diff --git a/internal/dis/component/web/web-https/docker-compose.yml b/internal/dis/component/web/docker-compose-https.yml similarity index 100% rename from internal/dis/component/web/web-https/docker-compose.yml rename to internal/dis/component/web/docker-compose-https.yml diff --git a/internal/dis/component/web/web.go b/internal/dis/component/web/web.go index ac28139..c3decf4 100644 --- a/internal/dis/component/web/web.go +++ b/internal/dis/component/web/web.go @@ -1,7 +1,9 @@ package web import ( + "bytes" "embed" + "io" "path/filepath" "github.com/FAU-CDI/wisski-distillery/internal/dis/component" @@ -26,46 +28,36 @@ func (*Web) Context(parent component.InstallationContext) component.Installation return parent } -func (web Web) Stack() component.StackWithResources { - if web.Config.HTTP.HTTPSEnabled() { - return web.stackHTTPS() - } else { - return web.stackHTTP() +//go:embed web.env +var webEnv embed.FS + +//go:embed docker-compose-http.yml +var dockerComposeHTTP []byte + +//go:embed docker-compose-https.yml +var dockerComposeHTTPS []byte + +func (web *Web) Stack() component.StackWithResources { + var stack component.StackWithResources + stack.Resources = webEnv + stack.EnvPath = "web.env" + + stack.EnvContext = map[string]string{ + "DOCKER_NETWORK_NAME": web.Config.Docker.Network, + "CERT_EMAIL": web.Config.HTTP.CertbotEmail, } -} - -//go:embed all:web-https -//go:embed web.env -var httpsResources embed.FS - -func (web *Web) stackHTTPS() component.StackWithResources { - return component.MakeStack(web, component.StackWithResources{ - Resources: httpsResources, - ContextPath: "web-https", - EnvPath: "web.env", - - EnvContext: map[string]string{ - "DOCKER_NETWORK_NAME": web.Config.Docker.Network, - "CERT_EMAIL": web.Config.HTTP.CertbotEmail, - }, - TouchFilesPerm: 0600, - TouchFiles: []string{"acme.json"}, - }) -} - -//go:embed all:web-http -//go:embed web.env -var httpResources embed.FS - -func (web *Web) stackHTTP() component.StackWithResources { - return component.MakeStack(web, component.StackWithResources{ - Resources: httpResources, - ContextPath: "web-http", - EnvPath: "web.env", - - EnvContext: map[string]string{ - "DOCKER_NETWORK_NAME": web.Config.Docker.Network, - "CERT_EMAIL": web.Config.HTTP.CertbotEmail, - }, - }) + + if web.Config.HTTP.HTTPSEnabled() { + stack.ReadComposeFile = func() (io.Reader, error) { + return bytes.NewReader(dockerComposeHTTPS), nil + } + stack.TouchFilesPerm = 0600 + stack.TouchFiles = []string{"acme.json"} + } else { + stack.ReadComposeFile = func() (io.Reader, error) { + return bytes.NewReader(dockerComposeHTTP), nil + } + } + + return component.MakeStack(web, stack) } diff --git a/pkg/compose/compose.go b/pkg/compose/compose.go new file mode 100644 index 0000000..4ed76b4 --- /dev/null +++ b/pkg/compose/compose.go @@ -0,0 +1,43 @@ +package compose + +import ( + "path/filepath" + + "github.com/compose-spec/compose-go/cli" + "github.com/compose-spec/compose-go/loader" + "github.com/compose-spec/compose-go/types" +) + +// ComposeProject represents a compose project +type Project = *types.Project + +// Open loads a docker compose project from the given path. +// The returned name indicates the name, as would be found by the 'docker compose' executable. +// If the project could not be found, an appropriate error is returned. +// +// NOTE: This intentionally omits using any kind of api for docker compose. +// This saves a *a lot* of dependencies. +func Open(path string) (project Project, err error) { + ppath, err := filepath.Abs(path) + if err != nil { + return nil, err + } + + opts, err := cli.NewProjectOptions( + /* configs = */ nil, + cli.WithWorkingDirectory(ppath), + cli.WithDefaultConfigPath, + cli.WithName(loader.NormalizeProjectName(filepath.Base(ppath))), + cli.WithDotEnv, + ) + if err != nil { + return nil, err + } + + proj, err := cli.ProjectFromOptions(opts) + if err != nil { + return nil, err + } + + return proj, nil +}