Files
portainer/api/http/handler/registries/registry_delete_test.go
2026-04-13 11:32:07 -03:00

236 lines
8.3 KiB
Go

package registries
import (
"errors"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/internal/testhelpers"
kubecli "github.com/portainer/portainer/api/kubernetes/cli"
"github.com/portainer/portainer/api/pendingactions"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kfake "k8s.io/client-go/kubernetes/fake"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// spyKubeClient for registry delete tests - same pattern as endpoint_registry_access_test.go
type deleteSpyKubeClient struct {
portainer.KubeClient
deleteSecretErrors map[string]error
removePullSecretErrors map[string]error
deletedSecrets []string
removedPullSecrets []string
}
func newDeleteSpy() *deleteSpyKubeClient {
return &deleteSpyKubeClient{
deleteSecretErrors: make(map[string]error),
removePullSecretErrors: make(map[string]error),
}
}
func (s *deleteSpyKubeClient) DeleteRegistrySecret(_ portainer.RegistryID, namespace string) error {
s.deletedSecrets = append(s.deletedSecrets, namespace)
return s.deleteSecretErrors[namespace]
}
func (s *deleteSpyKubeClient) RemoveImagePullSecretFromServiceAccount(namespace, _, _ string) error {
s.removedPullSecrets = append(s.removedPullSecrets, namespace)
return s.removePullSecretErrors[namespace]
}
func newTestHandler(t *testing.T) (*Handler, dataservices.DataStore) {
t.Helper()
_, store := datastore.MustNewTestStore(t, false, false)
require.NotNil(t, store)
handler := NewHandler(testhelpers.NewTestRequestBouncer())
handler.DataStore = store
return handler, store
}
// --- cleanupRegistryFromNamespaces unit tests ---
func TestCleanupRegistryFromNamespaces(t *testing.T) {
t.Parallel()
const registryID portainer.RegistryID = 3
const endpointID portainer.EndpointID = 1
t.Run("all namespaces succeed - returns empty failed list", func(t *testing.T) {
spy := newDeleteSpy()
failed := cleanupRegistryFromNamespaces(spy, registryID, []string{"ns-a", "ns-b"}, endpointID)
assert.Empty(t, failed)
assert.ElementsMatch(t, []string{"ns-a", "ns-b"}, spy.removedPullSecrets)
assert.ElementsMatch(t, []string{"ns-a", "ns-b"}, spy.deletedSecrets)
})
t.Run("SA removal fails - namespace in failed list and secret not deleted", func(t *testing.T) {
spy := newDeleteSpy()
spy.removePullSecretErrors["ns-a"] = errors.New("sa error")
failed := cleanupRegistryFromNamespaces(spy, registryID, []string{"ns-a", "ns-b"}, endpointID)
assert.Equal(t, []string{"ns-a"}, failed)
assert.ElementsMatch(t, []string{"ns-a", "ns-b"}, spy.removedPullSecrets)
assert.Equal(t, []string{"ns-b"}, spy.deletedSecrets, "ns-a secret must not be deleted when SA removal fails")
})
t.Run("secret deletion fails - namespace in failed list", func(t *testing.T) {
spy := newDeleteSpy()
spy.deleteSecretErrors["ns-a"] = errors.New("delete error")
failed := cleanupRegistryFromNamespaces(spy, registryID, []string{"ns-a", "ns-b"}, endpointID)
assert.Equal(t, []string{"ns-a"}, failed)
assert.ElementsMatch(t, []string{"ns-a", "ns-b"}, spy.removedPullSecrets)
assert.ElementsMatch(t, []string{"ns-a", "ns-b"}, spy.deletedSecrets)
})
t.Run("both operations fail for all namespaces - all in failed list", func(t *testing.T) {
spy := newDeleteSpy()
spy.removePullSecretErrors["ns-a"] = errors.New("err")
spy.removePullSecretErrors["ns-b"] = errors.New("err")
failed := cleanupRegistryFromNamespaces(spy, registryID, []string{"ns-a", "ns-b"}, endpointID)
assert.ElementsMatch(t, []string{"ns-a", "ns-b"}, failed)
assert.Empty(t, spy.deletedSecrets)
})
t.Run("empty namespace list - returns empty failed list", func(t *testing.T) {
spy := newDeleteSpy()
failed := cleanupRegistryFromNamespaces(spy, registryID, []string{}, endpointID)
assert.Empty(t, failed)
assert.Empty(t, spy.removedPullSecrets)
assert.Empty(t, spy.deletedSecrets)
})
}
// --- deleteKubernetesSecrets integration tests ---
func TestDeleteKubernetesSecrets(t *testing.T) {
t.Parallel()
const registryID portainer.RegistryID = 3
const endpointID portainer.EndpointID = 1
newHandlerWithFakeK8s := func(t *testing.T, endpoint *portainer.Endpoint, registry *portainer.Registry) (*Handler, *datastore.Store) {
t.Helper()
_, store := datastore.MustNewTestStore(t, true, false)
require.NoError(t, store.Endpoint().Create(endpoint))
require.NoError(t, store.Registry().Create(registry))
defaultSA := &v1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "ns-a"},
}
fakeK8s := kfake.NewSimpleClientset(defaultSA)
factory := kubecli.NewTestClientFactory(endpointID, kubecli.NewTestKubeClient(fakeK8s))
pas := pendingactions.NewService(store, nil)
h := &Handler{
DataStore: store,
K8sClientFactory: factory,
PendingActionsService: pas,
requestBouncer: testhelpers.NewTestRequestBouncer(),
}
return h, store
}
t.Run("GetPrivilegedKubeClient fails - no pending action created", func(t *testing.T) {
// KubernetesLocalEnvironment calls rest.InClusterConfig() which fails outside
// a real cluster, causing GetPrivilegedKubeClient to return an error gracefully.
endpoint := &portainer.Endpoint{
ID: endpointID,
Name: "test-env",
Type: portainer.KubernetesLocalEnvironment,
}
registry := &portainer.Registry{
ID: registryID,
RegistryAccesses: portainer.RegistryAccesses{
endpointID: portainer.RegistryAccessPolicies{Namespaces: []string{"ns-a"}},
},
}
_, store := datastore.MustNewTestStore(t, true, false)
require.NoError(t, store.Endpoint().Create(endpoint))
require.NoError(t, store.Registry().Create(registry))
// Empty factory: endpoint not in cache, CreateConfig will fail → returns error, not panic
emptyFactory, err := kubecli.NewClientFactory(nil, nil, nil, "test", "", "")
require.NoError(t, err)
pas := pendingactions.NewService(store, nil)
h := &Handler{
DataStore: store,
K8sClientFactory: emptyFactory,
PendingActionsService: pas,
requestBouncer: testhelpers.NewTestRequestBouncer(),
}
h.deleteKubernetesSecrets(store, registry)
actions, err := store.PendingActions().ReadAll(func(portainer.PendingAction) bool { return true })
require.NoError(t, err)
assert.Empty(t, actions, "no pending action should be created when kube client cannot be obtained")
})
t.Run("all namespaces succeed - no pending action created", func(t *testing.T) {
endpoint := &portainer.Endpoint{
ID: endpointID,
Name: "test-env",
Type: portainer.AgentOnKubernetesEnvironment,
}
registry := &portainer.Registry{
ID: registryID,
RegistryAccesses: portainer.RegistryAccesses{
endpointID: portainer.RegistryAccessPolicies{Namespaces: []string{"ns-a"}},
},
}
_, store := datastore.MustNewTestStore(t, true, false)
require.NoError(t, store.Endpoint().Create(endpoint))
require.NoError(t, store.Registry().Create(registry))
defaultSA := &v1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "ns-a"},
}
fakeK8s := kfake.NewSimpleClientset(defaultSA)
factory := kubecli.NewTestClientFactory(endpointID, kubecli.NewTestKubeClient(fakeK8s))
pas := pendingactions.NewService(store, nil)
h := &Handler{
DataStore: store,
K8sClientFactory: factory,
PendingActionsService: pas,
requestBouncer: testhelpers.NewTestRequestBouncer(),
}
h.deleteKubernetesSecrets(store, registry)
actions, err := store.PendingActions().ReadAll(func(portainer.PendingAction) bool { return true })
require.NoError(t, err)
assert.Empty(t, actions)
})
t.Run("registry with no Kubernetes namespaces - no operations attempted", func(t *testing.T) {
endpoint := &portainer.Endpoint{
ID: endpointID,
Name: "test-env",
Type: portainer.AgentOnKubernetesEnvironment,
}
registry := &portainer.Registry{
ID: registryID,
RegistryAccesses: portainer.RegistryAccesses{
endpointID: portainer.RegistryAccessPolicies{Namespaces: nil},
},
}
h, store := newHandlerWithFakeK8s(t, endpoint, registry)
h.deleteKubernetesSecrets(store, registry)
actions, err := store.PendingActions().ReadAll(func(portainer.PendingAction) bool { return true })
require.NoError(t, err)
assert.Empty(t, actions)
})
}