Compare commits

..

2 Commits

Author SHA1 Message Date
oscarzhou
82d732c4a4 chore(fs): change to a more meaningful function name 2023-12-04 22:30:31 +13:00
oscarzhou
1cf48d530e fix(fs): backup dir exist causes moving directory failure 2023-12-02 01:22:05 +13:00
30 changed files with 307 additions and 349 deletions

View File

@@ -144,8 +144,6 @@ func (connection *DbConnection) Open() error {
// Close closes the BoltDB database.
// Safe to being called multiple times.
func (connection *DbConnection) Close() error {
log.Info().Msg("closing PortainerDB")
if connection.DB != nil {
return connection.DB.Close()
}

View File

@@ -1,11 +1,9 @@
package datastore
import (
"fmt"
"os"
"path"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
@@ -16,22 +14,10 @@ func (store *Store) Backup() (string, error) {
backupFilename := store.backupFilename()
log.Info().Str("from", store.connection.GetDatabaseFilePath()).Str("to", backupFilename).Msgf("Backing up database")
// Close the store before backing up
err := store.Close()
err := store.fileService.Copy(store.connection.GetDatabaseFilePath(), backupFilename, true)
if err != nil {
return "", fmt.Errorf("failed to close store before backup: %w", err)
}
err = store.fileService.Copy(store.connection.GetDatabaseFilePath(), backupFilename, true)
if err != nil {
return "", fmt.Errorf("failed to create backup file: %w", err)
}
// reopen the store
_, err = store.Open()
if err != nil {
return "", fmt.Errorf("failed to reopen store after backup: %w", err)
log.Warn().Err(err).Msg("failed to create backup file")
return "", err
}
return backupFilename, nil
@@ -43,26 +29,31 @@ func (store *Store) Restore() error {
}
func (store *Store) RestoreFromFile(backupFilename string) error {
store.Close()
if exists, _ := store.fileService.FileExists(backupFilename); !exists {
log.Error().Str("backupFilename", backupFilename).Msg("backup file does not exist")
return os.ErrNotExist
}
if err := store.fileService.Copy(backupFilename, store.connection.GetDatabaseFilePath(), true); err != nil {
return fmt.Errorf("unable to restore backup file %q. err: %w", backupFilename, err)
log.Error().Err(err).Msg("error while restoring backup.")
return err
}
log.Info().Str("from", backupFilename).Str("to", store.connection.GetDatabaseFilePath()).Msgf("database restored")
_, err := store.Open()
if err != nil {
return fmt.Errorf("unable to determine version of restored portainer backup file: %w", err)
}
log.Info().Str("from", store.connection.GetDatabaseFilePath()).Str("to", backupFilename).Msgf("database restored")
// determine the db version
store.Open()
version, err := store.VersionService.Version()
if err != nil {
return fmt.Errorf("unable to determine restored database version. err: %w", err)
edition := "CE"
if version.Edition == 2 {
edition = "EE"
}
if err == nil {
log.Info().Str("version", version.SchemaVersion).Msgf("Restored database version: Portainer %s %s", edition, version.SchemaVersion)
}
editionLabel := portainer.SoftwareEdition(version.Edition).GetEditionLabel()
log.Info().Msgf("Restored database version: Portainer %s %s", editionLabel, version.SchemaVersion)
return nil
}
@@ -70,7 +61,8 @@ func (store *Store) createBackupPath() error {
backupDir := path.Join(store.connection.GetStorePath(), "backups")
if exists, _ := store.fileService.FileExists(backupDir); !exists {
if err := os.MkdirAll(backupDir, 0700); err != nil {
return fmt.Errorf("unable to create backup folder: %w", err)
log.Error().Err(err).Msg("error while creating backup folder")
return err
}
}
return nil

View File

@@ -944,6 +944,6 @@
}
],
"version": {
"VERSION": "{\"SchemaVersion\":\"2.19.4\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
"VERSION": "{\"SchemaVersion\":\"2.19.3\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
}
}

View File

@@ -932,9 +932,13 @@ func FileExists(filePath string) (bool, error) {
// SafeCopyDirectory copies a directory from src to dst in a safe way.
func (service *Service) SafeMoveDirectory(originalPath, newPath string) error {
return safeMoveDirectory(originalPath, newPath, CopyDir)
}
func safeMoveDirectory(src, dst string, copyDirFunc func(string, string, bool) error) error {
// 1. Backup the source directory to a different folder
backupDir := fmt.Sprintf("%s-%s", filepath.Dir(originalPath), "backup")
err := MoveDirectory(originalPath, backupDir)
backupDir := fmt.Sprintf("%s-%s", src, "backup")
err := moveDirectoryAndOverwrite(src, backupDir)
if err != nil {
return fmt.Errorf("failed to backup source directory: %w", err)
}
@@ -942,25 +946,25 @@ func (service *Service) SafeMoveDirectory(originalPath, newPath string) error {
defer func() {
if err != nil {
// If an error occurred, rollback the backup directory
restoreErr := restoreBackup(originalPath, backupDir)
restoreErr := restoreBackup(src, backupDir)
if restoreErr != nil {
log.Warn().Err(restoreErr).Msg("failed to restore backup during creating versioning folder")
}
}
// 3. Delete the backup directory
err = os.RemoveAll(backupDir)
if err != nil {
log.Warn().Err(err).Msg("failed to remove backup directory")
}
}()
// 2. Copy the backup directory to the destination directory
err = CopyDir(backupDir, newPath, false)
err = copyDirFunc(backupDir, dst, false)
if err != nil {
return fmt.Errorf("failed to copy backup directory to destination directory: %w", err)
}
// 3. Delete the backup directory
err = os.RemoveAll(backupDir)
if err != nil {
return fmt.Errorf("failed to delete backup directory: %w", err)
}
return nil
}
@@ -997,6 +1001,27 @@ func MoveDirectory(originalPath, newPath string) error {
return os.Rename(originalPath, newPath)
}
func moveDirectoryAndOverwrite(originalPath, newPath string) error {
if _, err := os.Stat(originalPath); err != nil {
return err
}
alreadyExists, err := FileExists(newPath)
if err != nil {
return err
}
if alreadyExists {
log.Info().Msgf("Target path already exists, removing it: %s", newPath)
err := os.RemoveAll(newPath)
if err != nil {
log.Warn().Err(err).Msgf("Failed to remove existing target path: %s", newPath)
}
}
return os.Rename(originalPath, newPath)
}
// StoreFDOProfileFileFromBytes creates a subfolder in the FDOProfileStorePath and stores a new file from bytes.
// It returns the path to the folder where the file is stored.
func (service *Service) StoreFDOProfileFileFromBytes(fdoProfileIdentifier string, data []byte) (string, error) {

View File

@@ -1,6 +1,7 @@
package filesystem
import (
"errors"
"os"
"path"
"testing"
@@ -20,3 +21,62 @@ func createService(t *testing.T) *Service {
return service
}
func prepareDir(t *testing.T, dir string) error {
err := os.MkdirAll(path.Join(dir, "data", "compose", "1"), 0755)
if err != nil {
return err
}
file, err := os.Create(path.Join(dir, "data", "compose", "1", "docker-compose.yml"))
if err != nil {
return err
}
file.Close()
return nil
}
func TestSafeSafeMoveDirectory(t *testing.T) {
t.Run("able to migrate original folder when the backup folder already exists", func(t *testing.T) {
testDir := path.Join(t.TempDir(), t.Name())
err := prepareDir(t, testDir)
assert.NoError(t, err, "prepareDir should not fail")
defer os.RemoveAll(testDir)
err = os.MkdirAll(path.Join(testDir, "data", "compose", "1-backup"), 0755)
assert.NoError(t, err, "create backupdir should not fail")
assert.DirExists(t, path.Join(testDir, "data", "compose", "1-backup"), "backupdir should exist")
src := path.Join(testDir, "data", "compose", "1")
dst := path.Join(testDir, "data", "compose", "1", "v1")
err = safeMoveDirectory(src, dst, CopyDir)
assert.NoError(t, err, "safeMoveDirectory should not fail")
assert.FileExists(t, path.Join(testDir, "data", "compose", "1", "v1", "docker-compose.yml"), "docker-compose.yml should be migrated")
assert.NoDirExists(t, path.Join(testDir, "data", "compose", "1-backup"), "backupdir should not exist")
})
t.Run("original folder can be restored if error occurs", func(t *testing.T) {
testDir := path.Join(t.TempDir(), t.Name())
err := prepareDir(t, testDir)
assert.NoError(t, err, "prepareDir should not fail")
defer os.RemoveAll(testDir)
src := path.Join(testDir, "data", "compose", "1")
dst := path.Join(testDir, "data", "compose", "1", "v1")
copyDirFunc := func(string, string, bool) error {
return errors.New("mock copy dir error")
}
err = safeMoveDirectory(src, dst, copyDirFunc)
assert.Error(t, err, "safeMoveDirectory should fail")
assert.FileExists(t, path.Join(testDir, "data", "compose", "1", "docker-compose.yml"), "original folder should be restored")
assert.NoDirExists(t, path.Join(testDir, "data", "compose", "1", "v1"), "the target folder should not exist")
assert.NoFileExists(t, path.Join(testDir, "data", "compose", "1", "v1", "docker-compose.yml"), "docker-compose.yml should not be migrated")
assert.NoDirExists(t, path.Join(testDir, "data", "compose", "1-backup"), "backupdir should not exist")
})
}

View File

@@ -149,7 +149,7 @@ func isValidNote(note string) bool {
// @success 200 {object} portainer.CustomTemplate
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /custom_templates/create/string [post]
// @router /custom_templates/string [post]
func (handler *Handler) createCustomTemplateFromFileContent(r *http.Request) (*portainer.CustomTemplate, error) {
var payload customTemplateFromFileContentPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
@@ -263,7 +263,7 @@ func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request)
// @success 200 {object} portainer.CustomTemplate
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /custom_templates/create/repository [post]
// @router /custom_templates/repository [post]
func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (*portainer.CustomTemplate, error) {
var payload customTemplateFromGitRepositoryPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
@@ -443,7 +443,7 @@ func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) er
// @success 200 {object} portainer.CustomTemplate
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /custom_templates/create/file [post]
// @router /custom_templates/file [post]
func (handler *Handler) createCustomTemplateFromFileUpload(r *http.Request) (*portainer.CustomTemplate, error) {
payload := &customTemplateFromFileUploadPayload{}
err := payload.Validate(r)

View File

@@ -89,8 +89,6 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
return httperror.InternalServerError("Unable to find an environment with the specified identifier inside the database", err)
}
updateEndpointProxy := shouldReloadTLSConfiguration(endpoint, &payload)
if payload.Name != nil {
name := *payload.Name
isUnique, err := handler.isNameUnique(name, endpoint.ID)
@@ -106,9 +104,8 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
}
if payload.URL != nil && *payload.URL != endpoint.URL {
if payload.URL != nil {
endpoint.URL = *payload.URL
updateEndpointProxy = true
}
if payload.PublicURL != nil {
@@ -182,8 +179,6 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
}
if endpoint.Type == portainer.AzureEnvironment {
updateEndpointProxy = true
credentials := endpoint.AzureCredentials
if payload.AzureApplicationID != nil {
credentials.ApplicationID = *payload.AzureApplicationID
@@ -252,7 +247,10 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
}
}
if updateEndpointProxy {
if (payload.URL != nil && *payload.URL != endpoint.URL) ||
(payload.TLS != nil && endpoint.TLSConfig.TLS != *payload.TLS) ||
endpoint.Type == portainer.AzureEnvironment ||
shouldReloadTLSConfiguration(endpoint, &payload) {
handler.ProxyManager.DeleteEndpointProxy(endpoint.ID)
_, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint)
if err != nil {
@@ -293,12 +291,6 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
}
func shouldReloadTLSConfiguration(endpoint *portainer.Endpoint, payload *endpointUpdatePayload) bool {
// If we change anything in the tls config then we need to reload the proxy
if payload.TLS != nil && endpoint.TLSConfig.TLS != *payload.TLS {
return true
}
// When updating Docker API environment, as long as TLS is true and TLSSkipVerify is false,
// we assume that new TLS files have been uploaded and we need to reload the TLS configuration.
if endpoint.Type != portainer.DockerEnvironment ||

View File

@@ -84,7 +84,7 @@ type Handler struct {
}
// @title PortainerCE API
// @version 2.19.4
// @version 2.19.3
// @description.markdown api-description.md
// @termsOfService

View File

@@ -1561,7 +1561,7 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.19.4"
APIVersion = "2.19.3"
// Edition is what this edition of Portainer is called
Edition = PortainerCE
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax

View File

@@ -34,7 +34,7 @@ angular.module('portainer.docker', ['portainer.app', reactModule]).config([
endpoint.Status = status;
if (status === EnvironmentStatus.Down) {
throw new Error(`The environment named ${endpoint.Name} is unreachable.`);
throw new Error('Environment is unreachable.');
}
await StateManager.updateEndpointState(endpoint);

View File

@@ -53,7 +53,7 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
try {
await getSelfSubjectAccessReview(endpoint.Id, 'default');
} catch (e) {
throw new Error(`The environment named ${endpoint.Name} is unreachable.`);
throw new Error('Environment is unreachable.');
}
} catch (e) {
let params = {};

View File

@@ -60,93 +60,6 @@
<rd-widget>
<rd-widget-body>
<form class="form-horizontal mt-4" name="kubernetesApplicationCreationForm" autocomplete="off">
<div ng-if="ctrl.isExternalApplication()">
<div class="col-sm-12 form-section-title" ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"> Namespace </div>
<!-- #region NAMESPACE -->
<div class="form-group" ng-if="ctrl.formValues.ResourcePool">
<label for="resource-pool-selector" class="col-sm-1 control-label text-left">Namespace</label>
<div class="col-sm-11">
<select
class="form-control"
id="resource-pool-selector"
ng-model="ctrl.formValues.ResourcePool"
ng-options="resourcePool.Namespace.Name for resourcePool in ctrl.resourcePools"
ng-change="ctrl.onResourcePoolSelectionChange()"
ng-disabled="ctrl.state.isEdit"
data-cy="k8sAppCreate-nsSelect"
></select>
</div>
</div>
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded() && ctrl.formValues.ResourcePool">
<div class="col-sm-12 small text-danger">
<pr-icon icon="'alert-circle'" mode="'danger'"></pr-icon>
This namespace has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the
namespace.
</div>
</div>
<div class="form-group" ng-if="!ctrl.formValues.ResourcePool">
<div class="col-sm-12 small text-muted">
<pr-icon icon="'alert-circle'" mode="'warning'"></pr-icon>
You do not have access to any namespace. Contact your administrator to get access to a namespace.
</div>
</div>
<!-- kubernetes services options -->
<div ng-if="ctrl.formValues.ResourcePool">
<kube-services-form
on-change="(ctrl.onServicesChange)"
values="ctrl.formValues.Services"
app-name="ctrl.formValues.Name"
selector="ctrl.formValues.Selector"
validation-data="{nodePortServices: ctrl.state.nodePortServices, formServices: ctrl.formValues.Services, ingressPaths: ctrl.ingressPaths, originalIngressPaths: ctrl.originalIngressPaths}"
is-edit-mode="ctrl.state.isEdit"
namespace="ctrl.formValues.ResourcePool.Namespace.Name"
></kube-services-form>
</div>
<!-- kubernetes services options -->
<kubernetes-summary-view form-values="ctrl.formValues" old-form-values="ctrl.savedFormValues"></kubernetes-summary-view>
<!-- #region ACTIONS -->
<div class="col-sm-12 form-section-title !mt-6" ng-if="ctrl.state.appType !== ctrl.KubernetesDeploymentTypes.GIT" ng-hide="ctrl.stack.IsComposeFormat"> Actions </div>
<div class="form-group" ng-hide="ctrl.stack.IsComposeFormat">
<div class="col-sm-12">
<button
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"
type="button"
class="btn btn-primary btn-sm !ml-0"
ng-disabled="!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled() || !ctrl.imageValidityIsValid() || ctrl.hasPortErrors() || !ctrl.formValues.ResourcePool"
ng-click="ctrl.deployApplication()"
button-spinner="ctrl.state.actionInProgress"
data-cy="k8sAppCreate-deployButton"
>
<span ng-show="!ctrl.state.isEdit && !ctrl.state.actionInProgress">Deploy application</span>
<span ng-show="!ctrl.state.isEdit && ctrl.state.actionInProgress">Deployment in progress...</span>
<span ng-show="ctrl.state.isEdit && !ctrl.state.actionInProgress">Update application</span>
<span ng-show="ctrl.state.isEdit && ctrl.state.actionInProgress">Update in progress...</span>
</button>
<button
ng-if="ctrl.state.isEdit && !ctrl.state.actionInProgress && ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"
type="button"
class="btn btn-sm btn-default"
ui-sref="kubernetes.applications.application({ name: ctrl.application.Name, namespace: ctrl.application.ResourcePool })"
data-cy="k8sAppCreate-appCancelButton"
>
Cancel
</button>
<!-- #Web editor buttons -->
<button
class="btn btn-sm btn-primary"
ng-click="ctrl.updateApplicationViaWebEditor()"
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.CONTENT || ctrl.state.updateWebEditorInProgress"
ng-disabled="!kubernetesApplicationCreationForm.$valid || !ctrl.state.isEditorDirty || ctrl.state.updateWebEditorInProgress"
style="margin-top: 7px; margin-left: 0"
button-spinner="ctrl.state.updateWebEditorInProgress"
>
<span ng-show="!ctrl.state.updateWebEditorInProgress">Update the application</span>
<span ng-show="ctrl.state.updateWebEditorInProgress">Update in progress...</span>
</button>
</div>
</div>
<!-- #endregion -->
</div>
<div ng-if="!ctrl.isExternalApplication()">
<git-form-info-panel
ng-if="ctrl.state.appType == ctrl.KubernetesDeploymentTypes.GIT"
@@ -1402,54 +1315,105 @@
></kube-services-form>
</div>
<!-- kubernetes services options -->
<!-- summary -->
<kubernetes-summary-view
ng-if="!(!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled() || !ctrl.state.pullImageValidity)"
form-values="ctrl.formValues"
old-form-values="ctrl.savedFormValues"
></kubernetes-summary-view>
</div>
<!-- #region ACTIONS -->
<div class="col-sm-12 form-section-title !mt-6" ng-if="ctrl.state.appType !== ctrl.KubernetesDeploymentTypes.GIT" ng-hide="ctrl.stack.IsComposeFormat"> Actions </div>
<div class="form-group" ng-hide="ctrl.stack.IsComposeFormat">
<div class="col-sm-12">
<button
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"
type="button"
class="btn btn-primary btn-sm !ml-0"
ng-disabled="!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled() || !ctrl.imageValidityIsValid() || ctrl.hasPortErrors() || !ctrl.formValues.ResourcePool"
ng-click="ctrl.deployApplication()"
button-spinner="ctrl.state.actionInProgress"
data-cy="k8sAppCreate-deployButton"
>
<span ng-show="!ctrl.state.isEdit && !ctrl.state.actionInProgress">Deploy application</span>
<span ng-show="!ctrl.state.isEdit && ctrl.state.actionInProgress">Deployment in progress...</span>
<span ng-show="ctrl.state.isEdit && !ctrl.state.actionInProgress">Update application</span>
<span ng-show="ctrl.state.isEdit && ctrl.state.actionInProgress">Update in progress...</span>
</button>
<button
ng-if="ctrl.state.isEdit && !ctrl.state.actionInProgress && ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"
type="button"
class="btn btn-sm btn-default"
ui-sref="kubernetes.applications.application({ name: ctrl.application.Name, namespace: ctrl.application.ResourcePool })"
data-cy="k8sAppCreate-appCancelButton"
>
Cancel
</button>
<!-- #Web editor buttons -->
<button
class="btn btn-sm btn-primary"
ng-click="ctrl.updateApplicationViaWebEditor()"
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.CONTENT || ctrl.state.updateWebEditorInProgress"
ng-disabled="!kubernetesApplicationCreationForm.$valid || !ctrl.state.isEditorDirty || ctrl.state.updateWebEditorInProgress"
style="margin-top: 7px; margin-left: 0"
button-spinner="ctrl.state.updateWebEditorInProgress"
>
<span ng-show="!ctrl.state.updateWebEditorInProgress">Update the application</span>
<span ng-show="ctrl.state.updateWebEditorInProgress">Update in progress...</span>
</button>
</div>
<div ng-if="ctrl.isExternalApplication()">
<div class="col-sm-12 form-section-title" ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"> Namespace </div>
<!-- #region NAMESPACE -->
<div class="form-group" ng-if="ctrl.formValues.ResourcePool">
<label for="resource-pool-selector" class="col-sm-1 control-label text-left">Namespace</label>
<div class="col-sm-11">
<select
class="form-control"
id="resource-pool-selector"
ng-model="ctrl.formValues.ResourcePool"
ng-options="resourcePool.Namespace.Name for resourcePool in ctrl.resourcePools"
ng-change="ctrl.onResourcePoolSelectionChange()"
ng-disabled="ctrl.state.isEdit"
data-cy="k8sAppCreate-nsSelect"
></select>
</div>
</div>
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded() && ctrl.formValues.ResourcePool">
<div class="col-sm-12 small text-danger">
<pr-icon icon="'alert-circle'" mode="'danger'"></pr-icon>
This namespace has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the
namespace.
</div>
</div>
<div class="form-group" ng-if="!ctrl.formValues.ResourcePool">
<div class="col-sm-12 small text-muted">
<pr-icon icon="'alert-circle'" mode="'warning'"></pr-icon>
You do not have access to any namespace. Contact your administrator to get access to a namespace.
</div>
</div>
<!-- kubernetes services options -->
<div ng-if="ctrl.formValues.ResourcePool">
<kube-services-form
on-change="(ctrl.onServicesChange)"
values="ctrl.formValues.Services"
app-name="ctrl.formValues.Name"
selector="ctrl.formValues.Selector"
validation-data="{nodePortServices: ctrl.state.nodePortServices, formServices: ctrl.formValues.Services, ingressPaths: ctrl.ingressPaths, originalIngressPaths: ctrl.originalIngressPaths}"
is-edit-mode="ctrl.state.isEdit"
namespace="ctrl.formValues.ResourcePool.Namespace.Name"
></kube-services-form>
</div>
<!-- kubernetes services options -->
</div>
<!-- kubernetes summary for external application -->
<kubernetes-summary-view ng-if="ctrl.isExternalApplication()" form-values="ctrl.formValues" old-form-values="ctrl.savedFormValues"></kubernetes-summary-view>
<!-- kubernetes summary for external application -->
<div class="col-sm-12 form-section-title !mt-6" ng-if="ctrl.state.appType !== ctrl.KubernetesDeploymentTypes.GIT" ng-hide="ctrl.stack.IsComposeFormat"> Actions </div>
<!-- #region ACTIONS -->
<div class="form-group" ng-hide="ctrl.stack.IsComposeFormat">
<div class="col-sm-12">
<button
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"
type="button"
class="btn btn-primary btn-sm !ml-0"
ng-disabled="!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled() || !ctrl.imageValidityIsValid() || ctrl.hasPortErrors() || !ctrl.formValues.ResourcePool"
ng-click="ctrl.deployApplication()"
button-spinner="ctrl.state.actionInProgress"
data-cy="k8sAppCreate-deployButton"
>
<span ng-show="!ctrl.state.isEdit && !ctrl.state.actionInProgress">Deploy application</span>
<span ng-show="!ctrl.state.isEdit && ctrl.state.actionInProgress">Deployment in progress...</span>
<span ng-show="ctrl.state.isEdit && !ctrl.state.actionInProgress">Update application</span>
<span ng-show="ctrl.state.isEdit && ctrl.state.actionInProgress">Update in progress...</span>
</button>
<button
ng-if="ctrl.state.isEdit && !ctrl.state.actionInProgress && ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"
type="button"
class="btn btn-sm btn-default"
ui-sref="kubernetes.applications.application({ name: ctrl.application.Name, namespace: ctrl.application.ResourcePool })"
data-cy="k8sAppCreate-appCancelButton"
>
Cancel
</button>
<!-- #Web editor buttons -->
<button
class="btn btn-sm btn-primary"
ng-click="ctrl.updateApplicationViaWebEditor()"
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.CONTENT || ctrl.state.updateWebEditorInProgress"
ng-disabled="!kubernetesApplicationCreationForm.$valid || !ctrl.state.isEditorDirty || ctrl.state.updateWebEditorInProgress"
style="margin-top: 7px; margin-left: 0"
button-spinner="ctrl.state.updateWebEditorInProgress"
>
<span ng-show="!ctrl.state.updateWebEditorInProgress">Update the application</span>
<span ng-show="ctrl.state.updateWebEditorInProgress">Update in progress...</span>
</button>
</div>
</div>
<!-- #endregion -->
</form>
</rd-widget-body>
</rd-widget>

View File

@@ -1114,10 +1114,10 @@ class KubernetesCreateApplicationController {
});
if (this.resourcePools.length) {
this.namespaceWithQuota = await this.KubernetesResourcePoolService.get(this.resourcePools[0].Namespace.Name);
this.formValues.ResourcePool.Quota = this.namespaceWithQuota.Quota;
this.updateNamespaceLimits(this.namespaceWithQuota);
this.updateSliders(this.namespaceWithQuota);
const namespaceWithQuota = await this.KubernetesResourcePoolService.get(this.resourcePools[0].Namespace.Name);
this.formValues.ResourcePool.Quota = namespaceWithQuota.Quota;
this.updateNamespaceLimits(namespaceWithQuota);
this.updateSliders(namespaceWithQuota);
}
this.formValues.ResourcePool = this.resourcePools[0];
if (!this.formValues.ResourcePool) {
@@ -1140,8 +1140,6 @@ class KubernetesCreateApplicationController {
this.nodesLabels,
this.ingresses
);
this.formValues.Services = this.formValues.Services || [];
this.originalServicePorts = structuredClone(this.formValues.Services.flatMap((service) => service.Ports));
this.originalIngressPaths = structuredClone(this.originalServicePorts.flatMap((port) => port.ingressPaths).filter((ingressPath) => ingressPath.Host));
@@ -1162,8 +1160,6 @@ class KubernetesCreateApplicationController {
this.formValues.OriginalIngresses = this.ingresses;
this.formValues.ImageModel = await this.parseImageConfiguration(this.formValues.ImageModel);
this.savedFormValues = angular.copy(this.formValues);
this.updateNamespaceLimits(this.namespaceWithQuota);
this.updateSliders(this.namespaceWithQuota);
delete this.formValues.ApplicationType;
if (this.application.ApplicationType !== KubernetesApplicationTypes.STATEFULSET) {

View File

@@ -194,7 +194,7 @@ class KubernetesDeployController {
this.state.templateContent = await this.CustomTemplateService.customTemplateFile(templateId, template.GitConfig !== null);
this.onChangeFileContent(this.state.templateContent);
this.state.isEditorReadOnly = false;
this.state.isEditorReadOnly = true;
} catch (err) {
this.state.templateLoadFailed = true;
throw err;

View File

@@ -42,7 +42,7 @@
<button
type="submit"
class="btn btn-primary btn-sm vertical-center"
ng-disabled="ctrl.availableUsersAndTeams.length === 0 || ctrl.formValues.multiselectOutput.length === 0 || ctrl.actionInProgress"
ng-disabled="ctrl.formValues.multiselectOutput.length === 0 || ctrl.actionInProgress"
ng-click="ctrl.authorizeAccess()"
button-spinner="ctrl.actionInProgress"
data-cy="access-createAccess"

View File

@@ -94,7 +94,6 @@ class PorAccessManagementController {
const roles = await this.RoleService.roles();
this.roles = _.orderBy(roles, 'Priority', 'asc');
this.formValues = {
multiselectOutput: [],
selectedRole: this.roles.find((role) => !this.isRoleLimitedToBE(role)),
};

View File

@@ -144,18 +144,18 @@
<table-column-header
col-title="'Created'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'CreationDate'"
is-sorted-desc="$ctrl.state.orderBy === 'CreationDate' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('CreationDate')"
is-sorted="$ctrl.state.orderBy === 'ResourceControl.CreationDate'"
is-sorted-desc="$ctrl.state.orderBy === 'ResourceControl.CreationDate' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('ResourceControl.CreationDate')"
></table-column-header>
</th>
<th ng-if="$ctrl.columnVisibility.columns.updated.display">
<table-column-header
col-title="'Updated'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'UpdateDate'"
is-sorted-desc="$ctrl.state.orderBy === 'UpdateDate' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('UpdateDate')"
is-sorted="$ctrl.state.orderBy === 'ResourceControl.UpdateDate'"
is-sorted-desc="$ctrl.state.orderBy === 'ResourceControl.UpdateDate' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('ResourceControl.UpdateDate')"
></table-column-header>
</th>
<th>

View File

@@ -13,9 +13,9 @@ export function StackViewModel(data) {
this.ResourceControl = new ResourceControlViewModel(data.ResourceControl);
}
this.Status = data.Status;
this.CreationDate = data.CreationDate || undefined; // set undefined for angular sorting
this.CreationDate = data.CreationDate;
this.CreatedBy = data.CreatedBy;
this.UpdateDate = data.UpdateDate || undefined; // set undefined for angular sorting
this.UpdateDate = data.UpdateDate;
this.UpdatedBy = data.UpdatedBy;
this.Regular = true;
this.External = false;
@@ -30,7 +30,7 @@ export function StackViewModel(data) {
export function ExternalStackViewModel(name, type, creationDate) {
this.Name = name;
this.Type = type;
this.CreationDate = creationDate || undefined; // set undefined for angular sorting
this.CreationDate = creationDate;
this.Regular = false;
this.External = true;
@@ -50,9 +50,9 @@ export function OrphanedStackViewModel(data) {
this.ResourceControl = new ResourceControlViewModel(data.ResourceControl);
}
this.Status = data.Status;
this.CreationDate = data.CreationDate || undefined; // set undefined for angular sorting
this.CreationDate = data.CreationDate;
this.CreatedBy = data.CreatedBy;
this.UpdateDate = data.UpdateDate || undefined; // set undefined for angular sorting
this.UpdateDate = data.UpdateDate;
this.UpdatedBy = data.UpdatedBy;
this.Regular = false;

View File

@@ -306,7 +306,7 @@ angular
$scope.state.templateContent = await this.CustomTemplateService.customTemplateFile(templateId, template.GitConfig !== null);
onChangeFileContent($scope.state.templateContent);
$scope.state.isEditorReadOnly = false;
$scope.state.isEditorReadOnly = true;
} catch (err) {
$scope.state.templateLoadFailed = true;
throw err;

View File

@@ -4,14 +4,14 @@ import { DockerImage } from '@/react/docker/images/types';
import { columnHelper } from './helper';
export const tags = columnHelper.accessor((item) => item.RepoTags.join(','), {
export const tags = columnHelper.accessor('RepoTags', {
id: 'tags',
header: 'Tags',
cell: Cell,
});
function Cell({ row: { original: item } }: CellContext<DockerImage, unknown>) {
const repoTags = item.RepoTags;
function Cell({ getValue }: CellContext<DockerImage, string[]>) {
const repoTags = getValue();
return (
<>

View File

@@ -9,7 +9,6 @@ import UpToDate from '@/assets/ico/icon_up-to-date.svg?c';
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { getDashboardRoute } from '@/react/portainer/environments/utils';
import { cleanGitRepoUrl } from '@/react/portainer/gitops/utils';
import { Button } from '@@/buttons';
import { Icon } from '@@/Icon';
@@ -188,9 +187,7 @@ function TargetVersionCell({
{row.original.TargetCommitHash ? (
<div>
<a
href={`${cleanGitRepoUrl(row.original.GitConfigURL)}/commit/${
row.original.TargetCommitHash
}`}
href={`${row.original.GitConfigURL}/commit/${row.original.TargetCommitHash}`}
target="_blank"
rel="noreferrer"
>
@@ -239,9 +236,7 @@ function DeployedVersionCell({
<div>
{statusIcon}
<a
href={`${cleanGitRepoUrl(row.original.GitConfigURL)}/commit/${
row.original.TargetCommitHash
}`}
href={`${row.original.GitConfigURL}/commit/${row.original.TargetCommitHash}`}
target="_blank"
rel="noreferrer"
>

View File

@@ -3,7 +3,6 @@ import _ from 'lodash';
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { cleanGitRepoUrl } from '@/react/portainer/gitops/utils';
import { buildNameColumn } from '@@/datatables/NameCell';
import { Link } from '@@/Link';
@@ -152,9 +151,7 @@ export const columns = _.compact([
<div className="text-center">
<a
target="_blank"
href={`${cleanGitRepoUrl(item.GitConfig.URL)}/commit/${
item.GitConfig.ConfigHash
}`}
href={`${item.GitConfig.URL}/commit/${item.GitConfig.ConfigHash}`}
rel="noreferrer"
>
{item.GitConfig.ConfigHash.slice(0, 7)}

View File

@@ -1,4 +1,4 @@
import { Pod } from 'kubernetes-types/core/v1';
import { EnvVar, Pod } from 'kubernetes-types/core/v1';
import { Asterisk, File, FileCode, Key, Lock } from 'lucide-react';
import { Icon } from '@@/Icon';
@@ -44,7 +44,7 @@ export function ApplicationEnvVarsTable({ namespace, app }: Props) {
{envVar.isInitContainer && (
<span>
<Icon icon={Asterisk} className="!ml-1" />
{envVar.fieldPath} (
{envVar.valueFrom?.fieldRef?.fieldPath} (
<a
href="https://kubernetes.io/docs/concepts/workloads/pods/init-containers/"
target="_blank"
@@ -56,13 +56,13 @@ export function ApplicationEnvVarsTable({ namespace, app }: Props) {
</span>
)}
</td>
<td data-cy="k8sAppDetail-envVarName">{envVar.key || '-'}</td>
<td data-cy="k8sAppDetail-envVarName">{envVar.name}</td>
<td data-cy="k8sAppDetail-envVarValue">
{envVar.value && <span>{envVar.value}</span>}
{envVar.fieldPath && (
{envVar.valueFrom?.fieldRef?.fieldPath && (
<span>
<Icon icon={Asterisk} className="!ml-1" />
{envVar.fieldPath} (
{envVar.valueFrom.fieldRef.fieldPath} (
<a
href="https://kubernetes.io/docs/tasks/inject-data-application/downward-api-volume-expose-pod-information/"
target="_blank"
@@ -73,36 +73,50 @@ export function ApplicationEnvVarsTable({ namespace, app }: Props) {
)
</span>
)}
{envVar.key ? (
{envVar.valueFrom?.secretKeyRef?.key && (
<span className="flex items-center">
<Icon icon={Key} className="!mr-1" />
{envVar.key}
{envVar.valueFrom.secretKeyRef.key}
</span>
) : (
'-'
)}
{envVar.valueFrom?.configMapKeyRef?.key && (
<span className="flex items-center">
<Icon icon={Key} className="!mr-1" />
{envVar.valueFrom.configMapKeyRef.key}
</span>
)}
{!envVar.value && !envVar.valueFrom && <span>-</span>}
</td>
<td data-cy="k8sAppDetail-configName">
{!envVar.resourseName && <span>-</span>}
{envVar.resourseName && (
{!envVar.valueFrom?.configMapKeyRef?.name &&
!envVar.valueFrom?.secretKeyRef?.name && <span>-</span>}
{envVar.valueFrom?.configMapKeyRef && (
<span>
<Link
to={
envVar.type === 'configMap'
? 'kubernetes.configmaps.configmap'
: 'kubernetes.secrets.secret'
}
to="kubernetes.configmaps.configmap"
params={{
name: envVar.resourseName,
name: envVar.valueFrom.configMapKeyRef.name,
namespace,
}}
className="flex items-center"
>
<Icon
icon={envVar.type === 'configMap' ? FileCode : Lock}
className="!mr-1"
/>
{envVar.resourseName}
<Icon icon={FileCode} className="!mr-1" />
{envVar.valueFrom.configMapKeyRef.name}
</Link>
</span>
)}
{envVar.valueFrom?.secretKeyRef && (
<span>
<Link
to="kubernetes.secrets.secret"
params={{
name: envVar.valueFrom.secretKeyRef.name,
namespace,
}}
className="flex items-center"
>
<Icon icon={Lock} className="!mr-1" />
{envVar.valueFrom.secretKeyRef.name}
</Link>
</span>
)}
@@ -116,14 +130,9 @@ export function ApplicationEnvVarsTable({ namespace, app }: Props) {
);
}
interface ContainerEnvVar {
key?: string;
value?: string;
fieldPath?: string;
interface ContainerEnvVar extends EnvVar {
containerName: string;
isInitContainer: boolean;
type: 'configMap' | 'secret';
resourseName: string;
}
function getApplicationEnvironmentVariables(
@@ -141,60 +150,23 @@ function getApplicationEnvironmentVariables(
// get all the environment variables for each container
const appContainersEnvVars =
appContainers?.flatMap((container) => {
const containerEnvVars: ContainerEnvVar[] =
appContainers?.flatMap(
(container) =>
container?.env?.map((envVar) => ({
key: envVar?.name,
fieldPath: envVar?.valueFrom?.fieldRef?.fieldPath,
...envVar,
containerName: container.name,
isInitContainer: false,
type: envVar?.valueFrom?.configMapKeyRef ? 'configMap' : 'secret',
resourseName:
envVar?.valueFrom?.configMapKeyRef?.name ||
envVar?.valueFrom?.secretKeyRef?.name ||
'',
})) || [];
const containerEnvFroms: ContainerEnvVar[] =
container?.envFrom?.map((envFrom) => ({
name: '',
resourseName:
envFrom?.configMapRef?.name || envFrom?.secretRef?.name || '',
containerName: container.name,
isInitContainer: false,
type: envFrom?.configMapRef ? 'configMap' : 'secret',
})) || [];
return [...containerEnvVars, ...containerEnvFroms];
}) || [];
})) || []
) || [];
const appInitContainersEnvVars =
appInitContainers?.flatMap((container) => {
const containerEnvVars: ContainerEnvVar[] =
appInitContainers?.flatMap(
(container) =>
container?.env?.map((envVar) => ({
key: envVar?.name,
fieldPath: envVar?.valueFrom?.fieldRef?.fieldPath,
...envVar,
containerName: container.name,
isInitContainer: false,
type: envVar?.valueFrom?.configMapKeyRef ? 'configMap' : 'secret',
resourseName:
envVar?.valueFrom?.configMapKeyRef?.name ||
envVar?.valueFrom?.secretKeyRef?.name ||
'',
})) || [];
const containerEnvFroms: ContainerEnvVar[] =
container?.envFrom?.map((envFrom) => ({
name: '',
resourseName:
envFrom?.configMapRef?.name || envFrom?.secretRef?.name || '',
containerName: container.name,
isInitContainer: false,
type: envFrom?.configMapRef ? 'configMap' : 'secret',
})) || [];
return [...containerEnvVars, ...containerEnvFroms];
}) || [];
isInitContainer: true,
})) || []
) || [];
return [...appContainersEnvVars, ...appInitContainersEnvVars];
}

View File

@@ -250,12 +250,7 @@ async function getApplicationsByKind<T extends ApplicationList>(
const { data } = await axios.get<T>(
buildUrl(environmentId, namespace, `${appKind}s`)
);
const items = (data.items || []).map((app) => ({
...app,
kind: appKind,
apiVersion: data.apiVersion,
}));
return items as T['items'];
return data.items as T['items'];
} catch (e) {
throw parseAxiosError(e as Error, `Unable to retrieve ${appKind}s`);
}

View File

@@ -19,15 +19,7 @@ export async function getNamespacePods(
},
}
);
const items = (data.items || []).map(
(pod) =>
<Pod>{
...pod,
kind: 'Pod',
apiVersion: data.apiVersion,
}
);
return items;
return data.items;
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to retrieve pods');
}

View File

@@ -14,16 +14,12 @@ export function getIsConfigMapInUse(
? app?.spec
: app?.spec?.template?.spec;
const hasEnvVarReference = appSpec?.containers.some((container) => {
const valueFromEnv = container.env?.some(
const hasEnvVarReference = appSpec?.containers.some((container) =>
container.env?.some(
(envVar) =>
envVar.valueFrom?.configMapKeyRef?.name === configMap.metadata?.name
);
const envFromEnv = container.envFrom?.some(
(envVar) => envVar.configMapRef?.name === configMap.metadata?.name
);
return valueFromEnv || envFromEnv;
});
)
);
const hasVolumeReference = appSpec?.volumes?.some(
(volume) => volume.configMap?.name === configMap.metadata?.name
);

View File

@@ -11,16 +11,12 @@ export function getIsSecretInUse(secret: Secret, applications: Application[]) {
? app?.spec
: app?.spec?.template?.spec;
const hasEnvVarReference = appSpec?.containers.some((container) => {
const valueFromEnv = container.env?.some(
const hasEnvVarReference = appSpec?.containers.some((container) =>
container.env?.some(
(envVar) =>
envVar.valueFrom?.secretKeyRef?.name === secret.metadata?.name
);
const envFromEnv = container.envFrom?.some(
(envVar) => envVar.secretRef?.name === secret.metadata?.name
);
return valueFromEnv || envFromEnv;
});
)
);
const hasVolumeReference = appSpec?.volumes?.some(
(volume) => volume.secret?.secretName === secret.metadata?.name
);

View File

@@ -32,10 +32,3 @@ export function confirmEnableTLSVerify() {
'Enabling the verification of TLS certificates without ensuring the correct configuration of your Certificate Authority (CA) for self-signed certificates can result in deployment failures.',
});
}
export function cleanGitRepoUrl(url: string) {
return url
.trim() // remove leading and trailing whitespace
.replace(/\/$/, '') // if there's a trailing slash, remove it
.replace(/\.git$/, ''); // if there's a trailing .git extension, remove it
}

View File

@@ -1,6 +1,5 @@
import { Download } from 'lucide-react';
import { useLocalStorage } from '@/react/hooks/useLocalStorage';
import { useState } from 'react';
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
import { FormSection } from '@@/form-components/FormSection';
@@ -11,14 +10,11 @@ import { BackupFileForm } from './BackupFileForm';
import { BackupS3Form } from './BackupS3Form';
export function BackupSettingsPanel() {
const [backupType, setBackupType] = useLocalStorage(
'settings_backup_type',
options[0].value
);
const [backupType, setBackupType] = useState(options[0].value);
return (
<Widget>
<WidgetTitle icon={Download} title="Back up Portainer" />
<WidgetTitle icon={Download} title="Backup up Portainer" />
<WidgetBody>
<div className="form-horizontal">
<FormSection title="Backup configuration">

View File

@@ -2,7 +2,7 @@
"author": "Portainer.io",
"name": "portainer",
"homepage": "http://portainer.io",
"version": "2.19.4",
"version": "2.19.3",
"repository": {
"type": "git",
"url": "git@github.com:portainer/portainer.git"
@@ -231,4 +231,4 @@
"**/moment": "^2.21.0"
},
"browserslist": "last 2 versions"
}
}