mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 19:13:34 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4398f9f06 | ||
|
|
a53fa2c1d8 | ||
|
|
5e2b2ac836 | ||
|
|
43f28284f1 | ||
|
|
b17fd7531f | ||
|
|
9859be50a6 | ||
|
|
f209851986 | ||
|
|
4da51636fd | ||
|
|
76a9f8e8b6 | ||
|
|
a477db6b16 | ||
|
|
04fd1b1425 | ||
|
|
c0bf1f2930 | ||
|
|
0bf759333a | ||
|
|
b482d836aa | ||
|
|
3d2d9b78e8 | ||
|
|
99219dc376 | ||
|
|
408703360c | ||
|
|
ac48b8bee6 | ||
|
|
e3a310397b | ||
|
|
b2f7c6fb33 | ||
|
|
653f47f507 | ||
|
|
5b66cec627 | ||
|
|
0d9a2b6e07 | ||
|
|
fb68303825 | ||
|
|
cfe5b018f3 | ||
|
|
b451f2cf7c |
@@ -67,6 +67,9 @@ platform, whether you're running on a $5 VPS, a spare Mac mini, or a rack of bar
|
|||||||
1. Install Uncloud CLI:
|
1. Install Uncloud CLI:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
brew install psviderski/tap/uncloud
|
||||||
|
|
||||||
|
# or using curl (macOS/Linux)
|
||||||
curl -fsS https://get.uncloud.run/install.sh | sh
|
curl -fsS https://get.uncloud.run/install.sh | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -76,8 +79,8 @@ curl -fsS https://get.uncloud.run/install.sh | sh
|
|||||||
uc machine init root@your-server-ip
|
uc machine init root@your-server-ip
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Create a DNS A record in your domain registrar that points `app.example.com` to your server's IP address. Allow a few
|
3. Create a DNS A record in your DNS provider (Cloudflare, Namecheap, etc.) that points `app.example.com` to your
|
||||||
minutes for DNS propagation.
|
server's IP address. Allow a few minutes for DNS propagation.
|
||||||
4. Deploy your app from a Docker image:
|
4. Deploy your app from a Docker image:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -86,6 +89,14 @@ uc run -p app.example.com:8000/https my-app-image
|
|||||||
|
|
||||||
That's it! Your app is now running and accessible at https://app.example.com ✨
|
That's it! Your app is now running and accessible at https://app.example.com ✨
|
||||||
|
|
||||||
|
5. Clean up when you're done:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uc ls
|
||||||
|
# Copy the service ID from the output and remove it:
|
||||||
|
uc rm my-app-id
|
||||||
|
```
|
||||||
|
|
||||||
## ⚙️ How it works
|
## ⚙️ How it works
|
||||||
|
|
||||||
Check out the [design document](docs/design.md) to understand Uncloud's design philosophy and goals. Here, let's peek
|
Check out the [design document](docs/design.md) to understand Uncloud's design philosophy and goals. Here, let's peek
|
||||||
@@ -164,3 +175,11 @@ I'd love your input! Here's how you can contribute:
|
|||||||
features, and be the first to know when it's ready for production use.
|
features, and be the first to know when it's ready for production use.
|
||||||
* Watch this repository for releases.
|
* Watch this repository for releases.
|
||||||
* Follow [@psviderski](https://github.com/psviderski) on GitHub.
|
* Follow [@psviderski](https://github.com/psviderski) on GitHub.
|
||||||
|
|
||||||
|
## ❤️ Contributors
|
||||||
|
|
||||||
|
Thank you [@cedws](https://github.com/cedws) for being the first contributor to Uncloud! 🎉
|
||||||
|
|
||||||
|
<a href="https://github.com/psviderski/uncloud/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=psviderski/uncloud" />
|
||||||
|
</a>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/docker/cli/cli/streams"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"os"
|
||||||
"uncloud/internal/cli"
|
"uncloud/internal/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,10 +43,12 @@ func rm(ctx context.Context, uncli *cli.CLI, opts rmOptions) error {
|
|||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
for _, s := range opts.services {
|
for _, s := range opts.services {
|
||||||
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
if err = client.RemoveService(ctx, s); err != nil {
|
if err = client.RemoveService(ctx, s); err != nil {
|
||||||
return fmt.Errorf("remove service %q: %w", s, err)
|
return fmt.Errorf("remove service '%s': %w", s, err)
|
||||||
}
|
}
|
||||||
fmt.Printf("Service %q removed.\n", s)
|
return nil
|
||||||
|
}, streams.NewOut(os.Stdout), "Removing service "+s)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ func NewRootCommand() *cobra.Command {
|
|||||||
Short: "Manage services in an Uncloud cluster.",
|
Short: "Manage services in an Uncloud cluster.",
|
||||||
}
|
}
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
|
NewInspectCommand(),
|
||||||
NewListCommand(),
|
NewListCommand(),
|
||||||
NewRmCommand(),
|
NewRmCommand(),
|
||||||
NewRunCommand(),
|
NewRunCommand(),
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
+125
-27
@@ -1,9 +1,11 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"regexp"
|
"github.com/docker/go-units"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -15,30 +17,38 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Container struct {
|
type Container struct {
|
||||||
types.Container
|
types.ContainerJSON
|
||||||
|
}
|
||||||
|
|
||||||
|
// NameWithoutSlash returns the container name without the leading slash.
|
||||||
|
func (c *Container) NameWithoutSlash() string {
|
||||||
|
return c.Name[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(encoded) == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
publishPorts := strings.Split(encoded, ",")
|
publishPorts := strings.Split(encoded, ",")
|
||||||
ports := make([]PortSpec, len(publishPorts))
|
ports := make([]PortSpec, len(publishPorts))
|
||||||
@@ -53,34 +63,122 @@ func (c *Container) ServicePorts() ([]PortSpec, error) {
|
|||||||
return ports, nil
|
return ports, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// runningStatusRegex matches the status string of a running container.
|
// ServiceSpec constructs a service spec from the container's configuration.
|
||||||
// - "Up 3 minutes (healthy)" -> groups: ["Up 3 minutes (healthy)", "healthy"]
|
func (c *Container) ServiceSpec() (ServiceSpec, error) {
|
||||||
// - "Up 5 seconds" -> groups: ["Up 5 seconds", ""]
|
ports, err := c.ServicePorts()
|
||||||
// - "Up 2 hours (unhealthy)" -> groups: ["Up 2 hours (unhealthy)", "unhealthy"]
|
if err != nil {
|
||||||
// - "Up 1 minute (health: starting)" -> groups: ["Up 1 minute (health: starting)", "health: starting"]
|
return ServiceSpec{}, fmt.Errorf("get service ports: %w", err)
|
||||||
// - "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.
|
return ServiceSpec{
|
||||||
|
Container: ContainerSpec{
|
||||||
|
Command: c.Config.Cmd,
|
||||||
|
Image: c.Config.Image,
|
||||||
|
Init: c.HostConfig.Init,
|
||||||
|
Volumes: c.HostConfig.Binds,
|
||||||
|
},
|
||||||
|
Mode: c.ServiceMode(),
|
||||||
|
Name: c.ServiceName(),
|
||||||
|
Ports: ports,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Healthy determines if the container is running and healthy.
|
||||||
// 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 != "running" {
|
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
|
}
|
||||||
|
|
||||||
|
// HumanState returns a human-readable description of the container's state. Based on the Docker implementation:
|
||||||
|
// https://github.com/moby/moby/blob/b343d235a0a1f30c8f05b1d651238e72158dc25d/container/state.go#L79-L113
|
||||||
|
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.
|
||||||
|
func (c *Container) ConflictingServicePorts(ports []PortSpec) ([]PortSpec, error) {
|
||||||
|
svcPorts, err := c.ServicePorts()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get service ports: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var conflicting []PortSpec
|
||||||
|
for _, p := range ports {
|
||||||
|
if p.Mode != PortModeHost {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two host ports conflict if they have the same published port number and protocol, and either:
|
||||||
|
// * At least one host IP is not set (meaning it uses all interfaces)
|
||||||
|
// * Both host IPs are identical
|
||||||
|
for _, svcPort := range svcPorts {
|
||||||
|
if svcPort.Mode != PortModeHost ||
|
||||||
|
svcPort.PublishedPort != p.PublishedPort ||
|
||||||
|
svcPort.Protocol != p.Protocol {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !svcPort.HostIP.IsValid() || !p.HostIP.IsValid() || svcPort.HostIP.Compare(p.HostIP) == 0 {
|
||||||
|
conflicting = append(conflicting, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return conflicting, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+294
-40
@@ -2,91 +2,345 @@ 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"
|
||||||
|
"net/netip"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestContainer_ServiceSpec(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
init := true
|
||||||
|
ctr := &Container{ContainerJSON: types.ContainerJSON{
|
||||||
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
|
HostConfig: &container.HostConfig{
|
||||||
|
Binds: []string{"/host/path:/container/path"},
|
||||||
|
Init: &init,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Config: &container.Config{
|
||||||
|
Cmd: []string{"/app/server"},
|
||||||
|
Image: "app:latest",
|
||||||
|
Labels: map[string]string{
|
||||||
|
LabelServiceID: "test-service-id",
|
||||||
|
LabelServiceName: "test-service-name",
|
||||||
|
LabelServicePorts: "app.example.com:8000/https",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
expectedSpec := ServiceSpec{
|
||||||
|
Container: ContainerSpec{
|
||||||
|
Command: []string{"/app/server"},
|
||||||
|
Image: "app:latest",
|
||||||
|
Init: &init,
|
||||||
|
Volumes: []string{"/host/path:/container/path"},
|
||||||
|
},
|
||||||
|
Name: "test-service-name",
|
||||||
|
Ports: []PortSpec{
|
||||||
|
{
|
||||||
|
Hostname: "app.example.com",
|
||||||
|
ContainerPort: 8000,
|
||||||
|
Protocol: ProtocolHTTPS,
|
||||||
|
Mode: PortModeIngress,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
spec, err := ctr.ServiceSpec()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, reflect.DeepEqual(spec, expectedSpec))
|
||||||
|
}
|
||||||
|
|
||||||
func TestContainer_Healthy(t *testing.T) {
|
func TestContainer_Healthy(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
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())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContainer_ConflictingServicePorts(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
containerPorts string
|
||||||
|
checkPorts []PortSpec
|
||||||
|
want []PortSpec
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no conflicts when container has no ports",
|
||||||
|
containerPorts: "",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolTCP},
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode ports with same published port and protocol conflict",
|
||||||
|
containerPorts: "8080:80/tcp@host",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolTCP},
|
||||||
|
},
|
||||||
|
want: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolTCP},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode ports with same port but different protocols don't conflict",
|
||||||
|
containerPorts: "8080:80/tcp@host",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolUDP},
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple protocols on same port don't conflict",
|
||||||
|
containerPorts: "8080:80/tcp@host,8080:80/udp@host",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolUDP},
|
||||||
|
},
|
||||||
|
want: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolUDP},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode ports with different published ports don't conflict",
|
||||||
|
containerPorts: "8080:80/tcp@host",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8081, ContainerPort: 80, Protocol: ProtocolTCP},
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode ports with same published port but different host IPs don't conflict",
|
||||||
|
containerPorts: "127.0.0.1:8080:80/tcp@host",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{
|
||||||
|
Mode: PortModeHost,
|
||||||
|
HostIP: netip.MustParseAddr("127.0.0.2"),
|
||||||
|
PublishedPort: 8080,
|
||||||
|
ContainerPort: 80,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode ports with same published port, protocol, and host IP conflict",
|
||||||
|
containerPorts: "127.0.0.1:8080:80/tcp@host",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{
|
||||||
|
Mode: PortModeHost,
|
||||||
|
HostIP: netip.MustParseAddr("127.0.0.1"),
|
||||||
|
PublishedPort: 8080,
|
||||||
|
ContainerPort: 80,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []PortSpec{
|
||||||
|
{
|
||||||
|
Mode: PortModeHost,
|
||||||
|
HostIP: netip.MustParseAddr("127.0.0.1"),
|
||||||
|
PublishedPort: 8080,
|
||||||
|
ContainerPort: 80,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode port with no host IP conflicts with specific host IP on same port and protocol",
|
||||||
|
containerPorts: "8080:80/tcp@host",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{
|
||||||
|
Mode: PortModeHost,
|
||||||
|
HostIP: netip.MustParseAddr("127.0.0.1"),
|
||||||
|
PublishedPort: 8080,
|
||||||
|
ContainerPort: 80,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []PortSpec{
|
||||||
|
{
|
||||||
|
Mode: PortModeHost,
|
||||||
|
HostIP: netip.MustParseAddr("127.0.0.1"),
|
||||||
|
PublishedPort: 8080,
|
||||||
|
ContainerPort: 80,
|
||||||
|
Protocol: ProtocolTCP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode port with no host IP doesn't conflict with different protocol",
|
||||||
|
containerPorts: "8080:80/tcp@host",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{
|
||||||
|
Mode: PortModeHost,
|
||||||
|
HostIP: netip.MustParseAddr("127.0.0.1"),
|
||||||
|
PublishedPort: 8080,
|
||||||
|
ContainerPort: 80,
|
||||||
|
Protocol: ProtocolUDP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ingress mode ports don't conflict with host mode ports",
|
||||||
|
containerPorts: "8080:80/tcp",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolTCP},
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "container with invalid port spec returns error",
|
||||||
|
containerPorts: "invalid:port:spec",
|
||||||
|
checkPorts: []PortSpec{
|
||||||
|
{Mode: PortModeHost, PublishedPort: 8080, ContainerPort: 80, Protocol: ProtocolTCP},
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ctr := &Container{ContainerJSON: types.ContainerJSON{
|
||||||
|
Config: &container.Config{
|
||||||
|
Labels: map[string]string{
|
||||||
|
LabelServicePorts: tt.containerPorts,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
got, err := ctr.ConflictingServicePorts(tt.checkPorts)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
|
"reflect"
|
||||||
"uncloud/internal/machine/api/pb"
|
"uncloud/internal/machine/api/pb"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -37,6 +38,11 @@ func (s *ServiceSpec) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ServiceSpec) Equals(spec ServiceSpec) bool {
|
||||||
|
// TODO: ignore order of ports.
|
||||||
|
return reflect.DeepEqual(*s, spec)
|
||||||
|
}
|
||||||
|
|
||||||
type ContainerSpec struct {
|
type ContainerSpec struct {
|
||||||
Command []string
|
Command []string
|
||||||
Image string
|
Image string
|
||||||
@@ -47,8 +53,7 @@ type ContainerSpec struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *ContainerSpec) Validate() error {
|
func (s *ContainerSpec) Validate() error {
|
||||||
_, err := reference.ParseDockerRef(s.Image)
|
if _, err := reference.ParseDockerRef(s.Image); err != nil {
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid image: %w", err)
|
return fmt.Errorf("invalid image: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-2
@@ -4,15 +4,17 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/charmbracelet/huh"
|
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"uncloud/internal/cli/client"
|
"uncloud/internal/cli/client"
|
||||||
"uncloud/internal/cli/client/connector"
|
"uncloud/internal/cli/client/connector"
|
||||||
"uncloud/internal/cli/config"
|
"uncloud/internal/cli/config"
|
||||||
|
"uncloud/internal/fs"
|
||||||
"uncloud/internal/machine"
|
"uncloud/internal/machine"
|
||||||
"uncloud/internal/machine/api/pb"
|
"uncloud/internal/machine/api/pb"
|
||||||
"uncloud/internal/sshexec"
|
"uncloud/internal/sshexec"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultClusterName = "default"
|
const defaultClusterName = "default"
|
||||||
@@ -89,10 +91,14 @@ func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parse SSH connection %q: %w", conn.SSH, err)
|
return nil, fmt.Errorf("parse SSH connection %q: %w", conn.SSH, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
keyPath := fs.ExpandHomeDir(conn.SSHKeyFile)
|
||||||
|
|
||||||
sshConfig := &connector.SSHConnectorConfig{
|
sshConfig := &connector.SSHConnectorConfig{
|
||||||
User: user,
|
User: user,
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: port,
|
Port: port,
|
||||||
|
KeyPath: keyPath,
|
||||||
}
|
}
|
||||||
return client.New(ctx, connector.NewSSHConnector(sshConfig))
|
return client.New(ctx, connector.NewSSHConnector(sshConfig))
|
||||||
} else if conn.TCP.IsValid() {
|
} else if conn.TCP.IsValid() {
|
||||||
@@ -157,9 +163,11 @@ func (cli *CLI) initRemoteMachine(
|
|||||||
return fmt.Errorf("set current cluster: %w", err)
|
return fmt.Errorf("set current cluster: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the machine's SSH connection details in the cluster config.
|
// Save the machine's SSH connection details in the cluster config.
|
||||||
connCfg := config.MachineConnection{
|
connCfg := config.MachineConnection{
|
||||||
SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port),
|
SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port),
|
||||||
|
SSHKeyFile: remoteMachine.KeyPath,
|
||||||
}
|
}
|
||||||
cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].Connections, connCfg)
|
cli.config.Clusters[clusterName].Connections = append(cli.config.Clusters[clusterName].Connections, connCfg)
|
||||||
if err = cli.config.Save(); err != nil {
|
if err = cli.config.Save(); err != nil {
|
||||||
@@ -246,6 +254,7 @@ func (cli *CLI) AddMachine(ctx context.Context, remoteMachine RemoteMachine, clu
|
|||||||
// Save the machine's SSH connection details in the cluster config.
|
// Save the machine's SSH connection details in the cluster config.
|
||||||
connCfg := config.MachineConnection{
|
connCfg := config.MachineConnection{
|
||||||
SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port),
|
SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port),
|
||||||
|
SSHKeyFile: remoteMachine.KeyPath,
|
||||||
}
|
}
|
||||||
if clusterName == "" {
|
if clusterName == "" {
|
||||||
clusterName = cli.config.CurrentCluster
|
clusterName = cli.config.CurrentCluster
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
"os"
|
"os"
|
||||||
"uncloud/internal/machine/api/pb"
|
"uncloud/internal/machine/api/pb"
|
||||||
"uncloud/internal/machine/docker"
|
"uncloud/internal/machine/docker"
|
||||||
@@ -20,12 +21,11 @@ type Client struct {
|
|||||||
|
|
||||||
pb.MachineClient
|
pb.MachineClient
|
||||||
pb.ClusterClient
|
pb.ClusterClient
|
||||||
*DockerClient
|
// Docker is a namespaced client for the Docker service to distinguish Uncloud-specific service container operations
|
||||||
|
// from generic Docker operations.
|
||||||
|
Docker *docker.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// DockerClient is a type alias for the Docker client to embed it in Client with a more specific name.
|
|
||||||
type DockerClient = docker.Client
|
|
||||||
|
|
||||||
// Connector is an interface for establishing a connection to the machine API.
|
// Connector is an interface for establishing a connection to the machine API.
|
||||||
type Connector interface {
|
type Connector interface {
|
||||||
Connect(ctx context.Context) (*grpc.ClientConn, error)
|
Connect(ctx context.Context) (*grpc.ClientConn, error)
|
||||||
@@ -46,7 +46,7 @@ func New(ctx context.Context, connector Connector) (*Client, error) {
|
|||||||
|
|
||||||
c.MachineClient = pb.NewMachineClient(c.conn)
|
c.MachineClient = pb.NewMachineClient(c.conn)
|
||||||
c.ClusterClient = pb.NewClusterClient(c.conn)
|
c.ClusterClient = pb.NewClusterClient(c.conn)
|
||||||
c.DockerClient = docker.NewClient(c.conn)
|
c.Docker = docker.NewClient(c.conn)
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,3 +58,10 @@ func (cli *Client) Close() error {
|
|||||||
func (cli *Client) progressOut() *streams.Out {
|
func (cli *Client) progressOut() *streams.Out {
|
||||||
return streams.NewOut(os.Stdout)
|
return streams.NewOut(os.Stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// proxyToMachine returns a new context that proxies gRPC requests to the specified machine.
|
||||||
|
func proxyToMachine(ctx context.Context, machine *pb.MachineInfo) context.Context {
|
||||||
|
machineIP, _ := machine.Network.ManagementIp.ToAddr()
|
||||||
|
md := metadata.Pairs("machines", machineIP.String())
|
||||||
|
return metadata.NewOutgoingContext(ctx, md)
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,21 @@ import (
|
|||||||
"uncloud/internal/machine/api/pb"
|
"uncloud/internal/machine/api/pb"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (cli *Client) InspectMachine(ctx context.Context, id string) (*pb.MachineMember, error) {
|
||||||
|
machines, err := cli.ListMachines(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range machines {
|
||||||
|
if m.Machine.Id == id || m.Machine.Name == id {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
func (cli *Client) ListMachines(ctx context.Context) ([]*pb.MachineMember, error) {
|
func (cli *Client) ListMachines(ctx context.Context) ([]*pb.MachineMember, error) {
|
||||||
resp, err := cli.ClusterClient.ListMachines(ctx, &emptypb.Empty{})
|
resp, err := cli.ClusterClient.ListMachines(ctx, &emptypb.Empty{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,324 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/network"
|
||||||
|
dockerclient "github.com/docker/docker/client"
|
||||||
|
"github.com/docker/docker/pkg/jsonmessage"
|
||||||
|
"github.com/docker/go-connections/nat"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"uncloud/internal/api"
|
||||||
|
machinedocker "uncloud/internal/machine/docker"
|
||||||
|
"uncloud/internal/secret"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateContainer creates a new container for the given service on the specified machine.
|
||||||
|
func (cli *Client) CreateContainer(
|
||||||
|
ctx context.Context, serviceID string, spec api.ServiceSpec, machineID string,
|
||||||
|
) (container.CreateResponse, error) {
|
||||||
|
var resp container.CreateResponse
|
||||||
|
|
||||||
|
if serviceID == "" {
|
||||||
|
return resp, errors.New("service ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
machine, err := cli.InspectMachine(ctx, machineID)
|
||||||
|
if err != nil {
|
||||||
|
return resp, fmt.Errorf("inspect machine '%s': %w", machineID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
suffix, err := secret.RandomAlphaNumeric(4)
|
||||||
|
if err != nil {
|
||||||
|
return resp, fmt.Errorf("generate random suffix: %w", err)
|
||||||
|
}
|
||||||
|
containerName := fmt.Sprintf("%s-%s", spec.Name, suffix)
|
||||||
|
|
||||||
|
config := &container.Config{
|
||||||
|
Cmd: spec.Container.Command,
|
||||||
|
Image: spec.Container.Image,
|
||||||
|
Labels: map[string]string{
|
||||||
|
api.LabelServiceID: serviceID,
|
||||||
|
api.LabelServiceName: spec.Name,
|
||||||
|
api.LabelManaged: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if spec.Mode != "" {
|
||||||
|
config.Labels[api.LabelServiceMode] = spec.Mode
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(spec.Ports) > 0 {
|
||||||
|
encodedPorts := make([]string, len(spec.Ports))
|
||||||
|
for i, p := range spec.Ports {
|
||||||
|
encodedPorts[i], err = p.String()
|
||||||
|
if err != nil {
|
||||||
|
return resp, fmt.Errorf("encode service port spec: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Labels[api.LabelServicePorts] = strings.Join(encodedPorts, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
portBindings := make(nat.PortMap)
|
||||||
|
for _, p := range spec.Ports {
|
||||||
|
if p.Mode != api.PortModeHost {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
port := nat.Port(fmt.Sprintf("%d/%s", p.ContainerPort, p.Protocol))
|
||||||
|
portBindings[port] = []nat.PortBinding{
|
||||||
|
{
|
||||||
|
HostPort: strconv.Itoa(int(p.PublishedPort)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if p.HostIP.IsValid() {
|
||||||
|
portBindings[port][0].HostIP = p.HostIP.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hostConfig := &container.HostConfig{
|
||||||
|
Binds: spec.Container.Volumes,
|
||||||
|
Init: spec.Container.Init,
|
||||||
|
PortBindings: portBindings,
|
||||||
|
}
|
||||||
|
netConfig := &network.NetworkingConfig{
|
||||||
|
EndpointsConfig: map[string]*network.EndpointSettings{
|
||||||
|
machinedocker.NetworkName: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proxy Docker gRPC requests to the selected machine.
|
||||||
|
ctx = proxyToMachine(ctx, machine.Machine)
|
||||||
|
|
||||||
|
pw := progress.ContextWriter(ctx)
|
||||||
|
eventID := fmt.Sprintf("Container %s on %s", containerName, machine.Machine.Name)
|
||||||
|
|
||||||
|
pw.Event(progress.CreatingEvent(eventID))
|
||||||
|
resp, err = cli.Docker.CreateContainer(ctx, config, hostConfig, netConfig, nil, containerName)
|
||||||
|
if err != nil {
|
||||||
|
if !dockerclient.IsErrNotFound(err) {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull the missing image and create the container again.
|
||||||
|
if err = cli.pullImageWithProgress(ctx, config.Image, machine.Machine.Name, eventID); err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
if resp, err = cli.Docker.CreateContainer(ctx, config, hostConfig, netConfig, nil, containerName); err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pw.Event(progress.CreatedEvent(eventID))
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *Client) pullImageWithProgress(ctx context.Context, image, machineName, parentEventID string) error {
|
||||||
|
pw := progress.ContextWriter(ctx)
|
||||||
|
eventID := fmt.Sprintf("Image %s on %s", image, machineName)
|
||||||
|
pw.Event(progress.Event{
|
||||||
|
ID: eventID,
|
||||||
|
ParentID: parentEventID,
|
||||||
|
Status: progress.Working,
|
||||||
|
StatusText: "Pulling",
|
||||||
|
})
|
||||||
|
|
||||||
|
pullCh, err := cli.Docker.PullImage(ctx, image)
|
||||||
|
if err != nil {
|
||||||
|
pw.Event(progress.Event{
|
||||||
|
ID: eventID,
|
||||||
|
ParentID: parentEventID,
|
||||||
|
Text: "Error",
|
||||||
|
Status: progress.Error,
|
||||||
|
StatusText: errors.Unwrap(err).Error(),
|
||||||
|
})
|
||||||
|
return fmt.Errorf("pull image: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for pull to complete by reading all progress messages and converting them to events.
|
||||||
|
for msg := range pullCh {
|
||||||
|
if msg.Err != nil {
|
||||||
|
err = msg.Err
|
||||||
|
} else {
|
||||||
|
if msg.Message.Error != nil {
|
||||||
|
err = errors.New(msg.Message.Error.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
pw.Event(progress.Event{
|
||||||
|
ID: eventID,
|
||||||
|
ParentID: parentEventID,
|
||||||
|
Text: "Error",
|
||||||
|
Status: progress.Error,
|
||||||
|
StatusText: errors.Unwrap(err).Error(),
|
||||||
|
})
|
||||||
|
return fmt.Errorf("pull image: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add like in compose: --quiet-pull Pull without printing progress information
|
||||||
|
e := toPullProgressEvent(msg.Message)
|
||||||
|
if e != nil {
|
||||||
|
e.ID = fmt.Sprintf("%s on %s", e.ID, machineName)
|
||||||
|
e.ParentID = eventID
|
||||||
|
// Grand children events are not printed by the tty progress writer but they are still required
|
||||||
|
// to calculate the progress line of their parent.
|
||||||
|
pw.Event(*e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pw.Event(progress.Event{
|
||||||
|
ID: eventID,
|
||||||
|
ParentID: parentEventID,
|
||||||
|
Status: progress.Done,
|
||||||
|
StatusText: "Pulled",
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// toPullProgressEvent converts a JSON progress message from the Docker API to a progress event.
|
||||||
|
// It's based on toPullProgressEvent from Docker Compose.
|
||||||
|
func toPullProgressEvent(jm jsonmessage.JSONMessage) *progress.Event {
|
||||||
|
if jm.ID == "" || jm.Progress == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
total int64
|
||||||
|
percent int
|
||||||
|
current int64
|
||||||
|
)
|
||||||
|
text := jm.Progress.String()
|
||||||
|
stat := progress.Working
|
||||||
|
|
||||||
|
switch jm.Status {
|
||||||
|
case "Preparing", "Waiting", "Pulling fs layer":
|
||||||
|
percent = 0
|
||||||
|
case "Downloading", "Extracting", "Verifying Checksum":
|
||||||
|
current = jm.Progress.Current
|
||||||
|
total = jm.Progress.Total
|
||||||
|
if jm.Progress.Total > 0 {
|
||||||
|
percent = int(jm.Progress.Current * 100 / jm.Progress.Total)
|
||||||
|
}
|
||||||
|
case "Download complete", "Already exists", "Pull complete":
|
||||||
|
stat = progress.Done
|
||||||
|
percent = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(jm.Status, "Image is up to date") ||
|
||||||
|
strings.Contains(jm.Status, "Downloaded newer image") {
|
||||||
|
stat = progress.Done
|
||||||
|
percent = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
return &progress.Event{
|
||||||
|
ID: jm.ID,
|
||||||
|
Current: current,
|
||||||
|
Total: total,
|
||||||
|
Percent: percent,
|
||||||
|
Text: jm.Status,
|
||||||
|
Status: stat,
|
||||||
|
StatusText: text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InspectContainer returns the information about the specified container within the service.
|
||||||
|
func (cli *Client) InspectContainer(ctx context.Context, serviceID, containerID string) (api.MachineContainer, error) {
|
||||||
|
var ctr api.MachineContainer
|
||||||
|
|
||||||
|
svc, err := cli.InspectService(ctx, serviceID)
|
||||||
|
if err != nil {
|
||||||
|
return ctr, fmt.Errorf("inspect service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range svc.Containers {
|
||||||
|
if c.Container.ID == containerID || c.Container.NameWithoutSlash() == containerID {
|
||||||
|
ctr = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ctr.MachineID == "" {
|
||||||
|
return ctr, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartContainer starts the specified container within the service.
|
||||||
|
func (cli *Client) StartContainer(ctx context.Context, serviceID, containerID string) error {
|
||||||
|
ctr, err := cli.InspectContainer(ctx, serviceID, containerID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
machine, err := cli.InspectMachine(ctx, ctr.MachineID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect machine '%s': %w", ctr.MachineID, err)
|
||||||
|
}
|
||||||
|
ctx = proxyToMachine(ctx, machine.Machine)
|
||||||
|
|
||||||
|
pw := progress.ContextWriter(ctx)
|
||||||
|
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.NameWithoutSlash(), machine.Machine.Name)
|
||||||
|
|
||||||
|
pw.Event(progress.StartingEvent(eventID))
|
||||||
|
if err = cli.Docker.StartContainer(ctx, ctr.Container.ID, container.StartOptions{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pw.Event(progress.StartedEvent(eventID))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopContainer stops the specified container within the service.
|
||||||
|
func (cli *Client) StopContainer(
|
||||||
|
ctx context.Context, serviceID, containerID string, opts container.StopOptions,
|
||||||
|
) error {
|
||||||
|
ctr, err := cli.InspectContainer(ctx, serviceID, containerID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
machine, err := cli.InspectMachine(ctx, ctr.MachineID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect machine '%s': %w", ctr.MachineID, err)
|
||||||
|
}
|
||||||
|
ctx = proxyToMachine(ctx, machine.Machine)
|
||||||
|
|
||||||
|
pw := progress.ContextWriter(ctx)
|
||||||
|
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.NameWithoutSlash(), machine.Machine.Name)
|
||||||
|
|
||||||
|
pw.Event(progress.StoppingEvent(eventID))
|
||||||
|
if err = cli.Docker.StopContainer(ctx, ctr.Container.ID, opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pw.Event(progress.StoppedEvent(eventID))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveContainer removes the specified container within the service.
|
||||||
|
func (cli *Client) RemoveContainer(
|
||||||
|
ctx context.Context, serviceID, containerID string, opts container.RemoveOptions,
|
||||||
|
) error {
|
||||||
|
ctr, err := cli.InspectContainer(ctx, serviceID, containerID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
machine, err := cli.InspectMachine(ctx, ctr.MachineID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect machine '%s': %w", ctr.MachineID, err)
|
||||||
|
}
|
||||||
|
ctx = proxyToMachine(ctx, machine.Machine)
|
||||||
|
|
||||||
|
pw := progress.ContextWriter(ctx)
|
||||||
|
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.NameWithoutSlash(), machine.Machine.Name)
|
||||||
|
|
||||||
|
pw.Event(progress.RemovingEvent(eventID))
|
||||||
|
if err = cli.Docker.RemoveContainer(ctx, ctr.Container.ID, opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pw.Event(progress.RemovedEvent(eventID))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/distribution/reference"
|
||||||
|
"strings"
|
||||||
|
"uncloud/internal/api"
|
||||||
|
"uncloud/internal/secret"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Deployment manages the process of creating or updating a service to match a desired state.
|
||||||
|
// It coordinates the validation, planning, and execution of deployment operations.
|
||||||
|
type Deployment struct {
|
||||||
|
Service *api.Service
|
||||||
|
Spec api.ServiceSpec
|
||||||
|
Strategy Strategy
|
||||||
|
cli *Client
|
||||||
|
plan *Plan
|
||||||
|
}
|
||||||
|
|
||||||
|
type Plan struct {
|
||||||
|
ServiceID string
|
||||||
|
Operation
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDeployment creates a new deployment for the given service specification.
|
||||||
|
// If strategy is nil, a default RollingStrategy will be used.
|
||||||
|
func (cli *Client) NewDeployment(spec api.ServiceSpec, strategy Strategy) (*Deployment, error) {
|
||||||
|
if err := spec.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid service spec: %w", err)
|
||||||
|
}
|
||||||
|
if spec.Name == "" {
|
||||||
|
// Generate a random service name from the image when not provided.
|
||||||
|
img, err := reference.ParseDockerRef(spec.Container.Image)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid image: %w", err)
|
||||||
|
}
|
||||||
|
// Get the image name without the repository and tag/digest parts.
|
||||||
|
imageName := reference.FamiliarName(img)
|
||||||
|
// Get the last part of the image name (path), e.g. "nginx" from "bitnami/nginx".
|
||||||
|
if i := strings.LastIndex(imageName, "/"); i != -1 {
|
||||||
|
imageName = imageName[i+1:]
|
||||||
|
}
|
||||||
|
// Append a random suffix to the image name to generate an optimistically unique service name.
|
||||||
|
suffix, err := secret.RandomAlphaNumeric(4)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("generate random suffix: %w", err)
|
||||||
|
}
|
||||||
|
spec.Name = fmt.Sprintf("%s-%s", imageName, suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strategy == nil {
|
||||||
|
strategy = &RollingStrategy{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Deployment{
|
||||||
|
Spec: spec,
|
||||||
|
Strategy: strategy,
|
||||||
|
cli: cli,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plan returns a plan of operations to reconcile the service to the desired state.
|
||||||
|
// If a plan has already been created, the same plan will be returned.
|
||||||
|
func (d *Deployment) Plan(ctx context.Context) (Plan, error) {
|
||||||
|
if d.plan != nil {
|
||||||
|
return *d.plan, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the new spec before planning.
|
||||||
|
if err := d.Validate(ctx); err != nil {
|
||||||
|
return Plan{}, fmt.Errorf("invalid deployment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
plan, err := d.Strategy.Plan(ctx, d.cli, d.Service, d.Spec)
|
||||||
|
if err != nil {
|
||||||
|
return Plan{}, fmt.Errorf("create plan using %T: %w", d.Strategy, err)
|
||||||
|
}
|
||||||
|
d.plan = &plan
|
||||||
|
|
||||||
|
return plan, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the deployment specification is valid.
|
||||||
|
func (d *Deployment) Validate(ctx context.Context) error {
|
||||||
|
if err := d.Spec.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("invalid service spec: %w", err)
|
||||||
|
}
|
||||||
|
if d.Spec.Name == "" {
|
||||||
|
return errors.New("service name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Service == nil {
|
||||||
|
svc, err := d.cli.InspectService(ctx, d.Spec.Name)
|
||||||
|
if err == nil {
|
||||||
|
d.Service = &svc
|
||||||
|
} else if !errors.Is(err, ErrNotFound) {
|
||||||
|
return fmt.Errorf("inspect service: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// d.Service is nil if the service doesn't exist yet (first deployment).
|
||||||
|
if d.Service == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Service.Name != d.Spec.Name {
|
||||||
|
return errors.New("service name cannot be changed")
|
||||||
|
}
|
||||||
|
if d.Service.Mode != d.Spec.Mode {
|
||||||
|
return errors.New("service mode cannot be changed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run executes the deployment plan and returns the ID of the created or updated service.
|
||||||
|
// It will create a new plan if one hasn't been created yet. The deployment will either create a new service or update
|
||||||
|
// the existing one to match the desired specification.
|
||||||
|
// TODO: forbid to run the same deployment more than once.
|
||||||
|
func (d *Deployment) Run(ctx context.Context) (string, error) {
|
||||||
|
plan, err := d.Plan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("plan: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan.ServiceID, plan.Execute(ctx, d.cli)
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"strings"
|
||||||
|
"uncloud/internal/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Operation represents a single atomic operation in a deployment process.
|
||||||
|
// Operations can be composed to form complex deployment strategies.
|
||||||
|
type Operation interface {
|
||||||
|
Execute(ctx context.Context, cli *Client) error
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunContainerOperation creates and starts a new container on a specific machine.
|
||||||
|
type RunContainerOperation struct {
|
||||||
|
ServiceID string
|
||||||
|
Spec api.ServiceSpec
|
||||||
|
MachineID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *RunContainerOperation) Execute(ctx context.Context, cli *Client) error {
|
||||||
|
resp, err := cli.CreateContainer(ctx, o.ServiceID, o.Spec, o.MachineID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create container: %w", err)
|
||||||
|
}
|
||||||
|
if err = cli.StartContainer(ctx, o.ServiceID, resp.ID); err != nil {
|
||||||
|
return fmt.Errorf("start container: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: wait for the container to become healthy
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *RunContainerOperation) String() string {
|
||||||
|
return fmt.Sprintf("RunContainerOperation[%s, %s, %s]", o.ServiceID, o.Spec.Name, o.MachineID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopContainerOperation stops a container on a specific machine.
|
||||||
|
type StopContainerOperation struct {
|
||||||
|
ServiceID string
|
||||||
|
ContainerID string
|
||||||
|
MachineID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *StopContainerOperation) Execute(ctx context.Context, cli *Client) error {
|
||||||
|
if err := cli.StopContainer(ctx, o.ServiceID, o.ContainerID, container.StopOptions{}); err != nil {
|
||||||
|
return fmt.Errorf("stop container: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *StopContainerOperation) String() string {
|
||||||
|
return fmt.Sprintf("StopContainerOperation[%s, %s, %s]", o.ServiceID, o.ContainerID, o.MachineID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveContainerOperation stops and removes a container from a specific machine.
|
||||||
|
type RemoveContainerOperation struct {
|
||||||
|
ServiceID string
|
||||||
|
ContainerID string
|
||||||
|
MachineID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *RemoveContainerOperation) Execute(ctx context.Context, cli *Client) error {
|
||||||
|
if err := cli.StopContainer(ctx, o.ServiceID, o.ContainerID, container.StopOptions{}); err != nil {
|
||||||
|
return fmt.Errorf("stop container: %w", err)
|
||||||
|
}
|
||||||
|
if err := cli.RemoveContainer(ctx, o.ServiceID, o.ContainerID, container.RemoveOptions{}); err != nil {
|
||||||
|
return fmt.Errorf("remove container: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *RemoveContainerOperation) String() string {
|
||||||
|
return fmt.Sprintf("RemoveContainerOperation[%s, %s, %s]", o.ServiceID, o.ContainerID, o.MachineID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SequenceOperation is a composite operation that executes a sequence of operations in order.
|
||||||
|
type SequenceOperation struct {
|
||||||
|
Operations []Operation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *SequenceOperation) Execute(ctx context.Context, cli *Client) error {
|
||||||
|
for _, op := range o.Operations {
|
||||||
|
if err := op.Execute(ctx, cli); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *SequenceOperation) String() string {
|
||||||
|
ops := make([]string, len(o.Operations))
|
||||||
|
for i, op := range o.Operations {
|
||||||
|
ops[i] = op.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("SequenceOperation[%s]", strings.Join(ops, ", "))
|
||||||
|
}
|
||||||
+29
-280
@@ -8,32 +8,20 @@ import (
|
|||||||
"github.com/docker/compose/v2/pkg/progress"
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
"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/network"
|
|
||||||
dockerclient "github.com/docker/docker/client"
|
|
||||||
"github.com/docker/docker/pkg/jsonmessage"
|
|
||||||
"github.com/docker/go-connections/nat"
|
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/metadata"
|
"google.golang.org/grpc/metadata"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"uncloud/internal/api"
|
"uncloud/internal/api"
|
||||||
"uncloud/internal/machine/api/pb"
|
"uncloud/internal/machine/api/pb"
|
||||||
machinedocker "uncloud/internal/machine/docker"
|
|
||||||
"uncloud/internal/secret"
|
"uncloud/internal/secret"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RunServiceResponse struct {
|
type RunServiceResponse struct {
|
||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
Containers []MachineContainerID
|
|
||||||
}
|
|
||||||
|
|
||||||
type MachineContainerID struct {
|
|
||||||
MachineID string
|
|
||||||
ContainerID string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *Client) RunService(ctx context.Context, spec api.ServiceSpec) (RunServiceResponse, error) {
|
func (cli *Client) RunService(ctx context.Context, spec api.ServiceSpec) (RunServiceResponse, error) {
|
||||||
@@ -83,7 +71,21 @@ func (cli *Client) RunService(ctx context.Context, spec api.ServiceSpec) (RunSer
|
|||||||
case "", api.ServiceModeReplicated:
|
case "", api.ServiceModeReplicated:
|
||||||
resp, err = cli.runReplicatedService(ctx, serviceID, spec)
|
resp, err = cli.runReplicatedService(ctx, serviceID, spec)
|
||||||
case api.ServiceModeGlobal:
|
case api.ServiceModeGlobal:
|
||||||
resp, err = cli.runGlobalService(ctx, serviceID, spec)
|
deploy, err := cli.NewDeployment(spec, &RollingStrategy{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create deployment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceID, err = deploy.Run(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.ID = serviceID
|
||||||
|
// TODO: get the service name from the plan when it's available.
|
||||||
|
resp.Name = spec.Name
|
||||||
|
|
||||||
|
return nil
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid mode: %q", spec.Mode)
|
return fmt.Errorf("invalid mode: %q", spec.Mode)
|
||||||
}
|
}
|
||||||
@@ -126,16 +128,10 @@ func (cli *Client) runReplicatedService(ctx context.Context, id string, spec api
|
|||||||
return resp, errors.New("no available machine to run the service")
|
return resp, errors.New("no available machine to run the service")
|
||||||
}
|
}
|
||||||
|
|
||||||
runResp, err := cli.runContainer(ctx, id, spec, m.Machine)
|
if _, err = cli.runContainer(ctx, id, spec, m.Machine); err != nil {
|
||||||
if err != nil {
|
|
||||||
return resp, fmt.Errorf("run container: %w", err)
|
return resp, fmt.Errorf("run container: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.Containers = append(resp.Containers, MachineContainerID{
|
|
||||||
MachineID: m.Machine.Id,
|
|
||||||
ContainerID: runResp.ID,
|
|
||||||
})
|
|
||||||
|
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,265 +152,21 @@ func firstAvailableMachine(machines []*pb.MachineMember) *pb.MachineMember {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *Client) runGlobalService(ctx context.Context, id string, spec api.ServiceSpec) (RunServiceResponse, error) {
|
|
||||||
resp := RunServiceResponse{
|
|
||||||
ID: id,
|
|
||||||
Name: spec.Name,
|
|
||||||
}
|
|
||||||
|
|
||||||
machines, err := cli.ListMachines(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return resp, fmt.Errorf("list machines: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
errCh := make(chan error)
|
|
||||||
mu := sync.Mutex{}
|
|
||||||
|
|
||||||
// Run a service container on each available machine.
|
|
||||||
for _, m := range machines {
|
|
||||||
if m.State != pb.MachineMember_UP && m.State != pb.MachineMember_SUSPECT {
|
|
||||||
// TODO: return failed machines in the response.
|
|
||||||
fmt.Printf("WARNING: failed to run a service container on machine '%s' which is Down.\n", m.Machine.Name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
runResp, err := cli.runContainer(ctx, id, spec, m.Machine)
|
|
||||||
if err != nil {
|
|
||||||
errCh <- fmt.Errorf("run container on machine '%s': %w", m.Machine.Name, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mu.Lock()
|
|
||||||
resp.Containers = append(resp.Containers, MachineContainerID{
|
|
||||||
MachineID: m.Machine.Id,
|
|
||||||
ContainerID: runResp.ID,
|
|
||||||
})
|
|
||||||
mu.Unlock()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(errCh)
|
|
||||||
}()
|
|
||||||
|
|
||||||
err = nil
|
|
||||||
for e := range errCh {
|
|
||||||
err = errors.Join(err, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *Client) runContainer(
|
func (cli *Client) runContainer(
|
||||||
ctx context.Context, serviceID string, spec api.ServiceSpec, machine *pb.MachineInfo,
|
ctx context.Context, serviceID string, spec api.ServiceSpec, machine *pb.MachineInfo,
|
||||||
) (container.CreateResponse, error) {
|
) (container.CreateResponse, error) {
|
||||||
var resp container.CreateResponse
|
resp, err := cli.CreateContainer(ctx, serviceID, spec, machine.Name)
|
||||||
|
|
||||||
// Proxy Docker gRPC requests to the selected machine.
|
|
||||||
machineIP, _ := machine.Network.ManagementIp.ToAddr()
|
|
||||||
md := metadata.Pairs("machines", machineIP.String())
|
|
||||||
ctx = metadata.NewOutgoingContext(ctx, md)
|
|
||||||
|
|
||||||
suffix, err := secret.RandomAlphaNumeric(4)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, fmt.Errorf("generate random suffix: %w", err)
|
|
||||||
}
|
|
||||||
containerName := fmt.Sprintf("%s-%s", spec.Name, suffix)
|
|
||||||
|
|
||||||
config := &container.Config{
|
|
||||||
Cmd: spec.Container.Command,
|
|
||||||
Image: spec.Container.Image,
|
|
||||||
Labels: map[string]string{
|
|
||||||
api.LabelServiceID: serviceID,
|
|
||||||
api.LabelServiceName: spec.Name,
|
|
||||||
api.LabelManaged: "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if spec.Mode == api.ServiceModeGlobal {
|
|
||||||
config.Labels[api.LabelServiceMode] = api.ServiceModeGlobal
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(spec.Ports) > 0 {
|
|
||||||
encodedPorts := make([]string, len(spec.Ports))
|
|
||||||
for i, p := range spec.Ports {
|
|
||||||
encodedPorts[i], err = p.String()
|
|
||||||
if err != nil {
|
|
||||||
return resp, fmt.Errorf("encode service port spec: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.Labels[api.LabelServicePorts] = strings.Join(encodedPorts, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
portBindings := make(nat.PortMap)
|
|
||||||
for _, p := range spec.Ports {
|
|
||||||
if p.Mode != api.PortModeHost {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
port := nat.Port(fmt.Sprintf("%d/%s", p.ContainerPort, p.Protocol))
|
|
||||||
portBindings[port] = []nat.PortBinding{
|
|
||||||
{
|
|
||||||
HostPort: strconv.Itoa(int(p.PublishedPort)),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if p.HostIP.IsValid() {
|
|
||||||
portBindings[port][0].HostIP = p.HostIP.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hostConfig := &container.HostConfig{
|
|
||||||
Binds: spec.Container.Volumes,
|
|
||||||
Init: spec.Container.Init,
|
|
||||||
PortBindings: portBindings,
|
|
||||||
}
|
|
||||||
netConfig := &network.NetworkingConfig{
|
|
||||||
EndpointsConfig: map[string]*network.EndpointSettings{
|
|
||||||
machinedocker.NetworkName: {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pw := progress.ContextWriter(ctx)
|
|
||||||
eventID := fmt.Sprintf("Container %s on %s", containerName, machine.Name)
|
|
||||||
|
|
||||||
pw.Event(progress.CreatingEvent(eventID))
|
|
||||||
resp, err = cli.CreateContainer(ctx, config, hostConfig, netConfig, nil, containerName)
|
|
||||||
if err != nil {
|
|
||||||
if !dockerclient.IsErrNotFound(err) {
|
|
||||||
return resp, fmt.Errorf("create container: %w", err)
|
return resp, fmt.Errorf("create container: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pull the missing image and create the container again.
|
if err = cli.StartContainer(ctx, serviceID, resp.ID); err != nil {
|
||||||
if err = cli.pullImageWithProgress(ctx, config.Image, machine.Name, eventID); err != nil {
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
if resp, err = cli.CreateContainer(ctx, config, hostConfig, netConfig, nil, containerName); err != nil {
|
|
||||||
return resp, fmt.Errorf("create container: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pw.Event(progress.CreatedEvent(eventID))
|
|
||||||
|
|
||||||
pw.Event(progress.StartingEvent(eventID))
|
|
||||||
if err = cli.StartContainer(ctx, resp.ID, container.StartOptions{}); err != nil {
|
|
||||||
return resp, fmt.Errorf("start container: %w", err)
|
return resp, fmt.Errorf("start container: %w", err)
|
||||||
}
|
}
|
||||||
pw.Event(progress.StartedEvent(eventID))
|
|
||||||
|
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *Client) pullImageWithProgress(ctx context.Context, image, machineName, parentEventID string) error {
|
|
||||||
pw := progress.ContextWriter(ctx)
|
|
||||||
eventID := fmt.Sprintf("Image %s on %s", image, machineName)
|
|
||||||
pw.Event(progress.Event{
|
|
||||||
ID: eventID,
|
|
||||||
ParentID: parentEventID,
|
|
||||||
Status: progress.Working,
|
|
||||||
StatusText: "Pulling",
|
|
||||||
})
|
|
||||||
|
|
||||||
pullCh, err := cli.PullImage(ctx, image)
|
|
||||||
if err != nil {
|
|
||||||
pw.Event(progress.Event{
|
|
||||||
ID: eventID,
|
|
||||||
ParentID: parentEventID,
|
|
||||||
Text: "Error",
|
|
||||||
Status: progress.Error,
|
|
||||||
StatusText: errors.Unwrap(err).Error(),
|
|
||||||
})
|
|
||||||
return fmt.Errorf("pull image: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for pull to complete by reading all progress messages and converting them to events.
|
|
||||||
for msg := range pullCh {
|
|
||||||
if msg.Err != nil {
|
|
||||||
err = msg.Err
|
|
||||||
} else {
|
|
||||||
if msg.Message.Error != nil {
|
|
||||||
err = errors.New(msg.Message.Error.Message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
pw.Event(progress.Event{
|
|
||||||
ID: eventID,
|
|
||||||
ParentID: parentEventID,
|
|
||||||
Text: "Error",
|
|
||||||
Status: progress.Error,
|
|
||||||
StatusText: errors.Unwrap(err).Error(),
|
|
||||||
})
|
|
||||||
return fmt.Errorf("pull image: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: add like in compose: --quiet-pull Pull without printing progress information
|
|
||||||
e := toPullProgressEvent(msg.Message)
|
|
||||||
if e != nil {
|
|
||||||
e.ID = fmt.Sprintf("%s on %s", e.ID, machineName)
|
|
||||||
e.ParentID = eventID
|
|
||||||
// Grand children events are not printed by the tty progress writer but they are still required
|
|
||||||
// to calculate the progress line of their parent.
|
|
||||||
pw.Event(*e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pw.Event(progress.Event{
|
|
||||||
ID: eventID,
|
|
||||||
ParentID: parentEventID,
|
|
||||||
Status: progress.Done,
|
|
||||||
StatusText: "Pulled",
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// toPullProgressEvent converts a JSON progress message from the Docker API to a progress event.
|
|
||||||
// It's based on toPullProgressEvent from Docker Compose.
|
|
||||||
func toPullProgressEvent(jm jsonmessage.JSONMessage) *progress.Event {
|
|
||||||
if jm.ID == "" || jm.Progress == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
total int64
|
|
||||||
percent int
|
|
||||||
current int64
|
|
||||||
)
|
|
||||||
text := jm.Progress.String()
|
|
||||||
stat := progress.Working
|
|
||||||
|
|
||||||
switch jm.Status {
|
|
||||||
case "Preparing", "Waiting", "Pulling fs layer":
|
|
||||||
percent = 0
|
|
||||||
case "Downloading", "Extracting", "Verifying Checksum":
|
|
||||||
current = jm.Progress.Current
|
|
||||||
total = jm.Progress.Total
|
|
||||||
if jm.Progress.Total > 0 {
|
|
||||||
percent = int(jm.Progress.Current * 100 / jm.Progress.Total)
|
|
||||||
}
|
|
||||||
case "Download complete", "Already exists", "Pull complete":
|
|
||||||
stat = progress.Done
|
|
||||||
percent = 100
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(jm.Status, "Image is up to date") ||
|
|
||||||
strings.Contains(jm.Status, "Downloaded newer image") {
|
|
||||||
stat = progress.Done
|
|
||||||
percent = 100
|
|
||||||
}
|
|
||||||
|
|
||||||
return &progress.Event{
|
|
||||||
ID: jm.ID,
|
|
||||||
Current: current,
|
|
||||||
Total: total,
|
|
||||||
Percent: percent,
|
|
||||||
Text: jm.Status,
|
|
||||||
Status: stat,
|
|
||||||
StatusText: text,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InspectService returns detailed information about a service and its containers.
|
// InspectService returns detailed information about a service and its containers.
|
||||||
// The id parameter can be either a service ID or name.
|
// The id parameter can be either a service ID or name.
|
||||||
func (cli *Client) InspectService(ctx context.Context, id string) (api.Service, error) {
|
func (cli *Client) InspectService(ctx context.Context, id string) (api.Service, error) {
|
||||||
@@ -447,7 +199,7 @@ func (cli *Client) InspectService(ctx context.Context, id string) (api.Service,
|
|||||||
filters.Arg("label", api.LabelManaged),
|
filters.Arg("label", api.LabelManaged),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
machineContainers, err := cli.ListContainers(listCtx, opts)
|
machineContainers, err := cli.Docker.ListContainers(listCtx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return svc, fmt.Errorf("list containers: %w", err)
|
return svc, fmt.Errorf("list containers: %w", err)
|
||||||
}
|
}
|
||||||
@@ -483,7 +235,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,
|
||||||
@@ -513,7 +265,7 @@ func (cli *Client) InspectService(ctx context.Context, id string) (api.Service,
|
|||||||
serviceID := containers[0].Container.ServiceID()
|
serviceID := containers[0].Container.ServiceID()
|
||||||
for _, mc := range containers[1:] {
|
for _, mc := range containers[1:] {
|
||||||
if mc.Container.ServiceID() != serviceID {
|
if mc.Container.ServiceID() != serviceID {
|
||||||
return svc, fmt.Errorf("multiple services found with name: %s", id)
|
return svc, fmt.Errorf("multiple services found with name '%s', use the service ID instead", id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -582,19 +334,16 @@ func (cli *Client) RemoveService(ctx context.Context, id string) error {
|
|||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
machineIP, ok := machineManagementIPByID[mc.MachineID]
|
err := cli.StopContainer(ctx, svc.ID, mc.Container.ID, container.StopOptions{})
|
||||||
if !ok {
|
if err != nil {
|
||||||
errCh <- fmt.Errorf("machine not found by ID: %s", mc.MachineID)
|
errCh <- fmt.Errorf("stop container '%s': %w", mc.Container.ID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
removeCtx := metadata.NewOutgoingContext(ctx, metadata.Pairs("machines", machineIP))
|
|
||||||
// TODO: gracefully stop the container before removing it without force.
|
err = cli.RemoveContainer(ctx, svc.ID, mc.Container.ID, container.RemoveOptions{})
|
||||||
err := cli.RemoveContainer(removeCtx, mc.Container.ID, container.RemoveOptions{Force: true})
|
if err != nil && !errors.Is(err, ErrNotFound) {
|
||||||
if err != nil {
|
|
||||||
if !dockerclient.IsErrNotFound(err) {
|
|
||||||
errCh <- fmt.Errorf("remove container '%s': %w", mc.Container.ID, err)
|
errCh <- fmt.Errorf("remove container '%s': %w", mc.Container.ID, err)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -636,7 +385,7 @@ func (cli *Client) ListServices(ctx context.Context) ([]api.Service, error) {
|
|||||||
filters.Arg("label", api.LabelManaged),
|
filters.Arg("label", api.LabelManaged),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
machineContainers, err := cli.ListContainers(listCtx, opts)
|
machineContainers, err := cli.Docker.ListContainers(listCtx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("list containers: %w", err)
|
return nil, fmt.Errorf("list containers: %w", err)
|
||||||
}
|
}
|
||||||
@@ -653,7 +402,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"uncloud/internal/api"
|
||||||
|
"uncloud/internal/machine/api/pb"
|
||||||
|
"uncloud/internal/secret"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Strategy defines how a service should be deployed or updated. Different implementations can provide various
|
||||||
|
// deployment patterns such as rolling updates, blue/green deployments, etc.
|
||||||
|
type Strategy interface {
|
||||||
|
// Plan returns the operation to reconcile the service to the desired state.
|
||||||
|
// If the service does not exist (new deployment), svc will be nil.
|
||||||
|
Plan(ctx context.Context, cli *Client, svc *api.Service, spec api.ServiceSpec) (Plan, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RollingStrategy implements a rolling update deployment pattern where containers are updated one at a time
|
||||||
|
// to minimize service disruption.
|
||||||
|
type RollingStrategy struct{}
|
||||||
|
|
||||||
|
func (s *RollingStrategy) Plan(
|
||||||
|
ctx context.Context, cli *Client, svc *api.Service, spec api.ServiceSpec,
|
||||||
|
) (Plan, error) {
|
||||||
|
switch spec.Mode {
|
||||||
|
case "", api.ServiceModeReplicated:
|
||||||
|
return s.planReplicated(ctx, cli, svc, spec)
|
||||||
|
case api.ServiceModeGlobal:
|
||||||
|
return s.planGlobal(ctx, cli, svc, spec)
|
||||||
|
default:
|
||||||
|
return Plan{}, fmt.Errorf("unsupported service mode: %s", spec.Mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// planReplicated creates a plan for a replicated service deployment.
|
||||||
|
func (s *RollingStrategy) planReplicated(
|
||||||
|
ctx context.Context, cli *Client, svc *api.Service, spec api.ServiceSpec,
|
||||||
|
) (Plan, error) {
|
||||||
|
return Plan{}, errors.New("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// planGlobal creates a plan for a global service deployment, ensuring one container runs on each available machine.
|
||||||
|
// For machines with an existing container, it attempts to start a new container before removing the old one if
|
||||||
|
// possible. If the new container would have port conflicts with the existing one, the old container is removed first.
|
||||||
|
// It handles multiple containers per machine (though this should not occur in normal operation) and skips machines
|
||||||
|
// that are down.
|
||||||
|
func (s *RollingStrategy) planGlobal(
|
||||||
|
ctx context.Context, cli *Client, svc *api.Service, spec api.ServiceSpec,
|
||||||
|
) (Plan, error) {
|
||||||
|
var plan Plan
|
||||||
|
// Map machineID to service containers on that machine. For the global mode, there should be at most one
|
||||||
|
// container per machine but we use a slice to handle multiple containers that may exist due to a bug
|
||||||
|
// or interruption in the previous deployment.
|
||||||
|
containersOnMachine := make(map[string][]api.MachineContainer)
|
||||||
|
if svc != nil {
|
||||||
|
plan.ServiceID = svc.ID
|
||||||
|
for _, c := range svc.Containers {
|
||||||
|
containersOnMachine[c.MachineID] = append(containersOnMachine[c.MachineID], c)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Generate a new service ID for the first service deployment.
|
||||||
|
var err error
|
||||||
|
plan.ServiceID, err = secret.NewID()
|
||||||
|
if err != nil {
|
||||||
|
return plan, fmt.Errorf("generate service ID: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
machines, err := cli.ListMachines(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return plan, fmt.Errorf("list machines: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
seqOp := &SequenceOperation{}
|
||||||
|
// TODO: figure out how to return a warning if there are machines down. Embed the machinesDown in the plan?
|
||||||
|
// WARNING: failed to run a service container on machine '%s' which is Down.
|
||||||
|
var machinesDown []*pb.MachineInfo
|
||||||
|
for _, m := range machines {
|
||||||
|
// Skip machines that are down but collect them to report a warning later.
|
||||||
|
if m.State == pb.MachineMember_DOWN {
|
||||||
|
machinesDown = append(machinesDown, m.Machine)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
containers := containersOnMachine[m.Machine.Id]
|
||||||
|
ops, err := reconcileGlobalContainer(containers, spec, plan.ServiceID, m.Machine.Id)
|
||||||
|
if err != nil {
|
||||||
|
return plan, err
|
||||||
|
}
|
||||||
|
seqOp.Operations = append(seqOp.Operations, ops...)
|
||||||
|
}
|
||||||
|
plan.Operation = seqOp
|
||||||
|
|
||||||
|
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(
|
||||||
|
containers []api.MachineContainer, spec api.ServiceSpec, serviceID, machineID string,
|
||||||
|
) ([]Operation, error) {
|
||||||
|
var ops []Operation
|
||||||
|
|
||||||
|
if len(containers) == 0 {
|
||||||
|
// No containers on this machine, create a new one.
|
||||||
|
ops = append(ops, &RunContainerOperation{
|
||||||
|
ServiceID: serviceID,
|
||||||
|
Spec: spec,
|
||||||
|
MachineID: machineID,
|
||||||
|
})
|
||||||
|
return ops, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there is a container with the same spec already running. If so, remove the rest.
|
||||||
|
upToDate := false
|
||||||
|
for i, c := range containers {
|
||||||
|
if !c.Container.State.Running || c.Container.State.Paused {
|
||||||
|
// Skip containers that are not running.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
svcSpec, err := c.Container.ServiceSpec()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get service spec: %w", err)
|
||||||
|
}
|
||||||
|
if svcSpec.Equals(spec) {
|
||||||
|
// The container is already running with the same spec.
|
||||||
|
upToDate = true
|
||||||
|
for j, old := range containers {
|
||||||
|
if i == j {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ops = append(ops, &RemoveContainerOperation{
|
||||||
|
ServiceID: serviceID,
|
||||||
|
ContainerID: old.Container.ID,
|
||||||
|
MachineID: old.MachineID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if upToDate {
|
||||||
|
return ops, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The machine has containers but none of them match the new spec.
|
||||||
|
// Stop the old running containers that have conflicting ports with the new spec before running a new one.
|
||||||
|
for _, c := range containers {
|
||||||
|
if c.Container.State.Running {
|
||||||
|
conflictingPorts, err := c.Container.ConflictingServicePorts(spec.Ports)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("check conflicting ports: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(conflictingPorts) > 0 {
|
||||||
|
// Stop the running container with conflicting ports.
|
||||||
|
ops = append(ops, &StopContainerOperation{
|
||||||
|
ServiceID: serviceID,
|
||||||
|
ContainerID: c.Container.ID,
|
||||||
|
MachineID: c.MachineID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run a new container.
|
||||||
|
ops = append(ops, &RunContainerOperation{
|
||||||
|
ServiceID: serviceID,
|
||||||
|
Spec: spec,
|
||||||
|
MachineID: machineID,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Remove the old containers.
|
||||||
|
for _, c := range containers {
|
||||||
|
ops = append(ops, &RemoveContainerOperation{
|
||||||
|
ServiceID: serviceID,
|
||||||
|
ContainerID: c.Container.ID,
|
||||||
|
MachineID: c.MachineID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return ops, nil
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ type MachineConnection struct {
|
|||||||
TCP netip.AddrPort `toml:"tcp,omitempty"`
|
TCP netip.AddrPort `toml:"tcp,omitempty"`
|
||||||
Host string `toml:"host,omitempty"`
|
Host string `toml:"host,omitempty"`
|
||||||
PublicKey secret.Secret `toml:"public_key,omitempty"`
|
PublicKey secret.Secret `toml:"public_key,omitempty"`
|
||||||
|
SSHKeyFile string `toml:"ssh_key_file,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSHDestination represents an SSH destination string in the canonical form of "user@host:port".
|
// SSHDestination represents an SSH destination string in the canonical form of "user@host:port".
|
||||||
|
|||||||
+22
-10
@@ -17,24 +17,36 @@ type RemoteMachine struct {
|
|||||||
KeyPath string
|
KeyPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func installCmd(user string) string {
|
||||||
|
sudoPrefix := "sudo"
|
||||||
|
// Add the SSH user (non-root) to the uncloud group to allow access to the Uncloud daemon unix socket.
|
||||||
|
env := "UNCLOUD_GROUP_ADD_USER=" + user
|
||||||
|
|
||||||
|
curlBashCmd := fmt.Sprintf(
|
||||||
|
"curl -fsSL %s | %s %s bash", sshexec.Quote(installScriptURL), sudoPrefix, sshexec.Quote(env),
|
||||||
|
)
|
||||||
|
|
||||||
|
if user == "root" {
|
||||||
|
curlBashCmd = fmt.Sprintf(
|
||||||
|
"curl -fsSL %s | bash", sshexec.Quote(installScriptURL),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return curlBashCmd
|
||||||
|
}
|
||||||
|
|
||||||
// provisionMachine provisions the remote machine by downloading the Uncloud install script from GitHub and running it.
|
// provisionMachine provisions the remote machine by downloading the Uncloud install script from GitHub and running it.
|
||||||
func provisionMachine(ctx context.Context, exec sshexec.Executor) error {
|
func provisionMachine(ctx context.Context, exec sshexec.Executor) error {
|
||||||
user, err := exec.Run(ctx, "whoami")
|
user, err := exec.Run(ctx, "whoami")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("run whoami: %w", err)
|
return fmt.Errorf("run whoami: %w", err)
|
||||||
}
|
}
|
||||||
sudoPrefix, env := "", ""
|
|
||||||
if user != "root" {
|
installCmd := installCmd(user)
|
||||||
sudoPrefix = "sudo"
|
|
||||||
// Add the SSH user (non-root) to the uncloud group to allow access to the Uncloud daemon unix socket.
|
|
||||||
env = "UNCLOUD_GROUP_ADD_USER=" + user
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Downloading Uncloud install script:", installScriptURL)
|
fmt.Println("Downloading Uncloud install script:", installScriptURL)
|
||||||
curlBashCmd := fmt.Sprintf(
|
|
||||||
"curl -fsSL %s | %s %s bash", sshexec.Quote(installScriptURL), sudoPrefix, sshexec.Quote(env),
|
cmd := sshexec.QuoteCommand("bash", "-c", "set -o pipefail; "+installCmd)
|
||||||
)
|
|
||||||
cmd := sshexec.QuoteCommand("bash", "-c", "set -o pipefail; "+curlBashCmd)
|
|
||||||
if err = exec.Stream(ctx, cmd, os.Stdout, os.Stderr); err != nil {
|
if err = exec.Stream(ctx, cmd, os.Stdout, os.Stderr); err != nil {
|
||||||
return fmt.Errorf("download and run install script: %w", err)
|
return fmt.Errorf("download and run install script: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInstallCmd(t *testing.T) {
|
||||||
|
t.Run("root", func(t *testing.T) {
|
||||||
|
cmd := installCmd("root")
|
||||||
|
assert.NotContains(t, cmd, "sudo")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nonroot", func(t *testing.T) {
|
||||||
|
cmd := installCmd("nonroot")
|
||||||
|
assert.Contains(t, cmd, "sudo")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -5,8 +5,23 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func ExpandHomeDir(path string) string {
|
||||||
|
if len(path) == 0 {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
if path[0] == '~' {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
return strings.Replace(path, "~", home, 1)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
// LookupUIDGID returns the user and group IDs for the given username.
|
// LookupUIDGID returns the user and group IDs for the given username.
|
||||||
func LookupUIDGID(username string) (uid, gid int, err error) {
|
func LookupUIDGID(username string) (uid, gid int, err error) {
|
||||||
usr, err := user.Lookup(username)
|
usr, err := user.Lookup(username)
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package fs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExpandHomeDir(t *testing.T) {
|
||||||
|
t.Run("empty", func(t *testing.T) {
|
||||||
|
assert.Equal(t, "", ExpandHomeDir(""))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no home", func(t *testing.T) {
|
||||||
|
assert.Equal(t, "/path", ExpandHomeDir("/path"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("home", func(t *testing.T) {
|
||||||
|
t.Setenv("HOME", "/home/user")
|
||||||
|
assert.Equal(t, "/home/user/path", ExpandHomeDir("~/path"))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -152,6 +152,101 @@ func (x *CreateContainerResponse) GetResponse() []byte {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InspectContainerRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectContainerRequest) Reset() {
|
||||||
|
*x = InspectContainerRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[2]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectContainerRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*InspectContainerRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *InspectContainerRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[2]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use InspectContainerRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*InspectContainerRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{2}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectContainerRequest) GetId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type InspectContainerResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// JSON serialized container.InspectResponse.
|
||||||
|
Response []byte `protobuf:"bytes,1,opt,name=response,proto3" json:"response,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectContainerResponse) Reset() {
|
||||||
|
*x = InspectContainerResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[3]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectContainerResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*InspectContainerResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *InspectContainerResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[3]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use InspectContainerResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*InspectContainerResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{3}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *InspectContainerResponse) GetResponse() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Response
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type StartContainerRequest struct {
|
type StartContainerRequest struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -165,7 +260,7 @@ type StartContainerRequest struct {
|
|||||||
func (x *StartContainerRequest) Reset() {
|
func (x *StartContainerRequest) Reset() {
|
||||||
*x = StartContainerRequest{}
|
*x = StartContainerRequest{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[2]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[4]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -178,7 +273,7 @@ func (x *StartContainerRequest) String() string {
|
|||||||
func (*StartContainerRequest) ProtoMessage() {}
|
func (*StartContainerRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *StartContainerRequest) ProtoReflect() protoreflect.Message {
|
func (x *StartContainerRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[2]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[4]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -191,7 +286,7 @@ func (x *StartContainerRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use StartContainerRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use StartContainerRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*StartContainerRequest) Descriptor() ([]byte, []int) {
|
func (*StartContainerRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{2}
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{4}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *StartContainerRequest) GetId() string {
|
func (x *StartContainerRequest) GetId() string {
|
||||||
@@ -208,6 +303,62 @@ func (x *StartContainerRequest) GetOptions() []byte {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StopContainerRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||||
|
// JSON serialized container.StopOptions.
|
||||||
|
Options []byte `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *StopContainerRequest) Reset() {
|
||||||
|
*x = StopContainerRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[5]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *StopContainerRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*StopContainerRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *StopContainerRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[5]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use StopContainerRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*StopContainerRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{5}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *StopContainerRequest) GetId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *StopContainerRequest) GetOptions() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Options
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type ListContainersRequest struct {
|
type ListContainersRequest struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -220,7 +371,7 @@ type ListContainersRequest struct {
|
|||||||
func (x *ListContainersRequest) Reset() {
|
func (x *ListContainersRequest) Reset() {
|
||||||
*x = ListContainersRequest{}
|
*x = ListContainersRequest{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[3]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[6]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -233,7 +384,7 @@ func (x *ListContainersRequest) String() string {
|
|||||||
func (*ListContainersRequest) ProtoMessage() {}
|
func (*ListContainersRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ListContainersRequest) ProtoReflect() protoreflect.Message {
|
func (x *ListContainersRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[3]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[6]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -246,7 +397,7 @@ func (x *ListContainersRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ListContainersRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ListContainersRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*ListContainersRequest) Descriptor() ([]byte, []int) {
|
func (*ListContainersRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{3}
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{6}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ListContainersRequest) GetOptions() []byte {
|
func (x *ListContainersRequest) GetOptions() []byte {
|
||||||
@@ -268,7 +419,7 @@ type ListContainersResponse struct {
|
|||||||
func (x *ListContainersResponse) Reset() {
|
func (x *ListContainersResponse) Reset() {
|
||||||
*x = ListContainersResponse{}
|
*x = ListContainersResponse{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[4]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[7]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -281,7 +432,7 @@ func (x *ListContainersResponse) String() string {
|
|||||||
func (*ListContainersResponse) ProtoMessage() {}
|
func (*ListContainersResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ListContainersResponse) ProtoReflect() protoreflect.Message {
|
func (x *ListContainersResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[4]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[7]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -294,7 +445,7 @@ func (x *ListContainersResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ListContainersResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ListContainersResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*ListContainersResponse) Descriptor() ([]byte, []int) {
|
func (*ListContainersResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{4}
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{7}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ListContainersResponse) GetMessages() []*MachineContainers {
|
func (x *ListContainersResponse) GetMessages() []*MachineContainers {
|
||||||
@@ -310,14 +461,14 @@ 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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *MachineContainers) Reset() {
|
func (x *MachineContainers) Reset() {
|
||||||
*x = MachineContainers{}
|
*x = MachineContainers{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[5]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[8]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -330,7 +481,7 @@ func (x *MachineContainers) String() string {
|
|||||||
func (*MachineContainers) ProtoMessage() {}
|
func (*MachineContainers) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *MachineContainers) ProtoReflect() protoreflect.Message {
|
func (x *MachineContainers) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[5]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[8]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -343,7 +494,7 @@ func (x *MachineContainers) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use MachineContainers.ProtoReflect.Descriptor instead.
|
// Deprecated: Use MachineContainers.ProtoReflect.Descriptor instead.
|
||||||
func (*MachineContainers) Descriptor() ([]byte, []int) {
|
func (*MachineContainers) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{5}
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{8}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *MachineContainers) GetMetadata() *Metadata {
|
func (x *MachineContainers) GetMetadata() *Metadata {
|
||||||
@@ -373,7 +524,7 @@ type RemoveContainerRequest struct {
|
|||||||
func (x *RemoveContainerRequest) Reset() {
|
func (x *RemoveContainerRequest) Reset() {
|
||||||
*x = RemoveContainerRequest{}
|
*x = RemoveContainerRequest{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[6]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[9]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -386,7 +537,7 @@ func (x *RemoveContainerRequest) String() string {
|
|||||||
func (*RemoveContainerRequest) ProtoMessage() {}
|
func (*RemoveContainerRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *RemoveContainerRequest) ProtoReflect() protoreflect.Message {
|
func (x *RemoveContainerRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[6]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[9]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -399,7 +550,7 @@ func (x *RemoveContainerRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use RemoveContainerRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use RemoveContainerRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*RemoveContainerRequest) Descriptor() ([]byte, []int) {
|
func (*RemoveContainerRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{6}
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{9}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RemoveContainerRequest) GetId() string {
|
func (x *RemoveContainerRequest) GetId() string {
|
||||||
@@ -429,7 +580,7 @@ type PullImageRequest struct {
|
|||||||
func (x *PullImageRequest) Reset() {
|
func (x *PullImageRequest) Reset() {
|
||||||
*x = PullImageRequest{}
|
*x = PullImageRequest{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[7]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[10]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -442,7 +593,7 @@ func (x *PullImageRequest) String() string {
|
|||||||
func (*PullImageRequest) ProtoMessage() {}
|
func (*PullImageRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *PullImageRequest) ProtoReflect() protoreflect.Message {
|
func (x *PullImageRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[7]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[10]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -455,7 +606,7 @@ func (x *PullImageRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use PullImageRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use PullImageRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*PullImageRequest) Descriptor() ([]byte, []int) {
|
func (*PullImageRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{7}
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{10}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *PullImageRequest) GetImage() string {
|
func (x *PullImageRequest) GetImage() string {
|
||||||
@@ -484,7 +635,7 @@ type JSONMessage struct {
|
|||||||
func (x *JSONMessage) Reset() {
|
func (x *JSONMessage) Reset() {
|
||||||
*x = JSONMessage{}
|
*x = JSONMessage{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[8]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[11]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -497,7 +648,7 @@ func (x *JSONMessage) String() string {
|
|||||||
func (*JSONMessage) ProtoMessage() {}
|
func (*JSONMessage) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *JSONMessage) ProtoReflect() protoreflect.Message {
|
func (x *JSONMessage) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[8]
|
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[11]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -510,7 +661,7 @@ func (x *JSONMessage) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use JSONMessage.ProtoReflect.Descriptor instead.
|
// Deprecated: Use JSONMessage.ProtoReflect.Descriptor instead.
|
||||||
func (*JSONMessage) Descriptor() ([]byte, []int) {
|
func (*JSONMessage) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{8}
|
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{11}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *JSONMessage) GetMessage() []byte {
|
func (x *JSONMessage) GetMessage() []byte {
|
||||||
@@ -544,62 +695,82 @@ var file_internal_machine_api_pb_docker_proto_rawDesc = []byte{
|
|||||||
0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70,
|
0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70,
|
||||||
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
||||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
||||||
0x22, 0x41, 0x0a, 0x15, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
|
0x22, 0x29, 0x0a, 0x17, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61,
|
||||||
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18,
|
0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69,
|
||||||
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74,
|
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x36, 0x0a, 0x18, 0x49,
|
||||||
0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69,
|
0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52,
|
||||||
0x6f, 0x6e, 0x73, 0x22, 0x31, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61,
|
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f,
|
||||||
0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07,
|
0x6e, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f,
|
||||||
0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f,
|
0x6e, 0x73, 0x65, 0x22, 0x41, 0x0a, 0x15, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74,
|
||||||
0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x4c, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f,
|
0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02,
|
||||||
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07,
|
||||||
0x12, 0x32, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
|
0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f,
|
||||||
0x28, 0x0b, 0x32, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
|
0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x40, 0x0a, 0x14, 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f,
|
||||||
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73,
|
|
||||||
0x61, 0x67, 0x65, 0x73, 0x22, 0x5e, 0x0a, 0x11, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x43,
|
|
||||||
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74,
|
|
||||||
0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70,
|
|
||||||
0x69, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61,
|
|
||||||
0x64, 0x61, 0x74, 0x61, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
|
|
||||||
0x72, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69,
|
|
||||||
0x6e, 0x65, 0x72, 0x73, 0x22, 0x42, 0x0a, 0x16, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f,
|
|
||||||
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e,
|
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e,
|
||||||
0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18,
|
0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18,
|
||||||
0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52,
|
0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52,
|
||||||
0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x42, 0x0a, 0x10, 0x50, 0x75, 0x6c, 0x6c,
|
0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x31, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74,
|
||||||
0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05,
|
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
||||||
0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6d, 0x61,
|
0x74, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01,
|
||||||
0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20,
|
0x28, 0x0c, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x4c, 0x0a, 0x16, 0x4c,
|
||||||
0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x27, 0x0a, 0x0b,
|
0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73,
|
||||||
0x4a, 0x53, 0x4f, 0x4e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d,
|
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
|
||||||
0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6d, 0x65,
|
0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61,
|
||||||
0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0xe7, 0x02, 0x0a, 0x06, 0x44, 0x6f, 0x63, 0x6b, 0x65, 0x72,
|
0x63, 0x68, 0x69, 0x6e, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52,
|
||||||
0x12, 0x4c, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
|
0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x5e, 0x0a, 0x11, 0x4d, 0x61, 0x63,
|
||||||
0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
|
0x68, 0x69, 0x6e, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x29,
|
||||||
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
|
||||||
0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e,
|
0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52,
|
||||||
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44,
|
0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e,
|
||||||
0x0a, 0x0e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
|
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x63,
|
||||||
0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74,
|
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x22, 0x42, 0x0a, 0x16, 0x52, 0x65, 0x6d,
|
||||||
0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67,
|
0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75,
|
||||||
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
|
0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||||
0x6d, 0x70, 0x74, 0x79, 0x12, 0x49, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74,
|
0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02,
|
||||||
0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73,
|
0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x42, 0x0a,
|
||||||
0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
|
0x10, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
||||||
0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e,
|
0x74, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||||
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f,
|
||||||
0x46, 0x0a, 0x0f, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
|
0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e,
|
||||||
0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43,
|
0x73, 0x22, 0x27, 0x0a, 0x0b, 0x4a, 0x53, 0x4f, 0x4e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
|
||||||
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
|
0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||||
0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
|
0x0c, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0xfc, 0x03, 0x0a, 0x06, 0x44,
|
||||||
0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x36, 0x0a, 0x09, 0x50, 0x75, 0x6c, 0x6c, 0x49,
|
0x6f, 0x63, 0x6b, 0x65, 0x72, 0x12, 0x4c, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43,
|
||||||
0x6d, 0x61, 0x67, 0x65, 0x12, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x75, 0x6c, 0x6c, 0x49,
|
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43,
|
||||||
0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x61, 0x70,
|
0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65,
|
||||||
0x69, 0x2e, 0x4a, 0x53, 0x4f, 0x4e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x30, 0x01, 0x42,
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61,
|
||||||
0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73,
|
0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||||
0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64,
|
0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x10, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f,
|
||||||
0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e,
|
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e,
|
||||||
0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65,
|
||||||
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70,
|
||||||
|
0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70,
|
||||||
|
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e,
|
||||||
|
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61,
|
||||||
|
0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||||
|
0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||||
|
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0d, 0x53, 0x74,
|
||||||
|
0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x61, 0x70,
|
||||||
|
0x69, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52,
|
||||||
|
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
|
||||||
|
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x49,
|
||||||
|
0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73,
|
||||||
|
0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61,
|
||||||
|
0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61,
|
||||||
|
0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
|
||||||
|
0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0f, 0x52, 0x65, 0x6d,
|
||||||
|
0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61,
|
||||||
|
0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
|
||||||
|
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
|
||||||
|
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74,
|
||||||
|
0x79, 0x12, 0x36, 0x0a, 0x09, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x15,
|
||||||
|
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65,
|
||||||
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x53, 0x4f, 0x4e,
|
||||||
|
0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x30, 0x01, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74,
|
||||||
|
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73,
|
||||||
|
0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72,
|
||||||
|
0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f,
|
||||||
|
0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -614,35 +785,42 @@ func file_internal_machine_api_pb_docker_proto_rawDescGZIP() []byte {
|
|||||||
return file_internal_machine_api_pb_docker_proto_rawDescData
|
return file_internal_machine_api_pb_docker_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_internal_machine_api_pb_docker_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
|
var file_internal_machine_api_pb_docker_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
|
||||||
var file_internal_machine_api_pb_docker_proto_goTypes = []any{
|
var file_internal_machine_api_pb_docker_proto_goTypes = []any{
|
||||||
(*CreateContainerRequest)(nil), // 0: api.CreateContainerRequest
|
(*CreateContainerRequest)(nil), // 0: api.CreateContainerRequest
|
||||||
(*CreateContainerResponse)(nil), // 1: api.CreateContainerResponse
|
(*CreateContainerResponse)(nil), // 1: api.CreateContainerResponse
|
||||||
(*StartContainerRequest)(nil), // 2: api.StartContainerRequest
|
(*InspectContainerRequest)(nil), // 2: api.InspectContainerRequest
|
||||||
(*ListContainersRequest)(nil), // 3: api.ListContainersRequest
|
(*InspectContainerResponse)(nil), // 3: api.InspectContainerResponse
|
||||||
(*ListContainersResponse)(nil), // 4: api.ListContainersResponse
|
(*StartContainerRequest)(nil), // 4: api.StartContainerRequest
|
||||||
(*MachineContainers)(nil), // 5: api.MachineContainers
|
(*StopContainerRequest)(nil), // 5: api.StopContainerRequest
|
||||||
(*RemoveContainerRequest)(nil), // 6: api.RemoveContainerRequest
|
(*ListContainersRequest)(nil), // 6: api.ListContainersRequest
|
||||||
(*PullImageRequest)(nil), // 7: api.PullImageRequest
|
(*ListContainersResponse)(nil), // 7: api.ListContainersResponse
|
||||||
(*JSONMessage)(nil), // 8: api.JSONMessage
|
(*MachineContainers)(nil), // 8: api.MachineContainers
|
||||||
(*Metadata)(nil), // 9: api.Metadata
|
(*RemoveContainerRequest)(nil), // 9: api.RemoveContainerRequest
|
||||||
(*emptypb.Empty)(nil), // 10: google.protobuf.Empty
|
(*PullImageRequest)(nil), // 10: api.PullImageRequest
|
||||||
|
(*JSONMessage)(nil), // 11: api.JSONMessage
|
||||||
|
(*Metadata)(nil), // 12: api.Metadata
|
||||||
|
(*emptypb.Empty)(nil), // 13: google.protobuf.Empty
|
||||||
}
|
}
|
||||||
var file_internal_machine_api_pb_docker_proto_depIdxs = []int32{
|
var file_internal_machine_api_pb_docker_proto_depIdxs = []int32{
|
||||||
5, // 0: api.ListContainersResponse.messages:type_name -> api.MachineContainers
|
8, // 0: api.ListContainersResponse.messages:type_name -> api.MachineContainers
|
||||||
9, // 1: api.MachineContainers.metadata:type_name -> api.Metadata
|
12, // 1: api.MachineContainers.metadata:type_name -> api.Metadata
|
||||||
0, // 2: api.Docker.CreateContainer:input_type -> api.CreateContainerRequest
|
0, // 2: api.Docker.CreateContainer:input_type -> api.CreateContainerRequest
|
||||||
2, // 3: api.Docker.StartContainer:input_type -> api.StartContainerRequest
|
2, // 3: api.Docker.InspectContainer:input_type -> api.InspectContainerRequest
|
||||||
3, // 4: api.Docker.ListContainers:input_type -> api.ListContainersRequest
|
4, // 4: api.Docker.StartContainer:input_type -> api.StartContainerRequest
|
||||||
6, // 5: api.Docker.RemoveContainer:input_type -> api.RemoveContainerRequest
|
5, // 5: api.Docker.StopContainer:input_type -> api.StopContainerRequest
|
||||||
7, // 6: api.Docker.PullImage:input_type -> api.PullImageRequest
|
6, // 6: api.Docker.ListContainers:input_type -> api.ListContainersRequest
|
||||||
1, // 7: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse
|
9, // 7: api.Docker.RemoveContainer:input_type -> api.RemoveContainerRequest
|
||||||
10, // 8: api.Docker.StartContainer:output_type -> google.protobuf.Empty
|
10, // 8: api.Docker.PullImage:input_type -> api.PullImageRequest
|
||||||
4, // 9: api.Docker.ListContainers:output_type -> api.ListContainersResponse
|
1, // 9: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse
|
||||||
10, // 10: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty
|
3, // 10: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse
|
||||||
8, // 11: api.Docker.PullImage:output_type -> api.JSONMessage
|
13, // 11: api.Docker.StartContainer:output_type -> google.protobuf.Empty
|
||||||
7, // [7:12] is the sub-list for method output_type
|
13, // 12: api.Docker.StopContainer:output_type -> google.protobuf.Empty
|
||||||
2, // [2:7] is the sub-list for method input_type
|
7, // 13: api.Docker.ListContainers:output_type -> api.ListContainersResponse
|
||||||
|
13, // 14: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty
|
||||||
|
11, // 15: api.Docker.PullImage:output_type -> api.JSONMessage
|
||||||
|
9, // [9:16] is the sub-list for method output_type
|
||||||
|
2, // [2:9] is the sub-list for method input_type
|
||||||
2, // [2:2] is the sub-list for extension type_name
|
2, // [2:2] is the sub-list for extension type_name
|
||||||
2, // [2:2] is the sub-list for extension extendee
|
2, // [2:2] is the sub-list for extension extendee
|
||||||
0, // [0:2] is the sub-list for field type_name
|
0, // [0:2] is the sub-list for field type_name
|
||||||
@@ -680,7 +858,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_docker_proto_msgTypes[2].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_docker_proto_msgTypes[2].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*StartContainerRequest); i {
|
switch v := v.(*InspectContainerRequest); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -692,7 +870,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_docker_proto_msgTypes[3].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_docker_proto_msgTypes[3].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*ListContainersRequest); i {
|
switch v := v.(*InspectContainerResponse); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -704,7 +882,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_docker_proto_msgTypes[4].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_docker_proto_msgTypes[4].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*ListContainersResponse); i {
|
switch v := v.(*StartContainerRequest); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -716,7 +894,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_docker_proto_msgTypes[5].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_docker_proto_msgTypes[5].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*MachineContainers); i {
|
switch v := v.(*StopContainerRequest); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -728,7 +906,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_docker_proto_msgTypes[6].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_docker_proto_msgTypes[6].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*RemoveContainerRequest); i {
|
switch v := v.(*ListContainersRequest); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -740,7 +918,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_docker_proto_msgTypes[7].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_docker_proto_msgTypes[7].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*PullImageRequest); i {
|
switch v := v.(*ListContainersResponse); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -752,6 +930,42 @@ func file_internal_machine_api_pb_docker_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_machine_api_pb_docker_proto_msgTypes[8].Exporter = func(v any, i int) any {
|
file_internal_machine_api_pb_docker_proto_msgTypes[8].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*MachineContainers); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_docker_proto_msgTypes[9].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*RemoveContainerRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_docker_proto_msgTypes[10].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*PullImageRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_machine_api_pb_docker_proto_msgTypes[11].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*JSONMessage); i {
|
switch v := v.(*JSONMessage); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
@@ -770,7 +984,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
|
|||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: file_internal_machine_api_pb_docker_proto_rawDesc,
|
RawDescriptor: file_internal_machine_api_pb_docker_proto_rawDesc,
|
||||||
NumEnums: 0,
|
NumEnums: 0,
|
||||||
NumMessages: 9,
|
NumMessages: 12,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import "internal/machine/api/pb/common.proto";
|
|||||||
|
|
||||||
service Docker {
|
service Docker {
|
||||||
rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse);
|
rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse);
|
||||||
|
rpc InspectContainer(InspectContainerRequest) returns (InspectContainerResponse);
|
||||||
rpc StartContainer(StartContainerRequest) returns (google.protobuf.Empty);
|
rpc StartContainer(StartContainerRequest) returns (google.protobuf.Empty);
|
||||||
|
rpc StopContainer(StopContainerRequest) returns (google.protobuf.Empty);
|
||||||
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse);
|
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse);
|
||||||
rpc RemoveContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
|
rpc RemoveContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
|
||||||
rpc PullImage(PullImageRequest) returns (stream JSONMessage);
|
rpc PullImage(PullImageRequest) returns (stream JSONMessage);
|
||||||
@@ -32,12 +34,27 @@ message CreateContainerResponse {
|
|||||||
bytes response = 1;
|
bytes response = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message InspectContainerRequest {
|
||||||
|
string id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InspectContainerResponse {
|
||||||
|
// JSON serialized container.InspectResponse.
|
||||||
|
bytes response = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message StartContainerRequest {
|
message StartContainerRequest {
|
||||||
string id = 1;
|
string id = 1;
|
||||||
// JSON serialized container.StartOptions.
|
// JSON serialized container.StartOptions.
|
||||||
bytes options = 2;
|
bytes options = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message StopContainerRequest {
|
||||||
|
string id = 1;
|
||||||
|
// JSON serialized container.StopOptions.
|
||||||
|
bytes options = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message ListContainersRequest {
|
message ListContainersRequest {
|
||||||
// JSON serialized container.ListOptions.
|
// JSON serialized container.ListOptions.
|
||||||
bytes options = 1;
|
bytes options = 1;
|
||||||
@@ -50,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ const _ = grpc.SupportPackageIsVersion9
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
Docker_CreateContainer_FullMethodName = "/api.Docker/CreateContainer"
|
Docker_CreateContainer_FullMethodName = "/api.Docker/CreateContainer"
|
||||||
|
Docker_InspectContainer_FullMethodName = "/api.Docker/InspectContainer"
|
||||||
Docker_StartContainer_FullMethodName = "/api.Docker/StartContainer"
|
Docker_StartContainer_FullMethodName = "/api.Docker/StartContainer"
|
||||||
|
Docker_StopContainer_FullMethodName = "/api.Docker/StopContainer"
|
||||||
Docker_ListContainers_FullMethodName = "/api.Docker/ListContainers"
|
Docker_ListContainers_FullMethodName = "/api.Docker/ListContainers"
|
||||||
Docker_RemoveContainer_FullMethodName = "/api.Docker/RemoveContainer"
|
Docker_RemoveContainer_FullMethodName = "/api.Docker/RemoveContainer"
|
||||||
Docker_PullImage_FullMethodName = "/api.Docker/PullImage"
|
Docker_PullImage_FullMethodName = "/api.Docker/PullImage"
|
||||||
@@ -32,7 +34,9 @@ const (
|
|||||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
type DockerClient interface {
|
type DockerClient interface {
|
||||||
CreateContainer(ctx context.Context, in *CreateContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error)
|
CreateContainer(ctx context.Context, in *CreateContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error)
|
||||||
|
InspectContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*InspectContainerResponse, error)
|
||||||
StartContainer(ctx context.Context, in *StartContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
StartContainer(ctx context.Context, in *StartContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
|
StopContainer(ctx context.Context, in *StopContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
ListContainers(ctx context.Context, in *ListContainersRequest, opts ...grpc.CallOption) (*ListContainersResponse, error)
|
ListContainers(ctx context.Context, in *ListContainersRequest, opts ...grpc.CallOption) (*ListContainersResponse, error)
|
||||||
RemoveContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
RemoveContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
PullImage(ctx context.Context, in *PullImageRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[JSONMessage], error)
|
PullImage(ctx context.Context, in *PullImageRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[JSONMessage], error)
|
||||||
@@ -56,6 +60,16 @@ func (c *dockerClient) CreateContainer(ctx context.Context, in *CreateContainerR
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) InspectContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*InspectContainerResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(InspectContainerResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_InspectContainer_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *dockerClient) StartContainer(ctx context.Context, in *StartContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
func (c *dockerClient) StartContainer(ctx context.Context, in *StartContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(emptypb.Empty)
|
out := new(emptypb.Empty)
|
||||||
@@ -66,6 +80,16 @@ func (c *dockerClient) StartContainer(ctx context.Context, in *StartContainerReq
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) StopContainer(ctx context.Context, in *StopContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(emptypb.Empty)
|
||||||
|
err := c.cc.Invoke(ctx, Docker_StopContainer_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *dockerClient) ListContainers(ctx context.Context, in *ListContainersRequest, opts ...grpc.CallOption) (*ListContainersResponse, error) {
|
func (c *dockerClient) ListContainers(ctx context.Context, in *ListContainersRequest, opts ...grpc.CallOption) (*ListContainersResponse, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(ListContainersResponse)
|
out := new(ListContainersResponse)
|
||||||
@@ -110,7 +134,9 @@ type Docker_PullImageClient = grpc.ServerStreamingClient[JSONMessage]
|
|||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
type DockerServer interface {
|
type DockerServer interface {
|
||||||
CreateContainer(context.Context, *CreateContainerRequest) (*CreateContainerResponse, error)
|
CreateContainer(context.Context, *CreateContainerRequest) (*CreateContainerResponse, error)
|
||||||
|
InspectContainer(context.Context, *InspectContainerRequest) (*InspectContainerResponse, error)
|
||||||
StartContainer(context.Context, *StartContainerRequest) (*emptypb.Empty, error)
|
StartContainer(context.Context, *StartContainerRequest) (*emptypb.Empty, error)
|
||||||
|
StopContainer(context.Context, *StopContainerRequest) (*emptypb.Empty, error)
|
||||||
ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error)
|
ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error)
|
||||||
RemoveContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
|
RemoveContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
|
||||||
PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error
|
PullImage(*PullImageRequest, grpc.ServerStreamingServer[JSONMessage]) error
|
||||||
@@ -127,9 +153,15 @@ type UnimplementedDockerServer struct{}
|
|||||||
func (UnimplementedDockerServer) CreateContainer(context.Context, *CreateContainerRequest) (*CreateContainerResponse, error) {
|
func (UnimplementedDockerServer) CreateContainer(context.Context, *CreateContainerRequest) (*CreateContainerResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method CreateContainer not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method CreateContainer not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedDockerServer) InspectContainer(context.Context, *InspectContainerRequest) (*InspectContainerResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method InspectContainer not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedDockerServer) StartContainer(context.Context, *StartContainerRequest) (*emptypb.Empty, error) {
|
func (UnimplementedDockerServer) StartContainer(context.Context, *StartContainerRequest) (*emptypb.Empty, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method StartContainer not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method StartContainer not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedDockerServer) StopContainer(context.Context, *StopContainerRequest) (*emptypb.Empty, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method StopContainer not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedDockerServer) ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error) {
|
func (UnimplementedDockerServer) ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method ListContainers not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method ListContainers not implemented")
|
||||||
}
|
}
|
||||||
@@ -178,6 +210,24 @@ func _Docker_CreateContainer_Handler(srv interface{}, ctx context.Context, dec f
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _Docker_InspectContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(InspectContainerRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).InspectContainer(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_InspectContainer_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).InspectContainer(ctx, req.(*InspectContainerRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _Docker_StartContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _Docker_StartContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(StartContainerRequest)
|
in := new(StartContainerRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@@ -196,6 +246,24 @@ func _Docker_StartContainer_Handler(srv interface{}, ctx context.Context, dec fu
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _Docker_StopContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(StopContainerRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DockerServer).StopContainer(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Docker_StopContainer_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DockerServer).StopContainer(ctx, req.(*StopContainerRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _Docker_ListContainers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _Docker_ListContainers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(ListContainersRequest)
|
in := new(ListContainersRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@@ -254,10 +322,18 @@ var Docker_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "CreateContainer",
|
MethodName: "CreateContainer",
|
||||||
Handler: _Docker_CreateContainer_Handler,
|
Handler: _Docker_CreateContainer_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "InspectContainer",
|
||||||
|
Handler: _Docker_InspectContainer_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "StartContainer",
|
MethodName: "StartContainer",
|
||||||
Handler: _Docker_StartContainer_Handler,
|
Handler: _Docker_StartContainer_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "StopContainer",
|
||||||
|
Handler: _Docker_StopContainer_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "ListContainers",
|
MethodName: "ListContainers",
|
||||||
Handler: _Docker_ListContainers_Handler,
|
Handler: _Docker_ListContainers_Handler,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -210,9 +210,13 @@ func (c *Cluster) ListMachines(ctx context.Context, _ *emptypb.Empty) (*pb.ListM
|
|||||||
state := pb.MachineMember_DOWN
|
state := pb.MachineMember_DOWN
|
||||||
addr, _ := m.Network.ManagementIp.ToAddr()
|
addr, _ := m.Network.ManagementIp.ToAddr()
|
||||||
for _, s := range states {
|
for _, s := range states {
|
||||||
if s.Addr.Addr().Compare(addr) == 0 &&
|
if s.Addr.Addr().Compare(addr) == 0 {
|
||||||
(s.State == corrosion.MembershipStateAlive || s.State == corrosion.MembershipStateSuspect) {
|
switch s.State {
|
||||||
|
case corrosion.MembershipStateAlive:
|
||||||
state = pb.MachineMember_UP
|
state = pb.MachineMember_UP
|
||||||
|
case corrosion.MembershipStateSuspect:
|
||||||
|
state = pb.MachineMember_SUSPECT
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,26 @@ func (c *Client) CreateContainer(
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InspectContainer returns the container information for the given container ID.
|
||||||
|
func (c *Client) InspectContainer(ctx context.Context, id string) (types.ContainerJSON, error) {
|
||||||
|
var resp types.ContainerJSON
|
||||||
|
|
||||||
|
grpcResp, err := c.grpcClient.InspectContainer(ctx, &pb.InspectContainerRequest{Id: id})
|
||||||
|
if err != nil {
|
||||||
|
if s, ok := status.FromError(err); ok {
|
||||||
|
if s.Code() == codes.NotFound {
|
||||||
|
return resp, errdefs.NotFound(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.Unmarshal(grpcResp.Response, &resp); err != nil {
|
||||||
|
return resp, fmt.Errorf("unmarshal gRPC response: %w", err)
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
// StartContainer starts a container with the given ID and options.
|
// StartContainer starts a container with the given ID and options.
|
||||||
func (c *Client) StartContainer(ctx context.Context, id string, opts container.StartOptions) error {
|
func (c *Client) StartContainer(ctx context.Context, id string, opts container.StartOptions) error {
|
||||||
optsBytes, err := json.Marshal(opts)
|
optsBytes, err := json.Marshal(opts)
|
||||||
@@ -107,9 +127,30 @@ func (c *Client) StartContainer(ctx context.Context, id string, opts container.S
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StopContainer stops a container with the given ID and options.
|
||||||
|
func (c *Client) StopContainer(ctx context.Context, id string, opts container.StopOptions) error {
|
||||||
|
optsBytes, err := json.Marshal(opts)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal options: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = c.grpcClient.StopContainer(ctx, &pb.StopContainerRequest{
|
||||||
|
Id: id,
|
||||||
|
Options: optsBytes,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if s, ok := status.FromError(err); ok {
|
||||||
|
if s.Code() == codes.NotFound {
|
||||||
|
return errdefs.NotFound(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
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"
|
||||||
@@ -53,9 +54,9 @@ func (s *Server) CreateContainer(ctx context.Context, req *pb.CreateContainerReq
|
|||||||
resp, err := s.client.ContainerCreate(ctx, &config, &hostConfig, &networkConfig, &platform, req.Name)
|
resp, err := s.client.ContainerCreate(ctx, &config, &hostConfig, &networkConfig, &platform, req.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if client.IsErrNotFound(err) {
|
if client.IsErrNotFound(err) {
|
||||||
return nil, status.Errorf(codes.NotFound, "create container: %v", err)
|
return nil, status.Errorf(codes.NotFound, err.Error())
|
||||||
}
|
}
|
||||||
return nil, status.Errorf(codes.Internal, "create container: %v", err)
|
return nil, status.Errorf(codes.Internal, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
respBytes, err := json.Marshal(resp)
|
respBytes, err := json.Marshal(resp)
|
||||||
@@ -66,6 +67,24 @@ func (s *Server) CreateContainer(ctx context.Context, req *pb.CreateContainerReq
|
|||||||
return &pb.CreateContainerResponse{Response: respBytes}, nil
|
return &pb.CreateContainerResponse{Response: respBytes}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InspectContainer returns the container information for the given container ID.
|
||||||
|
func (s *Server) InspectContainer(ctx context.Context, req *pb.InspectContainerRequest) (*pb.InspectContainerResponse, error) {
|
||||||
|
resp, err := s.client.ContainerInspect(ctx, req.Id)
|
||||||
|
if err != nil {
|
||||||
|
if client.IsErrNotFound(err) {
|
||||||
|
return nil, status.Errorf(codes.NotFound, err.Error())
|
||||||
|
}
|
||||||
|
return nil, status.Errorf(codes.Internal, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
respBytes, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "marshal response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.InspectContainerResponse{Response: respBytes}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// StartContainer starts a container with the given ID and options.
|
// StartContainer starts a container with the given ID and options.
|
||||||
func (s *Server) StartContainer(ctx context.Context, req *pb.StartContainerRequest) (*emptypb.Empty, error) {
|
func (s *Server) StartContainer(ctx context.Context, req *pb.StartContainerRequest) (*emptypb.Empty, error) {
|
||||||
var opts container.StartOptions
|
var opts container.StartOptions
|
||||||
@@ -77,9 +96,28 @@ func (s *Server) StartContainer(ctx context.Context, req *pb.StartContainerReque
|
|||||||
|
|
||||||
if err := s.client.ContainerStart(ctx, req.Id, opts); err != nil {
|
if err := s.client.ContainerStart(ctx, req.Id, opts); err != nil {
|
||||||
if client.IsErrNotFound(err) {
|
if client.IsErrNotFound(err) {
|
||||||
return nil, status.Errorf(codes.NotFound, "start container: %v", err)
|
return nil, status.Errorf(codes.NotFound, err.Error())
|
||||||
}
|
}
|
||||||
return nil, status.Errorf(codes.Internal, "start container: %v", err)
|
return nil, status.Errorf(codes.Internal, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return &emptypb.Empty{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopContainer stops a container with the given ID and options.
|
||||||
|
func (s *Server) StopContainer(ctx context.Context, req *pb.StopContainerRequest) (*emptypb.Empty, error) {
|
||||||
|
var opts container.StopOptions
|
||||||
|
if len(req.Options) > 0 {
|
||||||
|
if err := json.Unmarshal(req.Options, &opts); err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "unmarshal options: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.client.ContainerStop(ctx, req.Id, opts); err != nil {
|
||||||
|
if client.IsErrNotFound(err) {
|
||||||
|
return nil, status.Errorf(codes.NotFound, err.Error())
|
||||||
|
}
|
||||||
|
return nil, status.Errorf(codes.Internal, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return &emptypb.Empty{}, nil
|
return &emptypb.Empty{}, nil
|
||||||
@@ -107,9 +145,17 @@ 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, "list container: %v", err)
|
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)
|
||||||
@@ -137,9 +183,9 @@ func (s *Server) RemoveContainer(ctx context.Context, req *pb.RemoveContainerReq
|
|||||||
|
|
||||||
if err := s.client.ContainerRemove(ctx, req.Id, opts); err != nil {
|
if err := s.client.ContainerRemove(ctx, req.Id, opts); err != nil {
|
||||||
if client.IsErrNotFound(err) {
|
if client.IsErrNotFound(err) {
|
||||||
return nil, status.Errorf(codes.NotFound, "remove container: %v", err)
|
return nil, status.Errorf(codes.NotFound, err.Error())
|
||||||
}
|
}
|
||||||
return nil, status.Errorf(codes.Internal, "remove container: %v", err)
|
return nil, status.Errorf(codes.Internal, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return &emptypb.Empty{}, nil
|
return &emptypb.Empty{}, nil
|
||||||
@@ -158,7 +204,7 @@ func (s *Server) PullImage(req *pb.PullImageRequest, stream grpc.ServerStreaming
|
|||||||
|
|
||||||
respBody, err := s.client.ImagePull(ctx, req.Image, opts)
|
respBody, err := s.client.ImagePull(ctx, req.Image, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status.Errorf(codes.Internal, "pull image: %v", err)
|
return status.Errorf(codes.Internal, err.Error())
|
||||||
}
|
}
|
||||||
defer respBody.Close()
|
defer respBody.Close()
|
||||||
|
|
||||||
@@ -189,7 +235,7 @@ func (s *Server) PullImage(req *pb.PullImageRequest, stream grpc.ServerStreaming
|
|||||||
case err = <-errCh:
|
case err = <-errCh:
|
||||||
return err
|
return err
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return status.Errorf(codes.Canceled, "pull image: %v", ctx.Err())
|
return status.Errorf(codes.Canceled, ctx.Err().Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ package sshexec
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"golang.org/x/crypto/ssh"
|
|
||||||
"golang.org/x/crypto/ssh/agent"
|
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
"golang.org/x/crypto/ssh/agent"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Connect(user, host string, port int, sshKeyPath string) (*ssh.Client, error) {
|
func Connect(user, host string, port int, sshKeyPath string) (*ssh.Client, error) {
|
||||||
@@ -34,6 +35,9 @@ func Connect(user, host string, port int, sshKeyPath string) (*ssh.Client, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
keyAuth, err := privateKeyAuth(sshKeyPath)
|
keyAuth, err := privateKeyAuth(sshKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
config := &ssh.ClientConfig{
|
config := &ssh.ClientConfig{
|
||||||
User: user,
|
User: user,
|
||||||
Auth: []ssh.AuthMethod{keyAuth},
|
Auth: []ssh.AuthMethod{keyAuth},
|
||||||
|
|||||||
+1
-1
@@ -123,7 +123,7 @@ install_uncloud_binaries() {
|
|||||||
error "Failed to download uncloudd binary."
|
error "Failed to download uncloudd binary."
|
||||||
fi
|
fi
|
||||||
tar -xf "${uncloudd_download_path}"
|
tar -xf "${uncloudd_download_path}"
|
||||||
if ! install "${uncloudd_download_path%.gz}" "${uncloudd_install_path}"; then
|
if ! install ./uncloudd "${uncloudd_install_path}"; then
|
||||||
error "Failed to install uncloud binary to ${uncloudd_install_path}"
|
error "Failed to install uncloud binary to ${uncloudd_install_path}"
|
||||||
fi
|
fi
|
||||||
log "✓ uncloudd binary installed: ${uncloudd_install_path}"
|
log "✓ uncloudd binary installed: ${uncloudd_install_path}"
|
||||||
|
|||||||
+21
-22
@@ -8,25 +8,25 @@ INSTALL_DIR=${INSTALL_DIR:-/usr/local/bin}
|
|||||||
VERSION=${VERSION:-latest}
|
VERSION=${VERSION:-latest}
|
||||||
|
|
||||||
print_manual_install() {
|
print_manual_install() {
|
||||||
# TODO: review
|
RELEASES_URL="https://github.com/${GITHUB_REPO}/releases/${VERSION}"
|
||||||
echo "You can manually install uncloud by:"
|
echo "Failed while attempting to install uncloud CLI. You can install it manually:"
|
||||||
echo "1. Opening $RELEASES_URL"
|
echo " 1. Open your web browser and go to ${RELEASES_URL}"
|
||||||
echo "2. Downloading uncloud_*_${OS}_${ARCH}.tar.gz for your platform"
|
echo " 2. Download uncloud_<OS>_<ARCH>.tar.gz for your platform (OS: linux/macos, ARCH: amd64/arm64)."
|
||||||
echo "3. Verifying the checksum from checksums.txt"
|
echo " 3. Extract the 'uncloud' binary from the archive: tar -xvf uncloud_*.tar.gz"
|
||||||
echo "4. Extracting the archive: tar xzf uncloud_*_${OS}_${ARCH}.tar.gz"
|
echo " 4. Install the binary to /usr/local/bin: sudo install ./uncloud ${INSTALL_DIR}/uncloud"
|
||||||
echo "5. Installing the binary: sudo install -m 755 uncloud /usr/local/bin/"
|
echo " 5. Optionally create a 'uc' symlink: sudo ln -sf ${INSTALL_DIR}/uncloud ${INSTALL_DIR}/uc"
|
||||||
echo "6. Creating symlink: sudo ln -sf /usr/local/bin/uncloud /usr/local/bin/uc"
|
echo " 6. Delete the downloaded archive and extracted binary: rm uncloud*"
|
||||||
|
echo " 7. Run 'uncloud --help' to verify the installation. Enjoy! ✨"
|
||||||
}
|
}
|
||||||
|
|
||||||
latest_version() {
|
fetch_latest_version() {
|
||||||
api_url="https://api.github.com/repos/${GITHUB_REPO}/releases/latest"
|
api_url="https://api.github.com/repos/${GITHUB_REPO}/releases/latest"
|
||||||
version=$(curl -fsSL "$api_url" | grep -o '"tag_name": "[^"]*' | cut -d'"' -f4)
|
VERSION=$(curl -fsSL "$api_url" | grep -o '"tag_name": "[^"]*' | cut -d'"' -f4)
|
||||||
if [ -z "$version" ]; then
|
if [ -z "$VERSION" ]; then
|
||||||
echo "Failed to fetch the latest version from GitHub."
|
echo "Failed to fetch the latest version from GitHub."
|
||||||
print_manual_install
|
print_manual_install
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "$version"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if not running as root and need to use sudo to write to INSTALL_DIR.
|
# Check if not running as root and need to use sudo to write to INSTALL_DIR.
|
||||||
@@ -62,7 +62,7 @@ esac
|
|||||||
|
|
||||||
# Use the latest version if not specified explicitly.
|
# Use the latest version if not specified explicitly.
|
||||||
if [ "$VERSION" = "latest" ]; then
|
if [ "$VERSION" = "latest" ]; then
|
||||||
VERSION=$(latest_version)
|
fetch_latest_version
|
||||||
fi
|
fi
|
||||||
BINARY_NAME="uncloud_${BINARY_OS}_${BINARY_ARCH}.tar.gz"
|
BINARY_NAME="uncloud_${BINARY_OS}_${BINARY_ARCH}.tar.gz"
|
||||||
BINARY_URL="https://github.com/${GITHUB_REPO}/releases/download/${VERSION}/${BINARY_NAME}"
|
BINARY_URL="https://github.com/${GITHUB_REPO}/releases/download/${VERSION}/${BINARY_NAME}"
|
||||||
@@ -78,15 +78,14 @@ curl -fsSL "$BINARY_URL" -o "${TMP_DIR}/${BINARY_NAME}"
|
|||||||
curl -fsSL "$CHECKSUM_URL" -o "${TMP_DIR}/checksums.txt"
|
curl -fsSL "$CHECKSUM_URL" -o "${TMP_DIR}/checksums.txt"
|
||||||
echo "Download complete."
|
echo "Download complete."
|
||||||
|
|
||||||
# TODO: fix name_template in goreleaser config.
|
echo "Verifying checksum..."
|
||||||
#echo "Verifying checksum..."
|
|
||||||
cd "$TMP_DIR"
|
cd "$TMP_DIR"
|
||||||
#if ! sha256sum --check --ignore-missing "checksums.txt"; then
|
if ! sha256sum --check --ignore-missing "checksums.txt"; then
|
||||||
# echo "Checksum verification failed."
|
echo "Checksum verification failed."
|
||||||
# print_manual_install
|
print_manual_install
|
||||||
# exit 1
|
exit 1
|
||||||
#fi
|
fi
|
||||||
#echo "Checksum is valid."
|
echo "Checksum is valid."
|
||||||
|
|
||||||
# Decompress and install the binary.
|
# Decompress and install the binary.
|
||||||
tar -xf "${BINARY_NAME}"
|
tar -xf "${BINARY_NAME}"
|
||||||
@@ -96,7 +95,7 @@ if [ -z "${SUDO}" ]; then
|
|||||||
else
|
else
|
||||||
echo "Installing uncloud binary to ${INSTALL_DIR} using sudo. You may be prompted for your password."
|
echo "Installing uncloud binary to ${INSTALL_DIR} using sudo. You may be prompted for your password."
|
||||||
fi
|
fi
|
||||||
if ! $SUDO install "${BINARY_NAME%.tar.gz}" "${INSTALL_DIR}/uncloud"; then
|
if ! $SUDO install ./uncloud "${INSTALL_DIR}/uncloud"; then
|
||||||
echo "Failed to install uncloud binary to ${INSTALL_DIR}"
|
echo "Failed to install uncloud binary to ${INSTALL_DIR}"
|
||||||
print_manual_install
|
print_manual_install
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
+203
-15
@@ -2,7 +2,8 @@ package e2e
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
dockerclient "github.com/docker/docker/client"
|
"errors"
|
||||||
|
"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"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -11,6 +12,161 @@ import (
|
|||||||
"uncloud/internal/ucind"
|
"uncloud/internal/ucind"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestDeployment(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
clusterName := "ucind-test.deployment"
|
||||||
|
ctx := context.Background()
|
||||||
|
c, _ := createTestCluster(t, clusterName, ucind.CreateClusterOptions{Machines: 3}, true)
|
||||||
|
|
||||||
|
cli, err := c.Machines[0].Connect(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Run("global", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
name := "global-deployment"
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err := cli.RemoveService(ctx, name)
|
||||||
|
if errors.Is(err, client.ErrNotFound) {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = cli.InspectService(ctx, name)
|
||||||
|
require.ErrorIs(t, err, client.ErrNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
spec := api.ServiceSpec{
|
||||||
|
Name: name,
|
||||||
|
Mode: api.ServiceModeGlobal,
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
deploy, err := cli.NewDeployment(spec, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = deploy.Validate(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
plan, err := deploy.Plan(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.IsType(t, &client.SequenceOperation{}, plan.Operation)
|
||||||
|
assert.Len(t, plan.Operation.(*client.SequenceOperation).Operations, 3) // 3 run
|
||||||
|
|
||||||
|
svcID, err := deploy.Run(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, svcID)
|
||||||
|
|
||||||
|
svc, err := cli.InspectService(ctx, name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, name, svc.Name)
|
||||||
|
assert.Equal(t, api.ServiceModeGlobal, svc.Mode)
|
||||||
|
assert.Len(t, svc.Containers, 3)
|
||||||
|
|
||||||
|
svcSpec, err := svc.Containers[0].Container.ServiceSpec()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, svcSpec.Equals(spec))
|
||||||
|
|
||||||
|
// Deploy a published port.
|
||||||
|
specWithPort := api.ServiceSpec{
|
||||||
|
Name: name,
|
||||||
|
Mode: api.ServiceModeGlobal,
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
},
|
||||||
|
Ports: []api.PortSpec{
|
||||||
|
{
|
||||||
|
PublishedPort: 8000,
|
||||||
|
ContainerPort: 8000,
|
||||||
|
Protocol: api.ProtocolTCP,
|
||||||
|
Mode: api.PortModeHost,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
deploy, err = cli.NewDeployment(specWithPort, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
plan, err = deploy.Plan(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.IsType(t, &client.SequenceOperation{}, plan.Operation)
|
||||||
|
assert.Len(t, plan.Operation.(*client.SequenceOperation).Operations, 6) // 3 run + 3 remove
|
||||||
|
|
||||||
|
svcID, err = deploy.Run(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, svcID)
|
||||||
|
|
||||||
|
svc, err = cli.InspectService(ctx, name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, name, svc.Name)
|
||||||
|
assert.Equal(t, api.ServiceModeGlobal, svc.Mode)
|
||||||
|
assert.Len(t, svc.Containers, 3)
|
||||||
|
|
||||||
|
svcSpec, err = svc.Containers[0].Container.ServiceSpec()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, svcSpec.Equals(specWithPort))
|
||||||
|
|
||||||
|
// Deploy the same conflicting port but with container spec changes
|
||||||
|
init := true
|
||||||
|
specWithPortAndInit := api.ServiceSpec{
|
||||||
|
Name: name,
|
||||||
|
Mode: api.ServiceModeGlobal,
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
Init: &init,
|
||||||
|
},
|
||||||
|
Ports: []api.PortSpec{
|
||||||
|
{
|
||||||
|
PublishedPort: 8000,
|
||||||
|
ContainerPort: 8000,
|
||||||
|
Protocol: api.ProtocolTCP,
|
||||||
|
Mode: api.PortModeHost,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
deploy, err = cli.NewDeployment(specWithPortAndInit, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
plan, err = deploy.Plan(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.IsType(t, &client.SequenceOperation{}, plan.Operation)
|
||||||
|
assert.Len(t, plan.Operation.(*client.SequenceOperation).Operations, 9) // 3 stop + 3 run + 3 remove
|
||||||
|
|
||||||
|
svcID, err = deploy.Run(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, svcID)
|
||||||
|
|
||||||
|
svc, err = cli.InspectService(ctx, name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, name, svc.Name)
|
||||||
|
assert.Equal(t, api.ServiceModeGlobal, svc.Mode)
|
||||||
|
assert.Len(t, svc.Containers, 3)
|
||||||
|
|
||||||
|
svcSpec, err = svc.Containers[0].Container.ServiceSpec()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, svcSpec.Equals(specWithPortAndInit))
|
||||||
|
|
||||||
|
// Deploying the same spec should be a no-op.
|
||||||
|
deploy, err = cli.NewDeployment(specWithPortAndInit, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
plan, err = deploy.Plan(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.IsType(t, &client.SequenceOperation{}, plan.Operation)
|
||||||
|
assert.Len(t, plan.Operation.(*client.SequenceOperation).Operations, 0) // no-op
|
||||||
|
|
||||||
|
svcID, err = deploy.Run(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, svcID)
|
||||||
|
|
||||||
|
svc, err = cli.InspectService(ctx, name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, name, svc.Name)
|
||||||
|
assert.Equal(t, api.ServiceModeGlobal, svc.Mode)
|
||||||
|
assert.Len(t, svc.Containers, 3)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunService(t *testing.T) {
|
func TestRunService(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -21,13 +177,50 @@ func TestRunService(t *testing.T) {
|
|||||||
cli, err := c.Machines[0].Connect(ctx)
|
cli, err := c.Machines[0].Connect(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Run("container lifecycle", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
svcName := "pause-container-lifecycle"
|
||||||
|
spec := api.ServiceSpec{
|
||||||
|
Name: svcName,
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "portainer/pause:latest",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
machineID := c.Machines[0].Name
|
||||||
|
|
||||||
|
ctr, err := cli.CreateContainer(ctx, svcName, spec, machineID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, ctr.ID)
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err := cli.RemoveContainer(ctx, svcName, ctr.ID, container.RemoveOptions{Force: true})
|
||||||
|
if !errors.Is(err, client.ErrNotFound) {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
err = cli.StartContainer(ctx, svcName, ctr.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
timeout := 1
|
||||||
|
err = cli.StopContainer(ctx, svcName, ctr.ID, container.StopOptions{Timeout: &timeout})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = cli.RemoveContainer(ctx, svcName, ctr.ID, container.RemoveOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = cli.RemoveContainer(ctx, svcName, ctr.ID, container.RemoveOptions{})
|
||||||
|
require.ErrorIs(t, err, client.ErrNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("1 replica", func(t *testing.T) {
|
t.Run("1 replica", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
name := "busybox-1-replica"
|
name := "pause-1-replica"
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
err := cli.RemoveService(ctx, name)
|
err := cli.RemoveService(ctx, name)
|
||||||
if !dockerclient.IsErrNotFound(err) {
|
if errors.Is(err, client.ErrNotFound) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,15 +232,13 @@ func TestRunService(t *testing.T) {
|
|||||||
Name: name,
|
Name: name,
|
||||||
Mode: api.ServiceModeReplicated,
|
Mode: api.ServiceModeReplicated,
|
||||||
Container: api.ContainerSpec{
|
Container: api.ContainerSpec{
|
||||||
Command: []string{"sleep", "infinity"},
|
Image: "portainer/pause:latest",
|
||||||
Image: "busybox:latest",
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.NotEmpty(t, resp.ID)
|
assert.NotEmpty(t, resp.ID)
|
||||||
assert.Equal(t, name, resp.Name)
|
assert.Equal(t, name, resp.Name)
|
||||||
assert.Len(t, resp.Containers, 1)
|
|
||||||
|
|
||||||
svc, err := cli.InspectService(ctx, name)
|
svc, err := cli.InspectService(ctx, name)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -76,10 +267,10 @@ func TestRunService(t *testing.T) {
|
|||||||
t.Run("1 replica with ports", func(t *testing.T) {
|
t.Run("1 replica with ports", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
name := "busybox-1-replica-ports"
|
name := "pause-1-replica-ports"
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
err := cli.RemoveService(ctx, name)
|
err := cli.RemoveService(ctx, name)
|
||||||
if !dockerclient.IsErrNotFound(err) {
|
if errors.Is(err, client.ErrNotFound) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,8 +282,7 @@ func TestRunService(t *testing.T) {
|
|||||||
Name: name,
|
Name: name,
|
||||||
Mode: api.ServiceModeReplicated,
|
Mode: api.ServiceModeReplicated,
|
||||||
Container: api.ContainerSpec{
|
Container: api.ContainerSpec{
|
||||||
Command: []string{"sleep", "infinity"},
|
Image: "portainer/pause:latest",
|
||||||
Image: "busybox:latest",
|
|
||||||
},
|
},
|
||||||
Ports: []api.PortSpec{
|
Ports: []api.PortSpec{
|
||||||
{
|
{
|
||||||
@@ -131,10 +321,10 @@ func TestRunService(t *testing.T) {
|
|||||||
t.Run("global mode", func(t *testing.T) {
|
t.Run("global mode", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
name := "busybox-global"
|
name := "pause-global"
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
err := cli.RemoveService(ctx, name)
|
err := cli.RemoveService(ctx, name)
|
||||||
if !dockerclient.IsErrNotFound(err) {
|
if errors.Is(err, client.ErrNotFound) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,15 +336,13 @@ func TestRunService(t *testing.T) {
|
|||||||
Name: name,
|
Name: name,
|
||||||
Mode: api.ServiceModeGlobal,
|
Mode: api.ServiceModeGlobal,
|
||||||
Container: api.ContainerSpec{
|
Container: api.ContainerSpec{
|
||||||
Command: []string{"sleep", "infinity"},
|
Image: "portainer/pause:latest",
|
||||||
Image: "busybox:latest",
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.NotEmpty(t, resp.ID)
|
assert.NotEmpty(t, resp.ID)
|
||||||
assert.Equal(t, name, resp.Name)
|
assert.Equal(t, name, resp.Name)
|
||||||
assert.Len(t, resp.Containers, 3, "expected 1 container on each machine")
|
|
||||||
|
|
||||||
svc, err := cli.InspectService(ctx, name)
|
svc, err := cli.InspectService(ctx, name)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
Reference in New Issue
Block a user