feat(kubeshell) allow overriding default kubeshell image EE-1756 (#5755)

* feat(kubeshell) allow overriding default kubeshell

* Add missing error check and struct tag

* Add migrator for kube shell image and add it as a default in the db

* Fix file name to match migrator pattern

* remove default as it's now coming from the db

* remove blank line

* - conflict resolution code update
- logging migration error on migration failures

* - migrateDBVersionTo34 -> migrateDBVersionToDB34 (naming consistency)

Co-authored-by: zees-dev <dev.786zshan@gmail.com>
pull/5740/merge
Matt Hook 2021-09-29 11:39:45 +13:00 committed by GitHub
parent 7611cc415a
commit 7b72130433
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 53 additions and 9 deletions

View File

@ -47,6 +47,7 @@ func (store *Store) Init() error {
HelmRepositoryURL: portainer.DefaultHelmRepositoryURL, HelmRepositoryURL: portainer.DefaultHelmRepositoryURL,
UserSessionTimeout: portainer.DefaultUserSessionTimeout, UserSessionTimeout: portainer.DefaultUserSessionTimeout,
KubeconfigExpiry: portainer.DefaultKubeconfigExpiry, KubeconfigExpiry: portainer.DefaultKubeconfigExpiry,
KubectlShellImage: portainer.DefaultKubectlShellImage,
} }
err = store.SettingsService.UpdateSettings(defaultSettings) err = store.SettingsService.UpdateSettings(defaultSettings)

View File

@ -301,9 +301,18 @@ func (m *Migrator) Migrate() error {
} }
} }
// Portainer 2.9.1
if m.currentDBVersion < 33 { if m.currentDBVersion < 33 {
if err := m.migrateDBVersionTo33(); err != nil { err := m.migrateDBVersionToDB33()
return migrationError(err, "migrateDBVersionTo33") if err != nil {
return migrationError(err, "migrateDBVersionToDB33")
}
}
// Portainer 2.10
if m.currentDBVersion < 34 {
if err := m.migrateDBVersionToDB34(); err != nil {
return migrationError(err, "migrateDBVersionToDB34")
} }
} }

View File

@ -0,0 +1,21 @@
package migrator
import portainer "github.com/portainer/portainer/api"
func (m *Migrator) migrateDBVersionToDB33() error {
if err := m.migrateSettingsToDB33(); err != nil {
return err
}
return nil
}
func (m *Migrator) migrateSettingsToDB33() error {
settings, err := m.settingsService.Settings()
if err != nil {
return err
}
settings.KubectlShellImage = portainer.DefaultKubectlShellImage
return m.settingsService.UpdateSettings(settings)
}

View File

@ -4,7 +4,7 @@ import (
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
) )
func (m *Migrator) migrateDBVersionTo33() error { func (m *Migrator) migrateDBVersionToDB34() error {
err := migrateStackEntryPoint(m.stackService) err := migrateStackEntryPoint(m.stackService)
if err != nil { if err != nil {
return err return err

View File

@ -14,7 +14,7 @@ import (
) )
func TestMigrateStackEntryPoint(t *testing.T) { func TestMigrateStackEntryPoint(t *testing.T) {
dbConn, err := bolt.Open(path.Join(t.TempDir(), "portainer-ee-mig-33.db"), 0600, &bolt.Options{Timeout: 1 * time.Second}) dbConn, err := bolt.Open(path.Join(t.TempDir(), "portainer-ee-mig-34.db"), 0600, &bolt.Options{Timeout: 1 * time.Second})
assert.NoError(t, err, "failed to init testing DB connection") assert.NoError(t, err, "failed to init testing DB connection")
defer dbConn.Close() defer dbConn.Close()

View File

@ -40,6 +40,8 @@ type settingsUpdatePayload struct {
EnableTelemetry *bool `example:"false"` EnableTelemetry *bool `example:"false"`
// Helm repository URL // Helm repository URL
HelmRepositoryURL *string `example:"https://charts.bitnami.com/bitnami"` HelmRepositoryURL *string `example:"https://charts.bitnami.com/bitnami"`
// Kubectl Shell Image
KubectlShellImage *string `example:"portainer/kubectl-shell:latest"`
} }
func (payload *settingsUpdatePayload) Validate(r *http.Request) error { func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
@ -178,6 +180,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
return tlsError return tlsError
} }
if payload.KubectlShellImage != nil {
settings.KubectlShellImage = *payload.KubectlShellImage
}
err = handler.DataStore.Settings().UpdateSettings(settings) err = handler.DataStore.Settings().UpdateSettings(settings)
if err != nil { if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist settings changes inside the database", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist settings changes inside the database", err}

View File

@ -45,7 +45,12 @@ func (handler *Handler) websocketShellPodExec(w http.ResponseWriter, r *http.Req
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find serviceaccount associated with user", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find serviceaccount associated with user", err}
} }
shellPod, err := cli.CreateUserShellPod(r.Context(), serviceAccount.Name) settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable read settings", err}
}
shellPod, err := cli.CreateUserShellPod(r.Context(), serviceAccount.Name, settings.KubectlShellImage)
if err != nil { if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create user shell", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create user shell", err}
} }

