feat: cleanup zombie networks
This commit is contained in:
parent
7664d14207
commit
b231e8736c
@ -1,15 +1,15 @@
|
|||||||
with-expecter: true
|
with-expecter: true
|
||||||
dir: "generated/mocks/{{ .InterfaceDirRelative }}"
|
dir: "{{ .InterfaceDir }}/mocks"
|
||||||
filename: "{{ .InterfaceName | lower }}_mock.go"
|
filename: "{{ .InterfaceName | lower }}_mock.go"
|
||||||
mockname: "{{ .InterfaceName }}"
|
mockname: "{{ .InterfaceName }}"
|
||||||
outpkg: "{{ .PackageName }}"
|
outpkg: mocks
|
||||||
issue-845-fix: true
|
issue-845-fix: true
|
||||||
resolve-type-alias: false
|
resolve-type-alias: false
|
||||||
packages:
|
packages:
|
||||||
git.netflux.io/rob/octoplex/container:
|
git.netflux.io/rob/octoplex/internal/container:
|
||||||
interfaces:
|
interfaces:
|
||||||
DockerClient:
|
DockerClient:
|
||||||
git.netflux.io/rob/octoplex/mediaserver:
|
git.netflux.io/rob/octoplex/internal/mediaserver:
|
||||||
interfaces:
|
interfaces:
|
||||||
httpClient:
|
httpClient:
|
||||||
config:
|
config:
|
||||||
|
@ -51,6 +51,7 @@ func Run(ctx context.Context, params RunParams) error {
|
|||||||
updateUI := func() { ui.SetState(*state) }
|
updateUI := func() { ui.SetState(*state) }
|
||||||
updateUI()
|
updateUI()
|
||||||
|
|
||||||
|
// TODO: check for unused networks.
|
||||||
if exists, err := containerClient.ContainerRunning(ctx, container.AllContainers()); err != nil {
|
if exists, err := containerClient.ContainerRunning(ctx, container.AllContainers()); err != nil {
|
||||||
return fmt.Errorf("check existing containers: %w", err)
|
return fmt.Errorf("check existing containers: %w", err)
|
||||||
} else if exists {
|
} else if exists {
|
||||||
@ -58,6 +59,9 @@ func Run(ctx context.Context, params RunParams) error {
|
|||||||
if err = containerClient.RemoveContainers(ctx, container.AllContainers()); err != nil {
|
if err = containerClient.RemoveContainers(ctx, container.AllContainers()); err != nil {
|
||||||
return fmt.Errorf("remove existing containers: %w", err)
|
return fmt.Errorf("remove existing containers: %w", err)
|
||||||
}
|
}
|
||||||
|
if err = containerClient.RemoveUnusedNetworks(ctx); err != nil {
|
||||||
|
return fmt.Errorf("remove unused networks: %w", err)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"maps"
|
"maps"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@ -45,6 +46,7 @@ type DockerClient interface {
|
|||||||
ImagePull(context.Context, string, image.PullOptions) (io.ReadCloser, error)
|
ImagePull(context.Context, string, image.PullOptions) (io.ReadCloser, error)
|
||||||
NetworkConnect(context.Context, string, string, *network.EndpointSettings) error
|
NetworkConnect(context.Context, string, string, *network.EndpointSettings) error
|
||||||
NetworkCreate(context.Context, string, network.CreateOptions) (network.CreateResponse, error)
|
NetworkCreate(context.Context, string, network.CreateOptions) (network.CreateResponse, error)
|
||||||
|
NetworkList(context.Context, network.ListOptions) ([]network.Summary, error)
|
||||||
NetworkRemove(context.Context, string) error
|
NetworkRemove(context.Context, string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,7 +75,14 @@ type Client struct {
|
|||||||
// NewClient creates a new Client.
|
// NewClient creates a new Client.
|
||||||
func NewClient(ctx context.Context, apiClient DockerClient, logger *slog.Logger) (*Client, error) {
|
func NewClient(ctx context.Context, apiClient DockerClient, logger *slog.Logger) (*Client, error) {
|
||||||
id := shortid.New()
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("network create: %w", err)
|
return nil, fmt.Errorf("network create: %w", err)
|
||||||
}
|
}
|
||||||
@ -441,6 +450,38 @@ func (a *Client) RemoveContainers(ctx context.Context, labelOptions LabelOptions
|
|||||||
return nil
|
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.
|
// LabelOptions is a function that returns a map of labels.
|
||||||
type LabelOptions func() map[string]string
|
type LabelOptions func() map[string]string
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.netflux.io/rob/octoplex/internal/container"
|
"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"
|
"git.netflux.io/rob/octoplex/internal/testhelpers"
|
||||||
dockercontainer "github.com/docker/docker/api/types/container"
|
dockercontainer "github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/events"
|
"github.com/docker/docker/api/types/events"
|
||||||
@ -32,12 +32,14 @@ func TestClientRunContainer(t *testing.T) {
|
|||||||
eventsC := make(chan events.Message)
|
eventsC := make(chan events.Message)
|
||||||
eventsErrC := make(chan error)
|
eventsErrC := make(chan error)
|
||||||
|
|
||||||
var dockerClient containermocks.DockerClient
|
var dockerClient mocks.DockerClient
|
||||||
defer dockerClient.AssertExpectations(t)
|
defer dockerClient.AssertExpectations(t)
|
||||||
|
|
||||||
dockerClient.
|
dockerClient.
|
||||||
EXPECT().
|
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)
|
Return(network.CreateResponse{ID: "test-network"}, nil)
|
||||||
dockerClient.
|
dockerClient.
|
||||||
EXPECT().
|
EXPECT().
|
||||||
@ -112,12 +114,14 @@ func TestClientRunContainer(t *testing.T) {
|
|||||||
func TestClientRunContainerErrorStartingContainer(t *testing.T) {
|
func TestClientRunContainerErrorStartingContainer(t *testing.T) {
|
||||||
logger := testhelpers.NewTestLogger()
|
logger := testhelpers.NewTestLogger()
|
||||||
|
|
||||||
var dockerClient containermocks.DockerClient
|
var dockerClient mocks.DockerClient
|
||||||
defer dockerClient.AssertExpectations(t)
|
defer dockerClient.AssertExpectations(t)
|
||||||
|
|
||||||
dockerClient.
|
dockerClient.
|
||||||
EXPECT().
|
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)
|
Return(network.CreateResponse{ID: "test-network"}, nil)
|
||||||
dockerClient.
|
dockerClient.
|
||||||
EXPECT().
|
EXPECT().
|
||||||
@ -156,12 +160,14 @@ func TestClientRunContainerErrorStartingContainer(t *testing.T) {
|
|||||||
func TestClientClose(t *testing.T) {
|
func TestClientClose(t *testing.T) {
|
||||||
logger := testhelpers.NewTestLogger()
|
logger := testhelpers.NewTestLogger()
|
||||||
|
|
||||||
var dockerClient containermocks.DockerClient
|
var dockerClient mocks.DockerClient
|
||||||
defer dockerClient.AssertExpectations(t)
|
defer dockerClient.AssertExpectations(t)
|
||||||
|
|
||||||
dockerClient.
|
dockerClient.
|
||||||
EXPECT().
|
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)
|
Return(network.CreateResponse{ID: "test-network"}, nil)
|
||||||
dockerClient.
|
dockerClient.
|
||||||
EXPECT().
|
EXPECT().
|
||||||
@ -189,3 +195,33 @@ func TestClientClose(t *testing.T) {
|
|||||||
|
|
||||||
require.NoError(t, containerClient.Close())
|
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"
|
"io"
|
||||||
"testing"
|
"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"
|
"git.netflux.io/rob/octoplex/internal/testhelpers"
|
||||||
"github.com/docker/docker/api/types/events"
|
"github.com/docker/docker/api/types/events"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -19,7 +19,7 @@ func TestHandleEvents(t *testing.T) {
|
|||||||
|
|
||||||
containerID := "b905f51b47242090ae504c184c7bc84d6274511ef763c1847039dcaa00a3ad27"
|
containerID := "b905f51b47242090ae504c184c7bc84d6274511ef763c1847039dcaa00a3ad27"
|
||||||
|
|
||||||
var dockerClient containermocks.DockerClient
|
var dockerClient mocks.DockerClient
|
||||||
defer dockerClient.AssertExpectations(t)
|
defer dockerClient.AssertExpectations(t)
|
||||||
|
|
||||||
dockerClient.
|
dockerClient.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// Code generated by mockery v2.52.2. DO NOT EDIT.
|
// Code generated by mockery v2.52.2. DO NOT EDIT.
|
||||||
|
|
||||||
package container
|
package mocks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
context "context"
|
context "context"
|
||||||
@ -746,6 +746,65 @@ func (_c *DockerClient_NetworkCreate_Call) RunAndReturn(run func(context.Context
|
|||||||
return _c
|
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
|
// NetworkRemove provides a mock function with given fields: _a0, _a1
|
||||||
func (_m *DockerClient) NetworkRemove(_a0 context.Context, _a1 string) error {
|
func (_m *DockerClient) NetworkRemove(_a0 context.Context, _a1 string) error {
|
||||||
ret := _m.Called(_a0, _a1)
|
ret := _m.Called(_a0, _a1)
|
@ -7,7 +7,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"testing"
|
"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"
|
"git.netflux.io/rob/octoplex/internal/testhelpers"
|
||||||
dockercontainer "github.com/docker/docker/api/types/container"
|
dockercontainer "github.com/docker/docker/api/types/container"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -24,7 +24,7 @@ func TestHandleStats(t *testing.T) {
|
|||||||
pr, pw := io.Pipe()
|
pr, pw := io.Pipe()
|
||||||
containerID := "b905f51b47242090ae504c184c7bc84d6274511ef763c1847039dcaa00a3ad27"
|
containerID := "b905f51b47242090ae504c184c7bc84d6274511ef763c1847039dcaa00a3ad27"
|
||||||
|
|
||||||
var dockerClient containermocks.DockerClient
|
var dockerClient mocks.DockerClient
|
||||||
defer dockerClient.AssertExpectations(t)
|
defer dockerClient.AssertExpectations(t)
|
||||||
|
|
||||||
dockerClient.
|
dockerClient.
|
||||||
@ -70,7 +70,7 @@ func TestHandleStatsWithContainerRestart(t *testing.T) {
|
|||||||
pr, pw := io.Pipe()
|
pr, pw := io.Pipe()
|
||||||
containerID := "d0adc747fb12b9ce2376408aed8538a0769de55aa9c239313f231d9d80402e39"
|
containerID := "d0adc747fb12b9ce2376408aed8538a0769de55aa9c239313f231d9d80402e39"
|
||||||
|
|
||||||
var dockerClient containermocks.DockerClient
|
var dockerClient mocks.DockerClient
|
||||||
defer dockerClient.AssertExpectations(t)
|
defer dockerClient.AssertExpectations(t)
|
||||||
|
|
||||||
dockerClient.
|
dockerClient.
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"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/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// Code generated by mockery v2.52.2. DO NOT EDIT.
|
// Code generated by mockery v2.52.2. DO NOT EDIT.
|
||||||
|
|
||||||
package mediaserver
|
package mocks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
http "net/http"
|
http "net/http"
|
Loading…
x
Reference in New Issue
Block a user