Compare commits
2 Commits
fix/EE-643
...
fix/releas
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82d732c4a4 | ||
|
|
1cf48d530e |
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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\"}"
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -84,7 +84,7 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
// @version 2.19.4
|
||||
// @version 2.19.3
|
||||
// @description.markdown api-description.md
|
||||
// @termsOfService
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)),
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user