View File

@ -12,15 +12,13 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
const shellPodImage = "portainer/kubectl-shell"
// CreateUserShellPod will create a kubectl based shell for the specified user by mounting their respective service account. // CreateUserShellPod will create a kubectl based shell for the specified user by mounting their respective service account.
// The lifecycle of the pod is managed in this function; this entails management of the following pod operations: // The lifecycle of the pod is managed in this function; this entails management of the following pod operations:
// - The shell pod will be scoped to specified service accounts access permissions // - The shell pod will be scoped to specified service accounts access permissions
// - The shell pod will be automatically removed if it's not ready after specified period of time // - The shell pod will be automatically removed if it's not ready after specified period of time
// - The shell pod will be automatically removed after a specified max life (prevent zombie pods) // - The shell pod will be automatically removed after a specified max life (prevent zombie pods)
// - The shell pod will be automatically removed if request is cancelled (or client closes websocket connection) // - The shell pod will be automatically removed if request is cancelled (or client closes websocket connection)
func (kcl *KubeClient) CreateUserShellPod(ctx context.Context, serviceAccountName string) (*portainer.KubernetesShellPod, error) { func (kcl *KubeClient) CreateUserShellPod(ctx context.Context, serviceAccountName, shellPodImage string) (*portainer.KubernetesShellPod, error) {
maxPodKeepAliveSecondsStr := fmt.Sprintf("%d", int(portainer.WebSocketKeepAlive.Seconds())) maxPodKeepAliveSecondsStr := fmt.Sprintf("%d", int(portainer.WebSocketKeepAlive.Seconds()))
podPrefix := userShellPodPrefix(serviceAccountName) podPrefix := userShellPodPrefix(serviceAccountName)

View File

@ -714,6 +714,8 @@ type (
EnableTelemetry bool `json:"EnableTelemetry" example:"false"` EnableTelemetry bool `json:"EnableTelemetry" example:"false"`
// Helm repository URL, defaults to "https://charts.bitnami.com/bitnami" // Helm repository URL, defaults to "https://charts.bitnami.com/bitnami"
HelmRepositoryURL string `json:"HelmRepositoryURL" example:"https://charts.bitnami.com/bitnami"` HelmRepositoryURL string `json:"HelmRepositoryURL" example:"https://charts.bitnami.com/bitnami"`
// KubectlImage, defaults to portainer/kubectl-shell
KubectlShellImage string `json:"KubectlShellImage" example:"portainer/kubectl-shell"`
// Deprecated fields // Deprecated fields
DisplayDonationHeader bool DisplayDonationHeader bool
@ -1264,7 +1266,7 @@ type (
SetupUserServiceAccount(userID int, teamIDs []int, restrictDefaultNamespace bool) error SetupUserServiceAccount(userID int, teamIDs []int, restrictDefaultNamespace bool) error
GetServiceAccount(tokendata *TokenData) (*v1.ServiceAccount, error) GetServiceAccount(tokendata *TokenData) (*v1.ServiceAccount, error)
GetServiceAccountBearerToken(userID int) (string, error) GetServiceAccountBearerToken(userID int) (string, error)
CreateUserShellPod(ctx context.Context, serviceAccountName string) (*KubernetesShellPod, error) CreateUserShellPod(ctx context.Context, serviceAccountName, shellPodImage string) (*KubernetesShellPod, error)
StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer, errChan chan error) StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer, errChan chan error)
NamespaceAccessPoliciesDeleteNamespace(namespace string) error NamespaceAccessPoliciesDeleteNamespace(namespace string) error
GetNodesLimits() (K8sNodesLimits, error) GetNodesLimits() (K8sNodesLimits, error)
@ -1498,6 +1500,8 @@ const (
DefaultUserSessionTimeout = "8h" DefaultUserSessionTimeout = "8h"
// DefaultUserSessionTimeout represents the default timeout after which the user session is cleared // DefaultUserSessionTimeout represents the default timeout after which the user session is cleared
DefaultKubeconfigExpiry = "0" DefaultKubeconfigExpiry = "0"
// DefaultKubectlShellImage represents the default image and tag for the kubectl shell
DefaultKubectlShellImage = "portainer/kubectl-shell"
// WebSocketKeepAlive web socket keep alive for edge environments // WebSocketKeepAlive web socket keep alive for edge environments
WebSocketKeepAlive = 1 * time.Hour WebSocketKeepAlive = 1 * time.Hour
) )