mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
refactor: api.Container to use the detailed ContainerJSON struct instead of Container (Summary)
This commit is contained in:
@@ -70,13 +70,20 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, ctr := range svc.Containers {
|
for _, ctr := range svc.Containers {
|
||||||
createdAt := time.Unix(ctr.Container.Created, 0)
|
createdAt, err := time.Parse(time.RFC3339Nano, ctr.Container.Created)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse created time: %w", err)
|
||||||
|
}
|
||||||
created := units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
|
created := units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
|
||||||
|
|
||||||
machine := machinesNamesByID[ctr.MachineID]
|
machine := machinesNamesByID[ctr.MachineID]
|
||||||
if machine == "" {
|
if machine == "" {
|
||||||
machine = ctr.MachineID
|
machine = ctr.MachineID
|
||||||
}
|
}
|
||||||
|
state, err := ctr.Container.HumanState()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get human state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
_, err = fmt.Fprintf(
|
_, err = fmt.Fprintf(
|
||||||
tw,
|
tw,
|
||||||
@@ -84,7 +91,7 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
|
|||||||
stringid.TruncateID(ctr.Container.ID),
|
stringid.TruncateID(ctr.Container.ID),
|
||||||
ctr.Container.Image,
|
ctr.Container.Image,
|
||||||
created,
|
created,
|
||||||
ctr.Container.Status,
|
state,
|
||||||
machine,
|
machine,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ require (
|
|||||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
|
github.com/OneOfOne/xxhash v1.2.8 // indirect
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||||
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 // indirect
|
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 // indirect
|
||||||
github.com/armon/go-metrics v0.4.1 // indirect
|
github.com/armon/go-metrics v0.4.1 // indirect
|
||||||
@@ -79,6 +80,7 @@ require (
|
|||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.0 // indirect
|
github.com/charmbracelet/x/term v0.2.0 // indirect
|
||||||
github.com/chzyer/readline v1.5.1 // indirect
|
github.com/chzyer/readline v1.5.1 // indirect
|
||||||
|
github.com/cloudflare/cfssl v1.6.4 // indirect
|
||||||
github.com/compose-spec/compose-go/v2 v2.4.5 // indirect
|
github.com/compose-spec/compose-go/v2 v2.4.5 // indirect
|
||||||
github.com/containerd/console v1.0.4 // indirect
|
github.com/containerd/console v1.0.4 // indirect
|
||||||
github.com/containerd/containerd v1.7.24 // indirect
|
github.com/containerd/containerd v1.7.24 // indirect
|
||||||
|
|||||||
@@ -45,8 +45,9 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
|
|||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/Microsoft/hcsshim v0.12.5 h1:bpTInLlDy/nDRWFVcefDZZ1+U8tS+rz3MxjKgu9boo0=
|
github.com/Microsoft/hcsshim v0.12.5 h1:bpTInLlDy/nDRWFVcefDZZ1+U8tS+rz3MxjKgu9boo0=
|
||||||
github.com/Microsoft/hcsshim v0.12.5/go.mod h1:tIUGego4G1EN5Hb6KC90aDYiUI2dqLSTTOCjVNpOgZ8=
|
github.com/Microsoft/hcsshim v0.12.5/go.mod h1:tIUGego4G1EN5Hb6KC90aDYiUI2dqLSTTOCjVNpOgZ8=
|
||||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
|
||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||||
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
||||||
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
|
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
|
||||||
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
||||||
@@ -166,8 +167,9 @@ github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38
|
|||||||
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ=
|
|
||||||
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
|
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
|
||||||
|
github.com/cloudflare/cfssl v1.6.4 h1:NMOvfrEjFfC63K3SGXgAnFdsgkmiq4kATme5BfcqrO8=
|
||||||
|
github.com/cloudflare/cfssl v1.6.4/go.mod h1:8b3CQMxfWPAeom3zBnGJ6sd+G1NkL5TXqmDXacb+1J0=
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||||
@@ -591,6 +593,8 @@ github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8c
|
|||||||
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc=
|
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc=
|
||||||
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/jmoiron/sqlx v1.3.3 h1:j82X0bf7oQ27XeqxicSZsTU5suPwKElg3oyxNn43iTk=
|
||||||
|
github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
|
||||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
@@ -1058,6 +1062,8 @@ github.com/warpfork/go-testmark v0.12.1 h1:rMgCpJfwy1sJ50x0M0NgyphxYYPMOODIJHhsX
|
|||||||
github.com/warpfork/go-testmark v0.12.1/go.mod h1:kHwy7wfvGSPh1rQJYKayD4AbtNaeyZdcGi9tNJTaa5Y=
|
github.com/warpfork/go-testmark v0.12.1/go.mod h1:kHwy7wfvGSPh1rQJYKayD4AbtNaeyZdcGi9tNJTaa5Y=
|
||||||
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
|
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
|
||||||
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
|
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
|
||||||
|
github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b h1:FsyNrX12e5BkplJq7wKOLk0+C6LZ+KGXvuEcKUYm5ss=
|
||||||
|
github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE=
|
||||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
@@ -1070,6 +1076,10 @@ github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCR
|
|||||||
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||||
|
github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc h1:zkGwegkOW709y0oiAraH/3D8njopUR/pARHv4tZZ6pw=
|
||||||
|
github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc/go.mod h1:FM4U1E3NzlNMRnSUTU3P1UdukWhYGifqEsjk9fn7BCk=
|
||||||
|
github.com/zmap/zlint/v3 v3.1.0 h1:WjVytZo79m/L1+/Mlphl09WBob6YTGljN5IGWZFpAv0=
|
||||||
|
github.com/zmap/zlint/v3 v3.1.0/go.mod h1:L7t8s3sEKkb0A2BxGy1IWrxt1ZATa1R4QfJZaQOD3zU=
|
||||||
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
|
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
|
||||||
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
|
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
|
||||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||||
|
|||||||
+63
-43
@@ -3,8 +3,9 @@ package api
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"regexp"
|
"github.com/docker/go-units"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -13,42 +14,30 @@ const (
|
|||||||
LabelServiceName = "uncloud.service.name"
|
LabelServiceName = "uncloud.service.name"
|
||||||
LabelServiceMode = "uncloud.service.mode"
|
LabelServiceMode = "uncloud.service.mode"
|
||||||
LabelServicePorts = "uncloud.service.ports"
|
LabelServicePorts = "uncloud.service.ports"
|
||||||
|
|
||||||
StateCreated = "created"
|
|
||||||
StateDead = "dead"
|
|
||||||
StateExited = "exited"
|
|
||||||
StatePaused = "paused"
|
|
||||||
StateRemoving = "removing"
|
|
||||||
StateRestarting = "restarting"
|
|
||||||
StateRunning = "running"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Container struct {
|
type Container struct {
|
||||||
types.Container
|
types.ContainerJSON
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Container) Name() string {
|
|
||||||
return c.Names[0][1:] // Remove leading slash.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceID returns the ID of the service this container belongs to.
|
// ServiceID returns the ID of the service this container belongs to.
|
||||||
func (c *Container) ServiceID() string {
|
func (c *Container) ServiceID() string {
|
||||||
return c.Labels[LabelServiceID]
|
return c.Config.Labels[LabelServiceID]
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceName returns the name of the service this container belongs to.
|
// ServiceName returns the name of the service this container belongs to.
|
||||||
func (c *Container) ServiceName() string {
|
func (c *Container) ServiceName() string {
|
||||||
return c.Labels[LabelServiceName]
|
return c.Config.Labels[LabelServiceName]
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceMode returns the replication mode of the service this container belongs to.
|
// ServiceMode returns the replication mode of the service this container belongs to.
|
||||||
func (c *Container) ServiceMode() string {
|
func (c *Container) ServiceMode() string {
|
||||||
return c.Labels[LabelServiceMode]
|
return c.Config.Labels[LabelServiceMode]
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServicePorts returns the ports this container publishes as part of its service.
|
// ServicePorts returns the ports this container publishes as part of its service.
|
||||||
func (c *Container) ServicePorts() ([]PortSpec, error) {
|
func (c *Container) ServicePorts() ([]PortSpec, error) {
|
||||||
encoded, ok := c.Labels[LabelServicePorts]
|
encoded, ok := c.Config.Labels[LabelServicePorts]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -75,41 +64,72 @@ func (c *Container) ServiceSpec() ServiceSpec {
|
|||||||
return ServiceSpec{}
|
return ServiceSpec{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// runningStatusRegex matches the status string of a running container.
|
// Healthy determines if the container is running and healthy.
|
||||||
// - "Up 3 minutes (healthy)" -> groups: ["Up 3 minutes (healthy)", "healthy"]
|
|
||||||
// - "Up 5 seconds" -> groups: ["Up 5 seconds", ""]
|
|
||||||
// - "Up 2 hours (unhealthy)" -> groups: ["Up 2 hours (unhealthy)", "unhealthy"]
|
|
||||||
// - "Up 1 minute (health: starting)" -> groups: ["Up 1 minute (health: starting)", "health: starting"]
|
|
||||||
// - "Restarting (0) 5 seconds ago" -> no match
|
|
||||||
// See https://github.com/moby/moby/blob/c130ce1f5d1e38b98a97044a39557de43bc0d58f/container/state.go#L77-L90
|
|
||||||
// for more details on how the status string for a running container is formatted.
|
|
||||||
var runningStatusRegex = regexp.MustCompile(`^Up [^(]+(?:\(([^)]+)\))?$`)
|
|
||||||
|
|
||||||
// Healthy determines if the container is running and healthy based on its status string.
|
|
||||||
// A running container with no health check configured is considered healthy.
|
// A running container with no health check configured is considered healthy.
|
||||||
func (c *Container) Healthy() bool {
|
func (c *Container) Healthy() bool {
|
||||||
if c.State != StateRunning {
|
if !c.State.Running || c.State.Paused || c.State.Restarting {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
matches := runningStatusRegex.FindStringSubmatch(c.Status)
|
// If there's no health status (no health check configured), container is considered healthy.
|
||||||
// Not "Up" or invalid format.
|
if c.State.Health == nil {
|
||||||
if matches == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's no health status (no health check configured so no parentheses), container is considered healthy.
|
|
||||||
if matches[1] == "" {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the health status in parentheses is "healthy", the container is considered healthy.
|
return c.State.Health.Status == types.Healthy
|
||||||
return matches[1] == types.Healthy
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stopped determines if the container is stopped and doesn't try to restart.
|
// HumanState returns a human-readable description of the container's state. Based on the Docker implementation:
|
||||||
func (c *Container) Stopped() bool {
|
// https://github.com/moby/moby/blob/b343d235a0a1f30c8f05b1d651238e72158dc25d/container/state.go#L79-L113
|
||||||
return c.State == StateCreated || c.State == StateDead || c.State == StateExited
|
func (c *Container) HumanState() (string, error) {
|
||||||
|
startedAt, err := time.Parse(time.RFC3339Nano, c.State.StartedAt)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("parse started time: %w", err)
|
||||||
|
}
|
||||||
|
finishedAt, err := time.Parse(time.RFC3339Nano, c.State.FinishedAt)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("parse finished time: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.State.Running {
|
||||||
|
if c.State.Paused {
|
||||||
|
return fmt.Sprintf("Up %s (Paused)", units.HumanDuration(time.Now().UTC().Sub(startedAt))), nil
|
||||||
|
}
|
||||||
|
if c.State.Restarting {
|
||||||
|
return fmt.Sprintf("Restarting (%d) %s ago",
|
||||||
|
c.State.ExitCode, units.HumanDuration(time.Now().UTC().Sub(finishedAt))), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.State.Health != nil {
|
||||||
|
status := c.State.Health.Status
|
||||||
|
if status == types.Starting {
|
||||||
|
status = "health: " + status
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("Up %s (%s)", units.HumanDuration(time.Now().UTC().Sub(startedAt)), status), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("Up %s", units.HumanDuration(time.Now().UTC().Sub(startedAt))), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.State.Status == "removing" {
|
||||||
|
return "Removal In Progress", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.State.Dead {
|
||||||
|
return "Dead", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if startedAt.IsZero() {
|
||||||
|
return "Created", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if finishedAt.IsZero() {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("Exited (%d) %s ago",
|
||||||
|
c.State.ExitCode, units.HumanDuration(time.Now().UTC().Sub(finishedAt))), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConflictingServicePorts returns a list of service ports that conflict with the given ports.
|
// ConflictingServicePorts returns a list of service ports that conflict with the given ports.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
@@ -13,81 +14,111 @@ func TestContainer_Healthy(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("exited", func(t *testing.T) {
|
t.Run("exited", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
c := &Container{Container: types.Container{
|
c := &Container{ContainerJSON: types.ContainerJSON{
|
||||||
State: "exited",
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
Status: "Exited (0) 2 minutes ago",
|
State: &types.ContainerState{
|
||||||
|
Running: false,
|
||||||
|
Dead: false,
|
||||||
|
ExitCode: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
assert.False(t, c.Healthy())
|
assert.False(t, c.Healthy())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("running with no health check", func(t *testing.T) {
|
t.Run("running with no health check", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
c := &Container{Container: types.Container{
|
c := &Container{ContainerJSON: types.ContainerJSON{
|
||||||
State: "running",
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
Status: "Up 5 minutes",
|
State: &types.ContainerState{
|
||||||
|
Running: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
assert.True(t, c.Healthy())
|
assert.True(t, c.Healthy())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("running and healthy", func(t *testing.T) {
|
t.Run("running and healthy", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
c := &Container{Container: types.Container{
|
c := &Container{ContainerJSON: types.ContainerJSON{
|
||||||
State: "running",
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
Status: "Up 3 minutes (healthy)",
|
State: &types.ContainerState{
|
||||||
|
Running: true,
|
||||||
|
Health: &types.Health{
|
||||||
|
Status: types.Healthy,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
assert.True(t, c.Healthy())
|
assert.True(t, c.Healthy())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("running but unhealthy", func(t *testing.T) {
|
t.Run("running but unhealthy", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
c := &Container{Container: types.Container{
|
c := &Container{ContainerJSON: types.ContainerJSON{
|
||||||
State: "running",
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
Status: "Up 2 hours (unhealthy)",
|
State: &types.ContainerState{
|
||||||
|
Running: true,
|
||||||
|
Health: &types.Health{
|
||||||
|
Status: types.Unhealthy,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
assert.False(t, c.Healthy())
|
assert.False(t, c.Healthy())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("running with health starting", func(t *testing.T) {
|
t.Run("running with health starting", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
c := &Container{Container: types.Container{
|
c := &Container{ContainerJSON: types.ContainerJSON{
|
||||||
State: "running",
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
Status: "Up 1 minute (health: starting)",
|
State: &types.ContainerState{
|
||||||
|
Running: true,
|
||||||
|
Health: &types.Health{
|
||||||
|
Status: "starting",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
assert.False(t, c.Healthy())
|
assert.False(t, c.Healthy())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("invalid up format no time", func(t *testing.T) {
|
t.Run("dead", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
c := &Container{Container: types.Container{
|
c := &Container{ContainerJSON: types.ContainerJSON{
|
||||||
State: "running",
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
Status: "Up",
|
State: &types.ContainerState{
|
||||||
}}
|
Dead: true,
|
||||||
assert.False(t, c.Healthy())
|
Running: false,
|
||||||
})
|
},
|
||||||
|
},
|
||||||
t.Run("invalid up format empty parentheses", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
c := &Container{Container: types.Container{
|
|
||||||
State: "running",
|
|
||||||
Status: "Up 5 minutes ()",
|
|
||||||
}}
|
|
||||||
assert.False(t, c.Healthy())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("malformed status", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
c := &Container{Container: types.Container{
|
|
||||||
State: "running",
|
|
||||||
Status: "Invalid status",
|
|
||||||
}}
|
}}
|
||||||
assert.False(t, c.Healthy())
|
assert.False(t, c.Healthy())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("restarting", func(t *testing.T) {
|
t.Run("restarting", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
c := &Container{Container: types.Container{
|
c := &Container{ContainerJSON: types.ContainerJSON{
|
||||||
State: "running",
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
Status: "Restarting (0) 5 seconds ago",
|
State: &types.ContainerState{
|
||||||
|
Restarting: true,
|
||||||
|
Running: true,
|
||||||
|
ExitCode: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
assert.False(t, c.Healthy())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("paused", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
c := &Container{ContainerJSON: types.ContainerJSON{
|
||||||
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
|
State: &types.ContainerState{
|
||||||
|
Paused: true,
|
||||||
|
Running: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
assert.False(t, c.Healthy())
|
assert.False(t, c.Healthy())
|
||||||
})
|
})
|
||||||
@@ -248,15 +279,15 @@ func TestContainer_ConflictingServicePorts(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
container := &Container{
|
ctr := &Container{ContainerJSON: types.ContainerJSON{
|
||||||
Container: types.Container{
|
Config: &container.Config{
|
||||||
Labels: map[string]string{
|
Labels: map[string]string{
|
||||||
LabelServicePorts: tt.containerPorts,
|
LabelServicePorts: tt.containerPorts,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}}
|
||||||
|
|
||||||
got, err := container.ConflictingServicePorts(tt.checkPorts)
|
got, err := ctr.ConflictingServicePorts(tt.checkPorts)
|
||||||
if tt.wantErr {
|
if tt.wantErr {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ func (cli *Client) InspectContainer(ctx context.Context, serviceID, containerID
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range svc.Containers {
|
for _, c := range svc.Containers {
|
||||||
if c.Container.ID == containerID || c.Container.Name() == containerID {
|
if c.Container.ID == containerID || c.Container.Name == containerID {
|
||||||
ctr = c
|
ctr = c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,7 +258,7 @@ func (cli *Client) StartContainer(ctx context.Context, serviceID, containerID st
|
|||||||
ctx = proxyToMachine(ctx, machine.Machine)
|
ctx = proxyToMachine(ctx, machine.Machine)
|
||||||
|
|
||||||
pw := progress.ContextWriter(ctx)
|
pw := progress.ContextWriter(ctx)
|
||||||
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.Name(), machine.Machine.Name)
|
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.Name, machine.Machine.Name)
|
||||||
|
|
||||||
pw.Event(progress.StartingEvent(eventID))
|
pw.Event(progress.StartingEvent(eventID))
|
||||||
if err = cli.Docker.StartContainer(ctx, ctr.Container.ID, container.StartOptions{}); err != nil {
|
if err = cli.Docker.StartContainer(ctx, ctr.Container.ID, container.StartOptions{}); err != nil {
|
||||||
@@ -285,7 +285,7 @@ func (cli *Client) StopContainer(
|
|||||||
ctx = proxyToMachine(ctx, machine.Machine)
|
ctx = proxyToMachine(ctx, machine.Machine)
|
||||||
|
|
||||||
pw := progress.ContextWriter(ctx)
|
pw := progress.ContextWriter(ctx)
|
||||||
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.Name(), machine.Machine.Name)
|
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.Name, machine.Machine.Name)
|
||||||
|
|
||||||
pw.Event(progress.StoppingEvent(eventID))
|
pw.Event(progress.StoppingEvent(eventID))
|
||||||
if err = cli.Docker.StopContainer(ctx, ctr.Container.ID, opts); err != nil {
|
if err = cli.Docker.StopContainer(ctx, ctr.Container.ID, opts); err != nil {
|
||||||
@@ -312,7 +312,7 @@ func (cli *Client) RemoveContainer(
|
|||||||
ctx = proxyToMachine(ctx, machine.Machine)
|
ctx = proxyToMachine(ctx, machine.Machine)
|
||||||
|
|
||||||
pw := progress.ContextWriter(ctx)
|
pw := progress.ContextWriter(ctx)
|
||||||
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.Name(), machine.Machine.Name)
|
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.Name, machine.Machine.Name)
|
||||||
|
|
||||||
pw.Event(progress.RemovingEvent(eventID))
|
pw.Event(progress.RemovingEvent(eventID))
|
||||||
if err = cli.Docker.RemoveContainer(ctx, ctr.Container.ID, opts); err != nil {
|
if err = cli.Docker.RemoveContainer(ctx, ctr.Container.ID, opts); err != nil {
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ func (cli *Client) InspectService(ctx context.Context, id string) (api.Service,
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range mc.Containers {
|
for _, c := range mc.Containers {
|
||||||
ctr := api.Container{Container: c}
|
ctr := api.Container{ContainerJSON: c}
|
||||||
if ctr.ServiceID() == id || ctr.ServiceName() == id {
|
if ctr.ServiceID() == id || ctr.ServiceName() == id {
|
||||||
containers = append(containers, api.MachineContainer{
|
containers = append(containers, api.MachineContainer{
|
||||||
MachineID: machineID,
|
MachineID: machineID,
|
||||||
@@ -455,7 +455,7 @@ func (cli *Client) ListServices(ctx context.Context) ([]api.Service, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range mc.Containers {
|
for _, c := range mc.Containers {
|
||||||
ctr := api.Container{Container: c}
|
ctr := api.Container{ContainerJSON: c}
|
||||||
if _, ok := servicesByID[ctr.ServiceID()]; ok {
|
if _, ok := servicesByID[ctr.ServiceID()]; ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ func (s *RollingStrategy) planGlobal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
plan := &SequenceOperation{}
|
plan := &SequenceOperation{}
|
||||||
|
// TODO: figure out how to return a warning if there are machines down.
|
||||||
var machinesDown []*pb.MachineInfo
|
var machinesDown []*pb.MachineInfo
|
||||||
for _, m := range machines {
|
for _, m := range machines {
|
||||||
// Skip machines that are down but collect them to report a warning later.
|
// Skip machines that are down but collect them to report a warning later.
|
||||||
@@ -93,6 +94,9 @@ func (s *RollingStrategy) planGlobal(
|
|||||||
return plan, nil
|
return plan, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reconcileGlobalContainer returns a sequence of operations to reconcile containers on a machine for a global service.
|
||||||
|
// It ensures exactly one container with the desired spec is running on the machine by creating a new container and
|
||||||
|
// removing old ones. If there is a host port conflict, it stops the old container before starting a new one.
|
||||||
func reconcileGlobalContainer(
|
func reconcileGlobalContainer(
|
||||||
containers []api.MachineContainer, spec api.ServiceSpec, serviceID, machineID string,
|
containers []api.MachineContainer, spec api.ServiceSpec, serviceID, machineID string,
|
||||||
) ([]Operation, error) {
|
) ([]Operation, error) {
|
||||||
@@ -111,7 +115,7 @@ func reconcileGlobalContainer(
|
|||||||
// Check if there is a container with the same spec already running. If so, remove the rest.
|
// Check if there is a container with the same spec already running. If so, remove the rest.
|
||||||
upToDate := false
|
upToDate := false
|
||||||
for i, c := range containers {
|
for i, c := range containers {
|
||||||
if c.Container.State != api.StateRunning && c.Container.State != api.StateRestarting {
|
if !c.Container.State.Running || c.Container.State.Paused {
|
||||||
// Skip containers that are not running.
|
// Skip containers that are not running.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -138,9 +142,9 @@ func reconcileGlobalContainer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The machine has containers but none of them match the new spec.
|
// The machine has containers but none of them match the new spec.
|
||||||
// Stop the old non-stopped containers that have conflicting ports with the new spec before running a new one.
|
// Stop the old running containers that have conflicting ports with the new spec before running a new one.
|
||||||
for _, c := range containers {
|
for _, c := range containers {
|
||||||
if !c.Container.Stopped() {
|
if c.Container.State.Running {
|
||||||
conflictingPorts, err := c.Container.ConflictingServicePorts(spec.Ports)
|
conflictingPorts, err := c.Container.ConflictingServicePorts(spec.Ports)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("check conflicting ports: %w", err)
|
return nil, fmt.Errorf("check conflicting ports: %w", err)
|
||||||
|
|||||||
@@ -461,7 +461,7 @@ type MachineContainers struct {
|
|||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"`
|
Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"`
|
||||||
// JSON serialized []container.Summary.
|
// JSON serialized []container.ContainerJSON.
|
||||||
Containers []byte `protobuf:"bytes,2,opt,name=containers,proto3" json:"containers,omitempty"`
|
Containers []byte `protobuf:"bytes,2,opt,name=containers,proto3" json:"containers,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ message ListContainersResponse {
|
|||||||
|
|
||||||
message MachineContainers {
|
message MachineContainers {
|
||||||
Metadata metadata = 1;
|
Metadata metadata = 1;
|
||||||
// JSON serialized []container.Summary.
|
// JSON serialized []container.ContainerJSON.
|
||||||
bytes containers = 2;
|
bytes containers = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,18 +89,18 @@ func (c *Controller) Run(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// filterAvailableContainers filters out containers that are likely unavailable from this machine. The availability
|
// filterAvailableContainers filters out containers from this machine that are likely unavailable. The availability
|
||||||
// is determined by the cluster membership state of the machine that the container is running on.
|
// is determined by the cluster membership state of the machine that the container is running on.
|
||||||
// TODO: implement machine membership check using Corrossion Admin client.
|
// TODO: implement machine membership check using Corrossion Admin client.
|
||||||
func (c *Controller) filterAvailableContainers(containerRecords []*store.ContainerRecord) ([]*api.Container, error) {
|
func (c *Controller) filterAvailableContainers(containerRecords []store.ContainerRecord) ([]api.Container, error) {
|
||||||
containers := make([]*api.Container, len(containerRecords))
|
containers := make([]api.Container, len(containerRecords))
|
||||||
for i, cr := range containerRecords {
|
for i, cr := range containerRecords {
|
||||||
containers[i] = cr.Container
|
containers[i] = cr.Container
|
||||||
}
|
}
|
||||||
return containers, nil
|
return containers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) generateConfig(containers []*api.Container) error {
|
func (c *Controller) generateConfig(containers []api.Container) error {
|
||||||
// Maps hostnames to lists of upstreams (container IP:port pairs).
|
// Maps hostnames to lists of upstreams (container IP:port pairs).
|
||||||
httpHostUpstreams := make(map[string][]string)
|
httpHostUpstreams := make(map[string][]string)
|
||||||
httpsHostUpstreams := make(map[string][]string)
|
httpsHostUpstreams := make(map[string][]string)
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ func (c *Client) StopContainer(ctx context.Context, id string, opts container.St
|
|||||||
|
|
||||||
type MachineContainers struct {
|
type MachineContainers struct {
|
||||||
Metadata *pb.Metadata
|
Metadata *pb.Metadata
|
||||||
Containers []types.Container
|
Containers []types.ContainerJSON
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) ListContainers(ctx context.Context, opts container.ListOptions) ([]MachineContainers, error) {
|
func (c *Client) ListContainers(ctx context.Context, opts container.ListOptions) ([]MachineContainers, error) {
|
||||||
|
|||||||
@@ -149,8 +149,9 @@ func (m *Manager) syncContainersToStore(ctx context.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("list containers from store: %w", err)
|
return fmt.Errorf("list containers from store: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// List only Uncloud service containers identified by their labels.
|
// List only Uncloud service containers identified by their labels.
|
||||||
containers, err := m.client.ContainerList(ctx, dockercontainer.ListOptions{
|
containerSummaries, err := m.client.ContainerList(ctx, dockercontainer.ListOptions{
|
||||||
Filters: filters.NewArgs(
|
Filters: filters.NewArgs(
|
||||||
filters.Arg("label", api.LabelServiceID),
|
filters.Arg("label", api.LabelServiceID),
|
||||||
filters.Arg("label", api.LabelServiceName),
|
filters.Arg("label", api.LabelServiceName),
|
||||||
@@ -161,11 +162,21 @@ func (m *Manager) syncContainersToStore(ctx context.Context) error {
|
|||||||
return fmt.Errorf("list Docker containers: %w", err)
|
return fmt.Errorf("list Docker containers: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete containers that are not present in the Docker daemon from the store.
|
// Inspect each container to get the full container details.
|
||||||
|
containers := make([]api.Container, len(containerSummaries))
|
||||||
|
for i, cs := range containerSummaries {
|
||||||
|
ctr, err := m.client.ContainerInspect(ctx, cs.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect container '%s': %w", cs.ID, err)
|
||||||
|
}
|
||||||
|
containers[i] = api.Container{ContainerJSON: ctr}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete containers from the store that are no longer present in the Docker daemon.
|
||||||
var deleteIDs []string
|
var deleteIDs []string
|
||||||
for _, sc := range storeContainers {
|
for _, sc := range storeContainers {
|
||||||
found := false
|
found := false
|
||||||
for i, _ := range containers {
|
for i := range containers {
|
||||||
if containers[i].ID == sc.Container.ID {
|
if containers[i].ID == sc.Container.ID {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
@@ -184,8 +195,7 @@ func (m *Manager) syncContainersToStore(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create or update the current Docker containers in the store.
|
// Create or update the current Docker containers in the store.
|
||||||
for _, dc := range containers {
|
for _, c := range containers {
|
||||||
c := &api.Container{Container: dc}
|
|
||||||
if err = m.store.CreateOrUpdateContainer(ctx, c, m.machineID); err != nil {
|
if err = m.store.CreateOrUpdateContainer(ctx, c, m.machineID); err != nil {
|
||||||
storeErr = errors.Join(storeErr, fmt.Errorf("create or update container %q: %w", c.ID, err))
|
storeErr = errors.Join(storeErr, fmt.Errorf("create or update container %q: %w", c.ID, err))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// EnsureUncloudNetwork is a stub for darwin.
|
// EnsureUncloudNetwork is a stub for darwin.
|
||||||
func (d *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix) error {
|
func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix) error {
|
||||||
return fmt.Errorf("not supported on darwin")
|
return fmt.Errorf("not supported on darwin")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ import (
|
|||||||
// EnsureUncloudNetwork creates the Docker bridge network NetworkName with the provided machine subnet
|
// EnsureUncloudNetwork creates the Docker bridge network NetworkName with the provided machine subnet
|
||||||
// if it doesn't exist. If the network exists but has a different subnet, it removes and recreates the network.
|
// if it doesn't exist. If the network exists but has a different subnet, it removes and recreates the network.
|
||||||
// It also configures iptables to allow container access from the WireGuard network.
|
// It also configures iptables to allow container access from the WireGuard network.
|
||||||
func (d *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix) error {
|
func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix) error {
|
||||||
// Ensure the Docker network 'uncloud' is created with the correct subnet.
|
// Ensure the Docker network 'uncloud' is created with the correct subnet.
|
||||||
needsCreation := false
|
needsCreation := false
|
||||||
nw, err := d.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{})
|
nw, err := m.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !client.IsErrNotFound(err) {
|
if !client.IsErrNotFound(err) {
|
||||||
return fmt.Errorf("inspect Docker network %q: %w", NetworkName, err)
|
return fmt.Errorf("inspect Docker network %q: %w", NetworkName, err)
|
||||||
@@ -29,7 +29,7 @@ func (d *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix)
|
|||||||
slog.Info(
|
slog.Info(
|
||||||
"Removing Docker network with old subnet.", "name", NetworkName, "subnet", nw.IPAM.Config[0].Subnet,
|
"Removing Docker network with old subnet.", "name", NetworkName, "subnet", nw.IPAM.Config[0].Subnet,
|
||||||
)
|
)
|
||||||
if err = d.client.NetworkRemove(ctx, NetworkName); err != nil {
|
if err = m.client.NetworkRemove(ctx, NetworkName); err != nil {
|
||||||
// It can still fail if the network is in use by a container. Leave it to the user to resolve the issue.
|
// It can still fail if the network is in use by a container. Leave it to the user to resolve the issue.
|
||||||
return fmt.Errorf("remove Docker network %q: %w", NetworkName, err)
|
return fmt.Errorf("remove Docker network %q: %w", NetworkName, err)
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ func (d *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if needsCreation {
|
if needsCreation {
|
||||||
if _, err = d.client.NetworkCreate(
|
if _, err = m.client.NetworkCreate(
|
||||||
ctx, NetworkName, dnetwork.CreateOptions{
|
ctx, NetworkName, dnetwork.CreateOptions{
|
||||||
Driver: "bridge",
|
Driver: "bridge",
|
||||||
Scope: "local",
|
Scope: "local",
|
||||||
@@ -54,7 +54,7 @@ func (d *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix)
|
|||||||
}
|
}
|
||||||
slog.Info("Docker network created.", "name", NetworkName, "subnet", subnet.String())
|
slog.Info("Docker network created.", "name", NetworkName, "subnet", subnet.String())
|
||||||
|
|
||||||
if nw, err = d.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{}); err != nil {
|
if nw, err = m.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{}); err != nil {
|
||||||
return fmt.Errorf("inspect Docker network %q: %w", NetworkName, err)
|
return fmt.Errorf("inspect Docker network %q: %w", NetworkName, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/docker/docker/api/types/image"
|
"github.com/docker/docker/api/types/image"
|
||||||
@@ -144,10 +145,18 @@ func (s *Server) ListContainers(ctx context.Context, req *pb.ListContainersReque
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
containers, err := s.client.ContainerList(ctx, opts)
|
containerSummaries, err := s.client.ContainerList(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, err.Error())
|
return nil, status.Errorf(codes.Internal, err.Error())
|
||||||
}
|
}
|
||||||
|
containers := make([]types.ContainerJSON, len(containerSummaries))
|
||||||
|
for i, cs := range containerSummaries {
|
||||||
|
c, err := s.client.ContainerInspect(ctx, cs.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "inspect container %s: %v", cs.ID, err)
|
||||||
|
}
|
||||||
|
containers[i] = c
|
||||||
|
}
|
||||||
|
|
||||||
containersBytes, err := json.Marshal(containers)
|
containersBytes, err := json.Marshal(containers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ContainerRecord struct {
|
type ContainerRecord struct {
|
||||||
Container *api.Container
|
Container api.Container
|
||||||
MachineID string
|
MachineID string
|
||||||
SyncStatus string
|
SyncStatus string
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
@@ -47,8 +47,11 @@ type DeleteOptions struct {
|
|||||||
|
|
||||||
// CreateOrUpdateContainer creates a new container record or updates an existing one in the store database.
|
// CreateOrUpdateContainer creates a new container record or updates an existing one in the store database.
|
||||||
// The container is associated with the given machine ID that indicates which machine the container is running on.
|
// The container is associated with the given machine ID that indicates which machine the container is running on.
|
||||||
func (s *Store) CreateOrUpdateContainer(ctx context.Context, c *api.Container, machineID string) error {
|
func (s *Store) CreateOrUpdateContainer(ctx context.Context, ctr api.Container, machineID string) error {
|
||||||
cJSON, err := json.Marshal(c)
|
// Remove the environment variables from the container record before storing it in the database
|
||||||
|
// to avoid leaking secrets.
|
||||||
|
ctr.Config.Env = nil
|
||||||
|
cJSON, err := json.Marshal(ctr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("marshal container: %w", err)
|
return fmt.Errorf("marshal container: %w", err)
|
||||||
}
|
}
|
||||||
@@ -63,19 +66,19 @@ func (s *Store) CreateOrUpdateContainer(ctx context.Context, c *api.Container, m
|
|||||||
updated_at = excluded.updated_at
|
updated_at = excluded.updated_at
|
||||||
WHERE containers.container != excluded.container
|
WHERE containers.container != excluded.container
|
||||||
OR containers.machine_id != excluded.machine_id`,
|
OR containers.machine_id != excluded.machine_id`,
|
||||||
c.ID, string(cJSON), machineID, SyncStatusSynced)
|
ctr.ID, string(cJSON), machineID, SyncStatusSynced)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("upsert query: %w", err)
|
return fmt.Errorf("upsert query: %w", err)
|
||||||
}
|
}
|
||||||
if res.RowsAffected > 0 {
|
if res.RowsAffected > 0 {
|
||||||
slog.Debug("Container record updated in store DB.", "id", c.ID, "machine_id", machineID)
|
slog.Debug("Container record updated in store DB.", "id", ctr.ID, "machine_id", machineID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListContainers returns a list of container records from the store database that match the given options.
|
// ListContainers returns a list of container records from the store database that match the given options.
|
||||||
func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]*ContainerRecord, error) {
|
func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]ContainerRecord, error) {
|
||||||
q := sq.Select("container", "machine_id", "sync_status", "updated_at").From("containers").
|
q := sq.Select("container", "machine_id", "sync_status", "updated_at").From("containers").
|
||||||
Where(sq.Eq{"sync_status": SyncStatusSynced})
|
Where(sq.Eq{"sync_status": SyncStatusSynced})
|
||||||
|
|
||||||
@@ -105,7 +108,7 @@ func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]*Contai
|
|||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
var containers []*ContainerRecord
|
var containers []ContainerRecord
|
||||||
var cJSON, machineID, syncStatus, updatedAtStr string
|
var cJSON, machineID, syncStatus, updatedAtStr string
|
||||||
var updatedAt time.Time
|
var updatedAt time.Time
|
||||||
|
|
||||||
@@ -121,8 +124,8 @@ func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]*Contai
|
|||||||
if updatedAt, err = time.Parse(time.DateTime, updatedAtStr); err != nil {
|
if updatedAt, err = time.Parse(time.DateTime, updatedAtStr); err != nil {
|
||||||
return nil, fmt.Errorf("parse updated_at: %w", err)
|
return nil, fmt.Errorf("parse updated_at: %w", err)
|
||||||
}
|
}
|
||||||
containers = append(containers, &ContainerRecord{
|
containers = append(containers, ContainerRecord{
|
||||||
Container: &c,
|
Container: c,
|
||||||
MachineID: machineID,
|
MachineID: machineID,
|
||||||
SyncStatus: syncStatus,
|
SyncStatus: syncStatus,
|
||||||
UpdatedAt: updatedAt,
|
UpdatedAt: updatedAt,
|
||||||
@@ -158,7 +161,7 @@ func (s *Store) DeleteContainers(ctx context.Context, opts DeleteOptions) error
|
|||||||
|
|
||||||
// SubscribeContainers returns a list of containers and a channel that signals changes to the list. The channel doesn't
|
// SubscribeContainers returns a list of containers and a channel that signals changes to the list. The channel doesn't
|
||||||
// receive any values, it just signals when a container(s) has been added, updated, or deleted in the database.
|
// receive any values, it just signals when a container(s) has been added, updated, or deleted in the database.
|
||||||
func (s *Store) SubscribeContainers(ctx context.Context) ([]*ContainerRecord, <-chan struct{}, error) {
|
func (s *Store) SubscribeContainers(ctx context.Context) ([]ContainerRecord, <-chan struct{}, error) {
|
||||||
// TODO: figure out whether we need sync_status at all.
|
// TODO: figure out whether we need sync_status at all.
|
||||||
q := sq.Select("container", "machine_id", "sync_status", "updated_at").From("containers").
|
q := sq.Select("container", "machine_id", "sync_status", "updated_at").From("containers").
|
||||||
Where(sq.Eq{"sync_status": SyncStatusSynced})
|
Where(sq.Eq{"sync_status": SyncStatusSynced})
|
||||||
@@ -172,7 +175,7 @@ func (s *Store) SubscribeContainers(ctx context.Context) ([]*ContainerRecord, <-
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var containers []*ContainerRecord
|
var containers []ContainerRecord
|
||||||
var cJSON, updatedAtStr string
|
var cJSON, updatedAtStr string
|
||||||
|
|
||||||
rows := sub.Rows()
|
rows := sub.Rows()
|
||||||
@@ -188,7 +191,7 @@ func (s *Store) SubscribeContainers(ctx context.Context) ([]*ContainerRecord, <-
|
|||||||
if cr.UpdatedAt, err = time.Parse(time.DateTime, updatedAtStr); err != nil {
|
if cr.UpdatedAt, err = time.Parse(time.DateTime, updatedAtStr); err != nil {
|
||||||
return nil, nil, fmt.Errorf("parse updated_at: %w", err)
|
return nil, nil, fmt.Errorf("parse updated_at: %w", err)
|
||||||
}
|
}
|
||||||
containers = append(containers, &cr)
|
containers = append(containers, cr)
|
||||||
}
|
}
|
||||||
events, err := sub.Changes()
|
events, err := sub.Changes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ CREATE TABLE containers
|
|||||||
-- container is a JSON-serialized api.Container struct.
|
-- container is a JSON-serialized api.Container struct.
|
||||||
container TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(container)),
|
container TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(container)),
|
||||||
machine_id TEXT NOT NULL DEFAULT '',
|
machine_id TEXT NOT NULL DEFAULT '',
|
||||||
service_id TEXT AS (json_extract(container, '$.Labels."uncloud.service.id"')),
|
service_id TEXT AS (json_extract(container, '$.Config.Labels."uncloud.service.id"')),
|
||||||
service_name TEXT AS (json_extract(container, '$.Labels."uncloud.service.name"')),
|
service_name TEXT AS (json_extract(container, '$.Config.Labels."uncloud.service.name"')),
|
||||||
-- sync_status indicates if the record reflects the actual Docker state of the container.
|
-- sync_status indicates if the record reflects the actual Docker state of the container.
|
||||||
sync_status TEXT NOT NULL DEFAULT '',
|
sync_status TEXT NOT NULL DEFAULT '',
|
||||||
-- updated_at is the last time the record was updated.
|
-- updated_at is the last time the record was updated.
|
||||||
|
|||||||
Reference in New Issue
Block a user