Compare commits

...

30 Commits

Author SHA1 Message Date
Yi Chen
a0a8499f0f Revert "feat(docker/stacks): add creation and update dates (#4418)"
This reverts commit bd98b8956a.
2020-12-17 13:30:11 +13:00
xAt0mZ
f8b226a1ef fix(k8s/application): ability to remove naked pods (#4598) 2020-12-17 13:05:31 +13:00
cong meng
342a0d6d22 fix(k8s/application): transform username to be dns compliant (#4595) (#4601)
* fix(k8s/application): transform username to be dns compliant (#4595)

* fix(k8s/application): transform username to be dns compliant for configurations and resource pools(#4595)

* fix(k8s/application): update regex to replace all special characters (#4595)

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2020-12-17 12:20:18 +13:00
Alice Groux
58bf76a58f feat(app/volumes): add confirmation modal before deleting volumes in volumes view and volume view (#4597) 2020-12-16 19:57:31 +13:00
Alice Groux
bd98b8956a feat(docker/stacks): add creation and update dates (#4418)
* feat(docker/stacks): add creation and update dates

* feat(docker/stacks): put ownership column as the last column

* feat(docker/stacks): fix the no stacks message
2020-12-16 16:11:59 +13:00
Alice Groux
4bc958f865 feat(app/logs): add download button on container logs and service logs views (#4529) 2020-12-16 12:30:16 +13:00
aravind-korada
b67c0e870c #4470 fix(stack): fix a display issue with the stack editor tab. (#4543) 2020-12-15 11:42:54 +13:00
Chaim Lev-Ari
067257df2b fix(services): prevent adding volume without source and target (#4538)
* feat(services): check that target mounts are non empty

* feat(services): prevent creating service when no source

* refactor(services): remove ng-form

* fix(services): check that every volume is valid
2020-12-14 16:27:05 +13:00
Alice Groux
5f2f7a87ab feat(app): add a preview for business edition features (#4578)
* feat(app): add a preview for business edition features

* feat(app): open links in new tab + show storage quota section + grey out unavailable providers
2020-12-14 14:31:59 +13:00
cong meng
f656ad7124 fix(frontend): fix incorrect datatable selection count on text filter change (#4474)
Co-authored-by: Simon Meng <simon@mcpacino.tk>
2020-12-14 12:25:00 +13:00
Alice Groux
f681e2d532 feat(endpoint): start Portainer without endpoint (#4460)
* feat(endpoint): start Portainer without endpoint

* feat(endpoint): minor UI update

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
2020-12-14 10:20:35 +13:00
Anthony Lapenna
fdb9bf09de docs(README): update README contribution link (#4587) 2020-12-14 09:18:41 +13:00
Alice Groux
92ad3e788d feat(k8s/configuration): rename create entry file button (#4515) 2020-12-13 21:42:54 +13:00
Alice Groux
bc2f5a3260 feat(k8s/advanced-deployment): update extra information message when kubernetes type is selected (#4542) 2020-12-13 17:54:38 +13:00
Alice Groux
487123491e fix(k8s/application): improve ux for instance count input in creation/edition application (#4498) 2020-12-13 17:22:46 +13:00
cong meng
380f106571 chore(build): bump Kompose version (#4475)
Co-authored-by: Simon Meng <simon@mcpacino.tk>
2020-12-13 16:22:18 +13:00
Alice Groux
341378e783 feat(app/endpoint): add deployment instructions for windows (#4442)
* feat(app/endpoint): add deployment instructions for windows

* feat(app/endpoint): hide instructions for kubernetes via load balancer and kubernetes via node port when windows is selected

* feat(endpoint): minor UI update

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
2020-12-13 15:50:42 +13:00
Alice Groux
b360936454 feat(app/endpoint): edge deployment for windows (#4443)
* feat(app/endpoint): edge deployment for windows

* feat(app/endpoint): hide instructions for kubernetes when windows is selected

* feat(app/endpoint): fix typo

* feat(endpoint): minor UI update

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
2020-12-11 17:40:56 +13:00
Mathieu Cantin
8204d32538 fix(configs): fix error with binary file (#3937) 2020-12-11 09:57:28 +13:00
Maxime Bajeux
60c5ab3eec feat(kube): Add a confirmation modal before deleting one or more application or configuration (#4522) 2020-12-10 20:46:58 +13:00
Anthony Lapenna
20cf948e53 fix(docker/resourcecontrol): fix an issue with resource deletion (#4524) 2020-12-10 20:31:31 +13:00
Alice Groux
45fcb1ad26 fix(k8s/configuration): save the owner when updating the configuration (#4517) 2020-12-10 19:49:25 +13:00
Alice Groux
7398d54ed0 fix(k8s/application): refreshing yaml panel doesn't change the selected panel (#4500) 2020-12-10 19:44:24 +13:00
Alice Groux
faded67deb fix(k8s/node): sort labels (#4417) 2020-12-10 15:57:35 +13:00
Alice Groux
eadd8b36d6 fix(applications/ports-mapping): load balancer link expand only if the item length > 1 (#4495) 2020-12-10 15:27:18 +13:00
cong meng
1ad4623b08 fix(frontend): override configuration keys disappear (#4547) (#4560)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2020-12-10 15:13:02 +13:00
Alice Groux
890bbf4058 fix(k8s/sidebar): accessing cluster setup not expand endpoint sidebar (#4496) 2020-12-10 15:11:45 +13:00
cong meng
865c8d899b fix(frontend): revalidate configuration name when change resource pool (#4553) (#4562)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2020-12-10 14:21:43 +13:00
cong meng
aa5277de2e fix(frontend): cannnot access configuration details view containing binary data (#4503) (#4561)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2020-12-10 13:58:10 +13:00
Anthony Lapenna
9136ba30eb feat(build-system): update pull-dog configuration (#4532)
* feat(build-system): update pull-dog configuration

* feat(build): update pull-dog configuration
2020-12-02 08:27:30 +13:00
58 changed files with 893 additions and 146 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -1042,3 +1042,7 @@ json-tree .branch-preview {
background-color: #337ab7;
}
/* !spinkit override */
.w-full {
width: 100%;
}

View File

@@ -7,5 +7,6 @@ angular.module('portainer.docker').component('logViewer', {
logCollectionChange: '<',
sinceTimestamp: '=',
lineCount: '=',
resourceName: '<',
},
});

View File

@@ -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()"

View File

@@ -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');
};
},
]);

View File

@@ -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) {

View File

@@ -12,4 +12,5 @@
display-timestamps="state.displayTimestamps"
line-count="state.lineCount"
since-timestamp="state.sinceTimestamp"
resource-name="container.Name"
></log-viewer>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -12,4 +12,5 @@
display-timestamps="state.displayTimestamps"
line-count="state.lineCount"
since-timestamp="state.sinceTimestamp"
resource-name="service.Name"
></log-viewer>

View File

@@ -13,4 +13,5 @@
display-timestamps="state.displayTimestamps"
line-count="state.lineCount"
since-timestamp="state.sinceTimestamp"
resource-name="task.Id"
></log-viewer>

View File

@@ -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) {

View File

@@ -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();
}
});
});
}
});
};

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -8,5 +8,9 @@ class KubernetesCommonHelper {
_.set(obj, path, value);
}
}
static ownerToLabel(owner) {
return _.replace(owner, /[^-A-Za-z0-9_.]/g, '.');
}
}
export default KubernetesCommonHelper;

View File

@@ -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');
}

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -146,7 +146,7 @@ class KubernetesApplicationController {
showEditor() {
this.state.showEditorTab = true;
this.selectTab(2);
this.selectTab(3);
}
isSystemNamespace() {

View File

@@ -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;
}

View File

@@ -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() {

View File

@@ -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) {

View File

@@ -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">

View File

@@ -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">

View File

@@ -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

View File

@@ -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

View File

@@ -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);
},
]);

View File

@@ -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">

View File

@@ -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>

View File

@@ -0,0 +1,11 @@
angular.module('portainer.app').component('accessViewerDatatable', {
templateUrl: './accessViewerDatatable.html',
controller: 'GenericDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
tableKey: '@',
orderBy: '@',
dataset: '<',
},
});

View File

@@ -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;
}

View File

@@ -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">

View File

@@ -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>

View File

@@ -0,0 +1,12 @@
angular.module('portainer.app').component('rolesDatatable', {
templateUrl: './rolesDatatable.html',
controller: 'GenericDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
},
});

View File

@@ -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);
})

View File

@@ -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 () {

View File

@@ -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"

View File

@@ -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;">

View File

@@ -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();
});

View File

@@ -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

View File

@@ -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 -->

View File

@@ -65,6 +65,10 @@ class InitEndpointController {
}
}
skipEndpointCreation() {
this.$state.go('portainer.home');
}
/**
* DOCKER_LOCAL (1)
*/

View 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>

View File

@@ -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 -->

View File

@@ -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' ||

View File

@@ -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"
>

View File

@@ -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:

View File

@@ -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,