Compare commits

..

15 Commits

Author SHA1 Message Date
MaximeBajeux
d3f7212adc fix(templates): App templates not loading with error in browser console 2021-03-02 22:04:25 +01:00
cong meng
60d889e043 feat(volume) change the way portainer creates NFS4 volumes (#4729) (#4735)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-03-02 22:04:25 +01:00
Chaim Lev-Ari
3bb825c10c feat(main): introduce description to fatal errors (#4468) 2021-03-02 22:04:25 +01:00
Chaim Lev-Ari
5bd7f8b636 feat(edge): show last check in date (#4782)
* feat(k8s): better form validation for configuration keys (#4728) (#4733)

Co-authored-by: Simon Meng <simon.meng@portainer.io>

* feat(home): show edge valid tag

* fix(endpoint): show right heartbeat

* style(endpoints): add some comments

Co-authored-by: cong meng <mcpacino@gmail.com>
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-03-02 22:04:25 +01:00
cong meng
88f0b66cdf feat(k8s): better form validation for configuration keys (#4728) (#4733)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-03-02 22:04:25 +01:00
Alice Groux
5b4696dd6c refactor(app): backport technical changes (#4679)
* refactor(app): backport technical changes

* refactor(app): remove EE only features

* feat(app): small review changes to match EE codebase layout on some files

Co-authored-by: xAt0mZ <baron_l@epitech.eu>
2021-03-02 22:04:25 +01:00
Chaim Lev-Ari
c8318d55b1 feat(datatable): save text filters in session storage (#4741)
* feat(datatable): save text filters in session storage

* refactor(session): as jsdoc comments
2021-03-02 22:04:25 +01:00
Alice Groux
2ff7ebebf5 feat(k8s/application): add the ability to redeploy external application (#4704)
* feat(k8s/application): add the ability to redeploy external application

* feat(k8s/application): remove extra whitespace for pod application
2021-03-02 22:04:25 +01:00
Chaim Lev-Ari
634ee9da72 feat(services): hide webhook interface (#4794) 2021-03-02 22:04:25 +01:00
Chaim Lev-Ari
5cbdb0b164 fix(db): skip resource control migration if stack doesn't exist (#4879) 2021-03-02 22:04:25 +01:00
Eduardo Brito
3821370fc2 feat(yaml-inspector): add button to expand/collapse yaml inspector (#4007) (#4828)
* #4007 feat(yaml-inspector): add button to expand/collapse yaml inspector

* feat(yaml-inspector): add button to expand/collapse yaml inspector

Better yamlInspector.html formatting

* feat(yaml-inspector): change name of toggle function

More descriptive name for the function that toggles the expansion of the YAML inspector.
2021-03-02 22:04:25 +01:00
Chaim Lev-Ari
e850aee966 feat(stacks): scope stack names to endpoint (#4520)
* refactor(stack): create unique name function

* refactor(stack): change stack resource control id

* feat(stacks): validate stack unique name in endpoint

* feat(stacks): prevent name collision with external stacks

* refactor(stacks): move resource id util

* refactor(stacks): supply resource id util with name and endpoint

* fix(docker): calculate swarm resource id

* feat(stack): prevent migration if stack name already exist

* feat(authorization): use stackutils
2021-03-02 22:04:25 +01:00
Alice Groux
18842045fc feat(app/containers): display IP (#4435) 2021-03-02 22:04:25 +01:00
Chaim Lev-Ari
846d7e633b fix(containers): fix layout in small screens (#4854) 2021-03-02 22:04:25 +01:00
Dmitry Salakhov
f443dd113d fix windows build 2021-02-25 11:53:04 +13:00
47 changed files with 42 additions and 871 deletions

View File

@@ -5,8 +5,7 @@
"@babel/preset-env",
{
"modules": false,
"useBuiltIns": "entry",
"corejs": "2"
"useBuiltIns": "entry"
}
]
]

View File

@@ -1,9 +1,7 @@
package team
import (
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/internal"
@@ -60,7 +58,7 @@ func (service *Service) TeamByName(name string) (*portainer.Team, error) {
return err
}
if strings.EqualFold(t.Name, name) {
if t.Name == name {
team = &t
break
}

View File

@@ -1,11 +1,10 @@
package user
import (
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/internal"
"strings"
"github.com/boltdb/bolt"
)
@@ -62,7 +61,7 @@ func (service *Service) UserByUsername(username string) (*portainer.User, error)
return err
}
if strings.EqualFold(u.Username, username) {
if strings.ToLower(u.Username) == username {
user = &u
break
}

View File

@@ -118,7 +118,6 @@ func snapshotNodes(snapshot *portainer.DockerSnapshot, cli *client.Client) error
}
snapshot.TotalCPU = int(nanoCpus / 1e9)
snapshot.TotalMemory = totalMem
snapshot.NodeCount = len(nodes)
return nil
}

View File

@@ -31,23 +31,6 @@ type (
}
)
func getUniqueElements(items string) []string {
result := []string{}
seen := make(map[string]struct{})
for _, item := range strings.Split(items, ",") {
v := strings.TrimSpace(item)
if v == "" {
continue
}
if _, ok := seen[v]; !ok {
result = append(result, v)
seen[v] = struct{}{}
}
}
return result
}
func (transport *Transport) newResourceControlFromPortainerLabels(labelsObject map[string]interface{}, resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
if labelsObject[resourceLabelForPortainerPublicResourceControl] != nil {
resourceControl := authorization.NewPublicResourceControl(resourceID, resourceType)
@@ -64,12 +47,12 @@ func (transport *Transport) newResourceControlFromPortainerLabels(labelsObject m
userNames := make([]string, 0)
if labelsObject[resourceLabelForPortainerTeamResourceControl] != nil {
concatenatedTeamNames := labelsObject[resourceLabelForPortainerTeamResourceControl].(string)
teamNames = getUniqueElements(concatenatedTeamNames)
teamNames = strings.Split(concatenatedTeamNames, ",")
}
if labelsObject[resourceLabelForPortainerUserResourceControl] != nil {
concatenatedUserNames := labelsObject[resourceLabelForPortainerUserResourceControl].(string)
userNames = getUniqueElements(concatenatedUserNames)
userNames = strings.Split(concatenatedUserNames, ",")
}
if len(teamNames) > 0 || len(userNames) > 0 {

View File

@@ -1,63 +0,0 @@
package docker
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_getUniqueElements(t *testing.T) {
cases := []struct {
description string
input string
expected []string
}{
{
description: "no items padded",
input: " ",
expected: []string{},
},
{
description: "single item",
input: "a",
expected: []string{"a"},
},
{
description: "single item padded",
input: " a ",
expected: []string{"a"},
},
{
description: "multiple items",
input: "a,b",
expected: []string{"a", "b"},
},
{
description: "multiple items padded",
input: " a , b ",
expected: []string{"a", "b"},
},
{
description: "multiple items with empty values",
input: " a , ,b ",
expected: []string{"a", "b"},
},
{
description: "duplicates",
input: " a , a ",
expected: []string{"a"},
},
{
description: "mix with duplicates",
input: " a ,b, a ",
expected: []string{"a", "b"},
},
}
for _, tt := range cases {
t.Run(tt.description, func(t *testing.T) {
result := getUniqueElements(tt.input)
assert.ElementsMatch(t, result, tt.expected)
})
}
}

View File

@@ -119,7 +119,6 @@ type (
ImageCount int `json:"ImageCount"`
ServiceCount int `json:"ServiceCount"`
StackCount int `json:"StackCount"`
NodeCount int `json:"NodeCount"`
SnapshotRaw DockerSnapshotRaw `json:"DockerSnapshotRaw"`
}

View File

@@ -259,10 +259,6 @@ angular.module('portainer.docker').factory('ContainerHelper', [
return bindings;
};
helper.getContainerNames = function (containers) {
return _.map(_.flatten(_.map(containers, 'Names')), (name) => _.trimStart(name, '/'));
};
return helper;
},
]);

View File

@@ -28,7 +28,7 @@
name="mountType"
class="form-control"
ng-model="mount.Type"
ng-change="onChangeMountType(service, mount)"
ng-change="updateMount(service, mount)"
ng-disabled="isUpdating"
disable-authorization="DockerServiceUpdate"
>

View File

@@ -209,12 +209,6 @@ angular.module('portainer.docker').controller('ServiceController', [
updateServiceArray(service, 'ServiceMounts', service.ServiceMounts);
}
};
$scope.onChangeMountType = function onChangeMountType(service, mount) {
mount.Source = null;
$scope.updateMount(service, mount);
};
$scope.updateMount = function updateMount(service) {
updateServiceArray(service, 'ServiceMounts', service.ServiceMounts);
};

View File

@@ -247,16 +247,6 @@ angular.module('portainer.kubernetes', ['portainer.app']).config([
},
};
const volumeCreation = {
name: 'kubernetes.volumes.new',
url: '/new',
views: {
'content@': {
component: 'kubernetesCreateVolumeView',
},
},
};
$stateRegistryProvider.register(kubernetes);
$stateRegistryProvider.register(applications);
$stateRegistryProvider.register(applicationCreation);
@@ -280,6 +270,5 @@ angular.module('portainer.kubernetes', ['portainer.app']).config([
$stateRegistryProvider.register(resourcePoolAccess);
$stateRegistryProvider.register(volumes);
$stateRegistryProvider.register(volume);
$stateRegistryProvider.register(volumeCreation);
},
]);

View File

@@ -54,7 +54,6 @@
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.volumes.new"> <i class="fa fa-plus space-right" aria-hidden="true"></i>Add NFS volume </button>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
@@ -135,7 +134,6 @@
}}</a>
<span class="label label-info image-tag label-margins" ng-if="$ctrl.isSystemNamespace(item)">system</span>
<span class="label label-primary image-tag label-margins" ng-if="!$ctrl.isSystemNamespace(item) && $ctrl.isExternalVolume(item)">external</span>
<span class="label label-primary image-tag label-margins" ng-if="!$ctrl.isSystemNamespace(item) && $ctrl.isNFSVolume(item)">NFS</span>
<span class="label label-warning image-tag label-margins" ng-if="!$ctrl.isSystemNamespace(item) && !$ctrl.isUsed(item)">unused</span>
</td>
<td>
@@ -151,7 +149,7 @@
<span ng-if="!item.Applications.length">-</span>
</td>
<td>
{{ item.PersistentVolumeClaim.StorageClass.Name || '-' }}
{{ item.PersistentVolumeClaim.StorageClass.Name }}
</td>
<td>
{{ item.PersistentVolumeClaim.Storage }}

View File

@@ -4,13 +4,12 @@ import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
// TODO: review - refactor to use `extends GenericDatatableController`
class KubernetesVolumesDatatableController {
/* @ngInject */
constructor($async, $controller, Authentication, KubernetesNamespaceHelper, DatatableService, EndpointProvider) {
constructor($async, $controller, Authentication, KubernetesNamespaceHelper, DatatableService) {
this.$async = $async;
this.$controller = $controller;
this.Authentication = Authentication;
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
this.DatatableService = DatatableService;
this.EndpointProvider = EndpointProvider;
this.onInit = this.onInit.bind(this);
this.allowSelection = this.allowSelection.bind(this);
@@ -41,10 +40,6 @@ class KubernetesVolumesDatatableController {
return KubernetesVolumeHelper.isExternalVolume(item);
}
isNFSVolume(item) {
return KubernetesVolumeHelper.isNFSVolume(item);
}
allowSelection(item) {
return !this.disableRemove(item);
}
@@ -54,8 +49,6 @@ class KubernetesVolumesDatatableController {
this.prepareTableFromDataset();
this.isAdmin = this.Authentication.isAdmin();
this.settings.showSystem = false;
const endpoint = this.EndpointProvider.currentEndpoint();
this.storageClasses = endpoint.Kubernetes.Configuration.StorageClasses;
this.state.orderBy = this.orderBy;
var storedOrder = this.DatatableService.getDataTableOrder(this.tableKey);

View File

@@ -17,7 +17,6 @@ class KubernetesPersistentVolumeClaimConverter {
res.Yaml = yaml ? yaml.data : '';
res.ApplicationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationOwnerLabel] : '';
res.ApplicationName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationNameLabel] : '';
res.PersistentVolumeName = data.spec.volumeName;
return res;
}
@@ -58,20 +57,6 @@ class KubernetesPersistentVolumeClaimConverter {
return res;
}
/**
* Generate KubernetesPersistentVolumeClaim from KubernetesVolumeFormValues
* @param {KubernetesVolumeFormValues} formValues
*/
static volumeFormValuesToVolumeClaim(formValues) {
const pvc = new KubernetesPersistentVolumeClaim();
pvc.Name = formValues.Name;
pvc.Namespace = formValues.ResourcePool.Namespace.Name;
pvc.Storage = '' + formValues.Size + formValues.SizeUnit.charAt(0) + 'i';
pvc.MountPath = formValues.NFSMountPoint;
pvc.ApplicationOwner = formValues.ApplicationOwner;
return pvc;
}
static createPayload(pvc) {
const res = new KubernetesPersistentVolumClaimCreatePayload();
res.metadata.name = pvc.Name;
@@ -81,9 +66,6 @@ class KubernetesPersistentVolumeClaimConverter {
res.metadata.labels.app = pvc.ApplicationName;
res.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = pvc.ApplicationOwner;
res.metadata.labels[KubernetesPortainerApplicationNameLabel] = pvc.ApplicationName;
if (pvc.PersistentVolumeName) {
res.spec.volumeName = pvc.PersistentVolumeName;
}
return res;
}

View File

@@ -1,10 +1,9 @@
import { KubernetesVolume } from 'Kubernetes/models/volume/models';
class KubernetesVolumeConverter {
static apiToVolume(pvc, pv, pool) {
static pvcToVolume(claim, pool) {
const res = new KubernetesVolume();
res.PersistentVolumeClaim = pvc;
res.PersistentVolume = pv;
res.PersistentVolumeClaim = claim;
res.ResourcePool = pool;
return res;
}

View File

@@ -24,37 +24,5 @@ class KubernetesCommonHelper {
_.set(obj, path, value);
}
}
/**
* Format a string to be compliant with RFC 1123 - DNS subdomain specs
* https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names
* contain no more than 253 characters
* contain only lowercase alphanumeric characters, '-' or '.'
* start with an alphanumeric character
* end with an alphanumeric character
* @param {String} str String to format
*/
static formatToDnsSubdomainName(str) {
let res = _.replace(str, /[^a-z0-9.-]/g, '.');
res = _.replace(res, /(^[-.]*)|([-.]*$)/g, '');
res = _.truncate(res, { length: 253, omission: '' });
return res;
}
/**
* Format a string to be compliant with RFC 1123 - DNS subdomain specs
* https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names
* contain at most 63 characters
* contain only lowercase alphanumeric characters or '-'
* start with an alphanumeric character
* end with an alphanumeric character
* @param {String} str String to format
*/
static formatToDnsLabelName(str) {
let res = _.replace(str, /[^a-z0-9-]/g, '-');
res = _.replace(str, /(^[-]*)|([-]*$)/g, ''); // ensure alph on string start and end
res = _.truncate(res, { length: 63, omission: '' });
return res;
}
}
export default KubernetesCommonHelper;

View File

@@ -1,7 +1,6 @@
import _ from 'lodash-es';
import uuidv4 from 'uuid/v4';
import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
import KubernetesCommonHelper from './commonHelper';
class KubernetesVolumeHelper {
// TODO: review
@@ -33,15 +32,6 @@ class KubernetesVolumeHelper {
static isExternalVolume(volume) {
return !volume.PersistentVolumeClaim.ApplicationOwner;
}
static isNFSVolume(volume) {
return volume.PersistentVolume.NFSAddress;
}
static generateVolumeName(name) {
const n = `${name}-${uuidv4()}`;
return KubernetesCommonHelper.formatToDnsSubdomainName(n);
}
}
export default KubernetesVolumeHelper;

View File

@@ -1,19 +0,0 @@
/**
* FormValues for CreateVolume view
*/
export function KubernetesVolumeFormValues() {
return {
Id: '',
Name: '',
ResourcePool: {}, // KubernetesResourcePool
Size: '',
SizeUnit: '',
NFSAddress: '',
NFSMountPoint: '',
};
}
export const KubernetesVolumeFormValuesDefaults = {
Size: '10',
SizeUnit: 'MB',
};

View File

@@ -8,12 +8,11 @@ const _KubernetesPersistentVolumeClaim = Object.freeze({
PreviousName: '',
Namespace: '',
Storage: 0,
StorageClass: undefined, // KubernetesStorageClass
StorageClass: {}, // KubernetesStorageClass
CreationDate: '',
ApplicationOwner: '',
ApplicationName: '',
MountPath: '', // used for Application creation from ApplicationFormValues | not used from API conversion
PersistentVolumeName: '', // Name of KubernetesPersistentVolume
Yaml: '',
});
@@ -24,27 +23,12 @@ export class KubernetesPersistentVolumeClaim {
}
}
/**
* KubernetesPersistentVolume Model
*/
export function KubernetesPersistentVolume() {
return {
Id: '',
Name: '',
StorageClass: {}, // KubernetesStorageClass
Size: '',
NFSAddress: '',
NFSMountPoint: '',
};
}
/**
* KubernetesVolume Model (Composite)
*/
const _KubernetesVolume = Object.freeze({
ResourcePool: {}, // KubernetesResourcePool
PersistentVolumeClaim: {}, // KubernetesPersistentVolumeClaim
PersistentVolume: undefined, // KubernetesPersistentVolume
Applications: [], // KubernetesApplication
});

View File

@@ -1,45 +0,0 @@
import _ from 'lodash-es';
import { KubernetesPersistentVolume } from 'Kubernetes/models/volume/models';
import { KubernetesPersistentVolumeCreatePayload } from 'Kubernetes/persistent-volume/payloads';
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
class KubernetesPersistentVolumeConverter {
/**
* Converts KubernetesVolumeFormValues to KubernetesPersistentVolume
* @param {KubernetesVolumeFormValues} fv
*/
static formValuesToPersistentVolume(fv) {
const pv = new KubernetesPersistentVolume();
pv.Name = KubernetesVolumeHelper.generateVolumeName(fv.Name);
pv.Size = fv.Size + fv.SizeUnit;
pv.NFSAddress = fv.NFSAddress;
pv.NFSMountPoint = fv.NFSMountPoint;
return pv;
}
static apiToPersistentVolume(data, storageClasses) {
const pv = new KubernetesPersistentVolume();
pv.Id = data.metadata.id;
pv.Name = data.metadata.name;
pv.Size = data.spec.capacity.storage.replace('i', 'B');
if (data.spec.storageClassName) {
pv.StorageClass = _.find(storageClasses, { Name: data.spec.storageClassName });
}
if (data.spec.nfs) {
pv.NFSAddress = data.spec.nfs.server;
pv.NFSMountPoint = data.spec.nfs.path;
}
return pv;
}
static createPayload(pv) {
const res = new KubernetesPersistentVolumeCreatePayload();
res.metadata.name = pv.Name;
res.spec.capacity.storage = pv.Size.replace('B', 'i');
res.spec.nfs.path = pv.NFSMountPoint;
res.spec.nfs.server = pv.NFSAddress;
return res;
}
}
export default KubernetesPersistentVolumeConverter;

View File

@@ -1,20 +0,0 @@
import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloads';
/**
* KubernetesPersistentVolumeCreatePayload Model
*/
export function KubernetesPersistentVolumeCreatePayload() {
return {
metadata: new KubernetesCommonMetadataPayload(),
spec: {
accessModes: ['ReadWriteOnce'],
capacity: {
storage: '',
},
nfs: {
path: '',
server: '',
},
},
};
}

View File

@@ -1,23 +0,0 @@
angular.module('portainer.kubernetes').factory('KubernetesPersistentVolume', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function KubernetesPersistentVolume($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return function (namespace) {
const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/api/v1' + (namespace ? '/namespaces/:namespace' : '') + '/persistentvolumes/:id/:action';
return $resource(
url,
{
endpointId: EndpointProvider.endpointID,
namespace: namespace,
},
{
get: { method: 'GET' },
create: { method: 'POST' },
delete: { method: 'DELETE' },
}
);
};
},
]);

View File

@@ -1,90 +0,0 @@
import angular from 'angular';
import _ from 'lodash-es';
import PortainerError from 'Portainer/error';
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
import KubernetesPersistentVolumeConverter from 'Kubernetes/persistent-volume/converter';
class KubernetesPersistentVolumeService {
/* @ngInject */
constructor($async, KubernetesPersistentVolume, EndpointProvider) {
this.$async = $async;
this.KubernetesPersistentVolume = KubernetesPersistentVolume;
this.EndpointProvider = EndpointProvider;
this.getAsync = this.getAsync.bind(this);
this.getAllAsync = this.getAllAsync.bind(this);
this.createAsync = this.createAsync.bind(this);
this.deleteAsync = this.deleteAsync.bind(this);
}
/**
* GET
*/
async getAsync(name) {
try {
const params = new KubernetesCommonParams();
params.id = name;
const data = await this.KubernetesPersistentVolume().get(params).$promise;
const storageClasses = this.EndpointProvider.currentEndpoint().Kubernetes.Configuration.StorageClasses;
return KubernetesPersistentVolumeConverter.apiToPersistentVolume(data, storageClasses);
} catch (err) {
throw new PortainerError('Unable to retrieve persistent volume', err);
}
}
async getAllAsync() {
try {
const data = await this.KubernetesPersistentVolume().get().$promise;
const storageClasses = this.EndpointProvider.currentEndpoint().Kubernetes.Configuration.StorageClasses;
const res = _.map(data.items, (item) => KubernetesPersistentVolumeConverter.apiToPersistentVolume(item, storageClasses));
return res;
} catch (err) {
throw new PortainerError('Unable to retrieve persistent volumes', err);
}
}
get(name) {
if (name) {
return this.$async(this.getAsync, name);
}
return this.$async(this.getAllAsync);
}
/**
* CREATE
*/
async createAsync(pv) {
try {
const payload = KubernetesPersistentVolumeConverter.createPayload(pv);
const data = await this.KubernetesPersistentVolume().create(payload).$promise;
return data;
} catch (err) {
throw new PortainerError('Unable to create persistent volume', err);
}
}
create(pv) {
return this.$async(this.createAsync, pv);
}
/**
* DELETE
*/
async deleteAsync(pv) {
try {
const params = new KubernetesCommonParams();
params.id = pv.Name;
await this.KubernetesPersistentVolume().delete(params).$promise;
} catch (err) {
throw new PortainerError('Unable to delete persistent volume', err);
}
}
delete(pv) {
return this.$async(this.deleteAsync, pv);
}
}
export default KubernetesPersistentVolumeService;
angular.module('portainer.kubernetes').service('KubernetesPersistentVolumeService', KubernetesPersistentVolumeService);

View File

@@ -2,18 +2,14 @@ import angular from 'angular';
import _ from 'lodash-es';
import KubernetesVolumeConverter from 'Kubernetes/converters/volume';
import KubernetesPersistentVolumeConverter from 'Kubernetes/persistent-volume/converter';
import KubernetesPersistentVolumeClaimConverter from 'Kubernetes/converters/persistentVolumeClaim';
import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
class KubernetesVolumeService {
/* @ngInject */
constructor($async, KubernetesResourcePoolService, KubernetesApplicationService, KubernetesPersistentVolumeClaimService, KubernetesPersistentVolumeService) {
constructor($async, KubernetesResourcePoolService, KubernetesApplicationService, KubernetesPersistentVolumeClaimService) {
this.$async = $async;
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
this.KubernetesApplicationService = KubernetesApplicationService;
this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService;
this.KubernetesPersistentVolumeService = KubernetesPersistentVolumeService;
this.getAsync = this.getAsync.bind(this);
this.getAllAsync = this.getAllAsync.bind(this);
@@ -26,11 +22,7 @@ class KubernetesVolumeService {
async getAsync(namespace, name) {
try {
const [pvc, pool] = await Promise.all([this.KubernetesPersistentVolumeClaimService.get(namespace, name), this.KubernetesResourcePoolService.get(namespace)]);
let pv = undefined;
if (pvc.PersistentVolumeName) {
pv = await this.KubernetesPersistentVolumeService.get(pvc.PersistentVolumeName);
}
return KubernetesVolumeConverter.apiToVolume(pvc, pv, pool);
return KubernetesVolumeConverter.pvcToVolume(pvc, pool);
} catch (err) {
throw err;
}
@@ -43,18 +35,7 @@ class KubernetesVolumeService {
const res = await Promise.all(
_.map(pools, async (pool) => {
const pvcs = await this.KubernetesPersistentVolumeClaimService.get(pool.Namespace.Name);
const pvs = [];
await Promise.all(
_.map(pvcs, async (pvc) => {
if (pvc.PersistentVolumeName) {
pvs.push(await this.KubernetesPersistentVolumeService.get(pvc.PersistentVolumeName));
}
})
);
return _.map(pvcs, (pvc) => {
const pv = pvc.PersistentVolumeName ? _.find(pvs, { Name: pvc.PersistentVolumeName }) : undefined;
return KubernetesVolumeConverter.apiToVolume(pvc, pv, pool);
});
return _.map(pvcs, (pvc) => KubernetesVolumeConverter.pvcToVolume(pvc, pool));
})
);
return _.flatten(res);
@@ -70,34 +51,12 @@ class KubernetesVolumeService {
return this.$async(this.getAllAsync, namespace);
}
/**
* CREATE all KubernetesVolume composite elements (but the ResourcePool)
* @param {KubernetesVolumeFormValues} fv
*/
create(fv) {
return this.$async(async () => {
try {
fv.ApplicationOwner = KubernetesCommonHelper.ownerToLabel(fv.ApplicationOwner);
const pv = KubernetesPersistentVolumeConverter.formValuesToPersistentVolume(fv);
const data = await this.KubernetesPersistentVolumeService.create(pv);
const pvc = KubernetesPersistentVolumeClaimConverter.volumeFormValuesToVolumeClaim(fv);
pvc.PersistentVolumeName = data.metadata.name;
await this.KubernetesPersistentVolumeClaimService.create(pvc);
} catch (err) {
throw err;
}
});
}
/**
* DELETE
*/
async deleteAsync(volume) {
try {
await this.KubernetesPersistentVolumeClaimService.delete(volume.PersistentVolumeClaim);
if (volume.PersistentVolume) {
await this.KubernetesPersistentVolumeService.delete(volume.PersistentVolume);
}
} catch (err) {
throw err;
}

View File

@@ -1,138 +0,0 @@
<kubernetes-view-header ng-if="!ctrl.state.isEdit" title="Create volume" state="kubernetes.volumes.new" view-ready="ctrl.state.viewReady">
<a ui-sref="kubernetes.volumes">Volumes</a> &gt; Create a volume
</kubernetes-view-header>
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
<div ng-if="ctrl.state.viewReady">
<div class="row">
<div class="col-xs-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal" name="kubernetesVolumeCreationForm" autocomplete="off">
<!-- #region NAME FIELD -->
<div class="form-group">
<label for="volume_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
<input
type="text"
class="form-control"
name="volume_name"
ng-model="ctrl.formValues.Name"
ng-change="ctrl.onNameChange()"
placeholder="my-volume"
ng-pattern="/^[a-z]([-a-z0-9]*[a-z0-9])?$/"
auto-focus
required
/>
</div>
</div>
<div class="form-group" ng-show="kubernetesVolumeCreationForm.volume_name.$invalid || ctrl.state.alreadyExists">
<div class="col-sm-12 small text-warning">
<div ng-messages="kubernetesVolumeCreationForm.volume_name.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
<p ng-message="pattern"
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field must consist of lower case alphanumeric characters or '-', start with an alphabetic
character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123').</p
>
</div>
<p ng-if="ctrl.state.alreadyExists">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A volume with the same name already exists in this resource pool.
</p>
</div>
</div>
<!-- #endregion -->
<!-- #region RESOURCE POOL -->
<div class="col-sm-12 form-section-title">
Resource pool
</div>
<div class="form-group">
<label for="resource-pool-selector" class="col-sm-1 control-label text-left">Resource pool</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"
required
></select>
</div>
</div>
<!-- #endregion -->
<!-- #region NFS-SETTINGS -->
<div>
<div class="col-sm-12 form-section-title">
NFS Settings
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
<i class="fa fa-info-circle blue-icon space-right" aria-hidden="true"></i>
Please ensure that your hosts are running either nfs-utils or nfs-common software before attempting to use NFS
</span>
</div>
<div class="form-group">
<label for="address" class="col-sm-1 control-label text-left">Address</label>
<div class="col-sm-11">
<input type="text" class="form-control" name="address" ng-model="ctrl.formValues.NFSAddress" placeholder="e.g. my.nfs-server.com OR xxx.xxx.xxx.xxx" required />
</div>
</div>
<div class="form-group" ng-show="kubernetesVolumeCreationForm.address.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="kubernetesVolumeCreationForm.address.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<div class="form-group">
<label for="mount_point" class="col-sm-1 control-label text-left">Mount point</label>
<div class="col-sm-11">
<input
type="text"
class="form-control"
name="mount_point"
ng-model="ctrl.formValues.NFSMountPoint"
placeholder="e.g. /export/share, :/export/share, /share or :/share"
required
/>
</div>
</div>
<div class="form-group" ng-show="kubernetesVolumeCreationForm.mount_point.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="kubernetesVolumeCreationForm.mount_point.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
</div>
<!-- #endregion -->
<!-- #region ACTIONS -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="!kubernetesVolumeCreationForm.$valid"
button-spinner="ctrl.state.actionInProgress"
ng-click="ctrl.createVolume()"
>
<span ng-show="!ctrl.state.isEdit && !ctrl.state.actionInProgress">Create volume</span>
<span ng-show="!ctrl.state.isEdit && ctrl.state.actionInProgress">Creation in progress...</span>
</button>
</div>
</div>
<!-- #endregion -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
</div>

View File

@@ -1,8 +0,0 @@
angular.module('portainer.kubernetes').component('kubernetesCreateVolumeView', {
templateUrl: './createVolume.html',
controller: 'KubernetesCreateVolumeController',
controllerAs: 'ctrl',
bindings: {
$transition$: '<',
},
});

View File

@@ -1,83 +0,0 @@
import angular from 'angular';
import * as _ from 'lodash-es';
import { KubernetesVolumeFormValues, KubernetesVolumeFormValuesDefaults } from 'Kubernetes/models/volume/formValues';
class KubernetesCreateVolumeController {
/* @ngInject */
constructor($async, $state, Notifications, Authentication, KubernetesNamespaceHelper, KubernetesResourcePoolService, KubernetesVolumeService, KubernetesPersistentVolumeService) {
this.$async = $async;
this.$state = $state;
this.Notifications = Notifications;
this.Authentication = Authentication;
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
this.KubernetesVolumeService = KubernetesVolumeService;
this.KubernetesPersistentVolumeService = KubernetesPersistentVolumeService;
}
onNameChange() {
const existingVolume = _.find(this.volumes, ['PersistentVolumeClaim.Name', this.formValues.Name]);
this.state.alreadyExists = !!existingVolume;
}
refreshExistingVolumes() {
return this.$async(async () => {
try {
this.volumes = await this.KubernetesVolumeService.get(this.formValues.ResourcePool.Namespace.Name);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to refresh volumes for this Resource Pool');
}
});
}
onResourcePoolSelectionChange() {
return this.$async(async () => {
await this.refreshExistingVolumes();
this.onNameChange();
});
}
createVolume() {
return this.$async(async () => {
this.state.actionInProgress = true;
try {
await this.KubernetesVolumeService.create(this.formValues);
this.Notifications.success('Volume successfully created', this.formValues.Name);
this.$state.go('kubernetes.volumes');
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to create volume');
} finally {
this.state.actionInProgress = false;
}
});
}
$onInit() {
return this.$async(async () => {
try {
this.state = {
viewReady: false,
alreadyExists: false,
actionInProgress: false,
};
const resourcePools = await this.KubernetesResourcePoolService.get();
this.resourcePools = _.filter(resourcePools, (resourcePool) => !this.KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
this.formValues = new KubernetesVolumeFormValues();
this.formValues.ApplicationOwner = this.Authentication.getUserDetails().username;
this.formValues.Size = KubernetesVolumeFormValuesDefaults.Size;
this.formValues.SizeUnit = KubernetesVolumeFormValuesDefaults.SizeUnit;
this.formValues.ResourcePool = this.resourcePools[0];
await this.onResourcePoolSelectionChange();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to load view data');
} finally {
this.state.viewReady = true;
}
});
}
}
export default KubernetesCreateVolumeController;
angular.module('portainer.kubernetes').controller('KubernetesCreateVolumeController', KubernetesCreateVolumeController);

View File

@@ -23,7 +23,6 @@
<td>
{{ ctrl.volume.PersistentVolumeClaim.Name }}
<span class="label label-primary image-tag label-margins" ng-if="!ctrl.isSystemNamespace() && ctrl.isExternalVolume()">external</span>
<span class="label label-primary image-tag label-margins" ng-if="!ctrl.isSystemNamespace() && ctrl.isNFSVolume()">NFS</span>
<span class="label label-warning image-tag label-margins" ng-if="!ctrl.isSystemNamespace() && !ctrl.isUsed()">unused</span>
</td>
</tr>
@@ -34,11 +33,11 @@
<span style="margin-left: 5px;" class="label label-info image-tag" ng-if="ctrl.isSystemNamespace(item)">system</span>
</td>
</tr>
<tr ng-if="ctrl.volume.PersistentVolumeClaim.StorageClass">
<tr>
<td>Storage</td>
<td>{{ ctrl.volume.PersistentVolumeClaim.StorageClass.Name }}</td>
</tr>
<tr ng-if="ctrl.volume.PersistentVolumeClaim.StorageClass">
<tr>
<td>Shared Access Policy</td>
<td
>{{ ctrl.state.volumeSharedAccessPolicy }}
@@ -114,14 +113,6 @@
</form>
</td>
</tr>
<tr ng-if="ctrl.isNFSVolume()">
<td>NFS Address</td>
<td>{{ ctrl.volume.PersistentVolume.NFSAddress }}</td>
</tr>
<tr ng-if="ctrl.isNFSVolume()">
<td>NFS Mount point</td>
<td>{{ ctrl.volume.PersistentVolume.NFSMountPoint }}</td>
</tr>
</tbody>
</table>
</div>

View File

@@ -54,10 +54,6 @@ class KubernetesVolumeController {
return KubernetesVolumeHelper.isExternalVolume(this.volume);
}
isNFSVolume() {
return KubernetesVolumeHelper.isNFSVolume(this.volume);
}
isSystemNamespace() {
return this.KubernetesNamespaceHelper.isSystemNamespace(this.volume.ResourcePool.Namespace.Name);
}

View File

@@ -91,13 +91,14 @@ class KubernetesVolumesController {
this.KubernetesApplicationService.get(),
this.KubernetesStorageService.get(this.state.endpointId),
]);
this.volumes = _.map(volumes, (volume) => {
volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, applications);
return volume;
});
this.storages = buildStorages(storages, volumes);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve volumes');
this.Notifications.error('Failure', err, 'Unable to retreive resource pools');
}
}

View File

@@ -84,10 +84,6 @@
<span class="small text-muted">
{{ $ctrl.model.Snapshots[0].Swarm ? 'Swarm' : 'Standalone' }} {{ $ctrl.model.Snapshots[0].DockerVersion }}
<span ng-if="$ctrl.model.Type === 2">+ <i class="fa fa-bolt" aria-hidden="true"></i> Agent</span>
<span style="padding: 0 7px 0 0;" ng-if="$ctrl.model.Snapshots[0].Swarm">
<i class="fa fa-hdd space-left space-right" aria-hidden="true"></i>{{ $ctrl.model.Snapshots[0].NodeCount }}
{{ $ctrl.model.Snapshots[0].NodeCount === 1 ? 'node' : 'nodes' }}
</span>
</span>
</div>

View File

@@ -24,7 +24,7 @@ angular.module('portainer.app').controller('StackDuplicationFormController', [
}
function isFormValidForDuplication() {
return isFormValidForMigration() && ctrl.formValues.newName && !ctrl.yamlError;
return isFormValidForMigration() && ctrl.formValues.newName;
}
function duplicateStack() {

View File

@@ -33,9 +33,6 @@
<span ng-hide="$ctrl.state.duplicationInProgress"> <i class="fa fa-clone space-right" aria-hidden="true"></i> Duplicate </span>
<span ng-show="$ctrl.state.duplicationInProgress">Duplication in progress...</span>
</button>
<div ng-if="$ctrl.yamlError"
><span class="text-danger small">{{ $ctrl.yamlError }}</span></div
>
</div>
</div>
</div>

View File

@@ -7,6 +7,5 @@ angular.module('portainer.app').component('stackDuplicationForm', {
endpoints: '<',
groups: '<',
currentEndpointId: '<',
yamlError: '<',
},
});

View File

@@ -1,15 +0,0 @@
import _ from 'lodash-es';
class GenericHelper {
static findDeepAll(obj, target, res = []) {
if (typeof obj === 'object') {
_.forEach(obj, (child, key) => {
if (key === target) res.push(child);
if (typeof child === 'object') GenericHelper.findDeepAll(child, target, res);
});
}
return res;
}
}
export default GenericHelper;

View File

@@ -1,6 +1,5 @@
import _ from 'lodash-es';
import YAML from 'yaml';
import GenericHelper from '@/portainer/helpers/genericHelper';
import { ExternalStackViewModel } from '@/portainer/models/stack';
angular.module('portainer.app').factory('StackHelper', [
@@ -23,28 +22,6 @@ angular.module('portainer.app').factory('StackHelper', [
);
}
helper.validateYAML = function (yaml, containerNames) {
let yamlObject;
try {
yamlObject = YAML.parse(yaml);
} catch (err) {
return 'There is an error in the yaml syntax: ' + err;
}
const names = _.uniq(GenericHelper.findDeepAll(yamlObject, 'container_name'));
const duplicateContainers = _.intersection(containerNames, names);
if (duplicateContainers.length === 0) return;
return (
(duplicateContainers.length === 1 ? 'This container name is' : 'These container names are') +
' already used by another container running in this environment: ' +
_.join(duplicateContainers, ', ') +
'.'
);
};
return helper;
},
]);

View File

@@ -58,20 +58,10 @@ angular.module('portainer.app').factory('EndpointService', [
return Endpoints.remove({ id: endpointID }).$promise;
};
service.createLocalEndpoint = function (name = 'local', URL = '', PublicURL = '', groupID = 1, tagIds = []) {
service.createLocalEndpoint = function (name = 'local') {
var deferred = $q.defer();
var endpointURL = URL;
if (endpointURL !== '') {
if (endpointURL.indexOf('//./pipe/') == 0) {
// Windows named pipe
endpointURL = 'npipe://' + URL;
} else {
endpointURL = 'unix://' + URL;
}
}
FileUploadService.createEndpoint(name, PortainerEndpointCreationTypes.LocalDockerEnvironment, endpointURL, PublicURL, groupID, tagIds, false)
FileUploadService.createEndpoint(name, PortainerEndpointCreationTypes.LocalDockerEnvironment, '', '', 1, [], false)
.then(function success(response) {
deferred.resolve(response.data);
})

View File

@@ -100,18 +100,12 @@ angular
}
$scope.addDockerEndpoint = function () {
var name = $scope.formValues.Name;
var URL = $filter('stripprotocol')($scope.formValues.URL);
var publicURL = $scope.formValues.PublicURL;
var groupId = $scope.formValues.GroupId;
var tagIds = $scope.formValues.TagIds;
if ($scope.formValues.ConnectSocket) {
URL = $scope.formValues.SocketPath;
var endpointName = $scope.formValues.Name;
$scope.state.actionInProgress = true;
EndpointService.createLocalEndpoint(name, URL, publicURL, groupId, tagIds)
EndpointService.createLocalEndpoint(endpointName)
.then(function success() {
Notifications.success('Endpoint created', name);
Notifications.success('Endpoint created', endpointName);
$state.go('portainer.endpoints', {}, { reload: true });
})
.catch(function error(err) {
@@ -121,9 +115,11 @@ angular
$scope.state.actionInProgress = false;
});
} else {
if (publicURL === '') {
publicURL = URL.split(':')[0];
}
var name = $scope.formValues.Name;
var URL = $filter('stripprotocol')($scope.formValues.URL);
var publicURL = $scope.formValues.PublicURL === '' ? URL.split(':')[0] : $scope.formValues.PublicURL;
var groupId = $scope.formValues.GroupId;
var tagIds = $scope.formValues.TagIds;
var securityData = $scope.formValues.SecurityFormData;
var TLS = securityData.TLS;

View File

@@ -210,42 +210,6 @@
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="formValues.ConnectSocket" /><i></i> </label>
</div>
</div>
<div ng-if="state.EnvironmentType === 'docker' && formValues.ConnectSocket">
<div class="form-group" style="padding-left: 15px;">
<label for="override_socket" class="col-sm_12 control-label text-left">
Override default socket path
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="formValues.OverrideSocket" /><i></i> </label>
</div>
<div ng-if="formValues.OverrideSocket">
<div class="form-group">
<label for="socket_path" class="col-sm-3 col-lg-2 control-label text-left">
Socket path
<portainer-tooltip position="bottom" message="Path to the Docker socket. Remember to bind-mount the socket, see the important notice above for more information.">
</portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
name="socket_path"
ng-model="formValues.SocketPath"
placeholder="e.g. /var/run/docker.sock (on Linux) or //./pipe/docker_engine (on Windows)"
required
/>
</div>
</div>
<div class="form-group" ng-show="endpointCreationForm.socket_path.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="endpointCreationForm.socket_path.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
</div>
</div>
<!-- !connect-via-socket-input -->
<!-- endpoint-url-input -->
<div ng-if="(state.EnvironmentType === 'docker' && !formValues.ConnectSocket) || state.EnvironmentType === 'agent'">
@@ -414,7 +378,7 @@
</div>
<!-- !azure-details -->
<!-- endpoint-security -->
<por-endpoint-security ng-if="state.EnvironmentType === 'docker' && !formValues.ConnectSocket" form-data="formValues.SecurityFormData"></por-endpoint-security>
<por-endpoint-security ng-if="state.EnvironmentType === 'docker'" form-data="formValues.SecurityFormData"></por-endpoint-security>
<!-- !endpoint-security -->
<div class="col-sm-12 form-section-title">
Metadata

View File

@@ -8,18 +8,14 @@ angular
.controller('CreateStackController', function (
$scope,
$state,
$async,
StackService,
Authentication,
Notifications,
FormValidator,
ResourceControlService,
FormHelper,
EndpointProvider,
StackHelper,
ContainerHelper,
CustomTemplateService,
ContainerService
EndpointProvider
) {
$scope.formValues = {
Name: '',
@@ -40,8 +36,6 @@ angular
formValidationError: '',
actionInProgress: false,
StackType: null,
editorYamlValidationError: '',
uploadYamlValidationError: '',
};
$scope.addEnvironmentVariable = function () {
@@ -160,26 +154,6 @@ angular
$scope.editorUpdate = function (cm) {
$scope.formValues.StackFileContent = cm.getValue();
$scope.state.editorYamlValidationError = StackHelper.validateYAML($scope.formValues.StackFileContent, $scope.containerNames);
};
async function onFileLoadAsync(event) {
$scope.state.uploadYamlValidationError = StackHelper.validateYAML(event.target.result, $scope.containerNames);
}
function onFileLoad(event) {
return $async(onFileLoadAsync, event);
}
$scope.uploadFile = function (file) {
$scope.formValues.StackFile = file;
if (file) {
const temporaryFileReader = new FileReader();
temporaryFileReader.fileName = file.name;
temporaryFileReader.onload = onFileLoad;
temporaryFileReader.readAsText(file);
}
};
$scope.onChangeTemplate = async function onChangeTemplate(template) {
@@ -212,13 +186,6 @@ angular
} catch (err) {
Notifications.error('Failure', err, 'Unable to retrieve the ComposeSyntaxMaxVersion');
}
try {
$scope.containers = await ContainerService.containers();
$scope.containerNames = ContainerHelper.getContainerNames($scope.containers);
} catch (err) {
Notifications.error('Failure', err, 'Unable to retrieve Containers');
}
}
initView();

View File

@@ -90,9 +90,6 @@
<span class="col-sm-12 text-muted small">
You can get more information about Compose file format in the <a href="https://docs.docker.com/compose/compose-file/" target="_blank">official documentation</a>.
</span>
<div class="col-sm-12" ng-if="state.editorYamlValidationError"
><span class="text-danger small">{{ state.editorYamlValidationError }}</span></div
>
</div>
<div class="form-group">
<div class="col-sm-12">
@@ -115,13 +112,10 @@
<span class="col-sm-12 text-muted small">
You can upload a Compose file from your computer.
</span>
<div class="col-sm-12" ng-if="state.uploadYamlValidationError"
><span class="text-danger small">{{ state.uploadYamlValidationError }}</span></div
>
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-sm btn-primary" ngf-select="uploadFile($file)">Select file</button>
<button type="button" class="btn btn-sm btn-primary" ngf-select ng-model="formValues.StackFile">Select file</button>
<span style="margin-left: 5px;">
{{ formValues.StackFile.name }}
<i class="fa fa-times red-icon" ng-if="!formValues.StackFile" aria-hidden="true"></i>
@@ -241,11 +235,6 @@
<div class="col-sm-12 form-section-title">
Web editor
</div>
<div class="form-group">
<div class="col-sm-12" ng-if="state.editorYamlValidationError"
><span class="text-danger small">{{ state.editorYamlValidationError }}</span></div
>
</div>
<div class="form-group">
<div class="col-sm-12">
<code-editor
@@ -303,9 +292,9 @@
type="button"
class="btn btn-primary btn-sm"
ng-disabled="state.actionInProgress
|| (state.Method === 'editor' && (!formValues.StackFileContent || state.editorYamlValidationError))
|| (state.Method === 'upload' && (!formValues.StackFile || state.uploadYamlValidationError))
|| (state.Method === 'template' && (!formValues.StackFileContent || !selectedTemplate || state.editorYamlValidationError))
|| (state.Method === 'editor' && !formValues.StackFileContent)
|| (state.Method === 'upload' && !formValues.StackFile)
|| (state.Method === 'template' && (!formValues.StackFileContent || !selectedTemplate))
|| (state.Method === 'repository' && ((!formValues.RepositoryURL || !formValues.ComposeFilePathInRepository) || (formValues.RepositoryAuthentication && (!formValues.RepositoryUsername || !formValues.RepositoryPassword))))
|| !formValues.Name"
ng-click="deployStack()"

View File

@@ -84,7 +84,6 @@
current-endpoint-id="currentEndpointId"
on-duplicate="duplicateStack(name, endpointId)"
on-migrate="migrateStack(name, endpointId)"
yaml-error="state.yamlError"
>
</stack-duplication-form>
</div>

View File

@@ -17,7 +17,6 @@ angular.module('portainer.app').controller('StackController', [
'EndpointService',
'GroupService',
'ModalService',
'StackHelper',
function (
$async,
$q,
@@ -36,16 +35,13 @@ angular.module('portainer.app').controller('StackController', [
EndpointProvider,
EndpointService,
GroupService,
ModalService,
StackHelper,
ContainerHelper
ModalService
) {
$scope.state = {
actionInProgress: false,
migrationInProgress: false,
externalStack: false,
showEditorTab: false,
yamlError: false,
};
$scope.formValues = {
@@ -191,7 +187,6 @@ angular.module('portainer.app').controller('StackController', [
$scope.editorUpdate = function (cm) {
$scope.stackFileContent = cm.getValue();
$scope.state.yamlError = StackHelper.validateYAML($scope.stackFileContent, $scope.containerNames);
};
$scope.stopStack = stopStack;
@@ -248,14 +243,11 @@ angular.module('portainer.app').controller('StackController', [
$q.all({
stack: StackService.stack(id),
groups: GroupService.groups(),
containers: ContainerService.containers(),
})
.then(function success(data) {
var stack = data.stack;
$scope.groups = data.groups;
$scope.stack = stack;
$scope.containers = data.containers;
$scope.containerNames = ContainerHelper.getContainerNames($scope.containers);
let resourcesPromise = Promise.resolve({});
if (stack.Status === 1) {
@@ -276,8 +268,6 @@ angular.module('portainer.app').controller('StackController', [
assignComposeStackResources(data.resources);
}
}
$scope.state.yamlError = StackHelper.validateYAML($scope.stackFileContent, $scope.containerNames);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve stack details');

View File

@@ -254,7 +254,7 @@ angular.module('portainer.app').controller('TemplatesController', [
var endpointMode = $scope.applicationState.endpoint.mode;
var apiVersion = $scope.applicationState.endpoint.apiVersion;
this.state.provider = endpointMode.provider === 'DOCKER_STANDALONE' ? 2 : 1;
$scope.state.provider = endpointMode.provider === 'DOCKER_STANDALONE' ? 2 : 1;
$q.all({
templates: TemplateService.templates(),

View File

@@ -173,7 +173,7 @@ function shell_run_container() {
'docker rm -f portainer',
'docker run -d -p 8000:8000 -p 9000:9000 -v $(pwd)/dist:/app -v ' +
portainer_data +
':/data -v /var/run/docker.sock:/var/run/docker.sock:z -v /var/run/docker.sock:/var/run/alternative.sock:z -v /tmp:/tmp --name portainer portainer/base /app/portainer',
':/data -v /var/run/docker.sock:/var/run/docker.sock:z -v /tmp:/tmp --name portainer portainer/base /app/portainer',
].join(';');
}

View File

@@ -80,7 +80,6 @@
"chardet": "^1.3.0",
"chart.js": "~2.6.0",
"codemirror": "~5.30.0",
"core-js": "2",
"fast-json-patch": "^3.0.0-1",
"filesize": "~3.3.0",
"filesize-parser": "^1.5.0",

View File

@@ -2948,11 +2948,6 @@ core-js-pure@^3.0.0:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813"
integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==
core-js@2:
version "2.6.12"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
core-js@^2.6.5:
version "2.6.11"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"