mirror of
https://github.com/portainer/portainer.git
synced 2025-07-18 21:09:40 +02:00
refactor: replace the kubectl
binary with the upstream sdk (#524)
This commit is contained in:
parent
4d4360b86b
commit
bc29419c17
17 changed files with 354 additions and 182 deletions
|
@ -167,8 +167,8 @@ func checkDBSchemaServerVersionMatch(dbStore dataservices.DataStore, serverVersi
|
||||||
return v.SchemaVersion == serverVersion && v.Edition == serverEdition
|
return v.SchemaVersion == serverVersion && v.Edition == serverEdition
|
||||||
}
|
}
|
||||||
|
|
||||||
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore dataservices.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, assetsPath string) portainer.KubernetesDeployer {
|
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore dataservices.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager) portainer.KubernetesDeployer {
|
||||||
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager, assetsPath)
|
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initHelmPackageManager() (libhelmtypes.HelmPackageManager, error) {
|
func initHelmPackageManager() (libhelmtypes.HelmPackageManager, error) {
|
||||||
|
@ -423,7 +423,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||||
log.Fatal().Err(err).Msg("failed initializing swarm stack manager")
|
log.Fatal().Err(err).Msg("failed initializing swarm stack manager")
|
||||||
}
|
}
|
||||||
|
|
||||||
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager, *flags.Assets)
|
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager)
|
||||||
|
|
||||||
pendingActionsService := pendingactions.NewService(dataStore, kubernetesClientFactory)
|
pendingActionsService := pendingactions.NewService(dataStore, kubernetesClientFactory)
|
||||||
pendingActionsService.RegisterHandler(actions.CleanNAPWithOverridePolicies, handlers.NewHandlerCleanNAPWithOverridePolicies(authorizationService, dataStore))
|
pendingActionsService.RegisterHandler(actions.CleanNAPWithOverridePolicies, handlers.NewHandlerCleanNAPWithOverridePolicies(authorizationService, dataStore))
|
||||||
|
|
|
@ -12,3 +12,15 @@ type kubernetesMockDeployer struct {
|
||||||
func NewKubernetesDeployer() *kubernetesMockDeployer {
|
func NewKubernetesDeployer() *kubernetesMockDeployer {
|
||||||
return &kubernetesMockDeployer{}
|
return &kubernetesMockDeployer{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (deployer *kubernetesMockDeployer) Deploy(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (deployer *kubernetesMockDeployer) Remove(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (deployer *kubernetesMockDeployer) Restart(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,8 @@
|
||||||
package exec
|
package exec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
@ -15,13 +10,17 @@ import (
|
||||||
"github.com/portainer/portainer/api/http/proxy/factory"
|
"github.com/portainer/portainer/api/http/proxy/factory"
|
||||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||||
|
"github.com/portainer/portainer/pkg/libkubectl"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultServerURL = "https://kubernetes.default.svc"
|
||||||
|
)
|
||||||
|
|
||||||
// KubernetesDeployer represents a service to deploy resources inside a Kubernetes environment(endpoint).
|
// KubernetesDeployer represents a service to deploy resources inside a Kubernetes environment(endpoint).
|
||||||
type KubernetesDeployer struct {
|
type KubernetesDeployer struct {
|
||||||
binaryPath string
|
|
||||||
dataStore dataservices.DataStore
|
dataStore dataservices.DataStore
|
||||||
reverseTunnelService portainer.ReverseTunnelService
|
reverseTunnelService portainer.ReverseTunnelService
|
||||||
signatureService portainer.DigitalSignatureService
|
signatureService portainer.DigitalSignatureService
|
||||||
|
@ -31,9 +30,8 @@ type KubernetesDeployer struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewKubernetesDeployer initializes a new KubernetesDeployer service.
|
// NewKubernetesDeployer initializes a new KubernetesDeployer service.
|
||||||
func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, kubernetesClientFactory *cli.ClientFactory, datastore dataservices.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, binaryPath string) *KubernetesDeployer {
|
func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, kubernetesClientFactory *cli.ClientFactory, datastore dataservices.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager) *KubernetesDeployer {
|
||||||
return &KubernetesDeployer{
|
return &KubernetesDeployer{
|
||||||
binaryPath: binaryPath,
|
|
||||||
dataStore: datastore,
|
dataStore: datastore,
|
||||||
reverseTunnelService: reverseTunnelService,
|
reverseTunnelService: reverseTunnelService,
|
||||||
signatureService: signatureService,
|
signatureService: signatureService,
|
||||||
|
@ -93,48 +91,41 @@ func (deployer *KubernetesDeployer) command(operation string, userID portainer.U
|
||||||
return "", errors.Wrap(err, "failed generating a user token")
|
return "", errors.Wrap(err, "failed generating a user token")
|
||||||
}
|
}
|
||||||
|
|
||||||
command := path.Join(deployer.binaryPath, "kubectl")
|
serverURL := defaultServerURL
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
command = path.Join(deployer.binaryPath, "kubectl.exe")
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{"--token", token}
|
|
||||||
if namespace != "" {
|
|
||||||
args = append(args, "--namespace", namespace)
|
|
||||||
}
|
|
||||||
|
|
||||||
if endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
|
if endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
|
||||||
url, proxy, err := deployer.getAgentURL(endpoint)
|
url, proxy, err := deployer.getAgentURL(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.WithMessage(err, "failed generating endpoint URL")
|
return "", errors.WithMessage(err, "failed generating endpoint URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
defer proxy.Close()
|
defer proxy.Close()
|
||||||
args = append(args, "--server", url)
|
|
||||||
args = append(args, "--insecure-skip-tls-verify")
|
serverURL = url
|
||||||
}
|
}
|
||||||
|
|
||||||
if operation == "delete" {
|
client, err := libkubectl.NewClient(&libkubectl.ClientAccess{
|
||||||
args = append(args, "--ignore-not-found=true")
|
Token: token,
|
||||||
}
|
ServerUrl: serverURL,
|
||||||
|
}, namespace, "", true)
|
||||||
args = append(args, operation)
|
|
||||||
for _, path := range manifestFiles {
|
|
||||||
args = append(args, "-f", strings.TrimSpace(path))
|
|
||||||
}
|
|
||||||
|
|
||||||
var stderr bytes.Buffer
|
|
||||||
cmd := exec.Command(command, args...)
|
|
||||||
cmd.Env = os.Environ()
|
|
||||||
cmd.Env = append(cmd.Env, "POD_NAMESPACE=default")
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
|
|
||||||
output, err := cmd.Output()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.Wrapf(err, "failed to execute kubectl command: %q", stderr.String())
|
return "", errors.Wrap(err, "failed to create kubectl client")
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(output), nil
|
operations := map[string]func(context.Context, []string) (string, error){
|
||||||
|
"apply": client.Apply,
|
||||||
|
"delete": client.Delete,
|
||||||
|
}
|
||||||
|
|
||||||
|
operationFunc, ok := operations[operation]
|
||||||
|
if !ok {
|
||||||
|
return "", errors.Errorf("unsupported operation: %s", operation)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := operationFunc(context.Background(), manifestFiles)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrapf(err, "failed to execute kubectl %s command", operation)
|
||||||
|
}
|
||||||
|
|
||||||
|
return output, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (deployer *KubernetesDeployer) getAgentURL(endpoint *portainer.Endpoint) (string, *factory.ProxyServer, error) {
|
func (deployer *KubernetesDeployer) getAgentURL(endpoint *portainer.Endpoint) (string, *factory.ProxyServer, error) {
|
||||||
|
|
173
api/exec/kubernetes_deploy_test.go
Normal file
173
api/exec/kubernetes_deploy_test.go
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
package exec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockKubectlClient struct {
|
||||||
|
applyFunc func(ctx context.Context, files []string) error
|
||||||
|
deleteFunc func(ctx context.Context, files []string) error
|
||||||
|
rolloutRestartFunc func(ctx context.Context, resources []string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockKubectlClient) Apply(ctx context.Context, files []string) error {
|
||||||
|
if m.applyFunc != nil {
|
||||||
|
return m.applyFunc(ctx, files)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockKubectlClient) Delete(ctx context.Context, files []string) error {
|
||||||
|
if m.deleteFunc != nil {
|
||||||
|
return m.deleteFunc(ctx, files)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockKubectlClient) RolloutRestart(ctx context.Context, resources []string) error {
|
||||||
|
if m.rolloutRestartFunc != nil {
|
||||||
|
return m.rolloutRestartFunc(ctx, resources)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func testExecuteKubectlOperation(client *mockKubectlClient, operation string, manifestFiles []string) error {
|
||||||
|
operations := map[string]func(context.Context, []string) error{
|
||||||
|
"apply": client.Apply,
|
||||||
|
"delete": client.Delete,
|
||||||
|
"rollout-restart": client.RolloutRestart,
|
||||||
|
}
|
||||||
|
|
||||||
|
operationFunc, ok := operations[operation]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unsupported operation: %s", operation)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := operationFunc(context.Background(), manifestFiles); err != nil {
|
||||||
|
return fmt.Errorf("failed to execute kubectl %s command: %w", operation, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecuteKubectlOperation_Apply_Success(t *testing.T) {
|
||||||
|
called := false
|
||||||
|
mockClient := &mockKubectlClient{
|
||||||
|
applyFunc: func(ctx context.Context, files []string) error {
|
||||||
|
called = true
|
||||||
|
assert.Equal(t, []string{"manifest1.yaml", "manifest2.yaml"}, files)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
manifests := []string{"manifest1.yaml", "manifest2.yaml"}
|
||||||
|
err := testExecuteKubectlOperation(mockClient, "apply", manifests)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, called)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecuteKubectlOperation_Apply_Error(t *testing.T) {
|
||||||
|
expectedErr := errors.New("kubectl apply failed")
|
||||||
|
called := false
|
||||||
|
mockClient := &mockKubectlClient{
|
||||||
|
applyFunc: func(ctx context.Context, files []string) error {
|
||||||
|
called = true
|
||||||
|
assert.Equal(t, []string{"error.yaml"}, files)
|
||||||
|
return expectedErr
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
manifests := []string{"error.yaml"}
|
||||||
|
err := testExecuteKubectlOperation(mockClient, "apply", manifests)
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), expectedErr.Error())
|
||||||
|
assert.True(t, called)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecuteKubectlOperation_Delete_Success(t *testing.T) {
|
||||||
|
called := false
|
||||||
|
mockClient := &mockKubectlClient{
|
||||||
|
deleteFunc: func(ctx context.Context, files []string) error {
|
||||||
|
called = true
|
||||||
|
assert.Equal(t, []string{"manifest1.yaml"}, files)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
manifests := []string{"manifest1.yaml"}
|
||||||
|
err := testExecuteKubectlOperation(mockClient, "delete", manifests)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, called)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecuteKubectlOperation_Delete_Error(t *testing.T) {
|
||||||
|
expectedErr := errors.New("kubectl delete failed")
|
||||||
|
called := false
|
||||||
|
mockClient := &mockKubectlClient{
|
||||||
|
deleteFunc: func(ctx context.Context, files []string) error {
|
||||||
|
called = true
|
||||||
|
assert.Equal(t, []string{"error.yaml"}, files)
|
||||||
|
return expectedErr
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
manifests := []string{"error.yaml"}
|
||||||
|
err := testExecuteKubectlOperation(mockClient, "delete", manifests)
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), expectedErr.Error())
|
||||||
|
assert.True(t, called)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecuteKubectlOperation_RolloutRestart_Success(t *testing.T) {
|
||||||
|
called := false
|
||||||
|
mockClient := &mockKubectlClient{
|
||||||
|
rolloutRestartFunc: func(ctx context.Context, resources []string) error {
|
||||||
|
called = true
|
||||||
|
assert.Equal(t, []string{"deployment/nginx"}, resources)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resources := []string{"deployment/nginx"}
|
||||||
|
err := testExecuteKubectlOperation(mockClient, "rollout-restart", resources)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, called)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecuteKubectlOperation_RolloutRestart_Error(t *testing.T) {
|
||||||
|
expectedErr := errors.New("kubectl rollout restart failed")
|
||||||
|
called := false
|
||||||
|
mockClient := &mockKubectlClient{
|
||||||
|
rolloutRestartFunc: func(ctx context.Context, resources []string) error {
|
||||||
|
called = true
|
||||||
|
assert.Equal(t, []string{"deployment/error"}, resources)
|
||||||
|
return expectedErr
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resources := []string{"deployment/error"}
|
||||||
|
err := testExecuteKubectlOperation(mockClient, "rollout-restart", resources)
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), expectedErr.Error())
|
||||||
|
assert.True(t, called)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecuteKubectlOperation_UnsupportedOperation(t *testing.T) {
|
||||||
|
mockClient := &mockKubectlClient{}
|
||||||
|
|
||||||
|
err := testExecuteKubectlOperation(mockClient, "unsupported", []string{})
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "unsupported operation")
|
||||||
|
}
|
|
@ -7,21 +7,17 @@ ARCH=${2:-"amd64"}
|
||||||
BINARY_VERSION_FILE="./binary-version.json"
|
BINARY_VERSION_FILE="./binary-version.json"
|
||||||
|
|
||||||
dockerVersion=$(jq -r '.docker' < "${BINARY_VERSION_FILE}")
|
dockerVersion=$(jq -r '.docker' < "${BINARY_VERSION_FILE}")
|
||||||
helmVersion=$(jq -r '.helm' < "${BINARY_VERSION_FILE}")
|
|
||||||
kubectlVersion=$(jq -r '.kubectl' < "${BINARY_VERSION_FILE}")
|
|
||||||
mingitVersion=$(jq -r '.mingit' < "${BINARY_VERSION_FILE}")
|
mingitVersion=$(jq -r '.mingit' < "${BINARY_VERSION_FILE}")
|
||||||
|
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
|
|
||||||
echo "Checking and downloading binaries for docker ${dockerVersion}, helm ${helmVersion}, kubectl ${kubectlVersion} and mingit ${mingitVersion} (Windows only)"
|
echo "Checking and downloading binaries for docker ${dockerVersion}, and mingit ${mingitVersion} (Windows only)"
|
||||||
|
|
||||||
# Determine the binary file names based on the platform
|
# Determine the binary file names based on the platform
|
||||||
dockerBinary="dist/docker"
|
dockerBinary="dist/docker"
|
||||||
kubectlBinary="dist/kubectl"
|
|
||||||
|
|
||||||
if [ "$PLATFORM" == "windows" ]; then
|
if [ "$PLATFORM" == "windows" ]; then
|
||||||
dockerBinary="dist/docker.exe"
|
dockerBinary="dist/docker.exe"
|
||||||
kubectlBinary="dist/kubectl.exe"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check and download docker binary
|
# Check and download docker binary
|
||||||
|
@ -32,14 +28,6 @@ else
|
||||||
echo "Docker binary already exists, skipping download."
|
echo "Docker binary already exists, skipping download."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check and download kubectl binary
|
|
||||||
if [ ! -f "$kubectlBinary" ]; then
|
|
||||||
echo "Downloading kubectl binary..."
|
|
||||||
/usr/bin/env bash ./build/download_kubectl_binary.sh "$PLATFORM" "$ARCH" "$kubectlVersion"
|
|
||||||
else
|
|
||||||
echo "Kubectl binary already exists, skipping download."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check and download mingit binary only for Windows
|
# Check and download mingit binary only for Windows
|
||||||
if [ "$PLATFORM" == "windows" ]; then
|
if [ "$PLATFORM" == "windows" ]; then
|
||||||
if [ ! -f "dist/mingit" ]; then
|
if [ ! -f "dist/mingit" ]; then
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
if [[ $# -ne 3 ]]; then
|
|
||||||
echo "Illegal number of parameters" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
PLATFORM=$1
|
|
||||||
ARCH=$2
|
|
||||||
KUBECTL_VERSION=$3
|
|
||||||
|
|
||||||
if [[ ${PLATFORM} == "windows" ]]; then
|
|
||||||
wget --tries=3 --waitretry=30 --quiet -O "dist/kubectl.exe" "https://dl.k8s.io/${KUBECTL_VERSION}/bin/windows/amd64/kubectl.exe"
|
|
||||||
chmod +x "dist/kubectl.exe"
|
|
||||||
else
|
|
||||||
wget --tries=3 --waitretry=30 --quiet -O "dist/kubectl" "https://dl.k8s.io/${KUBECTL_VERSION}/bin/${PLATFORM}/${ARCH}/kubectl"
|
|
||||||
chmod +x "dist/kubectl"
|
|
||||||
fi
|
|
|
@ -11,7 +11,6 @@ LABEL org.opencontainers.image.title="Portainer" \
|
||||||
com.docker.extension.additional-urls="[{\"title\":\"Website\",\"url\":\"https://www.portainer.io?utm_campaign=DockerCon&utm_source=DockerDesktop\"},{\"title\":\"Documentation\",\"url\":\"https://docs.portainer.io\"},{\"title\":\"Support\",\"url\":\"https://join.slack.com/t/portainer/shared_invite/zt-txh3ljab-52QHTyjCqbe5RibC2lcjKA\"}]"
|
com.docker.extension.additional-urls="[{\"title\":\"Website\",\"url\":\"https://www.portainer.io?utm_campaign=DockerCon&utm_source=DockerDesktop\"},{\"title\":\"Documentation\",\"url\":\"https://docs.portainer.io\"},{\"title\":\"Support\",\"url\":\"https://join.slack.com/t/portainer/shared_invite/zt-txh3ljab-52QHTyjCqbe5RibC2lcjKA\"}]"
|
||||||
|
|
||||||
COPY dist/docker /
|
COPY dist/docker /
|
||||||
COPY dist/kubectl /
|
|
||||||
COPY dist/mustache-templates /mustache-templates/
|
COPY dist/mustache-templates /mustache-templates/
|
||||||
COPY dist/portainer /
|
COPY dist/portainer /
|
||||||
COPY dist/public /public/
|
COPY dist/public /public/
|
||||||
|
|
|
@ -11,7 +11,6 @@ LABEL org.opencontainers.image.title="Portainer" \
|
||||||
com.docker.extension.additional-urls="[{\"title\":\"Website\",\"url\":\"https://www.portainer.io?utm_campaign=DockerCon&utm_source=DockerDesktop\"},{\"title\":\"Documentation\",\"url\":\"https://docs.portainer.io\"},{\"title\":\"Support\",\"url\":\"https://join.slack.com/t/portainer/shared_invite/zt-txh3ljab-52QHTyjCqbe5RibC2lcjKA\"}]"
|
com.docker.extension.additional-urls="[{\"title\":\"Website\",\"url\":\"https://www.portainer.io?utm_campaign=DockerCon&utm_source=DockerDesktop\"},{\"title\":\"Documentation\",\"url\":\"https://docs.portainer.io\"},{\"title\":\"Support\",\"url\":\"https://join.slack.com/t/portainer/shared_invite/zt-txh3ljab-52QHTyjCqbe5RibC2lcjKA\"}]"
|
||||||
|
|
||||||
COPY dist/docker /
|
COPY dist/docker /
|
||||||
COPY dist/kubectl /
|
|
||||||
COPY dist/mustache-templates /mustache-templates/
|
COPY dist/mustache-templates /mustache-templates/
|
||||||
COPY dist/portainer /
|
COPY dist/portainer /
|
||||||
COPY dist/public /public/
|
COPY dist/public /public/
|
||||||
|
|
|
@ -10,7 +10,6 @@ USER ContainerAdministrator
|
||||||
|
|
||||||
COPY dist/mingit/ mingit/
|
COPY dist/mingit/ mingit/
|
||||||
COPY dist/docker.exe /
|
COPY dist/docker.exe /
|
||||||
COPY dist/kubectl.exe /
|
|
||||||
COPY dist/mustache-templates /mustache-templates/
|
COPY dist/mustache-templates /mustache-templates/
|
||||||
COPY dist/portainer.exe /
|
COPY dist/portainer.exe /
|
||||||
COPY dist/public /public/
|
COPY dist/public /public/
|
||||||
|
|
4
go.mod
4
go.mod
|
@ -25,6 +25,7 @@ require (
|
||||||
github.com/gofrs/uuid v4.2.0+incompatible
|
github.com/gofrs/uuid v4.2.0+incompatible
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||||
github.com/google/go-cmp v0.6.0
|
github.com/google/go-cmp v0.6.0
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/csrf v1.7.2
|
github.com/gorilla/csrf v1.7.2
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.0
|
||||||
|
@ -158,7 +159,6 @@ require (
|
||||||
github.com/google/gnostic-models v0.6.8 // indirect
|
github.com/google/gnostic-models v0.6.8 // indirect
|
||||||
github.com/google/gofuzz v1.2.0 // indirect
|
github.com/google/gofuzz v1.2.0 // indirect
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/gosuri/uitable v0.0.4 // indirect
|
github.com/gosuri/uitable v0.0.4 // indirect
|
||||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
||||||
|
@ -184,6 +184,7 @@ require (
|
||||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||||
github.com/lib/pq v1.10.9 // indirect
|
github.com/lib/pq v1.10.9 // indirect
|
||||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
||||||
|
github.com/lithammer/dedent v1.1.0 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||||
|
@ -242,6 +243,7 @@ require (
|
||||||
github.com/spf13/cast v1.7.0 // indirect
|
github.com/spf13/cast v1.7.0 // indirect
|
||||||
github.com/spf13/cobra v1.8.1 // indirect
|
github.com/spf13/cobra v1.8.1 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect
|
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect
|
||||||
github.com/theupdateframework/notary v0.7.0 // indirect
|
github.com/theupdateframework/notary v0.7.0 // indirect
|
||||||
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 // indirect
|
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 // indirect
|
||||||
|
|
23
pkg/libkubectl/apply.go
Normal file
23
pkg/libkubectl/apply.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package libkubectl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"k8s.io/kubectl/pkg/cmd/apply"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Client) Apply(ctx context.Context, manifests []string) (string, error) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
cmd := apply.NewCmdApply("kubectl", c.factory, c.streams)
|
||||||
|
cmd.SetArgs(manifestFilesToArgs(manifests))
|
||||||
|
cmd.SetOut(buf)
|
||||||
|
|
||||||
|
if err := cmd.ExecuteContext(ctx); err != nil {
|
||||||
|
return "", fmt.Errorf("error applying resources: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
|
@ -41,8 +41,8 @@ func NewClient(libKubectlAccess *ClientAccess, namespace, kubeconfig string, ins
|
||||||
// If server and token are provided, they will be used to connect to the cluster
|
// If server and token are provided, they will be used to connect to the cluster
|
||||||
// If neither kubeconfigPath or server and token are provided, an error will be returned
|
// If neither kubeconfigPath or server and token are provided, an error will be returned
|
||||||
func generateConfigFlags(token, server, namespace, kubeconfigPath string, insecure bool) (*genericclioptions.ConfigFlags, error) {
|
func generateConfigFlags(token, server, namespace, kubeconfigPath string, insecure bool) (*genericclioptions.ConfigFlags, error) {
|
||||||
if kubeconfigPath == "" && (server == "" || token == "") {
|
if kubeconfigPath == "" && server == "" {
|
||||||
return nil, errors.New("must provide either a kubeconfig path or a server and token")
|
return nil, errors.New("must provide either a kubeconfig path or a server")
|
||||||
}
|
}
|
||||||
|
|
||||||
configFlags := genericclioptions.NewConfigFlags(true)
|
configFlags := genericclioptions.NewConfigFlags(true)
|
||||||
|
|
|
@ -1,101 +0,0 @@
|
||||||
package libkubectl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewClient(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
libKubectlAccess ClientAccess
|
|
||||||
namespace string
|
|
||||||
kubeconfig string
|
|
||||||
insecure bool
|
|
||||||
wantErr bool
|
|
||||||
errContains string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "valid client with token and server",
|
|
||||||
libKubectlAccess: ClientAccess{Token: "test-token", ServerUrl: "https://localhost:6443"},
|
|
||||||
namespace: "default",
|
|
||||||
insecure: true,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid client with kubeconfig",
|
|
||||||
kubeconfig: "/path/to/kubeconfig",
|
|
||||||
namespace: "test-namespace",
|
|
||||||
insecure: false,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing both token/server and kubeconfig",
|
|
||||||
namespace: "default",
|
|
||||||
insecure: false,
|
|
||||||
wantErr: true,
|
|
||||||
errContains: "must provide either a kubeconfig path or a server and token",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing token with server",
|
|
||||||
libKubectlAccess: ClientAccess{ServerUrl: "https://localhost:6443"},
|
|
||||||
namespace: "default",
|
|
||||||
insecure: false,
|
|
||||||
wantErr: true,
|
|
||||||
errContains: "must provide either a kubeconfig path or a server and token",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing server with token",
|
|
||||||
libKubectlAccess: ClientAccess{Token: "test-token"},
|
|
||||||
namespace: "default",
|
|
||||||
insecure: false,
|
|
||||||
wantErr: true,
|
|
||||||
errContains: "must provide either a kubeconfig path or a server and token",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty namespace is valid",
|
|
||||||
libKubectlAccess: ClientAccess{Token: "test-token", ServerUrl: "https://localhost:6443"},
|
|
||||||
namespace: "",
|
|
||||||
insecure: false,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "insecure true with valid credentials",
|
|
||||||
libKubectlAccess: ClientAccess{Token: "test-token", ServerUrl: "https://localhost:6443"},
|
|
||||||
namespace: "default",
|
|
||||||
insecure: true,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
client, err := NewClient(&tt.libKubectlAccess, tt.namespace, tt.kubeconfig, tt.insecure)
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("NewClient() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil && tt.errContains != "" {
|
|
||||||
if got := err.Error(); got != tt.errContains {
|
|
||||||
t.Errorf("NewClient() error = %v, want error containing %v", got, tt.errContains)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tt.wantErr {
|
|
||||||
if client == nil {
|
|
||||||
t.Error("NewClient() returned nil client when no error was expected")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify client fields are properly initialized
|
|
||||||
if client.factory == nil {
|
|
||||||
t.Error("NewClient() client.factory is nil")
|
|
||||||
}
|
|
||||||
if client.out == nil {
|
|
||||||
t.Error("NewClient() client.out is nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
24
pkg/libkubectl/delete.go
Normal file
24
pkg/libkubectl/delete.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package libkubectl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"k8s.io/kubectl/pkg/cmd/delete"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Client) Delete(ctx context.Context, manifests []string) (string, error) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
cmd := delete.NewCmdDelete(c.factory, c.streams)
|
||||||
|
cmd.SetArgs(manifestFilesToArgs(manifests))
|
||||||
|
cmd.Flags().Set("ignore-not-found", "true")
|
||||||
|
cmd.SetOut(buf)
|
||||||
|
|
||||||
|
if err := cmd.ExecuteContext(ctx); err != nil {
|
||||||
|
return "", fmt.Errorf("error deleting resources: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
11
pkg/libkubectl/manifest.go
Normal file
11
pkg/libkubectl/manifest.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package libkubectl
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func manifestFilesToArgs(manifestFiles []string) []string {
|
||||||
|
args := []string{}
|
||||||
|
for _, path := range manifestFiles {
|
||||||
|
args = append(args, "-f", strings.TrimSpace(path))
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
48
pkg/libkubectl/manifest_test.go
Normal file
48
pkg/libkubectl/manifest_test.go
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
package libkubectl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestManifestFilesToArgsHelper(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
manifestFiles []string
|
||||||
|
expectedArgs []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty list",
|
||||||
|
manifestFiles: []string{},
|
||||||
|
expectedArgs: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single manifest",
|
||||||
|
manifestFiles: []string{"manifest.yaml"},
|
||||||
|
expectedArgs: []string{"-f", "manifest.yaml"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple manifests",
|
||||||
|
manifestFiles: []string{"manifest1.yaml", "manifest2.yaml"},
|
||||||
|
expectedArgs: []string{"-f", "manifest1.yaml", "-f", "manifest2.yaml"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "manifests with whitespace",
|
||||||
|
manifestFiles: []string{" manifest1.yaml ", " manifest2.yaml"},
|
||||||
|
expectedArgs: []string{"-f", "manifest1.yaml", "-f", "manifest2.yaml"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "kubernetes resource definitions",
|
||||||
|
manifestFiles: []string{"deployment/nginx", "service/web"},
|
||||||
|
expectedArgs: []string{"-f", "deployment/nginx", "-f", "service/web"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
args := manifestFilesToArgs(tt.manifestFiles)
|
||||||
|
assert.Equal(t, tt.expectedArgs, args)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
23
pkg/libkubectl/restart.go
Normal file
23
pkg/libkubectl/restart.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package libkubectl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"k8s.io/kubectl/pkg/cmd/rollout"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Client) RolloutRestart(ctx context.Context, manifests []string) (string, error) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
cmd := rollout.NewCmdRollout(c.factory, c.streams)
|
||||||
|
cmd.SetArgs(manifestFilesToArgs(manifests))
|
||||||
|
cmd.SetOut(buf)
|
||||||
|
|
||||||
|
if err := cmd.ExecuteContext(ctx); err != nil {
|
||||||
|
return "", fmt.Errorf("error restarting resources: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue