Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/real-project/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ go 1.25.7

replace github.com/Liphium/magic/v3 => ../../.

replace github.com/Liphium/magic/v3/pkg/databases/postgres-legacy => ../../pkg/databases/postgres-legacy
replace github.com/Liphium/magic/v3/pkg/databases/postgres => ../../pkg/databases/postgres

require (
github.com/Liphium/magic/v3 v3.0.0-00010101000000-000000000000
github.com/Liphium/magic/v3/pkg/databases/postgres-legacy v0.0.0-00010101000000-000000000000
github.com/Liphium/magic/v3/pkg/databases/postgres v0.0.0-00010101000000-000000000000
github.com/gofiber/fiber/v2 v2.52.9
github.com/google/uuid v1.6.0
github.com/stretchr/testify v1.11.1
Expand Down
4 changes: 2 additions & 2 deletions examples/real-project/starter/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (

"github.com/Liphium/magic/v3"
"github.com/Liphium/magic/v3/mconfig"
postgres_legacy "github.com/Liphium/magic/v3/pkg/databases/postgres-legacy"
"github.com/Liphium/magic/v3/pkg/databases/postgres"
"github.com/Liphium/magic/v3/scripting"
)

Expand All @@ -15,7 +15,7 @@ func BuildMagicConfig() magic.Config {
PlanDeployment: func(ctx *mconfig.Context) {

// Create a new driver for PostgreSQL databases
driver := postgres_legacy.NewDriver("postgres:17").
driver := postgres.NewDriver("postgres:18").
// Create a PostgreSQL database for the posts service (the driver supports a builder pattern with this method)
NewDatabase("posts")

Expand Down
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ require (
github.com/charmbracelet/huh v0.8.0
github.com/go-playground/validator/v10 v10.29.0
github.com/gofrs/flock v0.13.0
github.com/lib/pq v1.10.9
github.com/moby/moby/api v1.52.0
github.com/moby/moby/client v0.2.1
github.com/spf13/pflag v1.0.10
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
Expand Down
1 change: 1 addition & 0 deletions go.work
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ go 1.25.7

use (
.
./pkg/databases/postgres
./pkg/databases/postgres-legacy
)
1 change: 0 additions & 1 deletion mrunner/runner_deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (

"github.com/Liphium/magic/v3/mconfig"
"github.com/Liphium/magic/v3/util"
_ "github.com/lib/pq"
"github.com/moby/moby/api/types/mount"
"github.com/moby/moby/client"
)
Expand Down
191 changes: 191 additions & 0 deletions mrunner/services/containers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package mservices

import (
"context"
"fmt"
"log"
"net/netip"
"strings"

"github.com/Liphium/magic/v3/mconfig"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/mount"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/client"
)

// ContainerVolume describes a single named volume that should be mounted into
// the container. The volume name is derived from the container name so it
// survives container re-creation.
type ContainerVolume struct {
// Suffix appended to the container name to form the Docker volume name,
// e.g. "data" -> "<containerName>-data".
NameSuffix string
// Absolute path inside the container where the volume is mounted.
Target string
}

// ManagedContainerOptions holds everything needed to create (or re-create) a
// managed Docker container in a reproducible way.
type ManagedContainerOptions struct {
// Docker image to use, e.g. "postgres:17".
Image string
// Environment variables passed into the container.
Env []string
// Ports to expose. Each entry maps one container port (inside of the container) to one host port (chosen by Magic).
Ports []string
// Named volumes to attach. Existing mounts are reused across re-creations.
Volumes []ContainerVolume
}

// CreateContainer finds and removes any existing container with the
// given name, then creates a fresh one from the provided options.
//
// Existing Docker volumes are always preserved so that data survives a
// container re-creation. Returns the ID of the newly created container.
func CreateContainer(ctx context.Context, log *log.Logger, c *client.Client, a mconfig.ContainerAllocation, opts ManagedContainerOptions) (string, error) {
if opts.Image == "" {
return "", fmt.Errorf("please specify a proper image")
}

existingMounts, err := removeExistingContainer(ctx, log, c, a, opts)
if err != nil {
return "", err
}

mounts, err := buildMounts(a, opts.Volumes, existingMounts)
if err != nil {
return "", err
}

exposedPorts, portBindings, err := buildPortBindings(a, opts.Ports)
if err != nil {
return "", err
}

resp, err := c.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &container.Config{
Image: opts.Image,
Env: opts.Env,
ExposedPorts: exposedPorts,
},
HostConfig: &container.HostConfig{
PortBindings: portBindings,
Mounts: mounts,
},
Name: a.Name,
})
if err != nil {
return "", fmt.Errorf("couldn't create container %q: %s", a.Name, err)
}

return resp.ID, nil
}

// removeExistingContainer looks for an existing container with the allocation's
// name, recovers its mounts, and removes it. Returns a map of volume NameSuffix
// -> mount so the new container can reuse the same volumes.
func removeExistingContainer(ctx context.Context, log *log.Logger, c *client.Client, a mconfig.ContainerAllocation, opts ManagedContainerOptions) (map[string]mount.Mount, error) {
f := make(client.Filters)
f.Add("name", a.Name)
summary, err := c.ContainerList(ctx, client.ContainerListOptions{
Filters: f,
All: true,
})
if err != nil {
return nil, fmt.Errorf("couldn't list containers: %s", err)
}

existingMounts := map[string]mount.Mount{}

for _, ct := range summary.Items {
for _, n := range ct.Names {
if !strings.HasSuffix(n, a.Name) {
continue
}

log.Println("Found existing container, recovering mounts...")
if err := recoverMounts(ctx, c, ct.ID, opts.Volumes, existingMounts); err != nil {
return nil, err
}

log.Println("Removing old container...")
if _, err := c.ContainerRemove(ctx, ct.ID, client.ContainerRemoveOptions{
RemoveVolumes: false,
Force: true,
}); err != nil {
return nil, fmt.Errorf("couldn't remove existing container: %s", err)
}
}
}

return existingMounts, nil
}

// recoverMounts inspects a container and indexes its mounts by the matching
// ContainerVolume.NameSuffix into the provided map.
func recoverMounts(ctx context.Context, c *client.Client, containerID string, volumes []ContainerVolume, out map[string]mount.Mount) error {
resp, err := c.ContainerInspect(ctx, containerID, client.ContainerInspectOptions{})
if err != nil {
return fmt.Errorf("couldn't inspect container: %s", err)
}

for _, m := range resp.Container.HostConfig.Mounts {
for _, vol := range volumes {
if m.Target == vol.Target {
out[vol.NameSuffix] = m
}
}
}

return nil
}

// buildMounts constructs the mount list for the new container. Any volume whose
// target was found in existingMounts is reused as-is; otherwise a fresh named
// volume is created using "<containerName>-<nameSuffix>".
func buildMounts(a mconfig.ContainerAllocation, volumes []ContainerVolume, existingMounts map[string]mount.Mount) ([]mount.Mount, error) {
mounts := make([]mount.Mount, 0, len(volumes))

for _, vol := range volumes {
if existing, ok := existingMounts[vol.NameSuffix]; ok {
mounts = append(mounts, existing)
} else {
mounts = append(mounts, mount.Mount{
Type: mount.TypeVolume,
Source: fmt.Sprintf("%s-%s", a.Name, vol.NameSuffix),
Target: vol.Target,
})
}
}

return mounts, nil
}

// buildPortBindings converts the ports to what Docker actually needs.
func buildPortBindings(a mconfig.ContainerAllocation, ports []string) (network.PortSet, network.PortMap, error) {
exposedPorts := network.PortSet{}
portBindings := network.PortMap{}

// Make sure the amount of ports is correct
if len(a.Ports) != len(ports) {
return nil, nil, fmt.Errorf("expected %d ports, received only %d", len(ports), len(a.Ports))
}

for i, port := range ports {
p, err := network.ParsePort(port)
if err != nil {
return nil, nil, fmt.Errorf("couldn't parse container port %q: %s", port, err)
}

exposedPorts[p] = struct{}{}
portBindings[p] = []network.PortBinding{
{
HostIP: netip.MustParseAddr("127.0.0.1"),
HostPort: fmt.Sprintf("%d", a.Ports[i]),
},
}
}

return exposedPorts, portBindings, nil
}
25 changes: 25 additions & 0 deletions mrunner/services/containers_exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package mservices

import (
"context"
"fmt"

"github.com/moby/moby/client"
)

// Simply execute a command inside of a container.
func ExecuteCommand(ctx context.Context, c *client.Client, id string, cmd []string) (client.ExecInspectResult, error) {
execIDResp, err := c.ExecCreate(ctx, id, client.ExecCreateOptions{
Cmd: cmd,
AttachStdout: true,
AttachStderr: true,
})
if err != nil {
return client.ExecInspectResult{}, fmt.Errorf("couldn't create exec: %s", err)
}
execStartCheck := client.ExecStartOptions{Detach: false, TTY: false}
if _, err := c.ExecStart(ctx, execIDResp.ID, execStartCheck); err != nil {
return client.ExecInspectResult{}, fmt.Errorf("couldn't start exec: %s", err)
}
return c.ExecInspect(ctx, execIDResp.ID, client.ExecInspectOptions{})
}
4 changes: 2 additions & 2 deletions pkg/databases/postgres-legacy/go.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module github.com/Liphium/magic/pkg/databases/postgres_legacy
module github.com/Liphium/magic/v3/pkg/databases/postgres_legacy

go 1.25.7

Expand All @@ -7,7 +7,6 @@ replace github.com/Liphium/magic/v3 => ../../../.
require (
github.com/Liphium/magic/v3 v3.0.0-00010101000000-000000000000
github.com/lib/pq v1.11.2
github.com/moby/moby/api v1.53.0
github.com/moby/moby/client v0.2.2
)

Expand All @@ -23,6 +22,7 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/moby/api v1.53.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
Expand Down
4 changes: 2 additions & 2 deletions pkg/databases/postgres-legacy/postgres_legacy.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ type PostgresDriver struct {

// Create a new PostgreSQL legacy service driver.
//
// It currently supports version PostgreSQL v14-17. Use NewPostgresDriver for v18 and beyond.
// It currently supports version PostgreSQL 14-17, older versions have not been tested. Use the new postgres driver for PostgreSQL 18 and beyond.
//
// This driver will eventually be deprecated and replaced by the one for v18 and above.
// This driver is deprecated and will be removed when PostgreSQL 20 comes out.
func NewDriver(image string) *PostgresDriver {
imageVersion := strings.Split(image, ":")[1]

Expand Down
Loading