From 5f00260cbe3253a918e388131829f420015ef927 Mon Sep 17 00:00:00 2001 From: Unbreathable <70802809+Unbreathable@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:32:59 +0100 Subject: [PATCH 1/9] feat: Add PostgreSQL 18 driver + upgrade real project example --- examples/real-project/go.mod | 4 +- examples/real-project/starter/config.go | 4 +- go.work | 1 + mrunner/services/containers.go | 191 ++++++++++++++++++ mrunner/services/containers_exec.go | 25 +++ pkg/databases/postgres-legacy/go.mod | 2 +- .../postgres-legacy/postgres_legacy.go | 4 +- .../postgres_legacy_container.go | 117 ++--------- pkg/databases/postgres/go.mod | 33 +++ pkg/databases/postgres/go.sum | 63 ++++++ pkg/databases/postgres/postgres.go | 123 +++++++++++ pkg/databases/postgres/postgres_container.go | 74 +++++++ pkg/databases/postgres/postgres_instruct.go | 85 ++++++++ 13 files changed, 616 insertions(+), 110 deletions(-) create mode 100644 mrunner/services/containers.go create mode 100644 mrunner/services/containers_exec.go create mode 100644 pkg/databases/postgres/go.mod create mode 100644 pkg/databases/postgres/go.sum create mode 100644 pkg/databases/postgres/postgres.go create mode 100644 pkg/databases/postgres/postgres_container.go create mode 100644 pkg/databases/postgres/postgres_instruct.go diff --git a/examples/real-project/go.mod b/examples/real-project/go.mod index 83090d4..8452bc3 100644 --- a/examples/real-project/go.mod +++ b/examples/real-project/go.mod @@ -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 diff --git a/examples/real-project/starter/config.go b/examples/real-project/starter/config.go index 2cb30dc..1a3a551 100644 --- a/examples/real-project/starter/config.go +++ b/examples/real-project/starter/config.go @@ -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" ) @@ -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") diff --git a/go.work b/go.work index e95b8cc..dc508d6 100644 --- a/go.work +++ b/go.work @@ -2,5 +2,6 @@ go 1.25.7 use ( . + ./pkg/databases/postgres ./pkg/databases/postgres-legacy ) diff --git a/mrunner/services/containers.go b/mrunner/services/containers.go new file mode 100644 index 0000000..8478822 --- /dev/null +++ b/mrunner/services/containers.go @@ -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" -> "-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 "-". +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 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 +} diff --git a/mrunner/services/containers_exec.go b/mrunner/services/containers_exec.go new file mode 100644 index 0000000..b0eba1a --- /dev/null +++ b/mrunner/services/containers_exec.go @@ -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 command for readiness of container: %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 command for readiness of container: %s", err) + } + return c.ExecInspect(ctx, execIDResp.ID, client.ExecInspectOptions{}) +} diff --git a/pkg/databases/postgres-legacy/go.mod b/pkg/databases/postgres-legacy/go.mod index 675c0bd..86f98ff 100644 --- a/pkg/databases/postgres-legacy/go.mod +++ b/pkg/databases/postgres-legacy/go.mod @@ -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 ) @@ -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 diff --git a/pkg/databases/postgres-legacy/postgres_legacy.go b/pkg/databases/postgres-legacy/postgres_legacy.go index e3ab38d..1654df1 100644 --- a/pkg/databases/postgres-legacy/postgres_legacy.go +++ b/pkg/databases/postgres-legacy/postgres_legacy.go @@ -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] diff --git a/pkg/databases/postgres-legacy/postgres_legacy_container.go b/pkg/databases/postgres-legacy/postgres_legacy_container.go index f1abc53..08f5725 100644 --- a/pkg/databases/postgres-legacy/postgres_legacy_container.go +++ b/pkg/databases/postgres-legacy/postgres_legacy_container.go @@ -4,133 +4,44 @@ import ( "context" "database/sql" "fmt" - "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" + mservices "github.com/Liphium/magic/v3/mrunner/services" "github.com/moby/moby/client" ) // Should create a new container for the database or use the existing one (returns container id + error in case one happened) func (pd *PostgresDriver) CreateContainer(ctx context.Context, c *client.Client, a mconfig.ContainerAllocation) (string, error) { - - // Set to default username and password when not set if pd.Image == "" { return "", fmt.Errorf("please specify a proper image") } - // Check if the container already exists - f := make(client.Filters) - f.Add("name", a.Name) - summary, err := c.ContainerList(ctx, client.ContainerListOptions{ - Filters: f, - All: true, - }) - if err != nil { - return "", fmt.Errorf("couldn't list containers: %s", err) - } - containerId := "" - var mounts []mount.Mount = nil - for _, container := range summary.Items { - for _, n := range container.Names { - if strings.HasSuffix(n, a.Name) { - pgLegacyLog.Println("Found existing container...") - containerId = container.ID - - // Inspect the container to get the mounts - resp, err := c.ContainerInspect(ctx, container.ID, client.ContainerInspectOptions{}) - if err != nil { - return "", fmt.Errorf("couldn't inspect container: %s", err) - } - mounts = resp.Container.HostConfig.Mounts - } - } - } - - // Delete the container if it exists - if containerId != "" { - pgLegacyLog.Println("Deleting old container...") - if _, err := c.ContainerRemove(ctx, containerId, client.ContainerRemoveOptions{ - RemoveVolumes: false, - Force: true, - }); err != nil { - return "", fmt.Errorf("couldn't delete database container: %s", err) - } - } - - // Create the port on the postgres container (this is not the port for outside) - port, err := network.ParsePort("5432/tcp") - if err != nil { - return "", fmt.Errorf("couldn't create port for postgres container: %s", err) - } - exposedPorts := network.PortSet{port: struct{}{}} - - // If no existing mounts, create a new volume for PostgreSQL data - if mounts == nil { - volumeName := fmt.Sprintf("%s-data", a.Name) - mounts = []mount.Mount{ - { - Type: mount.TypeVolume, - Source: volumeName, - Target: "/var/lib/postgresql/data", - }, - } - } - - // Create the network config for the container (exposes the container to the host) - networkConf := &container.HostConfig{ - PortBindings: network.PortMap{ - port: []network.PortBinding{{HostIP: netip.MustParseAddr("127.0.0.1"), HostPort: fmt.Sprintf("%d", a.Ports[0])}}, + return mservices.CreateContainer(ctx, pgLegacyLog, c, a, mservices.ManagedContainerOptions{ + Image: pd.Image, + Env: []string{ + fmt.Sprintf("POSTGRES_PASSWORD=%s", PostgresPassword), + fmt.Sprintf("POSTGRES_USER=%s", PostgresUsername), + "POSTGRES_DATABASE=postgres", }, - Mounts: mounts, - } - - // Create the container - resp, err := c.ContainerCreate(ctx, client.ContainerCreateOptions{ - Config: &container.Config{ - Image: pd.Image, - Env: []string{ - fmt.Sprintf("POSTGRES_PASSWORD=%s", PostgresPassword), - fmt.Sprintf("POSTGRES_USER=%s", PostgresUsername), - "POSTGRES_DATABASE=postgres", - }, - ExposedPorts: exposedPorts, + Ports: []string{ + "5432/tcp", + }, + Volumes: []mservices.ContainerVolume{ + {NameSuffix: "data", Target: "/var/lib/postgresql/data"}, }, - HostConfig: networkConf, - Name: a.Name, }) - if err != nil { - return "", fmt.Errorf("couldn't create postgres container: %s", err) - } - - return resp.ID, nil } // Check for postgres health func (pd *PostgresDriver) IsHealthy(ctx context.Context, c *client.Client, container mconfig.ContainerInformation) (bool, error) { readyCommand := "pg_isready -d postgres -U postgres -t 0" cmd := strings.Split(readyCommand, " ") - execConfig := client.ExecCreateOptions{ - Cmd: cmd, - AttachStdout: true, - AttachStderr: true, - } // Try to execute the command - execIDResp, err := c.ExecCreate(ctx, container.ID, execConfig) - if err != nil { - return false, fmt.Errorf("couldn't create command for readiness of container: %s", err) - } - execStartCheck := client.ExecStartOptions{Detach: false, TTY: false} - if _, err := c.ExecStart(ctx, execIDResp.ID, execStartCheck); err != nil { - return false, fmt.Errorf("couldn't start command for readiness of container: %s", err) - } - respInspect, err := c.ExecInspect(ctx, execIDResp.ID, client.ExecInspectOptions{}) + respInspect, err := mservices.ExecuteCommand(ctx, c, container.ID, cmd) if err != nil { - return false, fmt.Errorf("couldn't inspect command for readiness of container: %s", err) + return false, fmt.Errorf("couldn't execute command for readiness of container: %s", err) } if mconfig.VerboseLogging { diff --git a/pkg/databases/postgres/go.mod b/pkg/databases/postgres/go.mod new file mode 100644 index 0000000..85b3417 --- /dev/null +++ b/pkg/databases/postgres/go.mod @@ -0,0 +1,33 @@ +module github.com/Liphium/magic/pkg/databases/postgres + +go 1.25.7 + +replace github.com/Liphium/magic/v3 => ../../../. + +require ( + github.com/Liphium/magic/v3 v3.0.0-00010101000000-000000000000 + github.com/moby/moby/client v0.2.2 +) + +require ( + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + 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 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect + golang.org/x/sys v0.39.0 // indirect +) diff --git a/pkg/databases/postgres/go.sum b/pkg/databases/postgres/go.sum new file mode 100644 index 0000000..947697e --- /dev/null +++ b/pkg/databases/postgres/go.sum @@ -0,0 +1,63 @@ +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +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/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w= +github.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= +github.com/moby/moby/client v0.2.2 h1:Pt4hRMCAIlyjL3cr8M5TrXCwKzguebPAc2do2ur7dEM= +github.com/moby/moby/client v0.2.2/go.mod h1:2EkIPVNCqR05CMIzL1mfA07t0HvVUUOl85pasRz/GmQ= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/pkg/databases/postgres/postgres.go b/pkg/databases/postgres/postgres.go new file mode 100644 index 0000000..6507d67 --- /dev/null +++ b/pkg/databases/postgres/postgres.go @@ -0,0 +1,123 @@ +package postgres + +import ( + "encoding/json" + "fmt" + "log" + "os" + "strings" + + "github.com/Liphium/magic/v3/mconfig" + "github.com/Liphium/magic/v3/util" +) + +// Make sure the driver complies +var _ mconfig.ServiceDriver = &PostgresDriver{} + +// IMPORTANT: Having non-static passwords would make Magic not works as the Container allocation currently does not contain service driver data. +// +// This means that instruction calling would break if we added back password and username changing. +const ( + PostgresUsername = "postgres" + PostgresPassword = "postgres" +) + +var pgLog *log.Logger = log.New(os.Stdout, "postgres ", log.Default().Flags()) + +type PostgresDriver struct { + Image string `json:"image"` + Databases []string `json:"databases"` +} + +// Make sure to register the driver +func init() { + mconfig.RegisterDriver(&PostgresDriver{}) +} + +// Create a new PostgreSQL service driver. +// +// It currently supports PostgreSQL 18. +func NewDriver(image string) *PostgresDriver { + imageVersion := strings.Split(image, ":")[1] + + // Supported (confirmed and tested) major versions for this Postgres driver + var supportedPostgresVersions = []string{"18"} + + // Do a quick check to make sure the image version is actually supported + supported := false + for _, version := range supportedPostgresVersions { + if strings.HasPrefix(imageVersion, fmt.Sprintf("%s.", version)) || imageVersion == version { + supported = true + } + } + if !supported { + pgLog.Fatalln("ERROR: Version", imageVersion, "is currently not supported.") + } + + return &PostgresDriver{ + Image: image, + } +} + +func (pd *PostgresDriver) Load(data string) (mconfig.ServiceDriver, error) { + var driver PostgresDriver + if err := json.Unmarshal([]byte(data), &driver); err != nil { + return nil, err + } + return &driver, nil +} + +func (pd *PostgresDriver) Save() (string, error) { + bytes, err := json.Marshal(pd) + if err != nil { + return "", err + } + return string(bytes), nil +} + +func (pd *PostgresDriver) NewDatabase(name string) *PostgresDriver { + pd.Databases = append(pd.Databases, name) + return pd +} + +// A unique identifier for the database driver. This is appended to the container name to make sure we know it's the container from the driver. +func (pd *PostgresDriver) GetUniqueId() string { + return "postgres" +} + +func (pd *PostgresDriver) GetRequiredPortAmount() int { + return 1 +} + +func (pd *PostgresDriver) GetImage() string { + return pd.Image +} + +// Get the username of the databases in this driver as a EnvironmentValue for your config. +func (pd *PostgresDriver) Username() mconfig.EnvironmentValue { + return mconfig.ValueStatic(PostgresUsername) +} + +// Get the password for the user of the databases in this driver as a EnvironmentValue for your config. +func (pd *PostgresDriver) Password() mconfig.EnvironmentValue { + return mconfig.ValueStatic(PostgresPassword) +} + +// Get hostname of the database container created by the driver as a EnvironmentValue for your config. +func (pd *PostgresDriver) Host(ctx *mconfig.Context) mconfig.EnvironmentValue { + return mconfig.ValueStatic("127.0.0.1") +} + +// Get the port of the database container created by the driver as a EnvironmentValue for your config. +func (pd *PostgresDriver) Port(ctx *mconfig.Context) mconfig.EnvironmentValue { + return mconfig.ValueFunction(func() string { + for id, container := range ctx.Plan().Containers { + if id == pd.GetUniqueId() { + return fmt.Sprintf("%d", ctx.Plan().AllocatedPorts[container.Ports[0]]) + } + } + + util.Log.Fatalln("ERROR: Couldn't find port for Postgres container in plan!") + return "not found" + }) +} diff --git a/pkg/databases/postgres/postgres_container.go b/pkg/databases/postgres/postgres_container.go new file mode 100644 index 0000000..f727975 --- /dev/null +++ b/pkg/databases/postgres/postgres_container.go @@ -0,0 +1,74 @@ +package postgres + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "github.com/Liphium/magic/v3/mconfig" + mservices "github.com/Liphium/magic/v3/mrunner/services" + "github.com/moby/moby/client" +) + +// Should create a new container for the database or use the existing one (returns container id + error in case one happened) +func (pd *PostgresDriver) CreateContainer(ctx context.Context, c *client.Client, a mconfig.ContainerAllocation) (string, error) { + if pd.Image == "" { + return "", fmt.Errorf("please specify a proper image") + } + + return mservices.CreateContainer(ctx, pgLog, c, a, mservices.ManagedContainerOptions{ + Image: pd.Image, + Env: []string{ + fmt.Sprintf("POSTGRES_PASSWORD=%s", PostgresPassword), + fmt.Sprintf("POSTGRES_USER=%s", PostgresUsername), + "POSTGRES_DATABASE=postgres", + }, + Ports: []string{ + "5432/tcp", + }, + Volumes: []mservices.ContainerVolume{ + {NameSuffix: "data", Target: "/var/lib/postgresql"}, + }, + }) +} + +// Check for postgres health +func (pd *PostgresDriver) IsHealthy(ctx context.Context, c *client.Client, container mconfig.ContainerInformation) (bool, error) { + readyCommand := "pg_isready -d postgres -U postgres -t 0" + cmd := strings.Split(readyCommand, " ") + + // Try to execute the command + respInspect, err := mservices.ExecuteCommand(ctx, c, container.ID, cmd) + if err != nil { + return false, fmt.Errorf("couldn't execute command for readiness of container: %s", err) + } + + if mconfig.VerboseLogging { + pgLog.Println("Database health check response code:", respInspect.ExitCode) + } + + return respInspect.ExitCode == 0, nil +} + +// Initialize the internal container with a script (for example) +func (pd *PostgresDriver) Initialize(ctx context.Context, c *client.Client, container mconfig.ContainerInformation) error { + connStr := fmt.Sprintf("host=127.0.0.1 port=%d user=postgres password=postgres dbname=postgres sslmode=disable", container.Ports[0]) + + // Connect to the database + conn, err := sql.Open("postgres", connStr) + if err != nil { + return fmt.Errorf("couldn't connect to postgres: %s", err) + } + defer conn.Close() + + for _, db := range pd.Databases { + pgLog.Println("Creating database", db+"...") + _, err := conn.Exec(fmt.Sprintf("CREATE DATABASE %s", db)) + if err != nil && !strings.Contains(err.Error(), "already exists") { + return fmt.Errorf("couldn't create postgres database: %s", err) + } + } + + return nil +} diff --git a/pkg/databases/postgres/postgres_instruct.go b/pkg/databases/postgres/postgres_instruct.go new file mode 100644 index 0000000..76302cb --- /dev/null +++ b/pkg/databases/postgres/postgres_instruct.go @@ -0,0 +1,85 @@ +package postgres + +import ( + "context" + "database/sql" + "fmt" + + "github.com/Liphium/magic/v3/mconfig" + "github.com/moby/moby/client" +) + +// Handles the instructions for PostgreSQL. +// Supports the following instructions currently: +// - Clear tables +// - Drop tables +func (pd *PostgresDriver) HandleInstruction(ctx context.Context, c *client.Client, container mconfig.ContainerInformation, instruction mconfig.Instruction) error { + switch instruction { + case mconfig.InstructionClearTables: + return pd.ClearTables(container) + case mconfig.InstructionDropTables: + return pd.DropTables(container) + } + return nil +} + +// iterateTablesFn is a function that processes each table in the database +type iterateTablesFn func(tableName string, conn *sql.DB) error + +// iterateTables iterates through all tables in all databases and applies the given function +func (pd *PostgresDriver) iterateTables(container mconfig.ContainerInformation, fn iterateTablesFn) error { + // For all databases, connect and iterate tables + for _, db := range pd.Databases { + if err := func() error { + connStr := fmt.Sprintf("host=127.0.0.1 port=%d user=postgres password=postgres dbname=%s sslmode=disable", container.Ports[0], db) + + // Connect to the database + conn, err := sql.Open("postgres", connStr) + if err != nil { + return fmt.Errorf("couldn't connect to postgres: %v", err) + } + defer conn.Close() + + // Get all of the tables + res, err := conn.Query("SELECT table_name FROM information_schema.tables WHERE table_schema NOT IN ('pg_catalog', 'information_schema')") + if err != nil { + return fmt.Errorf("couldn't get database tables: %v", err) + } + for res.Next() { + var name string + if err := res.Scan(&name); err != nil { + return fmt.Errorf("couldn't get database table name: %v", err) + } + if err := fn(name, conn); err != nil { + return err + } + } + + return nil + }(); err != nil { + return err + } + } + + return nil +} + +// Clear all tables in all databases (keeps table schema alive, just removes the content of all tables) +func (pd *PostgresDriver) ClearTables(container mconfig.ContainerInformation) error { + return pd.iterateTables(container, func(tableName string, conn *sql.DB) error { + if _, err := conn.Exec(fmt.Sprintf("truncate %s CASCADE", tableName)); err != nil { + return fmt.Errorf("couldn't truncate table %s: %v", tableName, err) + } + return nil + }) +} + +// Drop all tables in all databases (actually deletes all of your tables) +func (pd *PostgresDriver) DropTables(container mconfig.ContainerInformation) error { + return pd.iterateTables(container, func(tableName string, conn *sql.DB) error { + if _, err := conn.Exec(fmt.Sprintf("DROP TABLE %s CASCADE", tableName)); err != nil { + return fmt.Errorf("couldn't drop table table %s: %v", tableName, err) + } + return nil + }) +} From a4c4edc84e09db42b2ee408a9843cb1bba45e357 Mon Sep 17 00:00:00 2001 From: Unbreathable <70802809+Unbreathable@users.noreply.github.com> Date: Sat, 7 Mar 2026 10:46:02 +0100 Subject: [PATCH 2/9] fix: Name the default database properly Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/databases/postgres-legacy/postgres_legacy_container.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/databases/postgres-legacy/postgres_legacy_container.go b/pkg/databases/postgres-legacy/postgres_legacy_container.go index 08f5725..00fe1b5 100644 --- a/pkg/databases/postgres-legacy/postgres_legacy_container.go +++ b/pkg/databases/postgres-legacy/postgres_legacy_container.go @@ -22,7 +22,7 @@ func (pd *PostgresDriver) CreateContainer(ctx context.Context, c *client.Client, Env: []string{ fmt.Sprintf("POSTGRES_PASSWORD=%s", PostgresPassword), fmt.Sprintf("POSTGRES_USER=%s", PostgresUsername), - "POSTGRES_DATABASE=postgres", + "POSTGRES_DB=postgres", }, Ports: []string{ "5432/tcp", From 255b08079f8a86663e5e92f0a5dc9f07d801d933 Mon Sep 17 00:00:00 2001 From: Unbreathable <70802809+Unbreathable@users.noreply.github.com> Date: Sat, 7 Mar 2026 10:51:06 +0100 Subject: [PATCH 3/9] fix: Make sure error messages are properly done Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- mrunner/services/containers_exec.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mrunner/services/containers_exec.go b/mrunner/services/containers_exec.go index b0eba1a..ee45d89 100644 --- a/mrunner/services/containers_exec.go +++ b/mrunner/services/containers_exec.go @@ -15,11 +15,11 @@ func ExecuteCommand(ctx context.Context, c *client.Client, id string, cmd []stri AttachStderr: true, }) if err != nil { - return client.ExecInspectResult{}, fmt.Errorf("couldn't create command for readiness of container: %s", err) + 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 command for readiness of container: %s", err) + return client.ExecInspectResult{}, fmt.Errorf("couldn't start exec: %s", err) } return c.ExecInspect(ctx, execIDResp.ID, client.ExecInspectOptions{}) } From a079c4a0d041a594bc8e348964355a5318234de2 Mon Sep 17 00:00:00 2001 From: Unbreathable <70802809+Unbreathable@users.noreply.github.com> Date: Sat, 7 Mar 2026 10:54:21 +0100 Subject: [PATCH 4/9] fix: Make sure database is named properly Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/databases/postgres/postgres_container.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/databases/postgres/postgres_container.go b/pkg/databases/postgres/postgres_container.go index f727975..7b6e31c 100644 --- a/pkg/databases/postgres/postgres_container.go +++ b/pkg/databases/postgres/postgres_container.go @@ -22,7 +22,7 @@ func (pd *PostgresDriver) CreateContainer(ctx context.Context, c *client.Client, Env: []string{ fmt.Sprintf("POSTGRES_PASSWORD=%s", PostgresPassword), fmt.Sprintf("POSTGRES_USER=%s", PostgresUsername), - "POSTGRES_DATABASE=postgres", + "POSTGRES_DB=postgres", }, Ports: []string{ "5432/tcp", From 5b1eee1a10c24ead65cfdbb3509f3ec36c71020d Mon Sep 17 00:00:00 2001 From: Unbreathable <70802809+Unbreathable@users.noreply.github.com> Date: Sat, 7 Mar 2026 11:00:24 +0100 Subject: [PATCH 5/9] fix: typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/databases/postgres/postgres_instruct.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/databases/postgres/postgres_instruct.go b/pkg/databases/postgres/postgres_instruct.go index 76302cb..e1b1cd5 100644 --- a/pkg/databases/postgres/postgres_instruct.go +++ b/pkg/databases/postgres/postgres_instruct.go @@ -78,7 +78,7 @@ func (pd *PostgresDriver) ClearTables(container mconfig.ContainerInformation) er func (pd *PostgresDriver) DropTables(container mconfig.ContainerInformation) error { return pd.iterateTables(container, func(tableName string, conn *sql.DB) error { if _, err := conn.Exec(fmt.Sprintf("DROP TABLE %s CASCADE", tableName)); err != nil { - return fmt.Errorf("couldn't drop table table %s: %v", tableName, err) + return fmt.Errorf("couldn't drop table %s: %v", tableName, err) } return nil }) From 7e1ccf9a7b34fa5d6994b8350724e1abd651fd5d Mon Sep 17 00:00:00 2001 From: Unbreathable <70802809+Unbreathable@users.noreply.github.com> Date: Sat, 7 Mar 2026 10:48:21 +0100 Subject: [PATCH 6/9] chore: Make sure the path in the go.mod is correct --- pkg/databases/postgres-legacy/go.mod | 2 +- pkg/databases/postgres/go.mod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/databases/postgres-legacy/go.mod b/pkg/databases/postgres-legacy/go.mod index 86f98ff..62cd168 100644 --- a/pkg/databases/postgres-legacy/go.mod +++ b/pkg/databases/postgres-legacy/go.mod @@ -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 diff --git a/pkg/databases/postgres/go.mod b/pkg/databases/postgres/go.mod index 85b3417..938fdb8 100644 --- a/pkg/databases/postgres/go.mod +++ b/pkg/databases/postgres/go.mod @@ -1,4 +1,4 @@ -module github.com/Liphium/magic/pkg/databases/postgres +module github.com/Liphium/magic/v3/pkg/databases/postgres go 1.25.7 From f10f8863256f7e704f919aa9be84a2900b2d7f1e Mon Sep 17 00:00:00 2001 From: Unbreathable <70802809+Unbreathable@users.noreply.github.com> Date: Sat, 7 Mar 2026 10:59:55 +0100 Subject: [PATCH 7/9] fix: Make sure tables are properly truncated / dropped --- pkg/databases/postgres-legacy/postgres_legacy_instruct.go | 6 +++--- pkg/databases/postgres/postgres_instruct.go | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pkg/databases/postgres-legacy/postgres_legacy_instruct.go b/pkg/databases/postgres-legacy/postgres_legacy_instruct.go index 3e9db9e..380c86a 100644 --- a/pkg/databases/postgres-legacy/postgres_legacy_instruct.go +++ b/pkg/databases/postgres-legacy/postgres_legacy_instruct.go @@ -67,7 +67,7 @@ func (pd *PostgresDriver) iterateTables(container mconfig.ContainerInformation, // Clear all tables in all databases (keeps table schema alive, just removes the content of all tables) func (pd *PostgresDriver) ClearTables(container mconfig.ContainerInformation) error { return pd.iterateTables(container, func(tableName string, conn *sql.DB) error { - if _, err := conn.Exec(fmt.Sprintf("truncate %s CASCADE", tableName)); err != nil { + if _, err := conn.Exec(fmt.Sprintf("truncate \"%s\" CASCADE", tableName)); err != nil { return fmt.Errorf("couldn't truncate table %s: %v", tableName, err) } return nil @@ -77,8 +77,8 @@ func (pd *PostgresDriver) ClearTables(container mconfig.ContainerInformation) er // Drop all tables in all databases (actually deletes all of your tables) func (pd *PostgresDriver) DropTables(container mconfig.ContainerInformation) error { return pd.iterateTables(container, func(tableName string, conn *sql.DB) error { - if _, err := conn.Exec(fmt.Sprintf("DROP TABLE %s CASCADE", tableName)); err != nil { - return fmt.Errorf("couldn't drop table table %s: %v", tableName, err) + if _, err := conn.Exec(fmt.Sprintf("DROP TABLE \"%s\" CASCADE", tableName)); err != nil { + return fmt.Errorf("couldn't drop table %s: %v", tableName, err) } return nil }) diff --git a/pkg/databases/postgres/postgres_instruct.go b/pkg/databases/postgres/postgres_instruct.go index e1b1cd5..fe24e16 100644 --- a/pkg/databases/postgres/postgres_instruct.go +++ b/pkg/databases/postgres/postgres_instruct.go @@ -45,6 +45,7 @@ func (pd *PostgresDriver) iterateTables(container mconfig.ContainerInformation, if err != nil { return fmt.Errorf("couldn't get database tables: %v", err) } + defer res.Close() for res.Next() { var name string if err := res.Scan(&name); err != nil { @@ -67,7 +68,7 @@ func (pd *PostgresDriver) iterateTables(container mconfig.ContainerInformation, // Clear all tables in all databases (keeps table schema alive, just removes the content of all tables) func (pd *PostgresDriver) ClearTables(container mconfig.ContainerInformation) error { return pd.iterateTables(container, func(tableName string, conn *sql.DB) error { - if _, err := conn.Exec(fmt.Sprintf("truncate %s CASCADE", tableName)); err != nil { + if _, err := conn.Exec(fmt.Sprintf("truncate \"%s\" CASCADE", tableName)); err != nil { return fmt.Errorf("couldn't truncate table %s: %v", tableName, err) } return nil @@ -77,7 +78,7 @@ func (pd *PostgresDriver) ClearTables(container mconfig.ContainerInformation) er // Drop all tables in all databases (actually deletes all of your tables) func (pd *PostgresDriver) DropTables(container mconfig.ContainerInformation) error { return pd.iterateTables(container, func(tableName string, conn *sql.DB) error { - if _, err := conn.Exec(fmt.Sprintf("DROP TABLE %s CASCADE", tableName)); err != nil { + if _, err := conn.Exec(fmt.Sprintf("DROP TABLE \"%s\" CASCADE", tableName)); err != nil { return fmt.Errorf("couldn't drop table %s: %v", tableName, err) } return nil From 1d6abc88df395a1a061beffab0f527db73ce7ce8 Mon Sep 17 00:00:00 2001 From: Unbreathable <70802809+Unbreathable@users.noreply.github.com> Date: Sat, 7 Mar 2026 11:03:27 +0100 Subject: [PATCH 8/9] fix: Only use postgres connection in drivers --- go.mod | 1 - go.sum | 2 -- mrunner/runner_deploy.go | 1 - pkg/databases/postgres-legacy/postgres_legacy_instruct.go | 1 + pkg/databases/postgres/go.mod | 1 + pkg/databases/postgres/go.sum | 2 ++ pkg/databases/postgres/postgres_instruct.go | 1 + 7 files changed, 5 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index a824fb6..3bf68b7 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index b909f38..71e5cdd 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/mrunner/runner_deploy.go b/mrunner/runner_deploy.go index f3cc0ed..2dfe4d8 100644 --- a/mrunner/runner_deploy.go +++ b/mrunner/runner_deploy.go @@ -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" ) diff --git a/pkg/databases/postgres-legacy/postgres_legacy_instruct.go b/pkg/databases/postgres-legacy/postgres_legacy_instruct.go index 380c86a..4d6cf83 100644 --- a/pkg/databases/postgres-legacy/postgres_legacy_instruct.go +++ b/pkg/databases/postgres-legacy/postgres_legacy_instruct.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/Liphium/magic/v3/mconfig" + _ "github.com/lib/pq" "github.com/moby/moby/client" ) diff --git a/pkg/databases/postgres/go.mod b/pkg/databases/postgres/go.mod index 938fdb8..4ebb8ac 100644 --- a/pkg/databases/postgres/go.mod +++ b/pkg/databases/postgres/go.mod @@ -6,6 +6,7 @@ replace github.com/Liphium/magic/v3 => ../../../. require ( github.com/Liphium/magic/v3 v3.0.0-00010101000000-000000000000 + github.com/lib/pq v1.10.9 github.com/moby/moby/client v0.2.2 ) diff --git a/pkg/databases/postgres/go.sum b/pkg/databases/postgres/go.sum index 947697e..f0e3a84 100644 --- a/pkg/databases/postgres/go.sum +++ b/pkg/databases/postgres/go.sum @@ -25,6 +25,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w= diff --git a/pkg/databases/postgres/postgres_instruct.go b/pkg/databases/postgres/postgres_instruct.go index fe24e16..dc1a4a1 100644 --- a/pkg/databases/postgres/postgres_instruct.go +++ b/pkg/databases/postgres/postgres_instruct.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/Liphium/magic/v3/mconfig" + _ "github.com/lib/pq" "github.com/moby/moby/client" ) From 269227ad6d87aa3d8cb3aaaae8e21d1b357397ff Mon Sep 17 00:00:00 2001 From: Unbreathable <70802809+Unbreathable@users.noreply.github.com> Date: Sat, 7 Mar 2026 11:07:25 +0100 Subject: [PATCH 9/9] fix: typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- mrunner/services/containers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mrunner/services/containers.go b/mrunner/services/containers.go index 8478822..1438c8a 100644 --- a/mrunner/services/containers.go +++ b/mrunner/services/containers.go @@ -162,7 +162,7 @@ func buildMounts(a mconfig.ContainerAllocation, volumes []ContainerVolume, exist return mounts, nil } -// buildPortBindings converts the the ports to what Docker actually needs. +// 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{}