detached d3752fd

Add restart policy update runtime support

commit
d3752fd580200c875fd02b8a8523ae7c41160bfc
author
brice · · 2026-05-05 08:48 UTC
parent
65d845e
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +231 -0
M internal/adapters/out/docker/runtime.go +38 -0
@@ -43,6 +43,8 @@ type Runtime struct {
4343 client *client.Client
4444 }
4545
46+var _ out.ContainerRuntime = (*Runtime)(nil)
47+
4648 type pipeReadCloser struct {
4749 pr *io.PipeReader
4850 pw *io.PipeWriter
@@ -361,6 +363,42 @@ func (r *Runtime) RenameContainer(ctx context.Context, containerID, newName stri
361363 return nil
362364 }
363365
366+// EnsureContainerRestartPolicy updates a container restart policy when it does not match.
367+func (r *Runtime) EnsureContainerRestartPolicy(ctx context.Context, containerID, policy string) error {
368+ ctx = zerowrap.CtxWithFields(ctx, map[string]any{
369+ zerowrap.FieldLayer: "adapter",
370+ zerowrap.FieldAdapter: "docker",
371+ zerowrap.FieldAction: "EnsureContainerRestartPolicy",
372+ zerowrap.FieldEntityID: containerID,
373+ "restart_policy": policy,
374+ })
375+ log := zerowrap.FromCtx(ctx)
376+
377+ inspect, err := r.client.ContainerInspect(ctx, containerID)
378+ if err != nil {
379+ return log.WrapErr(err, "failed to inspect container")
380+ }
381+ if inspect.HostConfig == nil {
382+ return fmt.Errorf("container inspect response missing host config: %s", containerID)
383+ }
384+ if string(inspect.HostConfig.RestartPolicy.Name) == policy {
385+ return nil
386+ }
387+
388+ updateConfig := container.UpdateConfig{
389+ Resources: inspect.HostConfig.Resources,
390+ RestartPolicy: container.RestartPolicy{Name: container.RestartPolicyMode(policy)},
391+ }
392+ resp, err := r.client.ContainerUpdate(ctx, containerID, updateConfig)
393+ if err != nil {
394+ return log.WrapErr(err, "failed to update container restart policy")
395+ }
396+ for _, warning := range resp.Warnings {
397+ log.Warn().Str("warning", warning).Msg("container restart policy update warning")
398+ }
399+ return nil
400+}
401+
364402 // ListContainers lists containers.
365403 func (r *Runtime) ListContainers(ctx context.Context, all bool) ([]*domain.Container, error) {
366404 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
@@ -43,6 +43,8 @@ type Runtime struct {
43 client *client.Client43 client *client.Client
44 }44 }
45 45
46+var _ out.ContainerRuntime = (*Runtime)(nil)
47+
46 type pipeReadCloser struct {48 type pipeReadCloser struct {
47 pr *io.PipeReader49 pr *io.PipeReader
48 pw *io.PipeWriter50 pw *io.PipeWriter
@@ -361,6 +363,42 @@ func (r *Runtime) RenameContainer(ctx context.Context, containerID, newName stri
361 return nil363 return nil
362 }364 }
363 365
366+// EnsureContainerRestartPolicy updates a container restart policy when it does not match.
367+func (r *Runtime) EnsureContainerRestartPolicy(ctx context.Context, containerID, policy string) error {
368+ ctx = zerowrap.CtxWithFields(ctx, map[string]any{
369+ zerowrap.FieldLayer: "adapter",
370+ zerowrap.FieldAdapter: "docker",
371+ zerowrap.FieldAction: "EnsureContainerRestartPolicy",
372+ zerowrap.FieldEntityID: containerID,
373+ "restart_policy": policy,
374+ })
375+ log := zerowrap.FromCtx(ctx)
376+
377+ inspect, err := r.client.ContainerInspect(ctx, containerID)
378+ if err != nil {
379+ return log.WrapErr(err, "failed to inspect container")
380+ }
381+ if inspect.HostConfig == nil {
382+ return fmt.Errorf("container inspect response missing host config: %s", containerID)
383+ }
384+ if string(inspect.HostConfig.RestartPolicy.Name) == policy {
385+ return nil
386+ }
387+
388+ updateConfig := container.UpdateConfig{
389+ Resources: inspect.HostConfig.Resources,
390+ RestartPolicy: container.RestartPolicy{Name: container.RestartPolicyMode(policy)},
391+ }
392+ resp, err := r.client.ContainerUpdate(ctx, containerID, updateConfig)
393+ if err != nil {
394+ return log.WrapErr(err, "failed to update container restart policy")
395+ }
396+ for _, warning := range resp.Warnings {
397+ log.Warn().Str("warning", warning).Msg("container restart policy update warning")
398+ }
399+ return nil
400+}
401+
364 // ListContainers lists containers.402 // ListContainers lists containers.
365 func (r *Runtime) ListContainers(ctx context.Context, all bool) ([]*domain.Container, error) {403 func (r *Runtime) ListContainers(ctx context.Context, all bool) ([]*domain.Container, error) {
366 ctx = zerowrap.CtxWithFields(ctx, map[string]any{404 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
A internal/adapters/out/docker/runtime_restart_policy_test.go +129 -0
@@ -0,0 +1,129 @@
1+package docker
2+
3+import (
4+ "context"
5+ "encoding/json"
6+ "net/http"
7+ "net/http/httptest"
8+ "strings"
9+ "testing"
10+
11+ "github.com/docker/docker/client"
12+ "github.com/stretchr/testify/assert"
13+ "github.com/stretchr/testify/require"
14+
15+ "github.com/bnema/gordon/internal/domain"
16+)
17+
18+func TestRuntime_EnsureContainerRestartPolicy_SkipsWhenAlreadySet(t *testing.T) {
19+ var updateCalled bool
20+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
21+ switch {
22+ case r.Method == http.MethodGet && r.URL.Path == "/v1.41/containers/abc123/json":
23+ w.Header().Set("Content-Type", "application/json")
24+ _, _ = w.Write([]byte(`{
25+ "Id": "abc123",
26+ "HostConfig": {
27+ "RestartPolicy": {
28+ "Name": "always"
29+ }
30+ }
31+ }`))
32+ case r.Method == http.MethodPost && r.URL.Path == "/v1.41/containers/abc123/update":
33+ updateCalled = true
34+ w.WriteHeader(http.StatusInternalServerError)
35+ default:
36+ t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
37+ }
38+ }))
39+ defer server.Close()
40+
41+ runtime := newRuntimeForRestartPolicyHTTPServer(t, server)
42+ err := runtime.EnsureContainerRestartPolicy(context.Background(), "abc123", domain.RestartPolicyAlways)
43+ require.NoError(t, err)
44+ assert.False(t, updateCalled, "update should not be called when restart policy is already set")
45+}
46+
47+func TestRuntime_EnsureContainerRestartPolicy_UpdatesAndPreservesResources(t *testing.T) {
48+ var updateBody map[string]any
49+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
50+ switch {
51+ case r.Method == http.MethodGet && r.URL.Path == "/v1.41/containers/abc123/json":
52+ w.Header().Set("Content-Type", "application/json")
53+ _, _ = w.Write([]byte(`{
54+ "Id": "abc123",
55+ "HostConfig": {
56+ "RestartPolicy": {
57+ "Name": ""
58+ },
59+ "Memory": 268435456,
60+ "NanoCpus": 500000000,
61+ "PidsLimit": 128,
62+ "Ulimits": [
63+ {
64+ "Name": "nofile",
65+ "Soft": 65536,
66+ "Hard": 65536
67+ }
68+ ]
69+ }
70+ }`))
71+ case r.Method == http.MethodPost && r.URL.Path == "/v1.41/containers/abc123/update":
72+ require.NoError(t, json.NewDecoder(r.Body).Decode(&updateBody))
73+ w.Header().Set("Content-Type", "application/json")
74+ _, _ = w.Write([]byte(`{"Warnings":null}`))
75+ default:
76+ t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
77+ }
78+ }))
79+ defer server.Close()
80+
81+ runtime := newRuntimeForRestartPolicyHTTPServer(t, server)
82+ err := runtime.EnsureContainerRestartPolicy(context.Background(), "abc123", domain.RestartPolicyAlways)
83+ require.NoError(t, err)
84+ require.NotNil(t, updateBody)
85+ assert.Equal(t, domain.RestartPolicyAlways, updateBody["RestartPolicy"].(map[string]any)["Name"])
86+ assert.Equal(t, float64(268435456), updateBody["Memory"])
87+ assert.Equal(t, float64(500000000), updateBody["NanoCpus"])
88+ assert.Equal(t, float64(128), updateBody["PidsLimit"])
89+ ulimits, ok := updateBody["Ulimits"].([]any)
90+ require.True(t, ok, "Ulimits should be an array")
91+ require.Greater(t, len(ulimits), 0)
92+ firstUlimit, ok := ulimits[0].(map[string]any)
93+ require.True(t, ok, "first Ulimit should be a map")
94+ assert.Equal(t, "nofile", firstUlimit["Name"])
95+ assert.Equal(t, float64(65536), firstUlimit["Soft"])
96+ assert.Equal(t, float64(65536), firstUlimit["Hard"])
97+}
98+
99+func TestRuntime_EnsureContainerRestartPolicy_ReturnsErrorWhenHostConfigMissing(t *testing.T) {
100+ var updateCalled bool
101+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
102+ switch {
103+ case r.Method == http.MethodGet && r.URL.Path == "/v1.41/containers/abc123/json":
104+ w.Header().Set("Content-Type", "application/json")
105+ _, _ = w.Write([]byte(`{
106+ "Id": "abc123"
107+ }`))
108+ case r.Method == http.MethodPost && r.URL.Path == "/v1.41/containers/abc123/update":
109+ updateCalled = true
110+ w.WriteHeader(http.StatusInternalServerError)
111+ default:
112+ t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
113+ }
114+ }))
115+ defer server.Close()
116+
117+ runtime := newRuntimeForRestartPolicyHTTPServer(t, server)
118+ err := runtime.EnsureContainerRestartPolicy(context.Background(), "abc123", domain.RestartPolicyAlways)
119+ require.Error(t, err)
120+ assert.Contains(t, err.Error(), "missing host config")
121+ assert.False(t, updateCalled, "update should not be called when host config is missing")
122+}
123+
124+func newRuntimeForRestartPolicyHTTPServer(t *testing.T, server *httptest.Server) *Runtime {
125+ host := strings.TrimPrefix(server.URL, "http://")
126+ cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client()))
127+ require.NoError(t, err)
128+ return NewRuntimeWithClient(cli)
129+}
@@ -0,0 +1,129 @@
1+package docker
2+
3+import (
4+ "context"
5+ "encoding/json"
6+ "net/http"
7+ "net/http/httptest"
8+ "strings"
9+ "testing"
10+
11+ "github.com/docker/docker/client"
12+ "github.com/stretchr/testify/assert"
13+ "github.com/stretchr/testify/require"
14+
15+ "github.com/bnema/gordon/internal/domain"
16+)
17+
18+func TestRuntime_EnsureContainerRestartPolicy_SkipsWhenAlreadySet(t *testing.T) {
19+ var updateCalled bool
20+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
21+ switch {
22+ case r.Method == http.MethodGet && r.URL.Path == "/v1.41/containers/abc123/json":
23+ w.Header().Set("Content-Type", "application/json")
24+ _, _ = w.Write([]byte(`{
25+ "Id": "abc123",
26+ "HostConfig": {
27+ "RestartPolicy": {
28+ "Name": "always"
29+ }
30+ }
31+ }`))
32+ case r.Method == http.MethodPost && r.URL.Path == "/v1.41/containers/abc123/update":
33+ updateCalled = true
34+ w.WriteHeader(http.StatusInternalServerError)
35+ default:
36+ t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
37+ }
38+ }))
39+ defer server.Close()
40+
41+ runtime := newRuntimeForRestartPolicyHTTPServer(t, server)
42+ err := runtime.EnsureContainerRestartPolicy(context.Background(), "abc123", domain.RestartPolicyAlways)
43+ require.NoError(t, err)
44+ assert.False(t, updateCalled, "update should not be called when restart policy is already set")
45+}
46+
47+func TestRuntime_EnsureContainerRestartPolicy_UpdatesAndPreservesResources(t *testing.T) {
48+ var updateBody map[string]any
49+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
50+ switch {
51+ case r.Method == http.MethodGet && r.URL.Path == "/v1.41/containers/abc123/json":
52+ w.Header().Set("Content-Type", "application/json")
53+ _, _ = w.Write([]byte(`{
54+ "Id": "abc123",
55+ "HostConfig": {
56+ "RestartPolicy": {
57+ "Name": ""
58+ },
59+ "Memory": 268435456,
60+ "NanoCpus": 500000000,
61+ "PidsLimit": 128,
62+ "Ulimits": [
63+ {
64+ "Name": "nofile",
65+ "Soft": 65536,
66+ "Hard": 65536
67+ }
68+ ]
69+ }
70+ }`))
71+ case r.Method == http.MethodPost && r.URL.Path == "/v1.41/containers/abc123/update":
72+ require.NoError(t, json.NewDecoder(r.Body).Decode(&updateBody))
73+ w.Header().Set("Content-Type", "application/json")
74+ _, _ = w.Write([]byte(`{"Warnings":null}`))
75+ default:
76+ t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
77+ }
78+ }))
79+ defer server.Close()
80+
81+ runtime := newRuntimeForRestartPolicyHTTPServer(t, server)
82+ err := runtime.EnsureContainerRestartPolicy(context.Background(), "abc123", domain.RestartPolicyAlways)
83+ require.NoError(t, err)
84+ require.NotNil(t, updateBody)
85+ assert.Equal(t, domain.RestartPolicyAlways, updateBody["RestartPolicy"].(map[string]any)["Name"])
86+ assert.Equal(t, float64(268435456), updateBody["Memory"])
87+ assert.Equal(t, float64(500000000), updateBody["NanoCpus"])
88+ assert.Equal(t, float64(128), updateBody["PidsLimit"])
89+ ulimits, ok := updateBody["Ulimits"].([]any)
90+ require.True(t, ok, "Ulimits should be an array")
91+ require.Greater(t, len(ulimits), 0)
92+ firstUlimit, ok := ulimits[0].(map[string]any)
93+ require.True(t, ok, "first Ulimit should be a map")
94+ assert.Equal(t, "nofile", firstUlimit["Name"])
95+ assert.Equal(t, float64(65536), firstUlimit["Soft"])
96+ assert.Equal(t, float64(65536), firstUlimit["Hard"])
97+}
98+
99+func TestRuntime_EnsureContainerRestartPolicy_ReturnsErrorWhenHostConfigMissing(t *testing.T) {
100+ var updateCalled bool
101+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
102+ switch {
103+ case r.Method == http.MethodGet && r.URL.Path == "/v1.41/containers/abc123/json":
104+ w.Header().Set("Content-Type", "application/json")
105+ _, _ = w.Write([]byte(`{
106+ "Id": "abc123"
107+ }`))
108+ case r.Method == http.MethodPost && r.URL.Path == "/v1.41/containers/abc123/update":
109+ updateCalled = true
110+ w.WriteHeader(http.StatusInternalServerError)
111+ default:
112+ t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
113+ }
114+ }))
115+ defer server.Close()
116+
117+ runtime := newRuntimeForRestartPolicyHTTPServer(t, server)
118+ err := runtime.EnsureContainerRestartPolicy(context.Background(), "abc123", domain.RestartPolicyAlways)
119+ require.Error(t, err)
120+ assert.Contains(t, err.Error(), "missing host config")
121+ assert.False(t, updateCalled, "update should not be called when host config is missing")
122+}
123+
124+func newRuntimeForRestartPolicyHTTPServer(t *testing.T, server *httptest.Server) *Runtime {
125+ host := strings.TrimPrefix(server.URL, "http://")
126+ cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client()))
127+ require.NoError(t, err)
128+ return NewRuntimeWithClient(cli)
129+}
M internal/boundaries/out/mocks/mock_container_runtime.go +63 -0
@@ -428,6 +428,69 @@ func (_c *MockContainerRuntime_DisconnectContainerFromNetwork_Call) RunAndReturn
428428 return _c
429429 }
430430
431+// EnsureContainerRestartPolicy provides a mock function for the type MockContainerRuntime
432+func (_mock *MockContainerRuntime) EnsureContainerRestartPolicy(ctx context.Context, containerID string, policy string) error {
433+ ret := _mock.Called(ctx, containerID, policy)
434+
435+ if len(ret) == 0 {
436+ panic("no return value specified for EnsureContainerRestartPolicy")
437+ }
438+
439+ var r0 error
440+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
441+ r0 = returnFunc(ctx, containerID, policy)
442+ } else {
443+ r0 = ret.Error(0)
444+ }
445+ return r0
446+}
447+
448+// MockContainerRuntime_EnsureContainerRestartPolicy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnsureContainerRestartPolicy'
449+type MockContainerRuntime_EnsureContainerRestartPolicy_Call struct {
450+ *mock.Call
451+}
452+
453+// EnsureContainerRestartPolicy is a helper method to define mock.On call
454+// - ctx context.Context
455+// - containerID string
456+// - policy string
457+func (_e *MockContainerRuntime_Expecter) EnsureContainerRestartPolicy(ctx interface{}, containerID interface{}, policy interface{}) *MockContainerRuntime_EnsureContainerRestartPolicy_Call {
458+ return &MockContainerRuntime_EnsureContainerRestartPolicy_Call{Call: _e.mock.On("EnsureContainerRestartPolicy", ctx, containerID, policy)}
459+}
460+
461+func (_c *MockContainerRuntime_EnsureContainerRestartPolicy_Call) Run(run func(ctx context.Context, containerID string, policy string)) *MockContainerRuntime_EnsureContainerRestartPolicy_Call {
462+ _c.Call.Run(func(args mock.Arguments) {
463+ var arg0 context.Context
464+ if args[0] != nil {
465+ arg0 = args[0].(context.Context)
466+ }
467+ var arg1 string
468+ if args[1] != nil {
469+ arg1 = args[1].(string)
470+ }
471+ var arg2 string
472+ if args[2] != nil {
473+ arg2 = args[2].(string)
474+ }
475+ run(
476+ arg0,
477+ arg1,
478+ arg2,
479+ )
480+ })
481+ return _c
482+}
483+
484+func (_c *MockContainerRuntime_EnsureContainerRestartPolicy_Call) Return(err error) *MockContainerRuntime_EnsureContainerRestartPolicy_Call {
485+ _c.Call.Return(err)
486+ return _c
487+}
488+
489+func (_c *MockContainerRuntime_EnsureContainerRestartPolicy_Call) RunAndReturn(run func(ctx context.Context, containerID string, policy string) error) *MockContainerRuntime_EnsureContainerRestartPolicy_Call {
490+ _c.Call.Return(run)
491+ return _c
492+}
493+
431494 // ExecInContainer provides a mock function for the type MockContainerRuntime
432495 func (_mock *MockContainerRuntime) ExecInContainer(ctx context.Context, containerID string, cmd []string) (*out.ExecResult, error) {
433496 ret := _mock.Called(ctx, containerID, cmd)
@@ -428,6 +428,69 @@ func (_c *MockContainerRuntime_DisconnectContainerFromNetwork_Call) RunAndReturn
428 return _c428 return _c
429 }429 }
430 430
431+// EnsureContainerRestartPolicy provides a mock function for the type MockContainerRuntime
432+func (_mock *MockContainerRuntime) EnsureContainerRestartPolicy(ctx context.Context, containerID string, policy string) error {
433+ ret := _mock.Called(ctx, containerID, policy)
434+
435+ if len(ret) == 0 {
436+ panic("no return value specified for EnsureContainerRestartPolicy")
437+ }
438+
439+ var r0 error
440+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
441+ r0 = returnFunc(ctx, containerID, policy)
442+ } else {
443+ r0 = ret.Error(0)
444+ }
445+ return r0
446+}
447+
448+// MockContainerRuntime_EnsureContainerRestartPolicy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnsureContainerRestartPolicy'
449+type MockContainerRuntime_EnsureContainerRestartPolicy_Call struct {
450+ *mock.Call
451+}
452+
453+// EnsureContainerRestartPolicy is a helper method to define mock.On call
454+// - ctx context.Context
455+// - containerID string
456+// - policy string
457+func (_e *MockContainerRuntime_Expecter) EnsureContainerRestartPolicy(ctx interface{}, containerID interface{}, policy interface{}) *MockContainerRuntime_EnsureContainerRestartPolicy_Call {
458+ return &MockContainerRuntime_EnsureContainerRestartPolicy_Call{Call: _e.mock.On("EnsureContainerRestartPolicy", ctx, containerID, policy)}
459+}
460+
461+func (_c *MockContainerRuntime_EnsureContainerRestartPolicy_Call) Run(run func(ctx context.Context, containerID string, policy string)) *MockContainerRuntime_EnsureContainerRestartPolicy_Call {
462+ _c.Call.Run(func(args mock.Arguments) {
463+ var arg0 context.Context
464+ if args[0] != nil {
465+ arg0 = args[0].(context.Context)
466+ }
467+ var arg1 string
468+ if args[1] != nil {
469+ arg1 = args[1].(string)
470+ }
471+ var arg2 string
472+ if args[2] != nil {
473+ arg2 = args[2].(string)
474+ }
475+ run(
476+ arg0,
477+ arg1,
478+ arg2,
479+ )
480+ })
481+ return _c
482+}
483+
484+func (_c *MockContainerRuntime_EnsureContainerRestartPolicy_Call) Return(err error) *MockContainerRuntime_EnsureContainerRestartPolicy_Call {
485+ _c.Call.Return(err)
486+ return _c
487+}
488+
489+func (_c *MockContainerRuntime_EnsureContainerRestartPolicy_Call) RunAndReturn(run func(ctx context.Context, containerID string, policy string) error) *MockContainerRuntime_EnsureContainerRestartPolicy_Call {
490+ _c.Call.Return(run)
491+ return _c
492+}
493+
431 // ExecInContainer provides a mock function for the type MockContainerRuntime494 // ExecInContainer provides a mock function for the type MockContainerRuntime
432 func (_mock *MockContainerRuntime) ExecInContainer(ctx context.Context, containerID string, cmd []string) (*out.ExecResult, error) {495 func (_mock *MockContainerRuntime) ExecInContainer(ctx context.Context, containerID string, cmd []string) (*out.ExecResult, error) {
433 ret := _mock.Called(ctx, containerID, cmd)496 ret := _mock.Called(ctx, containerID, cmd)
M internal/boundaries/out/runtime.go +1 -0
@@ -20,6 +20,7 @@ type ContainerRuntime interface {
2020 RestartContainer(ctx context.Context, containerID string) error
2121 RemoveContainer(ctx context.Context, containerID string, force bool) error
2222 RenameContainer(ctx context.Context, containerID, newName string) error
23+ EnsureContainerRestartPolicy(ctx context.Context, containerID, policy string) error
2324
2425 // Container inspection
2526 ListContainers(ctx context.Context, all bool) ([]*domain.Container, error)
@@ -20,6 +20,7 @@ type ContainerRuntime interface {
20 RestartContainer(ctx context.Context, containerID string) error20 RestartContainer(ctx context.Context, containerID string) error
21 RemoveContainer(ctx context.Context, containerID string, force bool) error21 RemoveContainer(ctx context.Context, containerID string, force bool) error
22 RenameContainer(ctx context.Context, containerID, newName string) error22 RenameContainer(ctx context.Context, containerID, newName string) error
23+ EnsureContainerRestartPolicy(ctx context.Context, containerID, policy string) error
23 24
24 // Container inspection25 // Container inspection
25 ListContainers(ctx context.Context, all bool) ([]*domain.Container, error)26 ListContainers(ctx context.Context, all bool) ([]*domain.Container, error)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close