Skip to content

Commit

Permalink
Add dcz package. (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
ibrt authored Dec 12, 2024
1 parent b941981 commit 462cdf4
Show file tree
Hide file tree
Showing 4 changed files with 349 additions and 4 deletions.
181 changes: 181 additions & 0 deletions dcz/dcz.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package dcz

import (
"bytes"
"fmt"
"os"
"runtime"
"time"

ct "github.com/compose-spec/compose-go/types"
"github.com/fatih/color"
"github.com/ibrt/golang-utils/errorz"
"github.com/ibrt/golang-utils/memz"
"gopkg.in/yaml.v3"

"github.com/ibrt/golang-dev/shellz"
)

var (
// DefaultRuntimeGOOS allows to inject different values of "runtime.GOOS" for tests.
DefaultRuntimeGOOS = runtime.GOOS
)

// RestoreDefaultRuntimeGOOS restores the default value of DefaultRuntimeGOOS.
func RestoreDefaultRuntimeGOOS() {
DefaultRuntimeGOOS = runtime.GOOS
}

// DockerCompose helps operate a Docker Compose config.
type DockerCompose struct {
projectName string
profiles []string
cfg *ct.Config
}

// NewDockerCompose initializes a new Docker Compose.
func NewDockerCompose(cfg *ct.Config) *DockerCompose {
errorz.Assertf(cfg != nil, "missing config")

return &DockerCompose{
projectName: "",
profiles: nil,
cfg: cfg,
}
}

// WithProjectName returns a clone of the Docker Compose with the given project name set.
func (d *DockerCompose) WithProjectName(projectName string) *DockerCompose {
return &DockerCompose{
projectName: projectName,
profiles: memz.ShallowCopySlice(d.profiles),
cfg: d.cfg,
}
}

// WithProfiles returns a clone of the Docker Compose with the given profiles set.
func (d *DockerCompose) WithProfiles(profiles ...string) *DockerCompose {
return &DockerCompose{
projectName: d.projectName,
profiles: memz.ShallowCopySlice(profiles),
cfg: d.cfg,
}
}

// GetProjectName returns the Docker Compose project name if set.
func (d *DockerCompose) GetProjectName() string {
return d.projectName
}

// GetProfiles returns the Docker Compose profiles if set.
func (d *DockerCompose) GetProfiles() []string {
return d.profiles
}

// GetConfig returns the Docker Compose config.
func (d *DockerCompose) GetConfig() *ct.Config {
return d.cfg
}

// GetMarshaledConfig returns the Docker Compose config marshaled to YAML.
func (d *DockerCompose) GetMarshaledConfig() []byte {
buf, err := yaml.Marshal(d.cfg)
errorz.MaybeMustWrap(err)
return buf
}

// GetUpCommand returns a pre-configured "docker compose up" command.
func (d *DockerCompose) GetUpCommand() *shellz.Command {
return d.GetCommand().
AddParams("up", "--detach", "--build", "--pull", "always", "--force-recreate", "--remove-orphans", "--wait")
}

// GetDownCommand returns a pre-configured "docker compose down" command.
func (d *DockerCompose) GetDownCommand() *shellz.Command {
return d.GetCommand().
AddParams("down", "--volumes", "--rmi", "local", "--remove-orphans", "--timeout", "5")
}

// GetPSCommand returns a pre-configured "docker compose ps" command.
func (d *DockerCompose) GetPSCommand() *shellz.Command {
return d.GetCommand().
AddParams("ps", "--all", "--orphans")
}

// GetCommand returns a pre-configured "docker compose" command.
func (d *DockerCompose) GetCommand() *shellz.Command {
cmd := shellz.NewCommand("docker", "compose")

if d.projectName != "" {
cmd = cmd.AddParams("--project-name", d.projectName)
}

for _, profile := range d.profiles {
cmd = cmd.AddParams("--profile", profile)
}

if color.NoColor || os.Getenv("CI") != "" {
cmd = cmd.AddParams("--ansi", "never", "--progress", "plain")
}

return cmd.
AddParams("-f", "-").
SetIn(bytes.NewReader(d.GetMarshaledConfig()))
}

// NewDockerComposeConfigDeploy is a Docker Compose config helper.
func NewDockerComposeConfigDeploy(memoryLimitMB int64, replicas uint64) *ct.DeployConfig {
dc := &ct.DeployConfig{
RestartPolicy: &ct.RestartPolicy{
Condition: "on-failure",
Delay: memz.Ptr(ct.Duration(10 * time.Second)),
MaxAttempts: memz.Ptr[uint64](3),
},
}

if memoryLimitMB > 0 {
dc.Resources = ct.Resources{
Limits: &ct.Resource{
MemoryBytes: ct.UnitBytes(memoryLimitMB * 1024 * 1024),
},
}
}

if replicas > 1 {
dc.Replicas = memz.Ptr(replicas)
dc.EndpointMode = "dnsrr" // DNS Round-Robing
}

return dc
}

// NewDockerComposeConfigExtraHosts is a Docker Compose config helper.
func NewDockerComposeConfigExtraHosts(extraHostsMaps ...map[string]string) ct.HostsList {
hostsList := ct.HostsList{}

if DefaultRuntimeGOOS == "linux" {
hostsList["host.docker.internal"] = "host-gateway"
}

for _, extraHostsMap := range extraHostsMaps {
for k, v := range extraHostsMap {
hostsList[k] = v
}
}

return hostsList
}

// NewDockerComposeConfigHealthCheckShell is a Docker Compose config helper.
func NewDockerComposeConfigHealthCheckShell(format string, a ...any) *ct.HealthCheckConfig {
return &ct.HealthCheckConfig{
StartPeriod: memz.Ptr(ct.Duration(30 * time.Second)),
Interval: memz.Ptr(ct.Duration(5 * time.Second)),
Timeout: memz.Ptr(ct.Duration(3 * time.Second)),
Retries: memz.Ptr(uint64(3)),
Test: ct.HealthCheckTest{
"CMD-SHELL",
fmt.Sprintf(format, a...),
},
}
}
130 changes: 130 additions & 0 deletions dcz/dcz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package dcz_test

import (
"testing"
"time"

ct "github.com/compose-spec/compose-go/types"
"github.com/ibrt/golang-utils/fixturez"
"github.com/ibrt/golang-utils/memz"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"

"github.com/ibrt/golang-dev/dcz"
)

type DockerSuite struct {
// intentionally empty
}

func TestDockerSuite(t *testing.T) {
fixturez.RunSuite(t, &DockerSuite{})
}

func (*DockerSuite) TestDockerCompose(g *WithT) {
dc := dcz.NewDockerCompose(&ct.Config{Name: "test"})

g.Expect(dc.GetProjectName()).To(BeEmpty())
g.Expect(dc.GetProfiles()).To(BeEmpty())
g.Expect(dc.GetConfig()).To(Equal(&ct.Config{Name: "test"}))
g.Expect(string(dc.GetMarshaledConfig())).To(Equal("name: test\nservices: {}\n"))

g.Expect(dc.GetUpCommand().GetParams()).To(HaveExactElements(
"compose", "--ansi", "never", "--progress", "plain", "-f", "-",
"up", "--detach", "--build", "--pull", "always", "--force-recreate", "--remove-orphans", "--wait"))
g.Eventually(gbytes.BufferReader(dc.GetUpCommand().GetIn())).Should(gbytes.Say("name: test\nservices: {}\n"))

g.Expect(dc.GetDownCommand().GetParams()).To(HaveExactElements(
"compose", "--ansi", "never", "--progress", "plain", "-f", "-",
"down", "--volumes", "--rmi", "local", "--remove-orphans", "--timeout", "5"))
g.Eventually(gbytes.BufferReader(dc.GetDownCommand().GetIn())).Should(gbytes.Say("name: test\nservices: {}\n"))

g.Expect(dc.GetPSCommand().GetParams()).To(HaveExactElements(
"compose", "--ansi", "never", "--progress", "plain", "-f", "-",
"ps", "--all", "--orphans"))
g.Eventually(gbytes.BufferReader(dc.GetPSCommand().GetIn())).Should(gbytes.Say("name: test\nservices: {}\n"))

g.Expect(dc.GetCommand().GetParams()).To(HaveExactElements(
"compose", "--ansi", "never", "--progress", "plain", "-f", "-"))
g.Eventually(gbytes.BufferReader(dc.GetCommand().GetIn())).Should(gbytes.Say("name: test\nservices: {}\n"))

dc = dc.WithProjectName("projectName")
g.Expect(dc.GetProjectName()).To(Equal("projectName"))
g.Expect(dc.GetProfiles()).To(BeEmpty())

dc = dc.WithProfiles("a", "b")
g.Expect(dc.GetProjectName()).To(Equal("projectName"))
g.Expect(dc.GetProfiles()).To(HaveExactElements("a", "b"))

g.Expect(dc.GetCommand().GetParams()).To(HaveExactElements(
"compose", "--project-name", "projectName", "--profile", "a", "--profile", "b",
"--ansi", "never", "--progress", "plain", "-f", "-"))
}

func (*DockerSuite) TestNewDockerComposeConfigDeploy(g *WithT) {
g.Expect(dcz.NewDockerComposeConfigDeploy(-1, 1)).To(Equal(
&ct.DeployConfig{
RestartPolicy: &ct.RestartPolicy{
Condition: "on-failure",
Delay: memz.Ptr(ct.Duration(10 * time.Second)),
MaxAttempts: memz.Ptr[uint64](3),
},
}))

g.Expect(dcz.NewDockerComposeConfigDeploy(512, 2)).To(Equal(
&ct.DeployConfig{
EndpointMode: "dnsrr",
Replicas: memz.Ptr[uint64](2),
Resources: ct.Resources{
Limits: &ct.Resource{
MemoryBytes: ct.UnitBytes(512 * 1024 * 1024),
},
},
RestartPolicy: &ct.RestartPolicy{
Condition: "on-failure",
Delay: memz.Ptr(ct.Duration(10 * time.Second)),
MaxAttempts: memz.Ptr[uint64](3),
},
}))
}

func (*DockerSuite) TestNewDockerComposeConfigExtraHosts(g *WithT) {
defer dcz.RestoreDefaultRuntimeGOOS()

dcz.DefaultRuntimeGOOS = "linux"

g.Expect(dcz.NewDockerComposeConfigExtraHosts(
map[string]string{"k1": "v1", "k2": "v2"},
map[string]string{"k3": "v3"})).
To(Equal(ct.HostsList{
"k1": "v1",
"k2": "v2",
"k3": "v3",
"host.docker.internal": "host-gateway",
}))

dcz.DefaultRuntimeGOOS = "darwin"

g.Expect(dcz.NewDockerComposeConfigExtraHosts(
map[string]string{"k1": "v1", "k2": "v2"},
map[string]string{"k3": "v3"})).
To(Equal(ct.HostsList{
"k1": "v1",
"k2": "v2",
"k3": "v3",
}))
}

func (*DockerSuite) TestNewDockerComposeConfigHealthCheck(g *WithT) {
g.Expect(dcz.NewDockerComposeConfigHealthCheckShell("cat %v", 1)).To(Equal(
&ct.HealthCheckConfig{
StartPeriod: memz.Ptr(ct.Duration(30 * time.Second)),
Interval: memz.Ptr(ct.Duration(5 * time.Second)),
Timeout: memz.Ptr(ct.Duration(3 * time.Second)),
Retries: memz.Ptr(uint64(3)),
Test: ct.HealthCheckTest{
"CMD-SHELL",
"cat 1",
},
}))
}
14 changes: 12 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,31 @@ go 1.23
require (
github.com/alecthomas/kong v1.6.0
github.com/axw/gocov v1.2.1
github.com/compose-spec/compose-go v1.20.2
github.com/fatih/color v1.18.0
github.com/ibrt/golang-lib v0.6.0
github.com/ibrt/golang-utils v0.3.0
github.com/onsi/gomega v1.36.1
github.com/rodaine/table v1.3.0
go.uber.org/mock v0.5.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.5.0 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-shellwords v1.0.12 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect
golang.org/x/net v0.31.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading

0 comments on commit 462cdf4

Please sign in to comment.