Compare commits

...

46 Commits

Author SHA1 Message Date
xAt0mZ
b6b126e8f6 fix(k8s/application): ability to remove naked pods 2020-12-15 19:29:57 +01: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
Stéphane Busso
3d9c10adf1 Merge pull request #4415 from portainer/feat/GH/4011-pods-as-applications
feat(k8s/applications): exposed naked pods as applications
2020-11-23 14:57:04 +13:00
Alice Groux
0d20988bef fix(rest): remove timeouts for all REST services (#4385) 2020-11-05 20:49:37 +13:00
Anthony Lapenna
1545a42f08 chore(github): update bug report template
Update documentation URLs
2020-11-03 06:16:18 +13:00
Mulia Nasution
a12f2ee893 Fix typo, change Matamo to Matomo (#4409) 2020-10-28 11:33:23 +13:00
xAt0mZ
174e28b850 feat(k8s/application): app details for pods 2020-10-26 19:48:38 +01:00
xAt0mZ
3da9751c82 feat(k8s/applications): add pod as new application type for apps list 2020-10-26 19:46:44 +01:00
Alice Groux
ccea7cca3d fix(endpoint): remove TLS settings for kubernetes (#4388) 2020-10-21 09:22:42 +13:00
Tim van den Eijnden
43891703c2 fix(endpoints): broken datatable sorting (#4373) 2020-10-20 12:07:24 +13:00
Tim van den Eijnden
74429d6d46 feat(frontend): show endpoint.name in page title (#4363)
* feat(frontend): show endpoint.name in page title

* feat(frontend): show endpoint.name in page title - use rootscope for defaultTitle
2020-10-16 22:28:46 +13:00
S.Hale
bb5c2c2875 fix(readme): fix grammar errors in readme (#4376) 2020-10-16 22:12:06 +13:00
itsconquest
3e82d01894 chore(project): update stalebot message (#4364) 2020-10-12 17:28:41 +13:00
Ranjan Purbey
9e80037e72 style(containers): fix word-break on container details table (#4359)
Co-authored-by: Rajesh Swarna <rajeshswarna123@gmail.com>
Co-authored-by: naveenrayudu <naveenkumar.rayudu@gmail.com>
Co-authored-by: Ranjan Purbey <ranjan.purbey@gmail.com>

Co-authored-by: Rajesh Swarna <rajeshswarna123@gmail.com>
Co-authored-by: naveenrayudu <naveenkumar.rayudu@gmail.com>
2020-10-05 11:00:13 +13:00
panchbhai1969
da29c2b6a5 #3741 fix(datatables): fixes datatable selection count on text filter (#4358) 2020-10-05 10:58:53 +13:00
Neil Cresswell
0ed4d443ee Update README.md 2020-09-30 15:40:20 +13:00
itsconquest
a4fa44f831 chore(testing): cleanup e2e for CE (#4349) 2020-09-29 11:01:49 +13:00
itsconquest
e479e41aee feat(ci): add missing powershell scripts & fix related grunt code (#4345)
* feat(ci): add missing powershell scripts & fix related grunt code

* feat(ci): download binaries direct to dist directory

* feat(ci): correctly pass in binary versions

* feat(ci): fix powershell errors

* feat(ci): fix cmdlet syntax

* feat(ci): fix typo

* feat(ci): fix additonal typo
2020-09-24 19:19:41 +12:00
itsconquest
d4c4c4e895 feat(project): refactor e2e testing (#4341)
* feat(project): refactor e2e testing

* feat(project): remove example text

* feat(project): add missing newlines

Co-authored-by: owner <owner@pop-os.localdomain>
2020-09-23 12:31:19 +12:00
Anthony Lapenna
466bd24648 feat(test/e2e): update image in cypress compose file 2020-09-01 09:28:45 +12:00
itsconquest
2fc60f14e1 docs(README): add privacy info (#4289) 2020-08-31 22:17:03 +12:00
Anthony Lapenna
9300603777 fix(k8s/applications): fix an issue with daemonset in 0/0 state (#4288) 2020-08-31 17:21:25 +12:00
Anthony Lapenna
8dac2df7bf fix(k8s/volumes): fix an issue with the system volume filter not working (#4284) 2020-08-31 17:21:15 +12:00
116 changed files with 2376 additions and 358 deletions

View File

@@ -16,7 +16,7 @@ already open. You can ensure this by searching the issue list for this
repository. If there is a duplicate, please close your issue and add a comment
to the existing issue instead.
Also, be sure to check our FAQ and documentation first: https://portainer.readthedocs.io
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
-->
**Bug description**
@@ -27,7 +27,7 @@ A clear and concise description of what you expected to happen.
**Portainer Logs**
Provide the logs of your Portainer container or Service.
You can see how [here](https://portainer.readthedocs.io/en/stable/faq.html#how-do-i-get-the-logs-from-portainer)
You can see how [here](https://documentation.portainer.io/archive/1.23.2/faq/#how-do-i-get-the-logs-from-portainer)
**Steps to reproduce the issue:**

2
.github/stale.yml vendored
View File

@@ -47,7 +47,7 @@ issues:
closeComment: >
Since no further activity has appeared on this issue it will be closed.
If you believe that it has been incorrectly closed, leave a comment
and mention @itsconquest. One of our staff will then review the issue.
mentioning `ametdoohan`, `balasu` or `keverv` and one of our staff will then review the issue.
Note - If it is an old bug report, make sure that it is reproduceable in the
latest version of Portainer as it may have already been fixed.

View File

@@ -10,7 +10,7 @@
**_Portainer_** is a lightweight management UI which allows you to **easily** manage your different Docker environments (Docker hosts or Swarm clusters).
**_Portainer_** is meant to be as **simple** to deploy as it is to use. It consists of a single container that can run on any Docker engine (can be deployed as Linux container or a Windows native container, supports other platforms too).
**_Portainer_** allows you to manage all your Docker resources (containers, images, volumes, networks and more) ! It is compatible with the _standalone Docker_ engine and with _Docker Swarm mode_.
**_Portainer_** allows you to manage all your Docker resources (containers, images, volumes, networks and more!) It is compatible with the _standalone Docker_ engine and with _Docker Swarm mode_.
## Demo
@@ -24,12 +24,12 @@ Alternatively, you can deploy a copy of the demo stack inside a [play-with-docke
- Sign in with your [Docker ID](https://docs.docker.com/docker-id)
- Follow [these](https://github.com/portainer/portainer-demo/blob/master/play-with-docker/docker-stack.yml#L5-L8) steps.
Unlike the public demo, the playground sessions are deleted after 4 hours. Apart from that, all the settings are same, including default credentials.
Unlike the public demo, the playground sessions are deleted after 4 hours. Apart from that, all the settings are the same, including default credentials.
## Getting started
- [Deploy Portainer](https://www.portainer.io/installation/)
- [Documentation](https://www.portainer.io/documentation/)
- [Documentation](https://documentation.portainer.io)
## Getting help
@@ -38,18 +38,24 @@ For FORMAL Support, please purchase a support subscription from here: https://ww
For community support: You can find more information about Portainer's community support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
- Issues: https://github.com/portainer/portainer/issues
- FAQ: https://www.portainer.io/documentation/faqs/
- FAQ: https://documentation.portainer.io
- Slack (chat): https://portainer.io/slack/
## 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
- Here at Portainer, we believe in [responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure) of security issues. If you have found a security issue, please report it to <security@portainer.io>.
## Privacy
**To make sure we focus our development effort in the right places we need to know which features get used most often. To give us this information we use [Matomo Analytics](https://matomo.org/), which is hosted in Germany and is fully GDPR compliant.**
When Portainer first starts, you are given the option to DISABLE analytics. If you **don't** choose to disable it, we collect anonymous usage as per [our privacy policy](https://www.portainer.io/documentation/in-app-analytics-and-privacy-policy/). **Please note**, there is no personally identifiable information sent or stored at any time and we only use the data to help us improve Portainer.
## Limitations
Portainer supports "Current - 2 docker versions only. Prior versions may operate, however these are not supported.

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

@@ -16,6 +16,7 @@ angular.module('portainer').run([
EndpointProvider.initialize();
$rootScope.$state = $state;
$rootScope.defaultTitle = document.title;
// Workaround to prevent the loading bar from going backward
// https://github.com/chieffancypants/angular-loading-bar/issues/273

View File

@@ -178,6 +178,11 @@ a[ng-click] {
word-break: break-word;
}
.widget .widget-body table.container-details-table > tbody > tr > td:first-child {
word-break: normal;
text-transform: uppercase;
}
.widget .widget-body table.description-table {
table-layout: fixed;
}
@@ -1037,3 +1042,7 @@ json-tree .branch-preview {
background-color: #337ab7;
}
/* !spinkit override */
.w-full {
width: 100%;
}

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

@@ -19,7 +19,6 @@ angular.module('portainer.docker').factory('Container', [
params: { all: 0, action: 'json', filters: '@filters' },
isArray: true,
interceptor: ContainersInterceptor,
timeout: 15000,
},
get: {
method: 'GET',
@@ -48,20 +47,17 @@ angular.module('portainer.docker').factory('Container', [
logs: {
method: 'GET',
params: { id: '@id', action: 'logs' },
timeout: 4500,
ignoreLoadingBar: true,
transformResponse: logsHandler,
},
stats: {
method: 'GET',
params: { id: '@id', stream: false, action: 'stats' },
timeout: 4500,
ignoreLoadingBar: true,
},
top: {
method: 'GET',
params: { id: '@id', action: 'top' },
timeout: 4500,
ignoreLoadingBar: true,
},
start: {

View File

@@ -16,7 +16,7 @@ angular.module('portainer.docker').factory('Image', [
endpointId: EndpointProvider.endpointID,
},
{
query: { method: 'GET', params: { all: 0, action: 'json' }, isArray: true, interceptor: ImagesInterceptor, timeout: 15000 },
query: { method: 'GET', params: { all: 0, action: 'json' }, isArray: true, interceptor: ImagesInterceptor },
get: { method: 'GET', params: { action: 'json' } },
search: { method: 'GET', params: { action: 'search' } },
history: { method: 'GET', params: { action: 'history' }, isArray: true },

View File

@@ -18,7 +18,6 @@ angular.module('portainer.docker').factory('Network', [
method: 'GET',
isArray: true,
interceptor: NetworksInterceptor,
timeout: 15000,
},
get: {
method: 'GET',

View File

@@ -35,7 +35,6 @@ angular.module('portainer.docker').factory('Service', [
logs: {
method: 'GET',
params: { id: '@id', action: 'logs' },
timeout: 4500,
ignoreLoadingBar: true,
transformResponse: logsHandler,
},

View File

@@ -18,10 +18,9 @@ angular.module('portainer.docker').factory('System', [
info: {
method: 'GET',
params: { action: 'info' },
timeout: 15000,
interceptor: InfoInterceptor,
},
version: { method: 'GET', params: { action: 'version' }, timeout: 4500, interceptor: VersionInterceptor },
version: { method: 'GET', params: { action: 'version' }, interceptor: VersionInterceptor },
events: {
method: 'GET',
params: { action: 'events', since: '@since', until: '@until' },

View File

@@ -17,7 +17,6 @@ angular.module('portainer.docker').factory('Task', [
logs: {
method: 'GET',
params: { id: '@id', action: 'logs' },
timeout: 4500,
ignoreLoadingBar: true,
transformResponse: logsHandler,
},

View File

@@ -18,7 +18,7 @@ angular.module('portainer.docker').factory('Volume', [
endpointId: EndpointProvider.endpointID,
},
{
query: { method: 'GET', interceptor: VolumesInterceptor, timeout: 15000 },
query: { method: 'GET', interceptor: VolumesInterceptor },
get: { method: 'GET', params: { id: '@id' } },
create: {
method: 'POST',

View File

@@ -214,7 +214,7 @@
<rd-widget>
<rd-widget-header icon="fa-server" title-text="Container details"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<table class="table container-details-table">
<tbody>
<tr>
<td>Image</td>

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,9 +12,9 @@ angular.module('portainer.integrations.storidge').factory('Storidge', [
{
rebootCluster: { method: 'POST', params: { resource: 'clusters', action: 'reboot' } },
shutdownCluster: { method: 'POST', params: { resource: 'clusters', action: 'shutdown' } },
queryEvents: { method: 'GET', params: { resource: 'clusters', action: 'events' }, timeout: 4500, ignoreLoadingBar: true, isArray: true },
queryEvents: { method: 'GET', params: { resource: 'clusters', action: 'events' }, ignoreLoadingBar: true, isArray: true },
getVersion: { method: 'GET', params: { resource: 'clusters', action: 'version' } },
getInfo: { method: 'GET', params: { resource: 'clusters', action: 'info' }, timeout: 4500, ignoreLoadingBar: true },
getInfo: { method: 'GET', params: { resource: 'clusters', action: 'info' }, ignoreLoadingBar: true },
queryNodes: { method: 'GET', params: { resource: 'nodes' } },
getNode: { method: 'GET', params: { resource: 'nodes', id: '@id' } },

View File

@@ -14,7 +14,6 @@ angular.module('portainer.kubernetes').factory('KubernetesComponentStatus', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
}

View File

@@ -57,7 +57,7 @@
<table class="table table-hover nowrap-cells">
<thead>
<tr>
<th>
<th ng-if="!$ctrl.isPod">
<a ng-click="$ctrl.changeOrderBy('PodName')">
Pod
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'PodName' && !$ctrl.state.reverseOrder"></i>
@@ -107,7 +107,7 @@
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit: $ctrl.tableKey))"
pagination-id="$ctrl.tableKey"
>
<td>{{ item.PodName }}</td>
<td ng-if="!$ctrl.isPod">{{ item.PodName }}</td>
<td>{{ item.Name }}</td>
<td>{{ item.Image }}</td>
<td
@@ -130,10 +130,10 @@
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="6" class="text-center text-muted">Loading...</td>
<td colspan="7" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="6" class="text-center text-muted">No pod available.</td>
<td colspan="7" class="text-center text-muted">No pod available.</td>
</tr>
</tbody>
</table>

View File

@@ -8,5 +8,6 @@ angular.module('portainer.kubernetes').component('kubernetesContainersDatatable'
tableKey: '@',
orderBy: '@',
refreshCallback: '<',
isPod: '<',
},
});

View File

@@ -151,11 +151,14 @@
>{{ item.Image }} <span ng-if="item.Containers.length > 1">+ {{ item.Containers.length - 1 }}</span></td
>
<td>{{ item.ApplicationType | kubernetesApplicationTypeText }}</td>
<td>
<td ng-if="item.ApplicationType !== $ctrl.KubernetesApplicationTypes.POD">
<span ng-if="item.DeploymentType === $ctrl.KubernetesApplicationDeploymentTypes.REPLICATED">Replicated</span>
<span ng-if="item.DeploymentType === $ctrl.KubernetesApplicationDeploymentTypes.GLOBAL">Global</span>
<code>{{ item.RunningPodsCount }}</code> / <code>{{ item.TotalPodsCount }}</code></td
>
<code>{{ item.RunningPodsCount }}</code> / <code>{{ item.TotalPodsCount }}</code>
</td>
<td ng-if="item.ApplicationType === $ctrl.KubernetesApplicationTypes.POD">
{{ item.Pods[0].Status }}
</td>
<td>
<span ng-if="item.PublishedPorts.length">
<span>

View File

@@ -1,4 +1,4 @@
import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models';
import { KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
angular.module('portainer.docker').controller('KubernetesApplicationsDatatableController', [
@@ -42,6 +42,7 @@ angular.module('portainer.docker').controller('KubernetesApplicationsDatatableCo
this.$onInit = function () {
this.isAdmin = Authentication.isAdmin();
this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
this.KubernetesApplicationTypes = KubernetesApplicationTypes;
this.setDefaults();
this.prepareTableFromDataset();

View File

@@ -33,7 +33,7 @@ class KubernetesVolumesDatatableController {
}
isDisplayed(item) {
return !this.isSystemNamespace(item) || this.showSystem;
return !this.isSystemNamespace(item) || this.settings.showSystem;
}
isExternalVolume(item) {
@@ -48,6 +48,7 @@ class KubernetesVolumesDatatableController {
this.setDefaults();
this.prepareTableFromDataset();
this.isAdmin = this.Authentication.isAdmin();
this.settings.showSystem = false;
this.state.orderBy = this.orderBy;
var storedOrder = this.DatatableService.getDataTableOrder(this.tableKey);
@@ -76,8 +77,6 @@ class KubernetesVolumesDatatableController {
this.settings.open = false;
}
this.onSettingsRepeaterChange();
this.settings.showSystem = false;
}
$onInit() {

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

@@ -50,7 +50,7 @@ function _apiPortsToPublishedPorts(pList, pRefs) {
class KubernetesApplicationConverter {
static applicationCommon(res, data, pods, service, ingresses) {
const containers = _.without(_.concat(data.spec.template.spec.containers, data.spec.template.spec.initContainers), undefined);
const containers = data.spec.template ? _.without(_.concat(data.spec.template.spec.containers, data.spec.template.spec.initContainers), undefined) : data.spec.containers;
res.Id = data.metadata.uid;
res.Name = data.metadata.name;
res.StackName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationStackNameLabel] || '-' : '-';
@@ -61,7 +61,7 @@ class KubernetesApplicationConverter {
res.Image = containers[0].image;
res.CreationDate = data.metadata.creationTimestamp;
res.Env = _.without(_.flatMap(_.map(containers, 'env')), undefined);
res.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods, data);
res.Pods = data.spec.selector ? KubernetesApplicationHelper.associatePodsAndApplication(pods, data.spec.selector) : [data];
const limits = {
Cpu: 0,
@@ -118,7 +118,11 @@ class KubernetesApplicationConverter {
res.PublishedPorts = ports;
}
res.Volumes = data.spec.template.spec.volumes ? data.spec.template.spec.volumes : [];
if (data.spec.template) {
res.Volumes = data.spec.template.spec.volumes ? data.spec.template.spec.volumes : [];
} else {
res.Volumes = data.spec.volumes;
}
// TODO: review
// this if() fixs direct use of PVC reference inside spec.template.spec.containers[0].volumeMounts
@@ -169,7 +173,7 @@ class KubernetesApplicationConverter {
res.PersistedFolders = _.without(res.PersistedFolders, undefined);
res.ConfigurationVolumes = _.reduce(
data.spec.template.spec.volumes,
res.Volumes,
(acc, volume) => {
if (volume.configMap || volume.secret) {
const matchingVolumeMount = _.find(_.flatMap(_.map(containers, 'volumeMounts')), { name: volume.name });
@@ -213,6 +217,13 @@ class KubernetesApplicationConverter {
);
}
static apiPodToApplication(data, pods, service, ingresses) {
const res = new KubernetesApplication();
KubernetesApplicationConverter.applicationCommon(res, data, pods, service, ingresses);
res.ApplicationType = KubernetesApplicationTypes.POD;
return res;
}
static apiDeploymentToApplication(data, pods, service, ingresses) {
const res = new KubernetesApplication();
KubernetesApplicationConverter.applicationCommon(res, data, pods, service, ingresses);
@@ -310,7 +321,7 @@ class KubernetesApplicationConverter {
} else if (daemonSet) {
app = KubernetesDaemonSetConverter.applicationFormValuesToDaemonSet(formValues, claims);
} else {
throw new PortainerError('Unable to determine which association to use');
throw new PortainerError('Unable to determine which association to use to convert form');
}
let headlessService;

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

@@ -15,7 +15,6 @@ angular.module('portainer.kubernetes').factory('KubernetesEndpoints', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
}

View File

@@ -57,6 +57,8 @@ angular
return KubernetesApplicationTypeStrings.DAEMONSET;
case KubernetesApplicationTypes.STATEFULSET:
return KubernetesApplicationTypeStrings.STATEFULSET;
case KubernetesApplicationTypes.POD:
return KubernetesApplicationTypeStrings.POD;
default:
return '-';
}

View File

@@ -3,14 +3,14 @@ import { KubernetesPortMapping, KubernetesPortMappingPort } from 'Kubernetes/mod
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
import {
KubernetesApplicationConfigurationFormValueOverridenKeyTypes,
KubernetesApplicationEnvironmentVariableFormValue,
KubernetesApplicationAutoScalerFormValue,
KubernetesApplicationConfigurationFormValue,
KubernetesApplicationConfigurationFormValueOverridenKey,
KubernetesApplicationConfigurationFormValueOverridenKeyTypes,
KubernetesApplicationEnvironmentVariableFormValue,
KubernetesApplicationPersistedFolderFormValue,
KubernetesApplicationPublishedPortFormValue,
KubernetesApplicationAutoScalerFormValue,
KubernetesApplicationPlacementFormValue,
KubernetesApplicationPublishedPortFormValue,
} from 'Kubernetes/models/application/formValues';
import {
KubernetesApplicationEnvConfigMapPayload,
@@ -23,13 +23,13 @@ import {
KubernetesApplicationVolumeSecretPayload,
} from 'Kubernetes/models/application/payloads';
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
import { KubernetesApplicationPlacementTypes, KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models';
import { KubernetesPodNodeAffinityNodeSelectorRequirementOperators, KubernetesPodAffinity } from 'Kubernetes/pod/models';
import { KubernetesApplicationDeploymentTypes, KubernetesApplicationPlacementTypes } from 'Kubernetes/models/application/models';
import { KubernetesPodAffinity, KubernetesPodNodeAffinityNodeSelectorRequirementOperators } from 'Kubernetes/pod/models';
import {
KubernetesNodeSelectorTermPayload,
KubernetesPreferredSchedulingTermPayload,
KubernetesPodNodeAffinityPayload,
KubernetesNodeSelectorRequirementPayload,
KubernetesNodeSelectorTermPayload,
KubernetesPodNodeAffinityPayload,
KubernetesPreferredSchedulingTermPayload,
} from 'Kubernetes/pod/payloads/affinities';
class KubernetesApplicationHelper {
@@ -38,8 +38,8 @@ class KubernetesApplicationHelper {
return !application.ApplicationOwner;
}
static associatePodsAndApplication(pods, app) {
return _.filter(pods, { Labels: app.spec.selector.matchLabels });
static associatePodsAndApplication(pods, selector) {
return _.filter(pods, ['metadata.labels', selector.matchLabels]);
}
static associateContainerPersistedFoldersAndConfigurations(app, containers) {
@@ -65,7 +65,7 @@ class KubernetesApplicationHelper {
}
static associateContainersAndApplication(app) {
if (!app.Pods) {
if (!app.Pods || app.Pods.length === 0) {
return [];
}
const containers = app.Pods[0].Containers;

View File

@@ -20,7 +20,7 @@ class KubernetesApplicationRollbackHelper {
result = KubernetesApplicationRollbackHelper._getStatefulSetPayload(application, targetRevision);
break;
default:
throw new PortainerError('Unable to determine which association to use');
throw new PortainerError('Unable to determine which association to use to convert patch');
}
return result;
}

View File

@@ -21,7 +21,7 @@ class KubernetesHistoryHelper {
[currentRevision, revisionsList] = KubernetesHistoryHelper._getStatefulSetRevisions(rawRevisions, application.Raw);
break;
default:
throw new PortainerError('Unable to determine which association to use');
throw new PortainerError('Unable to determine which association to use to get revisions');
}
revisionsList = _.sortBy(revisionsList, 'revision');
return [currentRevision, revisionsList];

View File

@@ -7,6 +7,9 @@ class KubernetesServiceHelper {
}
static findApplicationBoundService(services, rawApp) {
if (!rawApp.spec.template) {
return undefined;
}
return _.find(services, (item) => item.spec.selector && _.isMatch(rawApp.spec.template.metadata.labels, item.spec.selector));
}
}

View File

@@ -18,7 +18,8 @@ export class KubernetesHorizontalPodAutoScalerHelper {
return KubernetesApplicationTypeStrings.DAEMONSET;
} else if ((app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.STATEFULSET) || app instanceof KubernetesStatefulSet) {
return KubernetesApplicationTypeStrings.STATEFULSET;
// } else if () { ---> TODO: refactor - handle bare pod type !
} else if (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.POD) {
return KubernetesApplicationTypeStrings.POD;
} else {
throw new PortainerError('Unable to determine application type');
}

View File

@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesHorizontalPodAutoScale
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -15,7 +15,6 @@ function factory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -12,12 +12,14 @@ export const KubernetesApplicationTypes = Object.freeze({
DEPLOYMENT: 1,
DAEMONSET: 2,
STATEFULSET: 3,
POD: 4,
});
export const KubernetesApplicationTypeStrings = Object.freeze({
DEPLOYMENT: 'Deployment',
DAEMONSET: 'DaemonSet',
STATEFULSET: 'StatefulSet',
POD: 'Pod',
});
export const KubernetesApplicationPublishingTypes = Object.freeze({

View File

@@ -1,11 +1,8 @@
/**
* Generic params
*/
const _KubernetesCommonParams = Object.freeze({
id: '',
});
export class KubernetesCommonParams {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesCommonParams)));
}
export function KubernetesCommonParams() {
return {
id: '',
};
}

View File

@@ -16,7 +16,6 @@ angular.module('portainer.kubernetes').factory('KubernetesNodes', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -1,9 +1,7 @@
import _ from 'lodash-es';
import angular from 'angular';
import PortainerError from 'Portainer/error';
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
import KubernetesPodConverter from 'Kubernetes/pod/converter';
class KubernetesPodService {
/* @ngInject */
@@ -11,23 +9,43 @@ class KubernetesPodService {
this.$async = $async;
this.KubernetesPods = KubernetesPods;
this.getAsync = this.getAsync.bind(this);
this.getAllAsync = this.getAllAsync.bind(this);
this.logsAsync = this.logsAsync.bind(this);
this.deleteAsync = this.deleteAsync.bind(this);
}
async getAsync(namespace, name) {
try {
const params = new KubernetesCommonParams();
params.id = name;
const [raw, yaml] = await Promise.all([this.KubernetesPods(namespace).get(params).$promise, this.KubernetesPods(namespace).getYaml(params).$promise]);
const res = {
Raw: raw,
Yaml: yaml.data,
};
return res;
} catch (err) {
throw new PortainerError('Unable to retrieve pod', err);
}
}
/**
* GET ALL
*/
async getAllAsync(namespace) {
try {
const data = await this.KubernetesPods(namespace).get().$promise;
return _.map(data.items, (item) => KubernetesPodConverter.apiToModel(item));
return data.items;
} catch (err) {
throw new PortainerError('Unable to retrieve pods', err);
}
}
get(namespace) {
get(namespace, name) {
if (name) {
return this.$async(this.getAsync, namespace, name);
}
return this.$async(this.getAllAsync, namespace);
}

View File

@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesConfigMaps', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesControllerRevisions',
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesDaemonSets', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesDeployments', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -11,7 +11,6 @@ angular.module('portainer.kubernetes').factory('KubernetesEndpoints', function K
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
}

View File

@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesEvents', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -7,7 +7,7 @@ angular.module('portainer.kubernetes').factory('KubernetesHealth', [
API_ENDPOINT_ENDPOINTS + '/:id/kubernetes/healthz',
{},
{
ping: { method: 'GET', timeout: 15000, params: { id: 'id' } },
ping: { method: 'GET', params: { id: 'id' } },
}
);
},

View File

@@ -16,7 +16,6 @@ angular.module('portainer.kubernetes').factory('KubernetesNamespaces', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesPersistentVolumeClaims
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -18,7 +18,6 @@ angular.module('portainer.kubernetes').factory('KubernetesPods', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesReplicaSets', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesResourceQuotas', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesSecrets', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesServices', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesStatefulSets', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -16,7 +16,6 @@ angular.module('portainer.kubernetes').factory('KubernetesStorage', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -18,6 +18,7 @@ import KubernetesServiceHelper from 'Kubernetes/helpers/serviceHelper';
import { KubernetesHorizontalPodAutoScalerHelper } from 'Kubernetes/horizontal-pod-auto-scaler/helper';
import { KubernetesHorizontalPodAutoScalerConverter } from 'Kubernetes/horizontal-pod-auto-scaler/converter';
import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
import KubernetesPodConverter from 'Kubernetes/pod/converter';
class KubernetesApplicationService {
/* #region CONSTRUCTOR */
@@ -70,8 +71,10 @@ 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');
throw new PortainerError('Unable to determine which association to use to retrieve API Service');
}
return apiService;
}
@@ -87,15 +90,18 @@ class KubernetesApplicationService {
/* #region GET */
async getAsync(namespace, name) {
try {
const [deployment, daemonSet, statefulSet, pods, autoScalers, ingresses] = await Promise.allSettled([
const [deployment, daemonSet, statefulSet, pod, pods, autoScalers, ingresses] = await Promise.allSettled([
this.KubernetesDeploymentService.get(namespace, name),
this.KubernetesDaemonSetService.get(namespace, name),
this.KubernetesStatefulSetService.get(namespace, name),
this.KubernetesPodService.get(namespace, name),
this.KubernetesPodService.get(namespace),
this.KubernetesHorizontalPodAutoScalerService.get(namespace),
this.KubernetesIngressService.get(namespace),
]);
// const pod = _.find(pods.value, ['metadata.namespace', namespace, 'metadata.name', name]);
let rootItem;
let converterFunc;
if (deployment.status === 'fulfilled') {
@@ -107,8 +113,11 @@ class KubernetesApplicationService {
} else if (statefulSet.status === 'fulfilled') {
rootItem = statefulSet;
converterFunc = KubernetesApplicationConverter.apiStatefulSetToapplication;
} else if (pod.status === 'fulfilled') {
rootItem = pod;
converterFunc = KubernetesApplicationConverter.apiPodToApplication;
} else {
throw new PortainerError('Unable to determine which association to use');
throw new PortainerError('Unable to determine which association to use to convert application');
}
const services = await this.KubernetesServiceService.get(namespace);
@@ -118,6 +127,7 @@ class KubernetesApplicationService {
const application = converterFunc(rootItem.value.Raw, pods.value, service.Raw, ingresses.value);
application.Yaml = rootItem.value.Yaml;
application.Raw = rootItem.value.Raw;
application.Pods = _.map(application.Pods, (item) => KubernetesPodConverter.apiToModel(item));
application.Containers = KubernetesApplicationHelper.associateContainersAndApplication(application);
const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers.value, application);
@@ -173,7 +183,14 @@ class KubernetesApplicationService {
convertToApplication(item, KubernetesApplicationConverter.apiStatefulSetToapplication, services, pods, ingresses)
);
const applications = _.concat(deploymentApplications, daemonSetApplications, statefulSetApplications);
const boundPods = _.concat(_.flatMap(deploymentApplications, 'Pods'), _.flatMap(daemonSetApplications, 'Pods'), _.flatMap(statefulSetApplications, 'Pods'));
const unboundPods = _.without(pods, ...boundPods);
const nakedPodsApplications = _.map(unboundPods, (item) => convertToApplication(item, KubernetesApplicationConverter.apiPodToApplication, services, pods, ingresses));
const applications = _.concat(deploymentApplications, daemonSetApplications, statefulSetApplications, nakedPodsApplications);
_.forEach(applications, (app) => {
app.Pods = _.map(app.Pods, (item) => KubernetesPodConverter.apiToModel(item));
});
await Promise.all(
_.forEach(applications, async (application) => {
const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers, application);

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

@@ -32,13 +32,17 @@ class KubernetesHistoryService {
case KubernetesApplicationTypes.STATEFULSET:
rawRevisions = await this.KubernetesControllerRevisionService.get(namespace);
break;
case KubernetesApplicationTypes.POD:
rawRevisions = [];
break;
default:
throw new PortainerError('Unable to determine which association to use');
throw new PortainerError('Unable to determine which association to use for history');
}
if (rawRevisions.length) {
const [currentRevision, revisionsList] = KubernetesHistoryHelper.getRevisions(rawRevisions, application);
application.CurrentRevision = currentRevision;
application.Revisions = revisionsList;
}
const [currentRevision, revisionsList] = KubernetesHistoryHelper.getRevisions(rawRevisions, application);
application.CurrentRevision = currentRevision;
application.Revisions = revisionsList;
return application;
} catch (err) {
throw new PortainerError('', err);

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

@@ -43,16 +43,21 @@
</tr>
<tr>
<td>Status</td>
<td>
<td ng-if="ctrl.application.ApplicationType !== ctrl.KubernetesApplicationTypes.POD">
<span ng-if="ctrl.application.DeploymentType === ctrl.KubernetesApplicationDeploymentTypes.REPLICATED">Replicated</span>
<span ng-if="ctrl.application.DeploymentType === ctrl.KubernetesApplicationDeploymentTypes.GLOBAL">Global</span>
<code>{{ ctrl.application.RunningPodsCount }}</code> / <code>{{ ctrl.application.TotalPodsCount }}</code>
</td>
<td ng-if="ctrl.application.ApplicationType === ctrl.KubernetesApplicationTypes.POD">
{{ ctrl.application.Pods[0].Status }}
</td>
</tr>
<tr ng-if="ctrl.application.Requests.Cpu || ctrl.application.Requests.Memory">
<td>
<div>Resource reservations</div>
<div class="text-muted small">per instance</div>
<div ng-if="ctrl.application.ApplicationType !== ctrl.KubernetesApplicationTypes.POD" class="text-muted small">
per instance
</div>
</td>
<td>
<div ng-if="ctrl.application.Requests.Cpu">CPU {{ ctrl.application.Requests.Cpu | kubernetesApplicationCPUValue }}</div>
@@ -557,7 +562,8 @@
title-icon="fa-server"
dataset="ctrl.allContainers"
table-key="kubernetes.application.containers"
order-by="PodName"
is-pod="ctrl.application.ApplicationType === ctrl.KubernetesApplicationTypes.POD"
order-by="{{ ctrl.application.ApplicationType === ctrl.KubernetesApplicationTypes.POD ? 'Name' : 'PodName' }}"
>
</kubernetes-containers-datatable>
</div>

View File

@@ -1,7 +1,7 @@
import angular from 'angular';
import * as _ from 'lodash-es';
import * as JsonPatch from 'fast-json-patch';
import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models';
import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
@@ -40,6 +40,10 @@ function computeTolerations(nodes, application) {
// Some operators require empty "values" field, some only one element in "values" field, etc
function computeAffinities(nodes, application) {
if (!application.Pods || application.Pods.length === 0) {
return nodes;
}
const pod = application.Pods[0];
_.forEach(nodes, (n) => {
if (pod.NodeSelector) {
@@ -119,6 +123,8 @@ class KubernetesApplicationController {
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
this.KubernetesApplicationTypes = KubernetesApplicationTypes;
this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies;
this.KubernetesServiceTypes = KubernetesServiceTypes;
this.KubernetesPodContainerTypes = KubernetesPodContainerTypes;
@@ -140,7 +146,7 @@ class KubernetesApplicationController {
showEditor() {
this.state.showEditorTab = true;
this.selectTab(2);
this.selectTab(3);
}
isSystemNamespace() {
@@ -336,7 +342,6 @@ class KubernetesApplicationController {
SelectedRevision: undefined,
};
this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
await this.getApplication();
await this.getEvents();
this.state.viewReady = true;

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

@@ -63,7 +63,7 @@
</thead>
<tbody>
<tr
dir-paginate="item in $ctrl.state.filteredDataSet | itemsPerPage: $ctrl.state.paginatedItemLimit"
dir-paginate="item in $ctrl.state.filteredDataSet | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit"
total-items="$ctrl.state.totalFilteredDataSet"
ng-class="{ active: item.Checked }"
>

View File

@@ -70,7 +70,7 @@ angular.module('portainer.app').controller('GenericDatatableController', [
item.Checked = !item.Checked;
this.state.firstClickedItem = item;
}
this.state.selectedItems = this.state.filteredDataSet.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;">
@@ -175,7 +183,7 @@
</div>
<!-- !tags -->
<!-- endpoint-security -->
<div ng-if="endpointType === 'remote' && !state.azureEndpoint && !state.edgeEndpoint && endpoint.Type !== 6">
<div ng-if="endpointType === 'remote' && !state.azureEndpoint && !state.kubernetesEndpoint && !state.edgeEndpoint && endpoint.Type !== 6">
<div class="col-sm-12 form-section-title">
Security
</div>

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

@@ -61,10 +61,14 @@ class InitEndpointController {
case PortainerEndpointConnectionTypes.AGENT:
return this.createAgentEndpoint();
default:
this.Notifications.error('Failure', 'Unable to determine which action to do');
this.Notifications.error('Failure', 'Unable to determine which action to do to create endpoint');
}
}
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

@@ -1,4 +1,5 @@
angular.module('portainer.app').controller('SidebarController', [
'$rootScope',
'$q',
'$scope',
'$transitions',
@@ -7,7 +8,7 @@ angular.module('portainer.app').controller('SidebarController', [
'Authentication',
'UserService',
'EndpointProvider',
function ($q, $scope, $transitions, StateManager, Notifications, Authentication, UserService, EndpointProvider) {
function ($rootScope, $q, $scope, $transitions, StateManager, Notifications, Authentication, UserService, EndpointProvider) {
function checkPermissions(memberships) {
var isLeader = false;
angular.forEach(memberships, function (membership) {
@@ -52,6 +53,10 @@ angular.module('portainer.app').controller('SidebarController', [
$transitions.onEnter({}, async () => {
$scope.showStacks = await shouldShowStacks();
if ($scope.applicationState.endpoint.name) {
document.title = `${$rootScope.defaultTitle} | ${$scope.applicationState.endpoint.name}`;
}
});
},
]);

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

@@ -0,0 +1,7 @@
param (
[string]$kompose_version
)
$ErrorActionPreference = "Stop";
Invoke-WebRequest -O "dist/kompose.exe" "https://github.com/kubernetes/kompose/releases/download/$($kompose_version)/kompose-windows-amd64.exe"

View File

@@ -0,0 +1,7 @@
param (
[string]$kubectl_version
)
$ErrorActionPreference = "Stop";
Invoke-WebRequest -O "dist/kubectl.exe" "https://storage.googleapis.com/kubernetes-release/release/$($kubectl_version)/bin/windows/amd64/kubectl.exe"

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,
@@ -194,8 +194,9 @@ function shell_download_docker_binary(p, a) {
return ['if [ -f dist/docker ]; then', 'echo "docker binary exists";', 'else', 'build/download_docker_binary.sh ' + ip + ' ' + ia + ' ' + binaryVersion + ';', 'fi'].join(' ');
} else {
return [
'powershell -Command "& {if (Get-Item -Path dist/docker.exe -ErrorAction:SilentlyContinue) {',
'Write-Host "Docker binary exists"',
'powershell -Command "& {if (Test-Path -Path "dist/docker.exe") {',
'Write-Host "Skipping download, Docker binary exists"',
'return',
'} else {',
'& ".\\build\\download_docker_binary.ps1" -docker_version ' + binaryVersion + '',
'}}"',
@@ -210,10 +211,11 @@ function shell_download_kompose_binary(p, a) {
return ['if [ -f dist/kompose ]; then', 'echo "kompose binary exists";', 'else', 'build/download_kompose_binary.sh ' + p + ' ' + a + ' ' + binaryVersion + ';', 'fi'].join(' ');
} else {
return [
'powershell -Command "& {if (Get-Item -Path dist/kompose.exe -ErrorAction:SilentlyContinue) {',
'Write-Host "Docker binary exists"',
'powershell -Command "& {if (Test-Path -Path "dist/kompose.exe") {',
'Write-Host "Skipping download, Kompose binary exists"',
'return',
'} else {',
'& ".\\build\\download_kompose_binary.ps1" -docker_version ' + binaryVersion + '',
'& ".\\build\\download_kompose_binary.ps1" -kompose_version ' + binaryVersion + '',
'}}"',
].join(' ');
}
@@ -226,10 +228,11 @@ function shell_download_kubectl_binary(p, a) {
return ['if [ -f dist/kubectl ]; then', 'echo "kubectl binary exists";', 'else', 'build/download_kubectl_binary.sh ' + p + ' ' + a + ' ' + binaryVersion + ';', 'fi'].join(' ');
} else {
return [
'powershell -Command "& {if (Get-Item -Path dist/kubectl.exe -ErrorAction:SilentlyContinue) {',
'Write-Host "Docker binary exists"',
'powershell -Command "& {if (Test-Path -Path "dist/kubectl.exe") {',
'Write-Host "Skipping download, Kubectl binary exists"',
'return',
'} else {',
'& ".\\build\\download_kubectl_binary.ps1" -docker_version ' + binaryVersion + '',
'& ".\\build\\download_kubectl_binary.ps1" -kubectl_version ' + binaryVersion + '',
'}}"',
].join(' ');
}

Some files were not shown because too many files have changed in this diff Show More