feat: cleanup zombie networks
This commit is contained in:
parent
7664d14207
commit
b231e8736c
@ -1,15 +1,15 @@
|
||||
with-expecter: true
|
||||
dir: "generated/mocks/{{ .InterfaceDirRelative }}"
|
||||
dir: "{{ .InterfaceDir }}/mocks"
|
||||
filename: "{{ .InterfaceName | lower }}_mock.go"
|
||||
mockname: "{{ .InterfaceName }}"
|
||||
outpkg: "{{ .PackageName }}"
|
||||
outpkg: mocks
|
||||
issue-845-fix: true
|
||||
resolve-type-alias: false
|
||||
packages:
|
||||
git.netflux.io/rob/octoplex/container:
|
||||
git.netflux.io/rob/octoplex/internal/container:
|
||||
interfaces:
|
||||
DockerClient:
|
||||
git.netflux.io/rob/octoplex/mediaserver:
|
||||
git.netflux.io/rob/octoplex/internal/mediaserver:
|
||||
interfaces:
|
||||
httpClient:
|
||||
config:
|
||||
|
@ -51,6 +51,7 @@ func Run(ctx context.Context, params RunParams) error {
|
||||
updateUI := func() { ui.SetState(*state) }
|
||||
updateUI()
|
||||
|
||||
// TODO: check for unused networks.
|
||||
if exists, err := containerClient.ContainerRunning(ctx, container.AllContainers()); err != nil {
|
||||
return fmt.Errorf("check existing containers: %w", err)
|
||||
} else if exists {
|
||||
@ -58,6 +59,9 @@ func Run(ctx context.Context, params RunParams) error {
|
||||
if err = containerClient.RemoveContainers(ctx, container.AllContainers()); err != nil {
|
||||
return fmt.Errorf("remove existing containers: %w", err)
|
||||
}
|
||||
if err = containerClient.RemoveUnusedNetworks(ctx); err != nil {
|
||||
return fmt.Errorf("remove unused networks: %w", err)
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@ -45,6 +46,7 @@ type DockerClient interface {
|
||||
ImagePull(context.Context, string, image.PullOptions) (io.ReadCloser, error)
|
||||
NetworkConnect(context.Context, string, string, *network.EndpointSettings) error
|
||||
NetworkCreate(context.Context, string, network.CreateOptions) (network.CreateResponse, error)
|
||||
NetworkList(context.Context, network.ListOptions) ([]network.Summary, error)
|
||||
NetworkRemove(context.Context, string) error
|
||||
}
|
||||
|
||||
@ -73,7 +75,14 @@ type Client struct {
|
||||
// NewClient creates a new Client.
|
||||
func NewClient(ctx context.Context, apiClient DockerClient, logger *slog.Logger) (*Client, error) {
|
||||
id := shortid.New()
|
||||
network, err := apiClient.NetworkCreate(ctx, domain.AppName+"-"+id.String(), network.CreateOptions{Driver: "bridge"})
|
||||
network, err := apiClient.NetworkCreate(
|
||||
ctx,
|
||||
domain.AppName+"-"+id.String(),
|
||||
network.CreateOptions{
|
||||
Driver: "bridge",
|
||||
Labels: map[string]string{LabelApp: domain.AppName, LabelAppID: id.String()},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("network create: %w", err)
|
||||
}
|
||||
@ -441,6 +450,38 @@ func (a *Client) RemoveContainers(ctx context.Context, labelOptions LabelOptions
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveUnusedNetworks removes all networks that are not used by any
|
||||
// container.
|
||||
func (a *Client) RemoveUnusedNetworks(ctx context.Context) error {
|
||||
networks, err := a.otherNetworks(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("other networks: %w", err)
|
||||
}
|
||||
|
||||
for _, network := range networks {
|
||||
a.logger.Info("Removing network", "id", shortID(network.ID))
|
||||
if err = a.apiClient.NetworkRemove(ctx, network.ID); err != nil {
|
||||
a.logger.Error("Error removing network", "err", err, "id", shortID(network.ID))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Client) otherNetworks(ctx context.Context) ([]network.Summary, error) {
|
||||
filterArgs := filters.NewArgs()
|
||||
filterArgs.Add("label", LabelApp+"="+domain.AppName)
|
||||
|
||||
networks, err := a.apiClient.NetworkList(ctx, network.ListOptions{Filters: filterArgs})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("network list: %w", err)
|
||||
}
|
||||
|
||||
return slices.DeleteFunc(networks, func(n network.Summary) bool {
|
||||
return n.ID == a.networkID
|
||||
}), nil
|
||||
}
|
||||
|
||||
// LabelOptions is a function that returns a map of labels.
|
||||
type LabelOptions func() map[string]string
|
||||
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.netflux.io/rob/octoplex/internal/container"
|
||||
containermocks "git.netflux.io/rob/octoplex/internal/generated/mocks/container"
|
||||
"git.netflux.io/rob/octoplex/internal/container/mocks"
|
||||
"git.netflux.io/rob/octoplex/internal/testhelpers"
|
||||
dockercontainer "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/events"
|
||||
@ -32,12 +32,14 @@ func TestClientRunContainer(t *testing.T) {
|
||||
eventsC := make(chan events.Message)
|
||||
eventsErrC := make(chan error)
|
||||
|
||||
var dockerClient containermocks.DockerClient
|
||||
var dockerClient mocks.DockerClient
|
||||
defer dockerClient.AssertExpectations(t)
|
||||
|
||||
dockerClient.
|
||||
EXPECT().
|
||||
NetworkCreate(mock.Anything, mock.Anything, network.CreateOptions{Driver: "bridge"}).
|
||||
NetworkCreate(mock.Anything, mock.Anything, mock.MatchedBy(func(opts network.CreateOptions) bool {
|
||||
return opts.Driver == "bridge" && len(opts.Labels) > 0
|
||||
})).
|
||||
Return(network.CreateResponse{ID: "test-network"}, nil)
|
||||
dockerClient.
|
||||
EXPECT().
|
||||
@ -112,12 +114,14 @@ func TestClientRunContainer(t *testing.T) {
|
||||
func TestClientRunContainerErrorStartingContainer(t *testing.T) {
|
||||
logger := testhelpers.NewTestLogger()
|
||||
|
||||
var dockerClient containermocks.DockerClient
|
||||
var dockerClient mocks.DockerClient
|
||||
defer dockerClient.AssertExpectations(t)
|
||||
|
||||
dockerClient.
|
||||
EXPECT().
|
||||
NetworkCreate(mock.Anything, mock.Anything, network.CreateOptions{Driver: "bridge"}).
|
||||
NetworkCreate(mock.Anything, mock.Anything, mock.MatchedBy(func(opts network.CreateOptions) bool {
|
||||
return opts.Driver == "bridge" && len(opts.Labels) > 0
|
||||
})).
|
||||
Return(network.CreateResponse{ID: "test-network"}, nil)
|
||||
dockerClient.
|
||||
EXPECT().
|
||||
@ -156,12 +160,14 @@ func TestClientRunContainerErrorStartingContainer(t *testing.T) {
|
||||
func TestClientClose(t *testing.T) {
|
||||
logger := testhelpers.NewTestLogger()
|
||||
|
||||
var dockerClient containermocks.DockerClient
|
||||
var dockerClient mocks.DockerClient
|
||||
defer dockerClient.AssertExpectations(t)
|
||||
|
||||
dockerClient.
|
||||
EXPECT().
|
||||
NetworkCreate(mock.Anything, mock.Anything, network.CreateOptions{Driver: "bridge"}).
|
||||
NetworkCreate(mock.Anything, mock.Anything, mock.MatchedBy(func(opts network.CreateOptions) bool {
|
||||
return opts.Driver == "bridge" && len(opts.Labels) > 0
|
||||
})).
|
||||
Return(network.CreateResponse{ID: "test-network"}, nil)
|
||||
dockerClient.
|
||||
EXPECT().
|
||||
@ -189,3 +195,33 @@ func TestClientClose(t *testing.T) {
|
||||
|
||||
require.NoError(t, containerClient.Close())
|
||||
}
|
||||
|
||||
func TestRemoveUnusedNetworks(t *testing.T) {
|
||||
logger := testhelpers.NewTestLogger()
|
||||
|
||||
var dockerClient mocks.DockerClient
|
||||
defer dockerClient.AssertExpectations(t)
|
||||
|
||||
dockerClient.
|
||||
EXPECT().
|
||||
NetworkCreate(mock.Anything, mock.Anything, mock.MatchedBy(func(opts network.CreateOptions) bool {
|
||||
return opts.Driver == "bridge" && len(opts.Labels) > 0
|
||||
})).
|
||||
Return(network.CreateResponse{ID: "test-network"}, nil)
|
||||
dockerClient.
|
||||
EXPECT().
|
||||
NetworkList(mock.Anything, mock.Anything).
|
||||
Return([]network.Summary{
|
||||
{ID: "test-network"},
|
||||
{ID: "another-network"},
|
||||
}, nil)
|
||||
dockerClient.
|
||||
EXPECT().
|
||||
NetworkRemove(mock.Anything, "another-network").
|
||||
Return(nil)
|
||||
|
||||
containerClient, err := container.NewClient(t.Context(), &dockerClient, logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, containerClient.RemoveUnusedNetworks(t.Context()))
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
containermocks "git.netflux.io/rob/octoplex/internal/generated/mocks/container"
|
||||
"git.netflux.io/rob/octoplex/internal/container/mocks"
|
||||
"git.netflux.io/rob/octoplex/internal/testhelpers"
|
||||
"github.com/docker/docker/api/types/events"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -19,7 +19,7 @@ func TestHandleEvents(t *testing.T) {
|
||||
|
||||
containerID := "b905f51b47242090ae504c184c7bc84d6274511ef763c1847039dcaa00a3ad27"
|
||||
|
||||
var dockerClient containermocks.DockerClient
|
||||
var dockerClient mocks.DockerClient
|
||||
defer dockerClient.AssertExpectations(t)
|
||||
|
||||
dockerClient.
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Code generated by mockery v2.52.2. DO NOT EDIT.
|
||||
|
||||
package container
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
@ -746,6 +746,65 @@ func (_c *DockerClient_NetworkCreate_Call) RunAndReturn(run func(context.Context
|
||||
return _c
|
||||
}
|
||||
|
||||
// NetworkList provides a mock function with given fields: _a0, _a1
|
||||
func (_m *DockerClient) NetworkList(_a0 context.Context, _a1 network.ListOptions) ([]network.Summary, error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for NetworkList")
|
||||
}
|
||||
|
||||
var r0 []network.Summary
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, network.ListOptions) ([]network.Summary, error)); ok {
|
||||
return rf(_a0, _a1)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, network.ListOptions) []network.Summary); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]network.Summary)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, network.ListOptions) error); ok {
|
||||
r1 = rf(_a0, _a1)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// DockerClient_NetworkList_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NetworkList'
|
||||
type DockerClient_NetworkList_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// NetworkList is a helper method to define mock.On call
|
||||
// - _a0 context.Context
|
||||
// - _a1 network.ListOptions
|
||||
func (_e *DockerClient_Expecter) NetworkList(_a0 interface{}, _a1 interface{}) *DockerClient_NetworkList_Call {
|
||||
return &DockerClient_NetworkList_Call{Call: _e.mock.On("NetworkList", _a0, _a1)}
|
||||
}
|
||||
|
||||
func (_c *DockerClient_NetworkList_Call) Run(run func(_a0 context.Context, _a1 network.ListOptions)) *DockerClient_NetworkList_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(network.ListOptions))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *DockerClient_NetworkList_Call) Return(_a0 []network.Summary, _a1 error) *DockerClient_NetworkList_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *DockerClient_NetworkList_Call) RunAndReturn(run func(context.Context, network.ListOptions) ([]network.Summary, error)) *DockerClient_NetworkList_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NetworkRemove provides a mock function with given fields: _a0, _a1
|
||||
func (_m *DockerClient) NetworkRemove(_a0 context.Context, _a1 string) error {
|
||||
ret := _m.Called(_a0, _a1)
|
@ -7,7 +7,7 @@ import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
containermocks "git.netflux.io/rob/octoplex/internal/generated/mocks/container"
|
||||
"git.netflux.io/rob/octoplex/internal/container/mocks"
|
||||
"git.netflux.io/rob/octoplex/internal/testhelpers"
|
||||
dockercontainer "github.com/docker/docker/api/types/container"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -24,7 +24,7 @@ func TestHandleStats(t *testing.T) {
|
||||
pr, pw := io.Pipe()
|
||||
containerID := "b905f51b47242090ae504c184c7bc84d6274511ef763c1847039dcaa00a3ad27"
|
||||
|
||||
var dockerClient containermocks.DockerClient
|
||||
var dockerClient mocks.DockerClient
|
||||
defer dockerClient.AssertExpectations(t)
|
||||
|
||||
dockerClient.
|
||||
@ -70,7 +70,7 @@ func TestHandleStatsWithContainerRestart(t *testing.T) {
|
||||
pr, pw := io.Pipe()
|
||||
containerID := "d0adc747fb12b9ce2376408aed8538a0769de55aa9c239313f231d9d80402e39"
|
||||
|
||||
var dockerClient containermocks.DockerClient
|
||||
var dockerClient mocks.DockerClient
|
||||
defer dockerClient.AssertExpectations(t)
|
||||
|
||||
dockerClient.
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
mocks "git.netflux.io/rob/octoplex/internal/generated/mocks/mediaserver"
|
||||
"git.netflux.io/rob/octoplex/internal/mediaserver/mocks"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Code generated by mockery v2.52.2. DO NOT EDIT.
|
||||
|
||||
package mediaserver
|
||||
package mocks
|
||||
|
||||
import (
|
||||
http "net/http"
|
Loading…
x
Reference in New Issue
Block a user