Compare commits

...

15 Commits

Author SHA1 Message Date
congs
8c1977e0aa fix(permission): EE-3772 Team leaders are able to see all environments (#7330)
Some checks failed
/ triage (push) Has been cancelled
Lint / Run linters (push) Has been cancelled
Test / test-client (push) Has been cancelled
2022-07-26 11:02:33 +12:00
Matt Hook
c47fd9f9ed bump version to 2.14.2 (#7304) 2022-07-22 10:55:17 +12:00
LP B
767d1d1970 fix(edge): pagination on create/edit edge jobs/groups [EE-3219] (#7297) 2022-07-21 18:47:20 +02:00
itsconquest
ef81e5c0e0 fix(auth): correctly calculate LDAP teamsync [EE-3704] (#7292) 2022-07-21 21:29:27 +12:00
Prabhat Khera
234b7a3d5e fix(kubeconfig): fix kubeconfig url EE-3455 (#7283) 2022-07-21 16:59:48 +12:00
itsconquest
af49305e64 fix(TLS): remove file type validation [EE-3672] (#7279) 2022-07-21 16:25:19 +12:00
LP B
d181d1251c fix(app/mustache): reuse mustache variables in templates [EE-3689] (#7287) 2022-07-19 15:38:10 +02:00
LP B
5f7db66e95 fix(app/templates): handle special characters in mustache templates [EE-3708] (#7289) 2022-07-19 14:05:31 +02:00
Dmitry Salakhov
17378bdef6 fix(users): admin can change password with any auth method (#7269) [EE-3671] 2022-07-19 11:26:43 +12:00
Oscar Zhou
010542ac1e fix(setting): update the por switch field component property (#7258) 2022-07-15 08:27:53 +12:00
Matt Hook
1bb253479a bump version to 2.14.1 (#7237)
Some checks failed
Test / test-client (push) Has been cancelled
2022-07-12 11:03:49 +12:00
Dakota Walsh
f0a13a2ad1 fix(migration): close the database before running backups EE-3627 (#7217)
* fix(migration): close the database before running backups

On certain filesystems, particuarly NTFS when a network mounted windows
file server is used to store portainer's database, you are unable to
copy the database while it is open. To fix this we simply close the
database and then re-open it after a backup.

* handle close and open errors

* dont return error on nil
2022-07-08 21:04:55 +12:00
Matt Hook
f9b28aa0a1 fix(compose): use docker-compose plugin directly [EE-3631] (#7201)
* use simplifed method of calling compose directly with new compose wrapper

* download compose binary to docker-compose

* update to newer wrapper that fixes -H issue

* update to released
2022-07-08 16:02:37 +12:00
LP B
d26e1b6983 fix(k8s/app-templates): display moustache variables fields when deploying from app template (#7185) 2022-07-08 14:15:16 +12:00
Dmitry Salakhov
7b00fdd208 fix(users): enable manual user addition [EE-3639] (#7196) 2022-07-06 15:47:51 +12:00
29 changed files with 132 additions and 103 deletions

View File

@@ -103,8 +103,26 @@ func (store *Store) backupWithOptions(options *BackupOptions) (string, error) {
store.createBackupFolders()
options = store.setupOptions(options)
dbPath := store.databasePath()
return options.BackupPath, store.copyDBFile(store.databasePath(), options.BackupPath)
if err := store.Close(); err != nil {
return options.BackupPath, fmt.Errorf(
"error closing datastore before creating backup: %v",
err,
)
}
if err := store.copyDBFile(dbPath, options.BackupPath); err != nil {
return options.BackupPath, err
}
if _, err := store.Open(); err != nil {
return options.BackupPath, fmt.Errorf(
"error opening datastore after creating backup: %v",
err,
)
}
return options.BackupPath, nil
}
// RestoreWithOptions previously saved backup for the current Edition with options

View File

@@ -910,7 +910,7 @@
],
"version": {
"DB_UPDATING": "false",
"DB_VERSION": "50",
"DB_VERSION": "52",
"INSTANCE_ID": "null"
}
}

View File

@@ -81,7 +81,7 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
}
filePaths := stackutils.GetStackFilePaths(stack)
err = manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths)
err = manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths, "")
return errors.Wrap(err, "failed to remove a stack")
}

View File

@@ -32,7 +32,7 @@ require (
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
github.com/pkg/errors v0.9.1
github.com/portainer/docker-compose-wrapper v0.0.0-20220531190153-c597b853e410
github.com/portainer/docker-compose-wrapper v0.0.0-20220708023447-a69a4ebaa021
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108
github.com/portainer/libhttp v0.0.0-20211208103139-07a5f798eb3f

View File

@@ -807,10 +807,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/portainer/docker-compose-wrapper v0.0.0-20220526210722-e1574867298e h1:gW1Ooaj7RZ9YkwHxesnNEyOB5nUD71FlZ7cdb5h63vw=
github.com/portainer/docker-compose-wrapper v0.0.0-20220526210722-e1574867298e/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c=
github.com/portainer/docker-compose-wrapper v0.0.0-20220531190153-c597b853e410 h1:LjxLd8UGR8ae73ov/vLrt/0jedj/nh98XnONkr8DJj8=
github.com/portainer/docker-compose-wrapper v0.0.0-20220531190153-c597b853e410/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c=
github.com/portainer/docker-compose-wrapper v0.0.0-20220708023447-a69a4ebaa021 h1:GFTn2e5AyIoBuK6hXbdVNkuV2m450DQnYmgQDZRU3x8=
github.com/portainer/docker-compose-wrapper v0.0.0-20220708023447-a69a4ebaa021/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c=
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a h1:qY8TbocN75n5PDl16o0uVr5MevtM5IhdwSelXEd4nFM=
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a/go.mod h1:n54EEIq+MM0NNtqLeCby8ljL+l275VpolXO0ibHegLE=
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108 h1:5e8KAnDa2G3cEHK7aV/ue8lOaoQwBZUzoALslwWkR04=

View File

@@ -80,7 +80,7 @@ type Handler struct {
}
// @title PortainerCE API
// @version 2.14.0
// @version 2.14.2
// @description.markdown api-description.md
// @termsOfService

View File

@@ -2,7 +2,6 @@ package helm
import (
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/portainer/libhelm"
@@ -108,7 +107,7 @@ func (handler *Handler) getHelmClusterAccess(r *http.Request) (*options.Kubernet
hostURL := "localhost"
if !sslSettings.SelfSigned {
hostURL = strings.Split(r.Host, ":")[0]
hostURL = r.Host
}
kubeConfigInternal := handler.kubeClusterAccessService.GetData(hostURL, endpoint.ID)

View File

@@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"net/http"
"strings"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@@ -145,8 +144,7 @@ func (handler *Handler) buildConfig(r *http.Request, tokenData *portainer.TokenD
}
func (handler *Handler) buildCluster(r *http.Request, endpoint portainer.Endpoint) clientV1.NamedCluster {
hostURL := strings.Split(r.Host, ":")[0]
kubeConfigInternal := handler.kubeClusterAccessService.GetData(hostURL, endpoint.ID)
kubeConfigInternal := handler.kubeClusterAccessService.GetData(r.Host, endpoint.ID)
return clientV1.NamedCluster{
Name: buildClusterName(endpoint.Name),
Cluster: clientV1.Cluster{

View File

@@ -95,8 +95,10 @@ func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResp
}
}
//if LDAP authentication is on, compose the related fields from application settings
if publicSettings.AuthenticationMethod == portainer.AuthenticationLDAP {
publicSettings.TeamSync = len(appSettings.LDAPSettings.GroupSearchSettings) > 0
if publicSettings.AuthenticationMethod == portainer.AuthenticationLDAP && appSettings.LDAPSettings.GroupSearchSettings != nil {
if len(appSettings.LDAPSettings.GroupSearchSettings) > 0 {
publicSettings.TeamSync = len(appSettings.LDAPSettings.GroupSearchSettings[0].GroupBaseDN) > 0
}
}
return publicSettings
}

View File

@@ -81,11 +81,11 @@ func FilterRegistries(registries []portainer.Registry, user *portainer.User, tea
}
// FilterEndpoints filters environments(endpoints) based on user role and team memberships.
// Non administrator and non-team-leader only have access to authorized environments(endpoints) (can be inherited via endpoint groups).
// Non administrator only have access to authorized environments(endpoints) (can be inherited via endpoint groups).
func FilterEndpoints(endpoints []portainer.Endpoint, groups []portainer.EndpointGroup, context *RestrictedRequestContext) []portainer.Endpoint {
filteredEndpoints := endpoints
if !context.IsAdmin && !context.IsTeamLeader {
if !context.IsAdmin {
filteredEndpoints = make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
@@ -101,11 +101,11 @@ func FilterEndpoints(endpoints []portainer.Endpoint, groups []portainer.Endpoint
}
// FilterEndpointGroups filters environment(endpoint) groups based on user role and team memberships.
// Non administrator users and Non-team-leaders only have access to authorized environment(endpoint) groups.
// Non administrator users only have access to authorized environment(endpoint) groups.
func FilterEndpointGroups(endpointGroups []portainer.EndpointGroup, context *RestrictedRequestContext) []portainer.EndpointGroup {
filteredEndpointGroups := endpointGroups
if !context.IsAdmin && !context.IsTeamLeader {
if !context.IsAdmin {
filteredEndpointGroups = make([]portainer.EndpointGroup, 0)
for _, group := range endpointGroups {

View File

@@ -11,6 +11,7 @@ import (
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
)
// KubeClusterAccessService represents a service that is responsible for centralizing kube cluster access data
@@ -94,11 +95,20 @@ func (service *kubeClusterAccessService) IsSecure() bool {
// - pass down params to binaries
func (service *kubeClusterAccessService) GetData(hostURL string, endpointID portainer.EndpointID) kubernetesClusterAccessData {
baseURL := service.baseURL
// When the api call is internal, the baseURL should not be used.
if hostURL == "localhost" {
hostURL = hostURL + service.httpsBindAddr
baseURL = "/"
}
if baseURL != "/" {
baseURL = fmt.Sprintf("/%s/", strings.Trim(baseURL, "/"))
}
clusterURL := hostURL + service.httpsBindAddr + baseURL
logrus.Infof("[kubeconfig] [hostURL: %s, httpsBindAddr: %s, baseURL: %s]", hostURL, service.httpsBindAddr, baseURL)
clusterURL := hostURL + baseURL
clusterServerURL := fmt.Sprintf("https://%sapi/endpoints/%d/kubernetes", clusterURL, endpointID)

View File

@@ -115,7 +115,7 @@ func TestKubeClusterAccessService_GetKubeConfigInternal(t *testing.T) {
clusterAccessDetails := kcs.GetData("mysite.com", 1)
wantClusterAccessDetails := kubernetesClusterAccessData{
ClusterServerURL: "https://mysite.com:9443/api/endpoints/1/kubernetes",
ClusterServerURL: "https://mysite.com/api/endpoints/1/kubernetes",
CertificateAuthorityFile: "",
CertificateAuthorityData: "",
}

View File

@@ -1385,9 +1385,9 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.14.0"
APIVersion = "2.14.2"
// DBVersion is the version number of the Portainer database
DBVersion = 50
DBVersion = 52
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
ComposeSyntaxMaxVersion = "3.9"
// AssetsServerURL represents the URL of the Portainer asset server

View File

@@ -5,7 +5,7 @@ angular.module('portainer.kubernetes').factory('HelmFactory', HelmFactory);
/* @ngInject */
function HelmFactory($resource, API_ENDPOINT_ENDPOINTS) {
const helmUrl = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/helm';
const templatesUrl = '/api/templates/helm';
const templatesUrl = 'api/templates/helm';
return $resource(
helmUrl,

View File

@@ -93,7 +93,7 @@
></custom-template-selector>
<custom-templates-variables-field
ng-if="$ctrl.isTemplateVariablesEnabled && ctrl.state.template"
ng-if="ctrl.isTemplateVariablesEnabled && ctrl.state.template"
definitions="ctrl.state.template.Variables"
value="ctrl.formValues.Variables"
on-change="(ctrl.onChangeTemplateVariables)"

View File

@@ -11,7 +11,7 @@
loaded="$ctrl.loaded"
page-type="$ctrl.pageType"
table-type="available"
retrieve-page="$ctrl.getPaginatedEndpoints"
retrieve-page="$ctrl.getAvailableEndpoints"
dataset="$ctrl.endpoints.available"
entry-click="$ctrl.associateEndpoint"
pagination-state="$ctrl.state.available"
@@ -34,7 +34,7 @@
loaded="$ctrl.loaded"
page-type="$ctrl.pageType"
table-type="associated"
retrieve-page="$ctrl.getPaginatedEndpoints"
retrieve-page="$ctrl.getAssociatedEndpoints"
dataset="$ctrl.endpoints.associated"
entry-click="$ctrl.dissociateEndpoint"
pagination-state="$ctrl.state.associated"

View File

@@ -27,7 +27,7 @@ class AssoicatedEndpointsSelectorController {
available: null,
};
this.getEndpoints = this.getEndpoints.bind(this);
this.getAvailableEndpoints = this.getAvailableEndpoints.bind(this);
this.getEndpointsAsync = this.getEndpointsAsync.bind(this);
this.getAssociatedEndpoints = this.getAssociatedEndpoints.bind(this);
this.getAssociatedEndpointsAsync = this.getAssociatedEndpointsAsync.bind(this);
@@ -47,10 +47,10 @@ class AssoicatedEndpointsSelectorController {
loadData() {
this.getAssociatedEndpoints();
this.getEndpoints();
this.getAvailableEndpoints();
}
getEndpoints() {
getAvailableEndpoints() {
return this.$async(this.getEndpointsAsync);
}

View File

@@ -22,22 +22,3 @@ export function withFileSize(fileValidation: FileSchema, maxSize: number) {
return file.size <= maxSize;
}
}
export function withFileType(
fileValidation: FileSchema,
fileTypes: File['type'][]
) {
return fileValidation.test(
'file-type',
'Selected file has unsupported format.',
validateFileType
);
function validateFileType(file?: File) {
if (!file) {
return true;
}
return fileTypes.includes(file.type);
}
}

View File

@@ -42,14 +42,7 @@
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<input type="password" class="form-control" ng-model="formValues.confirmPassword" id="confirm_password" />
<span class="input-group-addon"
><i
ng-class="
{ true: 'fa fa-check green-icon', false: 'fa fa-times red-icon' }[
form.new_password.$viewValue !== '' && form.new_password.$viewValue === formValues.confirmPassword
]
"
aria-hidden="true"
></i
><i ng-class="{ true: 'fa fa-check green-icon', false: 'fa fa-times red-icon' }[formValues.newPassword === formValues.confirmPassword]" aria-hidden="true"></i
></span>
</div>
</div>
@@ -67,7 +60,7 @@
<button
type="submit"
class="btn btn-primary btn-sm"
ng-disabled="isDemoUser || (AuthenticationMethod !== 1 && !initialUser) || !formValues.currentPassword || !formValues.newPassword || !formValues.confirmPassword || form.$invalid || form.new_password.$viewValue !== formValues.confirmPassword"
ng-disabled="isDemoUser || (AuthenticationMethod !== 1 && !isInitialAdmin) || !formValues.currentPassword || !formValues.newPassword || form.$invalid || formValues.newPassword !== formValues.confirmPassword"
ng-click="updatePassword()"
>
Update password
@@ -75,11 +68,11 @@
<button type="submit" class="btn btn-primary btn-sm" ng-click="skipPasswordChange()" ng-if="forceChangePassword && timesPasswordChangeSkipped < 2"
>Remind me later</button
>
<span class="text-muted small" style="margin-left: 5px" ng-if="AuthenticationMethod === 2 && !initialUser">
<span class="text-muted small" style="margin-left: 5px" ng-if="AuthenticationMethod === 2 && !isInitialAdmin">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
You cannot change your password when using LDAP authentication.
</span>
<span class="text-muted small" style="margin-left: 5px" ng-if="AuthenticationMethod === 3 && !initialUser">
<span class="text-muted small" style="margin-left: 5px" ng-if="AuthenticationMethod === 3 && !isInitialAdmin">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
You cannot change your password when using OAuth authentication.
</span>

View File

@@ -118,6 +118,7 @@ angular.module('portainer.app').controller('AccountController', [
$scope.userID = userDetails.ID;
$scope.userRole = Authentication.getUserDetails().role;
$scope.forceChangePassword = userDetails.forceChangePassword;
$scope.isInitialAdmin = userDetails.ID === 1;
if (state.application.demoEnvironment.enabled) {
$scope.isDemoUser = state.application.demoEnvironment.users.includes($scope.userID);

View File

@@ -22,7 +22,7 @@
<div class="col-sm-12">
<por-switch-field
label="'Use custom logo'"
value="formValues.customLogo"
checked="formValues.customLogo"
name="'toggle_logo'"
disabled="state.isDemo"
on-change="(onToggleCustomLogo)"
@@ -50,7 +50,7 @@
<div class="col-sm-12">
<por-switch-field
label="'Allow the collection of anonymous statistics'"
value="formValues.enableTelemetry"
checked="formValues.enableTelemetry"
name="'toggle_enableTelemetry'"
on-change="(onToggleEnableTelemetry)"
disabled="state.isDemo"

View File

@@ -64,7 +64,7 @@
children-paths="['portainer.users.user' ,'portainer.teams' ,'portainer.teams.team' ,'portainer.roles' ,'portainer.roles.role' ,'portainer.roles.new']"
>
<sidebar-menu-item path="portainer.teams" class-name="sidebar-sublist" data-cy="portainerSidebar-teams" title="Teams">Teams</sidebar-menu-item>
<sidebar-menu-item path="portainer.roles" class-name="sidebar-sublist" data-cy="portainerSidebar-roles" title="Roles">Roles</sidebar-menu-item>
<sidebar-menu-item ng-if="isAdmin" path="portainer.roles" class-name="sidebar-sublist" data-cy="portainerSidebar-roles" title="Roles">Roles</sidebar-menu-item>
</sidebar-menu>
<div ng-if="isAdmin">

View File

@@ -5,6 +5,8 @@ import { AccessControlFormData } from '@/portainer/components/accessControlForm/
import { STACK_NAME_VALIDATION_REGEX } from '@/constants';
import { RepositoryMechanismTypes } from '@/kubernetes/models/deploy';
import { FeatureId } from 'Portainer/feature-flags/enums';
import { isBE } from '@/portainer/feature-flags/feature-flags.service';
import { renderTemplate } from '@/react/portainer/custom-templates/components/utils';
angular
.module('portainer.app')
@@ -31,6 +33,8 @@ angular
endpoint
) {
$scope.onChangeTemplateId = onChangeTemplateId;
$scope.onChangeTemplateVariables = onChangeTemplateVariables;
$scope.isTemplateVariablesEnabled = isBE;
$scope.buildAnalyticsProperties = buildAnalyticsProperties;
$scope.stackWebhookFeature = FeatureId.STACK_WEBHOOK;
$scope.STACK_NAME_VALIDATION_REGEX = STACK_NAME_VALIDATION_REGEX;
@@ -53,6 +57,7 @@ angular
RepositoryMechanism: RepositoryMechanismTypes.INTERVAL,
RepositoryFetchInterval: '5m',
RepositoryWebhookURL: WebhookHelper.returnStackWebhookUrl(uuidv4()),
Variables: {},
};
$scope.state = {
@@ -265,11 +270,12 @@ angular
});
};
$scope.onChangeFileContent = function onChangeFileContent(value) {
$scope.onChangeFileContent = onChangeFileContent;
function onChangeFileContent(value) {
$scope.formValues.StackFileContent = value;
$scope.state.editorYamlValidationError = StackHelper.validateYAML($scope.formValues.StackFileContent, $scope.containerNames);
$scope.state.isEditorDirty = true;
};
}
async function onFileLoadAsync(event) {
$scope.state.uploadYamlValidationError = StackHelper.validateYAML(event.target.result, $scope.containerNames);
@@ -292,18 +298,38 @@ angular
function onChangeTemplateId(templateId, template) {
return $async(async () => {
if (!template || ($scope.state.selectedTemplateId === templateId && $scope.state.selectedTemplate === template)) {
return;
}
try {
$scope.state.selectedTemplateId = templateId;
$scope.state.selectedTemplate = template;
const fileContent = await CustomTemplateService.customTemplateFile(templateId);
$scope.onChangeFileContent(fileContent);
$scope.state.templateContent = fileContent;
onChangeFileContent(fileContent);
if (template.Variables && template.Variables.length > 0) {
const variables = Object.fromEntries(template.Variables.map((variable) => [variable.name, '']));
onChangeTemplateVariables(variables);
}
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve Custom Template file');
Notifications.error('Failure', err, 'Unable to retrieve Custom Template file');
}
});
}
function onChangeTemplateVariables(value) {
onChangeFormValues({ Variables: value });
if (!$scope.isTemplateVariablesEnabled) {
return;
}
const rendered = renderTemplate($scope.state.templateContent, $scope.formValues.Variables, $scope.state.selectedTemplate.Variables);
onChangeFormValues({ StackFileContent: rendered });
}
async function initView() {
var endpointMode = $scope.applicationState.endpoint.mode;
$scope.state.StackType = 2;
@@ -328,8 +354,11 @@ angular
initView();
function onChangeFormValues(newValues) {
$scope.formValues = newValues;
function onChangeFormValues(values) {
$scope.formValues = {
...$scope.formValues,
...values,
};
}
}
);

View File

@@ -131,13 +131,21 @@
path-placeholder="docker-compose.yml"
></git-form>
<custom-template-selector
ng-show="state.Method === 'template'"
new-template-path="docker.templates.custom.new"
stack-type="state.StackType"
on-change="(onChangeTemplateId)"
value="state.selectedTemplateId"
></custom-template-selector>
<div ng-show="state.Method === 'template'">
<custom-template-selector
new-template-path="docker.templates.custom.new"
stack-type="state.StackType"
on-change="(onChangeTemplateId)"
value="state.selectedTemplateId"
></custom-template-selector>
<custom-templates-variables-field
ng-if="isTemplateVariablesEnabled && state.selectedTemplate"
definitions="state.selectedTemplate.Variables"
value="formValues.Variables"
on-change="(onChangeTemplateVariables)"
></custom-templates-variables-field>
</div>
<web-editor-form
ng-if="state.Method === 'editor' || (state.Method === 'template' && state.selectedTemplateId)"

View File

@@ -140,7 +140,7 @@
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="state.actionInProgress || !state.validUsername || formValues.Username === '' || !formValues.Password || !formValues.ConfirmPassword || (AuthenticationMethod === 1 && form.$invalid) || (AuthenticationMethod === 1 && form.password.$viewValue !== formValues.ConfirmPassword)"
ng-disabled="state.actionInProgress || !state.validUsername || formValues.Username === '' || (AuthenticationMethod === 1 && (!formValues.Password || form.$invalid || formValues.Password !== formValues.ConfirmPassword))"
ng-click="addUser()"
button-spinner="state.actionInProgress"
data-cy="user-createUserButton"

View File

@@ -38,12 +38,15 @@ export function intersectVariables(
) {
const oldVariablesWithLabel = oldVariables.filter((v) => !!v.label);
return [
...oldVariablesWithLabel,
...newVariables.filter(
(v) => !oldVariablesWithLabel.find(({ name }) => name === v.name)
),
];
return _.uniqBy(
[
...oldVariablesWithLabel,
...newVariables.filter(
(v) => !oldVariablesWithLabel.find(({ name }) => name === v.name)
),
],
'name'
);
}
export function renderTemplate(
@@ -68,5 +71,5 @@ export function renderTemplate(
)
);
return Mustache.render(template, state);
return Mustache.render(template, state, undefined, { escape: (t) => t });
}

View File

@@ -3,11 +3,7 @@ import { useFormikContext } from 'formik';
import { FileUploadField } from '@/portainer/components/form-components/FileUpload';
import { SwitchField } from '@/portainer/components/form-components/SwitchField';
import { FormControl } from '@/portainer/components/form-components/FormControl';
import {
file,
withFileSize,
withFileType,
} from '@/portainer/helpers/yup-file-validation';
import { file, withFileSize } from '@/portainer/helpers/yup-file-validation';
import { FormValues } from './types';
@@ -82,16 +78,9 @@ export function TLSFieldset() {
}
const MAX_FILE_SIZE = 5_242_880; // 5MB
const ALLOWED_FILE_TYPES = [
'application/x-x509-ca-cert',
'application/x-pem-file',
];
function certValidation() {
return withFileType(
withFileSize(file(), MAX_FILE_SIZE),
ALLOWED_FILE_TYPES
).when(['tls', 'skipVerify'], {
return withFileSize(file(), MAX_FILE_SIZE).when(['tls', 'skipVerify'], {
is: (tls: boolean, skipVerify: boolean) => tls && !skipVerify,
then: (schema) => schema.required('File is required'),
});

View File

@@ -21,10 +21,10 @@ fi
if [[ "$PLATFORM" == "windows" ]]; then
wget -O "dist/docker-compose.plugin.exe" "https://github.com/docker/compose/releases/download/$COMPOSE_VERSION/docker-compose-windows-${ARCH}.exe"
chmod +x "dist/docker-compose.plugin.exe"
wget -O "dist/docker-compose.exe" "https://github.com/docker/compose/releases/download/$COMPOSE_VERSION/docker-compose-windows-${ARCH}.exe"
chmod +x "dist/docker-compose.exe"
else
wget -O "dist/docker-compose.plugin" "https://github.com/docker/compose/releases/download/$COMPOSE_VERSION/docker-compose-${PLATFORM}-${ARCH}"
chmod +x "dist/docker-compose.plugin"
wget -O "dist/docker-compose" "https://github.com/docker/compose/releases/download/$COMPOSE_VERSION/docker-compose-${PLATFORM}-${ARCH}"
chmod +x "dist/docker-compose"
fi

View File

@@ -2,7 +2,7 @@
"author": "Portainer.io",
"name": "portainer",
"homepage": "http://portainer.io",
"version": "2.14.0",
"version": "2.14.2",
"repository": {
"type": "git",
"url": "git@github.com:portainer/portainer.git"