mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Merge pull request #2015 from rcourtman/maintainer/20260909T215736Z
Keep Docker inventory compatible with Moby API 1.56
This commit is contained in:
@@ -15,6 +15,23 @@
|
||||
|
||||
## Purpose
|
||||
|
||||
### Docker SDK dependency compatibility
|
||||
|
||||
The Docker consumers use Moby API v1.56.0 and client v0.6.0 together, without
|
||||
changing other direct module versions. API negotiation remains enabled: the
|
||||
new 1.56 ceiling is not a requirement that existing daemons implement 1.56.
|
||||
Container recreation must retain image, environment, persistent binds, restart
|
||||
policy and normalised capabilities without mutating caller-owned capability
|
||||
lists. `internal/dockeragent/moby_compatibility_test.go` exercises real HTTP
|
||||
negotiation and list/create payloads at API 1.44, 1.51 and 1.56; this is synthetic
|
||||
compatibility evidence, not native daemon or installed-agent acceptance.
|
||||
|
||||
Dependency-only changes to either `go.mod` or `go.sum` must invalidate a stale
|
||||
managed backend binary so development checks cannot unknowingly exercise the
|
||||
previous SDK. The manifest-only fixtures in
|
||||
`tests/integration/scripts/managed-local-backend.test.mjs` verify fresh, stale
|
||||
and rebuilt decisions independently for both manifests.
|
||||
|
||||
### Backend preflight resource evidence
|
||||
|
||||
The exact-source worker brackets the backend phase with bounded, read-only
|
||||
|
||||
@@ -15,8 +15,8 @@ require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/moby/moby/api v1.55.0
|
||||
github.com/moby/moby/client v0.5.0
|
||||
github.com/moby/moby/api v1.56.0
|
||||
github.com/moby/moby/client v0.6.0
|
||||
github.com/oklog/ulid/v2 v2.1.1
|
||||
github.com/opencontainers/image-spec v1.1.1
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
|
||||
@@ -140,10 +140,10 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
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.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
|
||||
github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||
github.com/moby/moby/client v0.5.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc=
|
||||
github.com/moby/moby/client v0.5.0/go.mod h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s=
|
||||
github.com/moby/moby/api v1.56.0 h1:GQzua3NA599ASSIICx0iFgiJeO9YkdDARvQsm23ZZuQ=
|
||||
github.com/moby/moby/api v1.56.0/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk=
|
||||
github.com/moby/moby/client v0.6.0 h1:AJjEB21QPbXSXjDsZorFBoDZPhMrfbpaPLgSMAW9Bgs=
|
||||
github.com/moby/moby/client v0.6.0/go.mod h1:OCo00wNRyA3m4lmJ228W3JbyCN4ZNNYjpOXiJydBdcQ=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package dockeragent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
)
|
||||
|
||||
// Exercise the real transport, not dockerClient mocks: dependency upgrades must
|
||||
// negotiate older daemons and preserve recreate payloads at the new API ceiling.
|
||||
func TestMobyNegotiatedContainerContracts(t *testing.T) {
|
||||
for _, version := range []string{"1.44", "1.51", "1.56"} {
|
||||
t.Run(version, func(t *testing.T) {
|
||||
var created container.CreateRequest
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch r.URL.Path {
|
||||
case "/_ping":
|
||||
w.Header().Set("API-Version", version)
|
||||
case "/v" + version + "/containers/json":
|
||||
if r.Method != http.MethodGet || r.URL.Query().Get("all") != "1" {
|
||||
t.Errorf("list request: %s %s", r.Method, r.URL)
|
||||
}
|
||||
_, _ = w.Write([]byte(`[{"Id":"fixture","Names":["/workload"],"State":"running","Image":"example:test"}]`))
|
||||
case "/v" + version + "/containers/create":
|
||||
if r.Method != http.MethodPost || r.URL.Query().Get("name") != "workload" {
|
||||
t.Errorf("create request: %s %s", r.Method, r.URL)
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&created); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_, _ = w.Write([]byte(`{"Id":"replacement","Warnings":[]}`))
|
||||
default:
|
||||
t.Errorf("unexpected request: %s %s", r.Method, r.URL)
|
||||
http.Error(w, "unexpected", http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
cli, err := client.New(client.WithHost(server.URL), client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer cli.Close()
|
||||
listed, err := cli.ContainerList(context.Background(), client.ContainerListOptions{All: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(listed.Items) != 1 || listed.Items[0].ID != "fixture" {
|
||||
t.Fatalf("inventory: %+v", listed)
|
||||
}
|
||||
host := &container.HostConfig{Binds: []string{"persistent:/data"}, CapAdd: []string{"net_admin", "CAP_NET_ADMIN"}, CapDrop: []string{"sys_admin"}, RestartPolicy: container.RestartPolicy{Name: "unless-stopped"}}
|
||||
result, err := cli.ContainerCreate(context.Background(), client.ContainerCreateOptions{Name: "workload", Config: &container.Config{Image: "example:test", Env: []string{"MODE=test"}}, HostConfig: host})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.ID != "replacement" || created.Image != "example:test" || !reflect.DeepEqual(created.Env, []string{"MODE=test"}) || created.HostConfig == nil {
|
||||
t.Fatalf("create result or config lost: %+v %+v", result, created)
|
||||
}
|
||||
if !reflect.DeepEqual(created.HostConfig.Binds, host.Binds) || created.HostConfig.RestartPolicy != host.RestartPolicy || !reflect.DeepEqual(created.HostConfig.CapAdd, []string{"CAP_NET_ADMIN"}) || !reflect.DeepEqual(created.HostConfig.CapDrop, []string{"CAP_SYS_ADMIN"}) {
|
||||
t.Fatalf("host configuration lost: %+v", created.HostConfig)
|
||||
}
|
||||
if !reflect.DeepEqual(host.CapAdd, []string{"net_admin", "CAP_NET_ADMIN"}) || !reflect.DeepEqual(host.CapDrop, []string{"sys_admin"}) {
|
||||
t.Fatalf("client mutated caller-owned recreation config: %+v", host)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -297,6 +297,36 @@ test('shouldBuildManagedLocalBackendBinary skips rebuild when binary is fresh',
|
||||
);
|
||||
});
|
||||
|
||||
// A dependency-only update must not accidentally exercise a cached old SDK.
|
||||
for (const manifest of ['go.mod', 'go.sum']) {
|
||||
test(`shouldBuildManagedLocalBackendBinary rebuilds after ${manifest}-only dependency update`, async (t) => {
|
||||
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'pulse-managed-moby-'));
|
||||
t.after(() => fs.rm(repoRoot, { recursive: true, force: true }));
|
||||
const binaryPath = path.join(repoRoot, 'pulse');
|
||||
const embeddedFrontendDistPath = path.join(repoRoot, 'internal', 'api', 'frontend-modern', 'dist');
|
||||
await fs.mkdir(embeddedFrontendDistPath, { recursive: true });
|
||||
const inputs = {
|
||||
'go.mod': 'module example.com/pulse\nrequire github.com/moby/moby/client v0.6.0\n',
|
||||
'go.sum': 'fixture dependency checksum\n',
|
||||
};
|
||||
const older = new Date('2026-03-12T09:00:00Z');
|
||||
const built = new Date('2026-03-12T10:00:00Z');
|
||||
const updated = new Date('2026-03-12T11:00:00Z');
|
||||
for (const [name, content] of Object.entries(inputs)) {
|
||||
await fs.writeFile(path.join(repoRoot, name), content);
|
||||
await fs.utimes(path.join(repoRoot, name), older, older);
|
||||
}
|
||||
await fs.writeFile(binaryPath, 'fixture binary');
|
||||
await fs.utimes(binaryPath, built, built);
|
||||
const state = { repoRoot, binaryPath, embeddedFrontendDistPath };
|
||||
assert.equal(await shouldBuildManagedLocalBackendBinary(state), false);
|
||||
await fs.utimes(path.join(repoRoot, manifest), updated, updated);
|
||||
assert.equal(await shouldBuildManagedLocalBackendBinary(state), true);
|
||||
await fs.utimes(binaryPath, updated, updated);
|
||||
assert.equal(await shouldBuildManagedLocalBackendBinary(state), false);
|
||||
});
|
||||
}
|
||||
|
||||
test('shouldBuildManagedLocalBackendBinary rebuilds enterprise variant when sibling enterprise source is newer', async () => {
|
||||
const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'pulse-managed-backend-enterprise-'));
|
||||
const pulseRepoRoot = path.join(workspaceRoot, 'pulse');
|
||||
|
||||
Reference in New Issue
Block a user