Compare commits
30 Commits
fix/GH/450
...
revert-441
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0a8499f0f | ||
|
|
f8b226a1ef | ||
|
|
342a0d6d22 | ||
|
|
58bf76a58f | ||
|
|
bd98b8956a | ||
|
|
4bc958f865 | ||
|
|
b67c0e870c | ||
|
|
067257df2b | ||
|
|
5f2f7a87ab | ||
|
|
f656ad7124 | ||
|
|
f681e2d532 | ||
|
|
fdb9bf09de | ||
|
|
92ad3e788d | ||
|
|
bc2f5a3260 | ||
|
|
487123491e | ||
|
|
380f106571 | ||
|
|
341378e783 | ||
|
|
b360936454 | ||
|
|
8204d32538 | ||
|
|
60c5ab3eec | ||
|
|
20cf948e53 | ||
|
|
45fcb1ad26 | ||
|
|
7398d54ed0 | ||
|
|
faded67deb | ||
|
|
eadd8b36d6 | ||
|
|
1ad4623b08 | ||
|
|
890bbf4058 | ||
|
|
865c8d899b | ||
|
|
aa5277de2e | ||
|
|
9136ba30eb |
@@ -44,7 +44,7 @@ For community support: You can find more information about Portainer's community
|
||||
## Reporting bugs and contributing
|
||||
|
||||
- Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new).
|
||||
- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://www.portainer.io/documentation/how-to-contribute/) to build it locally and make a pull request. We need all the help we can get!
|
||||
- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://documentation.portainer.io/contributing/instructions/) to build it locally and make a pull request. We need all the help we can get!
|
||||
|
||||
## Security
|
||||
|
||||
|
||||
@@ -559,16 +559,18 @@ func (transport *Transport) executeGenericResourceDeletionOperation(request *htt
|
||||
return response, err
|
||||
}
|
||||
|
||||
resourceControl, err := transport.dataStore.ResourceControl().ResourceControlByResourceIDAndType(resourceIdentifierAttribute, resourceType)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
if resourceControl != nil {
|
||||
err = transport.dataStore.ResourceControl().DeleteResourceControl(resourceControl.ID)
|
||||
if response.StatusCode == http.StatusNoContent || response.StatusCode == http.StatusOK {
|
||||
resourceControl, err := transport.dataStore.ResourceControl().ResourceControlByResourceIDAndType(resourceIdentifierAttribute, resourceType)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
if resourceControl != nil {
|
||||
err = transport.dataStore.ResourceControl().DeleteResourceControl(resourceControl.ID)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response, err
|
||||
|
||||
@@ -1042,3 +1042,7 @@ json-tree .branch-preview {
|
||||
background-color: #337ab7;
|
||||
}
|
||||
/* !spinkit override */
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -7,5 +7,6 @@ angular.module('portainer.docker').component('logViewer', {
|
||||
logCollectionChange: '<',
|
||||
sinceTimestamp: '=',
|
||||
lineCount: '=',
|
||||
resourceName: '<',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
Actions
|
||||
</label>
|
||||
<div class="col-sm-11">
|
||||
<button class="btn btn-primary btn-sm" type="button" ng-click="$ctrl.downloadLogs()" style="margin-left: 0;"><i class="fa fa-download"></i> Download logs</button>
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-click="$ctrl.copy()"
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash-es';
|
||||
|
||||
angular.module('portainer.docker').controller('LogViewerController', [
|
||||
'clipboard',
|
||||
function (clipboard) {
|
||||
'Blob',
|
||||
'FileSaver',
|
||||
function (clipboard, Blob, FileSaver) {
|
||||
this.state = {
|
||||
availableSinceDatetime: [
|
||||
{ desc: 'Last day', value: moment().subtract(1, 'days').format() },
|
||||
@@ -43,5 +46,10 @@ angular.module('portainer.docker').controller('LogViewerController', [
|
||||
this.state.selectedLines.splice(idx, 1);
|
||||
}
|
||||
};
|
||||
|
||||
this.downloadLogs = function () {
|
||||
const data = new Blob([_.reduce(this.state.filteredLogs, (acc, log) => acc + '\n' + log, '')]);
|
||||
FileSaver.saveAs(data, this.resourceName + '_logs.txt');
|
||||
};
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl';
|
||||
|
||||
function b64DecodeUnicode(str) {
|
||||
return decodeURIComponent(
|
||||
atob(str)
|
||||
.split('')
|
||||
.map(function (c) {
|
||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
})
|
||||
.join('')
|
||||
);
|
||||
try {
|
||||
return decodeURIComponent(
|
||||
atob(str)
|
||||
.split('')
|
||||
.map(function (c) {
|
||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
})
|
||||
.join('')
|
||||
);
|
||||
} catch (err) {
|
||||
return atob(str);
|
||||
}
|
||||
}
|
||||
|
||||
export function ConfigViewModel(data) {
|
||||
|
||||
@@ -12,4 +12,5 @@
|
||||
display-timestamps="state.displayTimestamps"
|
||||
line-count="state.lineCount"
|
||||
since-timestamp="state.sinceTimestamp"
|
||||
resource-name="container.Name"
|
||||
></log-viewer>
|
||||
|
||||
@@ -520,6 +520,12 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
||||
return true;
|
||||
}
|
||||
|
||||
$scope.volumesAreValid = volumesAreValid;
|
||||
function volumesAreValid() {
|
||||
const volumes = $scope.formValues.Volumes;
|
||||
return volumes.every((volume) => volume.Target && volume.Source);
|
||||
}
|
||||
|
||||
$scope.create = function createService() {
|
||||
var accessControlData = $scope.formValues.AccessControlData;
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="state.actionInProgress || !formValues.RegistryModel.Image"
|
||||
ng-disabled="state.actionInProgress || !formValues.RegistryModel.Image || !volumesAreValid()"
|
||||
ng-click="create()"
|
||||
button-spinner="state.actionInProgress"
|
||||
>
|
||||
@@ -298,13 +298,16 @@
|
||||
<!-- volume-line1 -->
|
||||
<div class="col-sm-12 form-inline">
|
||||
<!-- container-path -->
|
||||
<div class="input-group input-group-sm col-sm-6">
|
||||
<span class="input-group-addon">container</span>
|
||||
<input type="text" class="form-control" ng-model="volume.Target" placeholder="e.g. /path/in/container" />
|
||||
<div class="input-group col-sm-6">
|
||||
<div class="input-group input-group-sm w-full">
|
||||
<span class="input-group-addon">container</span>
|
||||
<input type="text" class="form-control" ng-model="volume.Target" placeholder="e.g. /path/in/container" />
|
||||
</div>
|
||||
<div class="small text-warning" ng-show="!volume.Target"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Target is required. </div>
|
||||
</div>
|
||||
<!-- !container-path -->
|
||||
<!-- volume-type -->
|
||||
<div class="input-group col-sm-5" style="margin-left: 5px;">
|
||||
<div class="input-group col-sm-5" style="margin-left: 5px; vertical-align: top;">
|
||||
<div class="btn-group btn-group-sm" ng-if="allowBindMounts">
|
||||
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'volume'" ng-click="volume.name = ''">Volume</label>
|
||||
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'bind'" ng-click="volume.Id = ''">Bind</label>
|
||||
@@ -318,27 +321,35 @@
|
||||
<!-- !volume-line1 -->
|
||||
<!-- volume-line2 -->
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 5px;">
|
||||
<i class="fa fa-long-arrow-alt-right" aria-hidden="true"></i>
|
||||
<div style="height: 30px; display: inline-block; vertical-align: top; display: inline-flex; align-items: center;">
|
||||
<i class="fa fa-long-arrow-alt-right" aria-hidden="true"></i>
|
||||
</div>
|
||||
<!-- volume -->
|
||||
<div class="input-group input-group-sm col-sm-6" ng-if="volume.Type === 'volume'">
|
||||
<span class="input-group-addon">volume</span>
|
||||
<select
|
||||
class="form-control"
|
||||
ng-model="volume.Source"
|
||||
ng-options="vol as ((vol.Id|truncate:30) + ' - ' + (vol.Driver|truncate:30)) for vol in availableVolumes"
|
||||
>
|
||||
<option selected disabled hidden value="">Select a volume</option>
|
||||
</select>
|
||||
<div class="col-sm-6 input-group" ng-if="volume.Type === 'volume'" style="float: none; padding: 0;">
|
||||
<div class="input-group input-group-sm w-full">
|
||||
<span class="input-group-addon">volume</span>
|
||||
<select
|
||||
class="form-control"
|
||||
ng-model="volume.Source"
|
||||
ng-options="vol as ((vol.Id|truncate:30) + ' - ' + (vol.Driver|truncate:30)) for vol in availableVolumes"
|
||||
>
|
||||
<option selected disabled hidden value="">Select a volume</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="small text-warning" ng-show="!volume.Source"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Source is required. </div>
|
||||
</div>
|
||||
<!-- !volume -->
|
||||
<!-- bind -->
|
||||
<div class="input-group input-group-sm col-sm-6" ng-if="volume.Type === 'bind'">
|
||||
<span class="input-group-addon">host</span>
|
||||
<input type="text" class="form-control" ng-model="volume.Source" placeholder="e.g. /path/on/host" />
|
||||
<div class="input-group input-group-sm w-full">
|
||||
<span class="input-group-addon">host</span>
|
||||
<input type="text" class="form-control" ng-model="volume.Source" placeholder="e.g. /path/on/host" />
|
||||
</div>
|
||||
<div class="small text-warning" ng-show="!volume.Source"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Source is required. </div>
|
||||
</div>
|
||||
<!-- !bind -->
|
||||
<!-- read-only -->
|
||||
<div class="input-group input-group-sm col-sm-5" style="margin-left: 5px;">
|
||||
<div class="input-group input-group-sm col-sm-5" style="margin-left: 5px; vertical-align: top;">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<label class="btn btn-primary" ng-model="volume.ReadOnly" uib-btn-radio="false">Writable</label>
|
||||
<label class="btn btn-primary" ng-model="volume.ReadOnly" uib-btn-radio="true">Read-only</label>
|
||||
|
||||
@@ -42,12 +42,14 @@
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name=""
|
||||
ng-model="mount.Source"
|
||||
placeholder="e.g. /tmp/portainer/data"
|
||||
ng-change="updateMount(service, mount)"
|
||||
ng-disabled="isUpdating || (!isAdmin && !allowBindMounts && mount.Type === 'bind')"
|
||||
ng-if="mount.Type === 'bind'"
|
||||
/>
|
||||
<div class="col-sm-12 small text-warning" ng-show="!mount.Source"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Source is required. </div>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
@@ -59,6 +61,7 @@
|
||||
ng-disabled="isUpdating"
|
||||
disable-authorization="DockerServiceUpdate"
|
||||
/>
|
||||
<div class="col-sm-12 small text-warning" ng-show="!mount.Target"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Target is required. </div>
|
||||
</td>
|
||||
<td authorization="DockerServiceUpdate">
|
||||
<input type="checkbox" class="form-control" ng-model="mount.ReadOnly" ng-change="updateMount(service, mount)" ng-disabled="isUpdating" />
|
||||
@@ -77,7 +80,9 @@
|
||||
<rd-widget-footer authorization="DockerServiceUpdate">
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['ServiceMounts'])" ng-click="updateService(service)">Apply changes</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!mountsAreValid() || !hasChanges(service, ['ServiceMounts'])" ng-click="updateService(service)">
|
||||
Apply changes
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
|
||||
@@ -378,6 +378,12 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||
return hasChanges;
|
||||
};
|
||||
|
||||
$scope.mountsAreValid = mountsAreValid;
|
||||
function mountsAreValid() {
|
||||
const mounts = $scope.service.ServiceMounts;
|
||||
return mounts.every((mount) => mount.Source && mount.Target);
|
||||
}
|
||||
|
||||
function buildChanges(service) {
|
||||
var config = ServiceHelper.serviceToConfig(service.Model);
|
||||
config.Name = service.Name;
|
||||
|
||||
@@ -12,4 +12,5 @@
|
||||
display-timestamps="state.displayTimestamps"
|
||||
line-count="state.lineCount"
|
||||
since-timestamp="state.sinceTimestamp"
|
||||
resource-name="service.Name"
|
||||
></log-viewer>
|
||||
|
||||
@@ -13,4 +13,5 @@
|
||||
display-timestamps="state.displayTimestamps"
|
||||
line-count="state.lineCount"
|
||||
since-timestamp="state.sinceTimestamp"
|
||||
resource-name="task.Id"
|
||||
></log-viewer>
|
||||
|
||||
@@ -51,14 +51,18 @@ angular.module('portainer.docker').controller('VolumeController', [
|
||||
};
|
||||
|
||||
$scope.removeVolume = function removeVolume() {
|
||||
VolumeService.remove($scope.volume)
|
||||
.then(function success() {
|
||||
Notifications.success('Volume successfully removed', $transition$.params().id);
|
||||
$state.go('docker.volumes', {});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove volume');
|
||||
});
|
||||
ModalService.confirmDeletion('Do you want to remove this volume?', (confirmed) => {
|
||||
if (confirmed) {
|
||||
VolumeService.remove($scope.volume)
|
||||
.then(function success() {
|
||||
Notifications.success('Volume successfully removed', $transition$.params().id);
|
||||
$state.go('docker.volumes', {});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove volume');
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function getVolumeDataFromContainer(container, volumeId) {
|
||||
|
||||
@@ -9,26 +9,31 @@ angular.module('portainer.docker').controller('VolumesController', [
|
||||
'HttpRequestHelper',
|
||||
'EndpointProvider',
|
||||
'Authentication',
|
||||
function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, EndpointProvider, Authentication) {
|
||||
'ModalService',
|
||||
function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, EndpointProvider, Authentication, ModalService) {
|
||||
$scope.removeAction = function (selectedItems) {
|
||||
var actionCount = selectedItems.length;
|
||||
angular.forEach(selectedItems, function (volume) {
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(volume.NodeName);
|
||||
VolumeService.remove(volume)
|
||||
.then(function success() {
|
||||
Notifications.success('Volume successfully removed', volume.Id);
|
||||
var index = $scope.volumes.indexOf(volume);
|
||||
$scope.volumes.splice(index, 1);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove volume');
|
||||
})
|
||||
.finally(function final() {
|
||||
--actionCount;
|
||||
if (actionCount === 0) {
|
||||
$state.reload();
|
||||
}
|
||||
ModalService.confirmDeletion('Do you want to remove the selected volume(s)?', (confirmed) => {
|
||||
if (confirmed) {
|
||||
var actionCount = selectedItems.length;
|
||||
angular.forEach(selectedItems, function (volume) {
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(volume.NodeName);
|
||||
VolumeService.remove(volume)
|
||||
.then(function success() {
|
||||
Notifications.success('Volume successfully removed', volume.Id);
|
||||
var index = $scope.volumes.indexOf(volume);
|
||||
$scope.volumes.splice(index, 1);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove volume');
|
||||
})
|
||||
.finally(function final() {
|
||||
--actionCount;
|
||||
if (actionCount === 0) {
|
||||
$state.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> Create entry
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-default" ngf-select="$ctrl.addEntryFromFile($file)" style="margin-left: 0;">
|
||||
<i class="fa fa-file-upload" aria-hidden="true"></i> Create entry from file
|
||||
<i class="fa fa-file-upload" aria-hidden="true"></i> Create key/value from file
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,6 @@
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="kubernetes.cluster({endpointId: $ctrl.endpointId})" ui-sref-active="active">Cluster <span class="menu-icon fa fa-server fa-fw"></span></a>
|
||||
<div class="sidebar-sublist" ng-if="$ctrl.adminAccess && ($ctrl.currentState === 'kubernetes.cluster' || $ctrl.currentState === 'portainer.endpoints.endpoint.kubernetesConfig')">
|
||||
<a ui-sref="portainer.endpoints.endpoint.kubernetesConfig({id: $ctrl.endpointId})">Setup</a>
|
||||
<a ui-sref="portainer.endpoints.endpoint.kubernetesConfig({id: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -27,6 +27,7 @@ import KubernetesServiceConverter from 'Kubernetes/converters/service';
|
||||
import KubernetesPersistentVolumeClaimConverter from 'Kubernetes/converters/persistentVolumeClaim';
|
||||
import PortainerError from 'Portainer/error';
|
||||
import { KubernetesIngressHelper } from 'Kubernetes/ingress/helper';
|
||||
import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
|
||||
|
||||
function _apiPortsToPublishedPorts(pList, pRefs) {
|
||||
const ports = _.map(pList, (item) => {
|
||||
@@ -118,7 +119,7 @@ class KubernetesApplicationConverter {
|
||||
res.PublishedPorts = ports;
|
||||
}
|
||||
|
||||
if (data.spec.templates) {
|
||||
if (data.spec.template) {
|
||||
res.Volumes = data.spec.template.spec.volumes ? data.spec.template.spec.volumes : [];
|
||||
} else {
|
||||
res.Volumes = data.spec.volumes;
|
||||
@@ -294,6 +295,8 @@ class KubernetesApplicationConverter {
|
||||
}
|
||||
|
||||
static applicationFormValuesToApplication(formValues) {
|
||||
formValues.ApplicationOwner = KubernetesCommonHelper.ownerToLabel(formValues.ApplicationOwner);
|
||||
|
||||
const claims = KubernetesPersistentVolumeClaimConverter.applicationFormValuesToVolumeClaims(formValues);
|
||||
const rwx = _.find(claims, (item) => _.includes(item.StorageClass.AccessModes, 'RWX')) !== undefined;
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ class KubernetesSecretConverter {
|
||||
const res = new KubernetesSecretUpdatePayload();
|
||||
res.metadata.name = secret.Name;
|
||||
res.metadata.namespace = secret.Namespace;
|
||||
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = secret.ConfigurationOwner;
|
||||
res.stringData = secret.Data;
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -8,5 +8,9 @@ class KubernetesCommonHelper {
|
||||
_.set(obj, path, value);
|
||||
}
|
||||
}
|
||||
|
||||
static ownerToLabel(owner) {
|
||||
return _.replace(owner, /[^-A-Za-z0-9_.]/g, '.');
|
||||
}
|
||||
}
|
||||
export default KubernetesCommonHelper;
|
||||
|
||||
@@ -71,6 +71,8 @@ class KubernetesApplicationService {
|
||||
apiService = this.KubernetesDaemonSetService;
|
||||
} else if (app instanceof KubernetesStatefulSet || (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.STATEFULSET)) {
|
||||
apiService = this.KubernetesStatefulSetService;
|
||||
} else if (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.POD) {
|
||||
apiService = this.KubernetesPodService;
|
||||
} else {
|
||||
throw new PortainerError('Unable to determine which association to use to retrieve API Service');
|
||||
}
|
||||
|
||||
@@ -24,8 +24,11 @@ class KubernetesConfigMapService {
|
||||
try {
|
||||
const params = new KubernetesCommonParams();
|
||||
params.id = name;
|
||||
const [raw, yaml] = await Promise.all([this.KubernetesConfigMaps(namespace).get(params).$promise, this.KubernetesConfigMaps(namespace).getYaml(params).$promise]);
|
||||
const configMap = KubernetesConfigMapConverter.apiToConfigMap(raw, yaml);
|
||||
const [rawPromise, yamlPromise] = await Promise.allSettled([
|
||||
this.KubernetesConfigMaps(namespace).get(params).$promise,
|
||||
this.KubernetesConfigMaps(namespace).getYaml(params).$promise,
|
||||
]);
|
||||
const configMap = KubernetesConfigMapConverter.apiToConfigMap(rawPromise.value, yamlPromise.value);
|
||||
return configMap;
|
||||
} catch (err) {
|
||||
if (err.status === 404) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import KubernetesConfigurationConverter from 'Kubernetes/converters/configuratio
|
||||
import KubernetesConfigMapConverter from 'Kubernetes/converters/configMap';
|
||||
import KubernetesSecretConverter from 'Kubernetes/converters/secret';
|
||||
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
|
||||
import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
|
||||
|
||||
class KubernetesConfigurationService {
|
||||
/* @ngInject */
|
||||
@@ -67,6 +68,8 @@ class KubernetesConfigurationService {
|
||||
* CREATE
|
||||
*/
|
||||
async createAsync(formValues) {
|
||||
formValues.ConfigurationOwner = KubernetesCommonHelper.ownerToLabel(formValues.ConfigurationOwner);
|
||||
|
||||
try {
|
||||
if (formValues.Type === KubernetesConfigurationTypes.CONFIGMAP) {
|
||||
const configMap = KubernetesConfigMapConverter.configurationFormValuesToConfigMap(formValues);
|
||||
|
||||
@@ -7,6 +7,7 @@ import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelpe
|
||||
import { KubernetesNamespace } from 'Kubernetes/models/namespace/models';
|
||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||
import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
|
||||
import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
|
||||
|
||||
class KubernetesResourcePoolService {
|
||||
/* @ngInject */
|
||||
@@ -73,6 +74,8 @@ class KubernetesResourcePoolService {
|
||||
* @param {KubernetesResourcePoolFormValues} formValues
|
||||
*/
|
||||
async createAsync(formValues) {
|
||||
formValues.Owner = KubernetesCommonHelper.ownerToLabel(formValues.Owner);
|
||||
|
||||
try {
|
||||
const namespace = new KubernetesNamespace();
|
||||
namespace.Name = formValues.Name;
|
||||
|
||||
@@ -79,7 +79,11 @@ class KubernetesApplicationsController {
|
||||
}
|
||||
|
||||
removeAction(selectedItems) {
|
||||
return this.$async(this.removeActionAsync, selectedItems);
|
||||
this.ModalService.confirmDeletion('Do you want to remove the selected application(s)?', (confirmed) => {
|
||||
if (confirmed) {
|
||||
return this.$async(this.removeActionAsync, selectedItems);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onPublishingModeClick(application) {
|
||||
@@ -87,7 +91,7 @@ class KubernetesApplicationsController {
|
||||
_.forEach(this.ports, (item) => {
|
||||
item.Expanded = false;
|
||||
item.Highlighted = false;
|
||||
if (item.Name === application.Name) {
|
||||
if (item.Name === application.Name && item.Ports.length > 1) {
|
||||
item.Expanded = true;
|
||||
item.Highlighted = true;
|
||||
}
|
||||
|
||||
@@ -778,6 +778,7 @@
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="replica_count"
|
||||
class="form-control"
|
||||
min="1"
|
||||
max="9999"
|
||||
@@ -785,10 +786,19 @@
|
||||
style="margin-left: 20px;"
|
||||
ng-model="ctrl.formValues.ReplicaCount"
|
||||
ng-disabled="!ctrl.supportScalableReplicaDeployment()"
|
||||
ng-change="ctrl.enforceReplicaCountMinimum()"
|
||||
ng-change="ctrl.onChangeVolumeRequestedSize()"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="kubernetesApplicationCreationForm['replica_count'].$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<ng-messages for="kubernetesApplicationCreationForm['replica_count'].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Instance count is required.</p>
|
||||
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Instance count must be greater than 0.</p>
|
||||
</ng-messages>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !replica count -->
|
||||
|
||||
<div
|
||||
|
||||
@@ -488,12 +488,6 @@ class KubernetesCreateApplicationController {
|
||||
return _.uniq(storageOptions).join(', ');
|
||||
}
|
||||
|
||||
enforceReplicaCountMinimum() {
|
||||
if (this.formValues.ReplicaCount === null) {
|
||||
this.formValues.ReplicaCount = 1;
|
||||
}
|
||||
}
|
||||
|
||||
resourceQuotaCapacityExceeded() {
|
||||
return !this.state.sliders.memory.max || !this.state.sliders.cpu.max;
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ class KubernetesApplicationController {
|
||||
|
||||
showEditor() {
|
||||
this.state.showEditorTab = true;
|
||||
this.selectTab(2);
|
||||
this.selectTab(3);
|
||||
}
|
||||
|
||||
isSystemNamespace() {
|
||||
|
||||
@@ -339,8 +339,8 @@ class KubernetesNodeController {
|
||||
|
||||
this.availableEffects = _.values(KubernetesNodeTaintEffects);
|
||||
this.formValues = KubernetesNodeConverter.nodeToFormValues(this.node);
|
||||
this.formValues.Labels = KubernetesNodeHelper.reorderLabels(this.formValues.Labels);
|
||||
this.formValues.Labels = KubernetesNodeHelper.computeUsedLabels(this.applications, this.formValues.Labels);
|
||||
this.formValues.Labels = KubernetesNodeHelper.reorderLabels(this.formValues.Labels);
|
||||
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@ import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelpe
|
||||
|
||||
class KubernetesConfigurationsController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, Notifications, KubernetesConfigurationService, KubernetesApplicationService) {
|
||||
constructor($async, $state, Notifications, KubernetesConfigurationService, KubernetesApplicationService, ModalService) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.Notifications = Notifications;
|
||||
this.KubernetesConfigurationService = KubernetesConfigurationService;
|
||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||
this.ModalService = ModalService;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.getConfigurations = this.getConfigurations.bind(this);
|
||||
@@ -56,7 +57,11 @@ class KubernetesConfigurationsController {
|
||||
}
|
||||
|
||||
removeAction(selectedItems) {
|
||||
return this.$async(this.removeActionAsync, selectedItems);
|
||||
this.ModalService.confirmDeletion('Do you want to remove the selected configuration(s)?', (confirmed) => {
|
||||
if (confirmed) {
|
||||
return this.$async(this.removeActionAsync, selectedItems);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getApplicationsAsync() {
|
||||
|
||||
@@ -25,6 +25,10 @@ class KubernetesCreateConfigurationController {
|
||||
this.state.alreadyExist = _.find(filteredConfigurations, (config) => config.Name === this.formValues.Name) !== undefined;
|
||||
}
|
||||
|
||||
onResourcePoolSelectionChange() {
|
||||
this.onChangeName();
|
||||
}
|
||||
|
||||
isFormValid() {
|
||||
const uniqueCheck = !this.state.alreadyExist && this.state.isDataValid;
|
||||
if (this.formValues.IsSimple) {
|
||||
|
||||
@@ -132,7 +132,53 @@
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Metrics
|
||||
Security
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
By default, all the users have access to the default namespace. Enable this option to set accesses on the default namespace.
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Restrict access to the default namespace
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" disabled /><i></i> </label>
|
||||
<span class="text-muted small" style="margin-left: 15px;">
|
||||
<i class="fa fa-user" aria-hidden="true"></i>
|
||||
This feature is available in <a href="https://www.portainer.io/business-upsell?from=k8s-setup-default" target="_blank"> Portainer Business Edition</a>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Resources and Metrics
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
By ENABLING resource over-commit, you are able to assign more resources to namespaces than is physically available in the cluster. This may lead to unexpected
|
||||
deployment failures if there is insufficient resource to service demand.
|
||||
<p style="margin-top: 2px;">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
By DISABLING resource over-commit (highly recommended), you are only able to assign resources to namespaces that are less (in aggregate) than the cluster total
|
||||
minus any system resource reservation.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Allow resource over-commit
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" checked disabled /><i></i> </label>
|
||||
<span class="text-muted small" style="margin-left: 15px;">
|
||||
<i class="fa fa-user" aria-hidden="true"></i>
|
||||
This feature is available in <a href="https://www.portainer.io/business-upsell?from=k8s-setup-overcommit" target="_blank"> Portainer Business Edition</a>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
|
||||
@@ -69,8 +69,14 @@
|
||||
</p>
|
||||
</span>
|
||||
<span class="col-sm-12 text-muted small" ng-show="ctrl.state.DeployType === ctrl.ManifestDeployTypes.KUBERNETES">
|
||||
You can get more information about Kubernetes file format in the
|
||||
<a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/" target="_blank">official documentation</a>.
|
||||
<p>
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This feature allows you to deploy any kind of Kubernetes resource in this environment (Deployment, Secret, ConfigMap...).
|
||||
</p>
|
||||
<p>
|
||||
You can get more information about Kubernetes file format in the
|
||||
<a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/" target="_blank">official documentation</a>.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
||||
@@ -129,6 +129,63 @@
|
||||
</div>
|
||||
<!-- #endregion -->
|
||||
|
||||
<!-- #region LOAD-BALANCERS -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Load balancers
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
You can set a quota on the amount of external load balancers that can be created inside this resource pool. Set this quota to 0 to effectively disable the use of
|
||||
load balancers in this resource pool.
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Load Balancer quota
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" disabled /><i></i> </label>
|
||||
<span class="text-muted small" style="margin-left: 15px;">
|
||||
<i class="fa fa-user" aria-hidden="true"></i>
|
||||
This feature is available in <a href="https://www.portainer.io/business-upsell?from=k8s-resourcepool-lbquota" target="_blank"> Portainer Business Edition</a>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- #endregion -->
|
||||
|
||||
<!-- #region STORAGES -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Storages
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Quotas can be set on each storage option to prevent users from exceeding a specific threshold when deploying applications. You can set a quota to 0 to effectively
|
||||
prevent the usage of a specific storage option inside this resource pool.
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-12 form-section-title">
|
||||
<i class="fa fa-route" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
standard
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Enable quota
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" disabled /><i></i> </label>
|
||||
<span class="text-muted small" style="margin-left: 15px;">
|
||||
<i class="fa fa-user" aria-hidden="true"></i>
|
||||
This feature is available in
|
||||
<a href="https://www.portainer.io/business-upsell?from=k8s-resourcepool-storagequota" target="_blank"> Portainer Business Edition</a>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- #endregion -->
|
||||
|
||||
<div ng-if="ctrl.state.canUseIngress">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Ingresses
|
||||
|
||||
@@ -250,6 +250,63 @@
|
||||
</div>
|
||||
<!-- #endregion -->
|
||||
</div>
|
||||
<!-- #region LOAD-BALANCERS -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Load balancers
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
You can set a quota on the amount of external load balancers that can be created inside this resource pool. Set this quota to 0 to effectively disable the use
|
||||
of load balancers in this resource pool.
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Load Balancer quota
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" disabled /><i></i> </label>
|
||||
<span class="text-muted small" style="margin-left: 15px;">
|
||||
<i class="fa fa-user" aria-hidden="true"></i>
|
||||
This feature is available in <a href="https://www.portainer.io/business-upsell?from=k8s-resourcepool-lbquota" target="_blank"> Portainer Business Edition</a>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- #endregion -->
|
||||
|
||||
<!-- #region LOAD-BALANCERS -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Storages
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Quotas can be set on each storage option to prevent users from exceeding a specific threshold when deploying applications. You can set a quota to 0 to
|
||||
effectively prevent the usage of a specific storage option inside this resource pool.
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-12 form-section-title">
|
||||
<i class="fa fa-route" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
standard
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Enable quota
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" disabled /><i></i> </label>
|
||||
<span class="text-muted small" style="margin-left: 15px;">
|
||||
<i class="fa fa-user" aria-hidden="true"></i>
|
||||
This feature is available in
|
||||
<a href="https://www.portainer.io/business-upsell?from=k8s-resourcepool-storagequota" target="_blank"> Portainer Business Edition</a>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- #endregion -->
|
||||
|
||||
<!-- actions -->
|
||||
<div ng-if="ctrl.isAdmin && ctrl.isEditable" class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
|
||||
@@ -392,6 +392,16 @@ angular.module('portainer.app', ['portainer.oauth']).config([
|
||||
},
|
||||
};
|
||||
|
||||
var roles = {
|
||||
name: 'portainer.roles',
|
||||
url: '/roles',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/roles/roles.html',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
$stateRegistryProvider.register(root);
|
||||
$stateRegistryProvider.register(endpointRoot);
|
||||
$stateRegistryProvider.register(portainer);
|
||||
@@ -422,6 +432,7 @@ angular.module('portainer.app', ['portainer.oauth']).config([
|
||||
$stateRegistryProvider.register(user);
|
||||
$stateRegistryProvider.register(teams);
|
||||
$stateRegistryProvider.register(team);
|
||||
$stateRegistryProvider.register(roles);
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@@ -28,6 +28,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Role
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-4">
|
||||
<span class="text-muted small">
|
||||
<i class="fa fa-user" aria-hidden="true"></i>
|
||||
This feature is available in <a href="https://www.portainer.io/business-upsell?from=k8s-rbac-access" target="_blank"> Portainer Business Edition</a>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- actions -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<div class="datatable">
|
||||
<div class="searchBar">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
<input type="text" class="searchInput" placeholder="Search..." ng-model-options="{ debounce: 300 }" />
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Endpoint
|
||||
</th>
|
||||
<th>
|
||||
Role
|
||||
</th>
|
||||
<th>Access origin</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="3" class="text-center text-muted">Select a user to show associated access and role</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
angular.module('portainer.app').component('accessViewerDatatable', {
|
||||
templateUrl: './accessViewerDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
dataset: '<',
|
||||
},
|
||||
});
|
||||
@@ -70,7 +70,7 @@ angular.module('portainer.app').controller('GenericDatatableController', [
|
||||
item.Checked = !item.Checked;
|
||||
this.state.firstClickedItem = item;
|
||||
}
|
||||
this.state.selectedItems = this.dataset.filter((i) => i.Checked);
|
||||
this.state.selectedItems = _.uniq(_.concat(this.state.selectedItems, this.state.filteredDataSet)).filter((i) => i.Checked);
|
||||
if (event && this.state.selectAll && this.state.selectedItems.length !== this.state.filteredDataSet.length) {
|
||||
this.state.selectAll = false;
|
||||
}
|
||||
|
||||
@@ -66,6 +66,9 @@
|
||||
</td>
|
||||
<td>
|
||||
<a ui-sref="portainer.registries.registry.access({id: item.Id})" ng-if="$ctrl.accessManagement"> <i class="fa fa-users" aria-hidden="true"></i> Manage access </a>
|
||||
<span class="text-muted space-left" style="cursor: pointer;" data-toggle="tooltip" title="This feature is available in Portainer Business Edition">
|
||||
<i class="fa fa-search" aria-hidden="true"></i> Browse</span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<div class="datatable">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
|
||||
</div>
|
||||
<div class="searchBar">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
<input type="text" class="searchInput" placeholder="Search..." auto-focus ng-model-options="{ debounce: 300 }" />
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Name
|
||||
</th>
|
||||
<th>
|
||||
Description
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-muted">Endpoint administrator</td>
|
||||
<td class="text-muted">Full control of all resources in an endpoint</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">Helpdesk</td>
|
||||
<td class="text-muted">Read-only access of all resources in an endpoint</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">Read-only user</td>
|
||||
<td class="text-muted">Read-only access of assigned resources in an endpoint</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">Standard user</td>
|
||||
<td class="text-muted">Full control of assigned resources in an endpoint</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="footer">
|
||||
<div class="paginationControls">
|
||||
<form class="form-inline">
|
||||
<span class="limitSelector">
|
||||
<span style="margin-right: 5px;">
|
||||
Items per page
|
||||
</span>
|
||||
<select class="form-control">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</span>
|
||||
<dir-pagination-controls max-size="5"></dir-pagination-controls>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
angular.module('portainer.app').component('rolesDatatable', {
|
||||
templateUrl: './rolesDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
},
|
||||
});
|
||||
@@ -56,10 +56,10 @@ angular.module('portainer.app').factory('EndpointService', [
|
||||
return Endpoints.remove({ id: endpointID }).$promise;
|
||||
};
|
||||
|
||||
service.createLocalEndpoint = function () {
|
||||
service.createLocalEndpoint = function (name = 'local') {
|
||||
var deferred = $q.defer();
|
||||
|
||||
FileUploadService.createEndpoint('local', PortainerEndpointCreationTypes.LocalDockerEnvironment, '', '', 1, [], false)
|
||||
FileUploadService.createEndpoint(name, PortainerEndpointCreationTypes.LocalDockerEnvironment, '', '', 1, [], false)
|
||||
.then(function success(response) {
|
||||
deferred.resolve(response.data);
|
||||
})
|
||||
@@ -117,10 +117,10 @@ angular.module('portainer.app').factory('EndpointService', [
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.createLocalKubernetesEndpoint = function () {
|
||||
service.createLocalKubernetesEndpoint = function (name = 'local') {
|
||||
var deferred = $q.defer();
|
||||
|
||||
FileUploadService.createEndpoint('local', PortainerEndpointCreationTypes.LocalKubernetesEnvironment, '', '', 1, [], true, true, true)
|
||||
FileUploadService.createEndpoint(name, PortainerEndpointCreationTypes.LocalKubernetesEnvironment, '', '', 1, [], true, true, true)
|
||||
.then(function success(response) {
|
||||
deferred.resolve(response.data);
|
||||
})
|
||||
|
||||
@@ -19,6 +19,7 @@ angular
|
||||
) {
|
||||
$scope.state = {
|
||||
EnvironmentType: 'agent',
|
||||
PlatformType: 'linux',
|
||||
actionInProgress: false,
|
||||
deploymentTab: 0,
|
||||
allowCreateTag: Authentication.isAdmin(),
|
||||
@@ -56,8 +57,12 @@ angular
|
||||
};
|
||||
|
||||
$scope.copyAgentCommand = function () {
|
||||
if ($scope.state.deploymentTab === 2) {
|
||||
if ($scope.state.deploymentTab === 2 && $scope.state.PlatformType === 'linux') {
|
||||
clipboard.copyText('curl -L https://downloads.portainer.io/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent');
|
||||
} else if ($scope.state.deploymentTab === 2 && $scope.state.PlatformType === 'windows') {
|
||||
clipboard.copyText(
|
||||
'curl -L https://downloads.portainer.io/agent-stack-windows.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack-windows.yml portainer-agent'
|
||||
);
|
||||
} else if ($scope.state.deploymentTab === 1) {
|
||||
clipboard.copyText('curl -L https://downloads.portainer.io/portainer-agent-k8s-nodeport.yaml -o portainer-agent-k8s.yaml; kubectl apply -f portainer-agent-k8s.yaml');
|
||||
} else {
|
||||
@@ -84,40 +89,72 @@ angular
|
||||
$scope.availableTags = $scope.availableTags.concat(tag);
|
||||
$scope.formValues.TagIds = $scope.formValues.TagIds.concat(tag.Id);
|
||||
} catch (err) {
|
||||
Notifications.error('Failue', err, 'Unable to create tag');
|
||||
Notifications.error('Failure', err, 'Unable to create tag');
|
||||
}
|
||||
}
|
||||
|
||||
$scope.addDockerEndpoint = function () {
|
||||
if ($scope.formValues.ConnectSocket) {
|
||||
var endpointName = $scope.formValues.Name;
|
||||
$scope.state.actionInProgress = true;
|
||||
EndpointService.createLocalEndpoint(endpointName)
|
||||
.then(function success() {
|
||||
Notifications.success('Endpoint created', endpointName);
|
||||
$state.go('portainer.endpoints', {}, { reload: true });
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to create endpoint');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.actionInProgress = false;
|
||||
});
|
||||
} else {
|
||||
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;
|
||||
var TLSMode = securityData.TLSMode;
|
||||
var TLSSkipVerify = TLS && (TLSMode === 'tls_client_noca' || TLSMode === 'tls_only');
|
||||
var TLSSkipClientVerify = TLS && (TLSMode === 'tls_ca' || TLSMode === 'tls_only');
|
||||
var TLSCAFile = TLSSkipVerify ? null : securityData.TLSCACert;
|
||||
var TLSCertFile = TLSSkipClientVerify ? null : securityData.TLSCert;
|
||||
var TLSKeyFile = TLSSkipClientVerify ? null : securityData.TLSKey;
|
||||
|
||||
addEndpoint(
|
||||
name,
|
||||
PortainerEndpointCreationTypes.LocalDockerEnvironment,
|
||||
URL,
|
||||
publicURL,
|
||||
groupId,
|
||||
tagIds,
|
||||
TLS,
|
||||
TLSSkipVerify,
|
||||
TLSSkipClientVerify,
|
||||
TLSCAFile,
|
||||
TLSCertFile,
|
||||
TLSKeyFile
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.addKubernetesEndpoint = function () {
|
||||
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;
|
||||
var TLSMode = securityData.TLSMode;
|
||||
var TLSSkipVerify = TLS && (TLSMode === 'tls_client_noca' || TLSMode === 'tls_only');
|
||||
var TLSSkipClientVerify = TLS && (TLSMode === 'tls_ca' || TLSMode === 'tls_only');
|
||||
var TLSCAFile = TLSSkipVerify ? null : securityData.TLSCACert;
|
||||
var TLSCertFile = TLSSkipClientVerify ? null : securityData.TLSCert;
|
||||
var TLSKeyFile = TLSSkipClientVerify ? null : securityData.TLSKey;
|
||||
|
||||
addEndpoint(
|
||||
name,
|
||||
PortainerEndpointCreationTypes.LocalDockerEnvironment,
|
||||
URL,
|
||||
publicURL,
|
||||
groupId,
|
||||
tagIds,
|
||||
TLS,
|
||||
TLSSkipVerify,
|
||||
TLSSkipClientVerify,
|
||||
TLSCAFile,
|
||||
TLSCertFile,
|
||||
TLSKeyFile
|
||||
);
|
||||
$scope.state.actionInProgress = true;
|
||||
EndpointService.createLocalKubernetesEndpoint(name)
|
||||
.then(function success(result) {
|
||||
Notifications.success('Endpoint created', name);
|
||||
$state.go('portainer.endpoints.endpoint.kubernetesConfig', { id: result.Id });
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to create endpoint');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.actionInProgress = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.addAgentEndpoint = function () {
|
||||
|
||||
@@ -44,6 +44,16 @@
|
||||
<p>Directly connect to the Docker API</p>
|
||||
</label>
|
||||
</div>
|
||||
<div ng-click="resetEndpointURL()">
|
||||
<input type="radio" id="kubernetes_endpoint" ng-model="state.EnvironmentType" value="kubernetes" />
|
||||
<label for="kubernetes_endpoint">
|
||||
<div class="boxselector_header">
|
||||
<i class="fas fa-dharmachakra" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Kubernetes
|
||||
</div>
|
||||
<p>Local Kubernetes environment</p>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="azure_endpoint" ng-model="state.EnvironmentType" value="azure" />
|
||||
<label for="azure_endpoint">
|
||||
@@ -61,10 +71,18 @@
|
||||
Important notice
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
The Docker API must be exposed over TCP. You can find more information about how to expose the Docker API over TCP
|
||||
<a href="https://docs.docker.com/engine/security/https/" target="_blank">in the Docker documentation</a>.
|
||||
</span>
|
||||
<p class="col-sm-12 text-muted small">
|
||||
You can connect Portainer to a Docker environment via socket or via TCP. You can find more information about how to expose the Docker API over TCP
|
||||
<a href="https://docs.docker.com/engine/security/https/"> in the Docker documentation</a>.
|
||||
</p>
|
||||
|
||||
<p class="col-sm-12 text-muted small">
|
||||
When using the socket, ensure that you have started the Portainer container with the following Docker flag
|
||||
<code> -v "/var/run/docker.sock:/var/run/docker.sock" </code>
|
||||
(on Linux) or
|
||||
<code> -v \.\pipe\docker_engine:\.\pipe\docker_engine </code>
|
||||
(on Windows).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="state.EnvironmentType === 'agent'">
|
||||
@@ -74,24 +92,33 @@
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
Ensure that you have deployed the Portainer agent in your cluster first. Refer to the platform related command below to deploy it.
|
||||
<div class="input-group input-group-sm" style="margin-top: 10px; margin-bottom: 10px;">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<label class="btn btn-primary" ng-model="state.PlatformType" uib-btn-radio="'linux'"><i class="fab fa-linux" style="margin-right: 2px;"></i> Linux</label>
|
||||
<label class="btn btn-primary" ng-model="state.PlatformType" uib-btn-radio="'windows'"><i class="fab fa-windows" style="margin-right: 2px;"></i> Windows</label>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 10px;">
|
||||
<uib-tabset active="state.deploymentTab">
|
||||
<uib-tab index="0" heading="Kubernetes via load balancer">
|
||||
<uib-tab index="0" ng-if="state.PlatformType === 'linux'" heading="Kubernetes via load balancer">
|
||||
<code style="display: block; white-space: pre-wrap; padding: 16px 90px;"
|
||||
>curl -L https://downloads.portainer.io/portainer-agent-k8s-lb.yaml -o portainer-agent-k8s.yaml; kubectl apply -f portainer-agent-k8s.yaml</code
|
||||
>
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab index="1" heading="Kubernetes via node port">
|
||||
<uib-tab index="1" ng-if="state.PlatformType === 'linux'" heading="Kubernetes via node port">
|
||||
<code style="display: block; white-space: pre-wrap; padding: 16px 90px;"
|
||||
>curl -L https://downloads.portainer.io/portainer-agent-k8s-nodeport.yaml -o portainer-agent-k8s.yaml; kubectl apply -f portainer-agent-k8s.yaml</code
|
||||
>
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab index="2" heading="Docker Swarm">
|
||||
<code style="display: block; white-space: pre-wrap; padding: 16px 90px;"
|
||||
<code ng-if="state.PlatformType === 'linux'" style="display: block; white-space: pre-wrap; padding: 16px 90px;"
|
||||
>curl -L https://downloads.portainer.io/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent</code
|
||||
>
|
||||
<code ng-if="state.PlatformType === 'windows'" style="display: block; white-space: pre-wrap; padding: 16px 90px;"
|
||||
>curl -L https://downloads.portainer.io/agent-stack-windows.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack-windows.yml portainer-agent</code
|
||||
>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
<div style="margin-top: 10px;">
|
||||
@@ -118,6 +145,21 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="state.EnvironmentType === 'kubernetes'">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Important notice
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<p class="col-sm-12 text-muted small">
|
||||
This will allow you to manage the Kubernetes environment where Portainer is running.
|
||||
</p>
|
||||
|
||||
<p class="col-sm-12 text-muted small">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
In order to manage a remote Kubernetes environment, please use the Agent or Edge agent options.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="state.EnvironmentType === 'azure'">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Information
|
||||
@@ -167,8 +209,18 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- connect-via-socket-input -->
|
||||
<div ng-if="state.EnvironmentType === 'docker'">
|
||||
<div class="form-group" style="padding-left: 15px;">
|
||||
<label for="connect_socket" class="col-sm_12 control-label text-left">
|
||||
Connect via socket
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="formValues.ConnectSocket" /><i></i> </label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !connect-via-socket-input -->
|
||||
<!-- endpoint-url-input -->
|
||||
<div ng-if="state.EnvironmentType === 'docker' || state.EnvironmentType === 'agent'">
|
||||
<div ng-if="(state.EnvironmentType === 'docker' && !formValues.ConnectSocket) || state.EnvironmentType === 'agent'">
|
||||
<div class="form-group">
|
||||
<label for="endpoint_url" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Endpoint URL
|
||||
@@ -394,6 +446,17 @@
|
||||
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
|
||||
<span ng-show="state.actionInProgress">Creating endpoint...</span>
|
||||
</button>
|
||||
<button
|
||||
ng-if="state.EnvironmentType === 'kubernetes'"
|
||||
type="submit"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="state.actionInProgress || !endpointCreationForm.$valid"
|
||||
ng-click="addKubernetesEndpoint()"
|
||||
button-spinner="state.actionInProgress"
|
||||
>
|
||||
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
|
||||
<span ng-show="state.actionInProgress">Creating endpoint...</span>
|
||||
</button>
|
||||
<button
|
||||
ng-if="state.EnvironmentType === 'azure'"
|
||||
type="submit"
|
||||
|
||||
@@ -33,18 +33,26 @@
|
||||
<p>
|
||||
The agent will communicate with Portainer via <u>{{ edgeKeyDetails.instanceURL }}</u> and <u>tcp://{{ edgeKeyDetails.tunnelServerAddr }}</u>
|
||||
</p>
|
||||
<div class="input-group input-group-sm" style="margin-top: 10px; margin-bottom: 10px;">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<label class="btn btn-primary" ng-model="state.platformType" uib-btn-radio="'linux'"><i class="fab fa-linux" style="margin-right: 2px;"></i> Linux</label>
|
||||
<label class="btn btn-primary" ng-model="state.platformType" uib-btn-radio="'windows'"><i class="fab fa-windows" style="margin-right: 2px;"></i> Windows</label>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 10px;">
|
||||
<uib-tabset active="state.deploymentTab">
|
||||
<uib-tab index="0" heading="Kubernetes">
|
||||
<uib-tab index="0" ng-if="state.platformType === 'linux'" heading="Kubernetes">
|
||||
<code style="display: block; white-space: pre-wrap; padding: 16px 90px;"
|
||||
>curl https://downloads.portainer.io/portainer-edge-agent-setup.sh | sudo bash -s -- {{ randomEdgeID }} {{ endpoint.EdgeKey }}</code
|
||||
>
|
||||
</uib-tab>
|
||||
<uib-tab index="1" heading="Docker Swarm">
|
||||
<code style="display: block; white-space: pre-wrap; padding: 16px 90px;">{{ dockerCommands.swarm }}</code>
|
||||
<code ng-if="state.platformType === 'linux'" style="display: block; white-space: pre-wrap; padding: 16px 90px;">{{ dockerCommands.linuxSwarm }}</code>
|
||||
<code ng-if="state.platformType === 'windows'" style="display: block; white-space: pre-wrap; padding: 16px 90px;">{{ dockerCommands.windowsSwarm }}</code>
|
||||
</uib-tab>
|
||||
<uib-tab index="2" heading="Docker Standalone">
|
||||
<code style="display: block; white-space: pre-wrap; padding: 16px 90px;">{{ dockerCommands.standalone }}</code>
|
||||
<code ng-if="state.platformType === 'linux'" style="display: block; white-space: pre-wrap; padding: 16px 90px;">{{ dockerCommands.linuxStandalone }}</code>
|
||||
<code ng-if="state.platformType === 'windows'" style="display: block; white-space: pre-wrap; padding: 16px 90px;">{{ dockerCommands.windowsStandalone }}</code>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
<div style="margin-top: 10px;">
|
||||
|
||||
@@ -29,6 +29,7 @@ angular
|
||||
kubernetesEndpoint: false,
|
||||
agentEndpoint: false,
|
||||
edgeEndpoint: false,
|
||||
platformType: 'linux',
|
||||
allowCreate: Authentication.isAdmin(),
|
||||
availableEdgeAgentCheckinOptions: [
|
||||
{ key: 'Use default interval', value: 0 },
|
||||
@@ -55,15 +56,23 @@ angular
|
||||
};
|
||||
|
||||
$scope.copyEdgeAgentDeploymentCommand = function () {
|
||||
if ($scope.state.deploymentTab === 2) {
|
||||
if ($scope.state.deploymentTab === 2 && $scope.state.platformType === 'linux') {
|
||||
clipboard.copyText(
|
||||
'docker run -d -v /var/run/docker.sock:/var/run/docker.sock -v /var/lib/docker/volumes:/var/lib/docker/volumes -v /:/host --restart always -e EDGE=1 -e EDGE_ID=' +
|
||||
'docker run -d -v /var/run/docker.sock:/var/run/docker.sock -v /var/lib/docker/volumes:/var/lib/docker/volumes -v /:/host -v portainer_agent_data:/data --restart always -e EDGE=1 -e EDGE_ID=' +
|
||||
$scope.randomEdgeID +
|
||||
' -e EDGE_KEY=' +
|
||||
$scope.endpoint.EdgeKey +
|
||||
' -e CAP_HOST_MANAGEMENT=1 -v portainer_agent_data:/data --name portainer_edge_agent portainer/agent'
|
||||
' -e CAP_HOST_MANAGEMENT=1 --name portainer_edge_agent portainer/agent'
|
||||
);
|
||||
} else if ($scope.state.deploymentTab === 1) {
|
||||
} else if ($scope.state.deploymentTab === 2 && $scope.state.platformType === 'windows') {
|
||||
clipboard.copyText(
|
||||
'docker run -d --mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine --mount type=bind,src=C:\\ProgramData\\docker\\volumes,dst=C:\\ProgramData\\docker\\volumes --mount type=volume,src=portainer_agent_data,dst=C:\\data -e EDGE=1 -e EDGE_ID=' +
|
||||
$scope.randomEdgeID +
|
||||
' -e EDGE_KEY=' +
|
||||
$scope.endpoint.EdgeKey +
|
||||
' -e CAP_HOST_MANAGEMENT=1 --name portainer_edge_agent portainer/agent'
|
||||
);
|
||||
} else if ($scope.state.deploymentTab === 1 && $scope.state.platformType === 'linux') {
|
||||
clipboard.copyText(
|
||||
'docker network create --driver overlay portainer_agent_network; docker service create --name portainer_edge_agent --network portainer_agent_network -e AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent -e EDGE=1 -e EDGE_ID=' +
|
||||
$scope.randomEdgeID +
|
||||
@@ -71,6 +80,14 @@ angular
|
||||
$scope.endpoint.EdgeKey +
|
||||
" -e CAP_HOST_MANAGEMENT=1 --mode global --constraint 'node.platform.os == linux' --mount type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock --mount type=bind,src=//var/lib/docker/volumes,dst=/var/lib/docker/volumes --mount type=bind,src=//,dst=/host --mount type=volume,src=portainer_agent_data,dst=/data portainer/agent"
|
||||
);
|
||||
} else if ($scope.state.deploymentTab === 1 && $scope.state.platformType === 'windows') {
|
||||
clipboard.copyText(
|
||||
'docker network create --driver overlay portainer_edge_agent_network && docker service create --name portainer_edge_agent --network portainer_edge_agent_network -e AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent -e EDGE=1 -e EDGE_ID=' +
|
||||
$scope.randomEdgeID +
|
||||
' -e EDGE_KEY=' +
|
||||
$scope.endpoint.EdgeKey +
|
||||
' -e CAP_HOST_MANAGEMENT=1 --mode global --constraint node.platform.os==windows --mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine --mount type=bind,src=C:\\ProgramData\\docker\\volumes,dst=C:\\ProgramData\\docker\\volumes --mount type=volume,src=portainer_agent_data,dst=C:\\data portainer/agent'
|
||||
);
|
||||
} else {
|
||||
clipboard.copyText('curl https://downloads.portainer.io/portainer-edge-agent-setup.sh | bash -s -- ' + $scope.randomEdgeID + ' ' + $scope.endpoint.EdgeKey);
|
||||
}
|
||||
@@ -206,8 +223,10 @@ angular
|
||||
$scope.edgeKeyDetails = decodeEdgeKey(endpoint.EdgeKey);
|
||||
$scope.randomEdgeID = uuidv4();
|
||||
$scope.dockerCommands = {
|
||||
standalone: buildStandaloneCommand($scope.randomEdgeID, endpoint.EdgeKey),
|
||||
swarm: buildSwarmCommand($scope.randomEdgeID, endpoint.EdgeKey),
|
||||
linuxStandalone: buildLinuxStandaloneCommand($scope.randomEdgeID, endpoint.EdgeKey),
|
||||
windowsStandalone: buildWindowsStandaloneCommand($scope.randomEdgeID, endpoint.EdgeKey),
|
||||
linuxSwarm: buildLinuxSwarmCommand($scope.randomEdgeID, endpoint.EdgeKey),
|
||||
windowsSwarm: buildWindowsSwarmCommand($scope.randomEdgeID, endpoint.EdgeKey),
|
||||
};
|
||||
|
||||
const settings = data.settings;
|
||||
@@ -223,21 +242,36 @@ angular
|
||||
});
|
||||
}
|
||||
|
||||
function buildStandaloneCommand(edgeId, edgeKey) {
|
||||
return `docker run -d -v /var/run/docker.sock:/var/run/docker.sock \\
|
||||
function buildLinuxStandaloneCommand(edgeId, edgeKey) {
|
||||
return `docker run -d \\
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \\
|
||||
-v /var/lib/docker/volumes:/var/lib/docker/volumes \\
|
||||
-v /:/host \\
|
||||
-v portainer_agent_data:/data \\
|
||||
--restart always \\
|
||||
-e EDGE=1 \\
|
||||
-e EDGE_ID=${edgeId} \\
|
||||
-e EDGE_KEY=${edgeKey} \\
|
||||
-e CAP_HOST_MANAGEMENT=1 \\
|
||||
--name portainer_edge_agent \\localhost
|
||||
portainer/agent`;
|
||||
}
|
||||
|
||||
function buildWindowsStandaloneCommand(edgeId, edgeKey) {
|
||||
return `docker run -d \\
|
||||
--mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine \\
|
||||
--mount type=bind,src=C:\\ProgramData\\docker\\volumes,dst=C:\\ProgramData\\docker\\volumes \\
|
||||
--mount type=volume,src=portainer_agent_data,dst=C:\\data \\
|
||||
--restart always \\
|
||||
-e EDGE=1 \\
|
||||
-e EDGE_ID=${edgeId} \\
|
||||
-e EDGE_KEY=${edgeKey} \\
|
||||
-e CAP_HOST_MANAGEMENT=1 \\
|
||||
-v portainer_agent_data:/data \\
|
||||
--name portainer_edge_agent \\
|
||||
portainer/agent`;
|
||||
}
|
||||
|
||||
function buildSwarmCommand(edgeId, edgeKey) {
|
||||
function buildLinuxSwarmCommand(edgeId, edgeKey) {
|
||||
return `docker network create \\
|
||||
--driver overlay \\
|
||||
portainer_agent_network;
|
||||
@@ -259,5 +293,25 @@ docker service create \\
|
||||
portainer/agent`;
|
||||
}
|
||||
|
||||
function buildWindowsSwarmCommand(edgeId, edgeKey) {
|
||||
return `docker network create \\
|
||||
--driver overlay \\
|
||||
portainer_edge_agent_network && \\
|
||||
docker service create \\
|
||||
--name portainer_edge_agent \\
|
||||
--network portainer_edge_agent_network \\
|
||||
-e AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent \\
|
||||
-e EDGE=1 \\
|
||||
-e EDGE_ID=${edgeId} \\
|
||||
-e EDGE_KEY=${edgeKey} \\
|
||||
-e CAP_HOST_MANAGEMENT=1 \\
|
||||
--mode global \\
|
||||
--constraint node.platform.os==windows \\
|
||||
--mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine \\
|
||||
--mount type=bind,src=C:\\ProgramData\\docker\\volumes,dst=C:\\ProgramData\\docker\\volumes \\
|
||||
--mount type=volume,src=portainer_agent_data,dst=C:\\data \\
|
||||
portainer/agent`;
|
||||
}
|
||||
|
||||
initView();
|
||||
});
|
||||
|
||||
@@ -30,6 +30,17 @@
|
||||
<i class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
|
||||
</div>
|
||||
|
||||
<information-panel ng-if="isAdmin && endpoints.length === 0" title-text="Information">
|
||||
<span class="small text-muted">
|
||||
<p>
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
No environment available for management. Please head over the
|
||||
<a ui-sref="portainer.endpoints.new"> endpoints view </a>
|
||||
to add an endpoint.
|
||||
</p>
|
||||
</span>
|
||||
</information-panel>
|
||||
|
||||
<div class="row" ng-if="!state.connectingToEdgeEndpoint">
|
||||
<div class="col-sm-12">
|
||||
<endpoint-list
|
||||
|
||||
@@ -62,6 +62,9 @@
|
||||
<span ng-hide="ctrl.state.actionInProgress"><i class="fa fa-bolt" aria-hidden="true"></i> Connect</span>
|
||||
<span ng-show="ctrl.state.actionInProgress">Connecting...</span>
|
||||
</button>
|
||||
<button type="submit" class="btn btn-sm btn-default" ng-click="ctrl.skipEndpointCreation()" button-spinner="ctrl.state.actionInProgress">
|
||||
<span ng-hide="ctrl.state.actionInProgress"><i class="fa fa-share" aria-hidden="true"></i> Skip</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
|
||||
@@ -65,6 +65,10 @@ class InitEndpointController {
|
||||
}
|
||||
}
|
||||
|
||||
skipEndpointCreation() {
|
||||
this.$state.go('portainer.home');
|
||||
}
|
||||
|
||||
/**
|
||||
* DOCKER_LOCAL (1)
|
||||
*/
|
||||
|
||||
56
app/portainer/views/roles/roles.html
Normal file
56
app/portainer/views/roles/roles.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<rd-header>
|
||||
<rd-header-title title-text="Roles">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="portainer.roles" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-sync" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Role management</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<information-panel title-text="Information">
|
||||
<span class="small">
|
||||
<p class="text-muted">
|
||||
<i class="fa fa-user" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This feature is available in <a href="https://www.portainer.io/business-upsell?from=k8s-rbac-roles" target="_blank">Portainer Business Edition</a>.
|
||||
</p>
|
||||
</span>
|
||||
</information-panel>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<roles-datatable title-text="Roles" title-icon="fa-file-code" table-key="roles" order-by="Name"></roles-datatable>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12" style="margin-bottom: 0px;">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-user-lock" title-text="Effective access viewer"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
User
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">
|
||||
No user available
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Access
|
||||
</div>
|
||||
<div>
|
||||
<div class="small text-muted" style="margin-bottom: 15px;">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Effective role for each endpoint will be displayed for the selected user
|
||||
</div>
|
||||
</div>
|
||||
<access-viewer-datatable table-key="access_viewer" order-by="EndpointName"> </access-viewer-datatable>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
@@ -366,6 +366,57 @@
|
||||
<!-- !group-search-settings -->
|
||||
</div>
|
||||
|
||||
<div ng-if="isOauthEnabled()">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Provider
|
||||
</div>
|
||||
<div class="form-group"></div>
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<div class="boxselector_wrapper">
|
||||
<div data-toggle="tooltip" title="This feature is available in Portainer Business Edition" style="color: #767676;">
|
||||
<input type="radio" id="microsoft" disabled />
|
||||
<label for="microsoft" style="cursor: pointer; border-color: #767676;">
|
||||
<div class="boxselector_header">
|
||||
<i class="fab fa-microsoft" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Microsoft
|
||||
</div>
|
||||
<p>Microsoft OAuth provider</p>
|
||||
</label>
|
||||
</div>
|
||||
<div data-toggle="tooltip" title="This feature is available in Portainer Business Edition" style="color: #767676;">
|
||||
<input type="radio" id="google" disabled />
|
||||
<label for="google" style="cursor: pointer; border-color: #767676;">
|
||||
<div class="boxselector_header">
|
||||
<i class="fab fa-google" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Google
|
||||
</div>
|
||||
<p>Google OAuth provider</p>
|
||||
</label>
|
||||
</div>
|
||||
<div data-toggle="tooltip" title="This feature is available in Portainer Business Edition" style="color: #767676;">
|
||||
<input type="radio" id="registry_auth" disabled />
|
||||
<label for="Github" style="cursor: pointer; border-color: #767676;">
|
||||
<div class="boxselector_header">
|
||||
<i class="fab fa-github" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Github
|
||||
</div>
|
||||
<p>Github OAuth provider</p>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="custom" ng-model="settings.AuthenticationMethod" ng-value="3" />
|
||||
<label for="custom">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-user-check" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Custom
|
||||
</div>
|
||||
<p>Custom OAuth provider</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<oauth-settings ng-if="isOauthEnabled()" settings="OAuthSettings" teams="teams"></oauth-settings>
|
||||
|
||||
<!-- actions -->
|
||||
|
||||
@@ -120,14 +120,32 @@
|
||||
($state.current.name === 'portainer.users' ||
|
||||
$state.current.name === 'portainer.users.user' ||
|
||||
$state.current.name === 'portainer.teams' ||
|
||||
$state.current.name === 'portainer.teams.team')
|
||||
$state.current.name === 'portainer.teams.team' ||
|
||||
$state.current.name === 'portainer.roles')
|
||||
"
|
||||
>
|
||||
<a ui-sref="portainer.teams" ui-sref-active="active">Teams</a>
|
||||
</div>
|
||||
<div
|
||||
class="sidebar-sublist"
|
||||
ng-if="
|
||||
toggle &&
|
||||
($state.current.name === 'portainer.users' ||
|
||||
$state.current.name === 'portainer.users.user' ||
|
||||
$state.current.name === 'portainer.teams' ||
|
||||
$state.current.name === 'portainer.teams.team' ||
|
||||
$state.current.name === 'portainer.roles')
|
||||
"
|
||||
>
|
||||
<a ui-sref="portainer.roles" ui-sref-active="active">Roles</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="isAdmin">
|
||||
<a ui-sref="portainer.endpoints" ui-sref-active="active">Endpoints <span class="menu-icon fa fa-plug fa-fw"></span></a>
|
||||
<a
|
||||
ui-sref="portainer.endpoints"
|
||||
ng-class="{ active: $state.current.name.includes('portainer.endpoints') && $state.current.name !== 'portainer.endpoints.endpoint.kubernetesConfig' }"
|
||||
>Endpoints <span class="menu-icon fa fa-plug fa-fw"></span
|
||||
></a>
|
||||
<div
|
||||
class="sidebar-sublist"
|
||||
ng-if="
|
||||
@@ -136,7 +154,6 @@
|
||||
$state.current.name === 'portainer.endpoints.endpoint' ||
|
||||
$state.current.name === 'portainer.endpoints.new' ||
|
||||
$state.current.name === 'portainer.endpoints.endpoint.access' ||
|
||||
$state.current.name === 'portainer.endpoints.endpoint.kubernetesConfig' ||
|
||||
$state.current.name === 'portainer.groups' ||
|
||||
$state.current.name === 'portainer.groups.group' ||
|
||||
$state.current.name === 'portainer.groups.group.access' ||
|
||||
@@ -154,7 +171,6 @@
|
||||
$state.current.name === 'portainer.endpoints.endpoint' ||
|
||||
$state.current.name === 'portainer.endpoints.new' ||
|
||||
$state.current.name === 'portainer.endpoints.endpoint.access' ||
|
||||
$state.current.name === 'portainer.endpoints.endpoint.kubernetesConfig' ||
|
||||
$state.current.name === 'portainer.groups' ||
|
||||
$state.current.name === 'portainer.groups.group' ||
|
||||
$state.current.name === 'portainer.groups.group.access' ||
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
</uib-tab>
|
||||
<!-- !tab-info -->
|
||||
<!-- tab-file -->
|
||||
<uib-tab index="1" ng-if="stackFileContent" select="showEditor()">
|
||||
<uib-tab index="1" select="showEditor()">
|
||||
<uib-tab-heading> <i class="fa fa-pencil-alt space-right" aria-hidden="true"></i> Editor </uib-tab-heading>
|
||||
<form class="form-horizontal" ng-if="state.showEditorTab" style="margin-top: 10px;">
|
||||
<div class="form-group">
|
||||
@@ -165,7 +165,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
ng-disabled="state.actionInProgress || stack.Status === 2"
|
||||
ng-disabled="state.actionInProgress || stack.Status === 2 || !stackFileContent"
|
||||
ng-click="deployStack()"
|
||||
button-spinner="state.actionInProgress"
|
||||
>
|
||||
|
||||
@@ -12,5 +12,48 @@ services:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- portainer_data:/data
|
||||
|
||||
swarm:
|
||||
image: docker:dind
|
||||
privileged: true
|
||||
restart: always
|
||||
volumes:
|
||||
- /tmp/manager_run:/var/run
|
||||
|
||||
swarm-init:
|
||||
image: docker:dind
|
||||
privileged: true
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
apk add curl
|
||||
curl -L https://raw.githubusercontent.com/eficode/wait-for/master/wait-for -o /bin/wait-for
|
||||
chmod +x /bin/wait-for
|
||||
wait-for swarm:2376 -- docker -H unix://swarm/run/docker.sock swarm init
|
||||
docker -H unix://swarm/run/docker.sock swarm init
|
||||
docker -H unix://swarm/run/docker.sock network create --driver overlay portainer_agent_network
|
||||
docker -H unix://swarm/run/docker.sock service create -q --name portainer_agent --network portainer_agent_network --publish mode=host,target=9001,published=9001 -e AGENT_CLUSTER_ADDR=tasks.portainer_agent --mode global --mount type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock --mount type=bind,src=//var/lib/docker/volumes,dst=/var/lib/docker/volumes --mount type=bind,src=/,dst=/host portainer/agent
|
||||
depends_on:
|
||||
- swarm
|
||||
volumes:
|
||||
- /tmp/manager_run:/swarm/run
|
||||
|
||||
kube:
|
||||
image: portainer/kube-tools:latest
|
||||
privileged: true
|
||||
command:
|
||||
- /bin/bash
|
||||
- -c
|
||||
- |
|
||||
service docker start
|
||||
sleep 3
|
||||
kind create cluster --config=/kind.yaml
|
||||
curl -L https://raw.githubusercontent.com/portainer/k8s/master/deploy/manifests/agent/portainer-agent-k8s-nodeport.yaml -o portainer-agent.yaml
|
||||
kubectl apply -f portainer-agent.yaml
|
||||
tail -f /dev/null
|
||||
volumes:
|
||||
- docker_data:/var/lib/docker
|
||||
|
||||
volumes:
|
||||
docker_data:
|
||||
portainer_data:
|
||||
|
||||
@@ -19,7 +19,7 @@ module.exports = function (grunt) {
|
||||
binaries: {
|
||||
dockerLinuxVersion: '18.09.3',
|
||||
dockerWindowsVersion: '17.09.0-ce',
|
||||
komposeVersion: 'v1.21.0',
|
||||
komposeVersion: 'v1.22.0',
|
||||
kubectlVersion: 'v1.18.0',
|
||||
},
|
||||
config: gruntfile_cfg.config,
|
||||
|
||||
Reference in New Issue
Block a user