236 lines
8.3 KiB
Go
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)
|
|
})
|
|
}
|