Compare commits
46 Commits
fix-k8s-sy
...
feat/GH/40
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6b126e8f6 | ||
|
|
b67c0e870c | ||
|
|
067257df2b | ||
|
|
5f2f7a87ab | ||
|
|
f656ad7124 | ||
|
|
f681e2d532 | ||
|
|
fdb9bf09de | ||
|
|
92ad3e788d | ||
|
|
bc2f5a3260 | ||
|
|
487123491e | ||
|
|
380f106571 | ||
|
|
341378e783 | ||
|
|
b360936454 | ||
|
|
8204d32538 | ||
|
|
60c5ab3eec | ||
|
|
20cf948e53 | ||
|
|
45fcb1ad26 | ||
|
|
7398d54ed0 | ||
|
|
faded67deb | ||
|
|
eadd8b36d6 | ||
|
|
1ad4623b08 | ||
|
|
890bbf4058 | ||
|
|
865c8d899b | ||
|
|
aa5277de2e | ||
|
|
9136ba30eb | ||
|
|
3d9c10adf1 | ||
|
|
0d20988bef | ||
|
|
1545a42f08 | ||
|
|
a12f2ee893 | ||
|
|
174e28b850 | ||
|
|
3da9751c82 | ||
|
|
ccea7cca3d | ||
|
|
43891703c2 | ||
|
|
74429d6d46 | ||
|
|
bb5c2c2875 | ||
|
|
3e82d01894 | ||
|
|
9e80037e72 | ||
|
|
da29c2b6a5 | ||
|
|
0ed4d443ee | ||
|
|
a4fa44f831 | ||
|
|
e479e41aee | ||
|
|
d4c4c4e895 | ||
|
|
466bd24648 | ||
|
|
2fc60f14e1 | ||
|
|
9300603777 | ||
|
|
8dac2df7bf |
4
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
4
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
@@ -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
2
.github/stale.yml
vendored
@@ -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.
|
||||
|
||||
16
README.md
16
README.md
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -18,7 +18,6 @@ angular.module('portainer.docker').factory('Network', [
|
||||
method: 'GET',
|
||||
isArray: true,
|
||||
interceptor: NetworksInterceptor,
|
||||
timeout: 15000,
|
||||
},
|
||||
get: {
|
||||
method: 'GET',
|
||||
|
||||
@@ -35,7 +35,6 @@ angular.module('portainer.docker').factory('Service', [
|
||||
logs: {
|
||||
method: 'GET',
|
||||
params: { id: '@id', action: 'logs' },
|
||||
timeout: 4500,
|
||||
ignoreLoadingBar: true,
|
||||
transformResponse: logsHandler,
|
||||
},
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -17,7 +17,6 @@ angular.module('portainer.docker').factory('Task', [
|
||||
logs: {
|
||||
method: 'GET',
|
||||
params: { id: '@id', action: 'logs' },
|
||||
timeout: 4500,
|
||||
ignoreLoadingBar: true,
|
||||
transformResponse: logsHandler,
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -520,6 +520,12 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
||||
return true;
|
||||
}
|
||||
|
||||
$scope.volumesAreValid = volumesAreValid;
|
||||
function volumesAreValid() {
|
||||
const volumes = $scope.formValues.Volumes;
|
||||
return volumes.every((volume) => volume.Target && volume.Source);
|
||||
}
|
||||
|
||||
$scope.create = function createService() {
|
||||
var accessControlData = $scope.formValues.AccessControlData;
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="state.actionInProgress || !formValues.RegistryModel.Image"
|
||||
ng-disabled="state.actionInProgress || !formValues.RegistryModel.Image || !volumesAreValid()"
|
||||
ng-click="create()"
|
||||
button-spinner="state.actionInProgress"
|
||||
>
|
||||
@@ -298,13 +298,16 @@
|
||||
<!-- volume-line1 -->
|
||||
<div class="col-sm-12 form-inline">
|
||||
<!-- container-path -->
|
||||
<div class="input-group input-group-sm col-sm-6">
|
||||
<span class="input-group-addon">container</span>
|
||||
<input type="text" class="form-control" ng-model="volume.Target" placeholder="e.g. /path/in/container" />
|
||||
<div class="input-group col-sm-6">
|
||||
<div class="input-group input-group-sm w-full">
|
||||
<span class="input-group-addon">container</span>
|
||||
<input type="text" class="form-control" ng-model="volume.Target" placeholder="e.g. /path/in/container" />
|
||||
</div>
|
||||
<div class="small text-warning" ng-show="!volume.Target"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Target is required. </div>
|
||||
</div>
|
||||
<!-- !container-path -->
|
||||
<!-- volume-type -->
|
||||
<div class="input-group col-sm-5" style="margin-left: 5px;">
|
||||
<div class="input-group col-sm-5" style="margin-left: 5px; vertical-align: top;">
|
||||
<div class="btn-group btn-group-sm" ng-if="allowBindMounts">
|
||||
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'volume'" ng-click="volume.name = ''">Volume</label>
|
||||
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'bind'" ng-click="volume.Id = ''">Bind</label>
|
||||
@@ -318,27 +321,35 @@
|
||||
<!-- !volume-line1 -->
|
||||
<!-- volume-line2 -->
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 5px;">
|
||||
<i class="fa fa-long-arrow-alt-right" aria-hidden="true"></i>
|
||||
<div style="height: 30px; display: inline-block; vertical-align: top; display: inline-flex; align-items: center;">
|
||||
<i class="fa fa-long-arrow-alt-right" aria-hidden="true"></i>
|
||||
</div>
|
||||
<!-- volume -->
|
||||
<div class="input-group input-group-sm col-sm-6" ng-if="volume.Type === 'volume'">
|
||||
<span class="input-group-addon">volume</span>
|
||||
<select
|
||||
class="form-control"
|
||||
ng-model="volume.Source"
|
||||
ng-options="vol as ((vol.Id|truncate:30) + ' - ' + (vol.Driver|truncate:30)) for vol in availableVolumes"
|
||||
>
|
||||
<option selected disabled hidden value="">Select a volume</option>
|
||||
</select>
|
||||
<div class="col-sm-6 input-group" ng-if="volume.Type === 'volume'" style="float: none; padding: 0;">
|
||||
<div class="input-group input-group-sm w-full">
|
||||
<span class="input-group-addon">volume</span>
|
||||
<select
|
||||
class="form-control"
|
||||
ng-model="volume.Source"
|
||||
ng-options="vol as ((vol.Id|truncate:30) + ' - ' + (vol.Driver|truncate:30)) for vol in availableVolumes"
|
||||
>
|
||||
<option selected disabled hidden value="">Select a volume</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="small text-warning" ng-show="!volume.Source"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Source is required. </div>
|
||||
</div>
|
||||
<!-- !volume -->
|
||||
<!-- bind -->
|
||||
<div class="input-group input-group-sm col-sm-6" ng-if="volume.Type === 'bind'">
|
||||
<span class="input-group-addon">host</span>
|
||||
<input type="text" class="form-control" ng-model="volume.Source" placeholder="e.g. /path/on/host" />
|
||||
<div class="input-group input-group-sm w-full">
|
||||
<span class="input-group-addon">host</span>
|
||||
<input type="text" class="form-control" ng-model="volume.Source" placeholder="e.g. /path/on/host" />
|
||||
</div>
|
||||
<div class="small text-warning" ng-show="!volume.Source"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Source is required. </div>
|
||||
</div>
|
||||
<!-- !bind -->
|
||||
<!-- read-only -->
|
||||
<div class="input-group input-group-sm col-sm-5" style="margin-left: 5px;">
|
||||
<div class="input-group input-group-sm col-sm-5" style="margin-left: 5px; vertical-align: top;">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<label class="btn btn-primary" ng-model="volume.ReadOnly" uib-btn-radio="false">Writable</label>
|
||||
<label class="btn btn-primary" ng-model="volume.ReadOnly" uib-btn-radio="true">Read-only</label>
|
||||
|
||||
@@ -42,12 +42,14 @@
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name=""
|
||||
ng-model="mount.Source"
|
||||
placeholder="e.g. /tmp/portainer/data"
|
||||
ng-change="updateMount(service, mount)"
|
||||
ng-disabled="isUpdating || (!isAdmin && !allowBindMounts && mount.Type === 'bind')"
|
||||
ng-if="mount.Type === 'bind'"
|
||||
/>
|
||||
<div class="col-sm-12 small text-warning" ng-show="!mount.Source"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Source is required. </div>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
@@ -59,6 +61,7 @@
|
||||
ng-disabled="isUpdating"
|
||||
disable-authorization="DockerServiceUpdate"
|
||||
/>
|
||||
<div class="col-sm-12 small text-warning" ng-show="!mount.Target"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Target is required. </div>
|
||||
</td>
|
||||
<td authorization="DockerServiceUpdate">
|
||||
<input type="checkbox" class="form-control" ng-model="mount.ReadOnly" ng-change="updateMount(service, mount)" ng-disabled="isUpdating" />
|
||||
@@ -77,7 +80,9 @@
|
||||
<rd-widget-footer authorization="DockerServiceUpdate">
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['ServiceMounts'])" ng-click="updateService(service)">Apply changes</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!mountsAreValid() || !hasChanges(service, ['ServiceMounts'])" ng-click="updateService(service)">
|
||||
Apply changes
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
|
||||
@@ -378,6 +378,12 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||
return hasChanges;
|
||||
};
|
||||
|
||||
$scope.mountsAreValid = mountsAreValid;
|
||||
function mountsAreValid() {
|
||||
const mounts = $scope.service.ServiceMounts;
|
||||
return mounts.every((mount) => mount.Source && mount.Target);
|
||||
}
|
||||
|
||||
function buildChanges(service) {
|
||||
var config = ServiceHelper.serviceToConfig(service.Model);
|
||||
config.Name = service.Name;
|
||||
|
||||
@@ -12,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' } },
|
||||
|
||||
@@ -14,7 +14,6 @@ angular.module('portainer.kubernetes').factory('KubernetesComponentStatus', [
|
||||
{
|
||||
get: {
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
ignoreLoadingBar: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -8,5 +8,6 @@ angular.module('portainer.kubernetes').component('kubernetesContainersDatatable'
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
refreshCallback: '<',
|
||||
isPod: '<',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> Create entry
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-default" ngf-select="$ctrl.addEntryFromFile($file)" style="margin-left: 0;">
|
||||
<i class="fa fa-file-upload" aria-hidden="true"></i> Create entry from file
|
||||
<i class="fa fa-file-upload" aria-hidden="true"></i> Create key/value from file
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,6 @@
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="kubernetes.cluster({endpointId: $ctrl.endpointId})" ui-sref-active="active">Cluster <span class="menu-icon fa fa-server fa-fw"></span></a>
|
||||
<div class="sidebar-sublist" ng-if="$ctrl.adminAccess && ($ctrl.currentState === 'kubernetes.cluster' || $ctrl.currentState === 'portainer.endpoints.endpoint.kubernetesConfig')">
|
||||
<a ui-sref="portainer.endpoints.endpoint.kubernetesConfig({id: $ctrl.endpointId})">Setup</a>
|
||||
<a ui-sref="portainer.endpoints.endpoint.kubernetesConfig({id: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ angular.module('portainer.kubernetes').factory('KubernetesEndpoints', [
|
||||
{
|
||||
get: {
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
ignoreLoadingBar: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@ angular
|
||||
return KubernetesApplicationTypeStrings.DAEMONSET;
|
||||
case KubernetesApplicationTypes.STATEFULSET:
|
||||
return KubernetesApplicationTypeStrings.STATEFULSET;
|
||||
case KubernetesApplicationTypes.POD:
|
||||
return KubernetesApplicationTypeStrings.POD;
|
||||
default:
|
||||
return '-';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesHorizontalPodAutoScale
|
||||
{
|
||||
get: {
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
ignoreLoadingBar: true,
|
||||
},
|
||||
getYaml: {
|
||||
|
||||
@@ -15,7 +15,6 @@ function factory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||
{
|
||||
get: {
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
ignoreLoadingBar: true,
|
||||
},
|
||||
getYaml: {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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: '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ angular.module('portainer.kubernetes').factory('KubernetesNodes', [
|
||||
{
|
||||
get: {
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
ignoreLoadingBar: true,
|
||||
},
|
||||
getYaml: {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesConfigMaps', [
|
||||
{
|
||||
get: {
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
ignoreLoadingBar: true,
|
||||
},
|
||||
getYaml: {
|
||||
|
||||
@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesControllerRevisions',
|
||||
{
|
||||
get: {
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
ignoreLoadingBar: true,
|
||||
},
|
||||
getYaml: {
|
||||
|
||||
@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesDaemonSets', [
|
||||
{
|
||||
get: {
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
ignoreLoadingBar: true,
|
||||
},
|
||||
getYaml: {
|
||||
|
||||
@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesDeployments', [
|
||||
{
|
||||
get: {
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
ignoreLoadingBar: true,
|
||||
},
|
||||
getYaml: {
|
||||
|
||||
@@ -11,7 +11,6 @@ angular.module('portainer.kubernetes').factory('KubernetesEndpoints', function K
|
||||
{
|
||||
get: {
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
ignoreLoadingBar: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesEvents', [
|
||||
{
|
||||
get: {
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
ignoreLoadingBar: true,
|
||||
},
|
||||
getYaml: {
|
||||
|
||||
@@ -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' } },
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
@@ -16,7 +16,6 @@ angular.module('portainer.kubernetes').factory('KubernetesNamespaces', [
|
||||
{
|
||||
get: {
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
ignoreLoadingBar: true,
|
||||
},
|
||||
getYaml: {
|
||||
|
||||
@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesPersistentVolumeClaims
|
||||
{
|
||||
get: {
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
ignoreLoadingBar: true,
|
||||
},
|
||||
getYaml: {
|
||||
|
||||
@@ -18,7 +18,6 @@ angular.module('portainer.kubernetes').factory('KubernetesPods', [
|
||||
{
|
||||
get: {
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
ignoreLoadingBar: true,
|
||||
},
|
||||
getYaml: {
|
||||
|
||||
@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesReplicaSets', [
|
||||
{
|
||||
get: {
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
ignoreLoadingBar: true,
|
||||
},
|
||||
getYaml: {
|
||||
|
||||
@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesResourceQuotas', [
|
||||
{
|
||||
get: {
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
ignoreLoadingBar: true,
|
||||
},
|
||||
getYaml: {
|
||||
|
||||
@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesSecrets', [
|
||||
{
|
||||
get: {
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
ignoreLoadingBar: true,
|
||||
},
|
||||
getYaml: {
|
||||
|
||||
@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesServices', [
|
||||
{
|
||||
get: {
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
ignoreLoadingBar: true,
|
||||
},
|
||||
getYaml: {
|
||||
|
||||
@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesStatefulSets', [
|
||||
{
|
||||
get: {
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
ignoreLoadingBar: true,
|
||||
},
|
||||
getYaml: {
|
||||
|
||||
@@ -16,7 +16,6 @@ angular.module('portainer.kubernetes').factory('KubernetesStorage', [
|
||||
{
|
||||
get: {
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
ignoreLoadingBar: true,
|
||||
},
|
||||
getYaml: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -79,7 +79,11 @@ class KubernetesApplicationsController {
|
||||
}
|
||||
|
||||
removeAction(selectedItems) {
|
||||
return this.$async(this.removeActionAsync, selectedItems);
|
||||
this.ModalService.confirmDeletion('Do you want to remove the selected application(s)?', (confirmed) => {
|
||||
if (confirmed) {
|
||||
return this.$async(this.removeActionAsync, selectedItems);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onPublishingModeClick(application) {
|
||||
@@ -87,7 +91,7 @@ class KubernetesApplicationsController {
|
||||
_.forEach(this.ports, (item) => {
|
||||
item.Expanded = false;
|
||||
item.Highlighted = false;
|
||||
if (item.Name === application.Name) {
|
||||
if (item.Name === application.Name && item.Ports.length > 1) {
|
||||
item.Expanded = true;
|
||||
item.Highlighted = true;
|
||||
}
|
||||
|
||||
@@ -778,6 +778,7 @@
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="replica_count"
|
||||
class="form-control"
|
||||
min="1"
|
||||
max="9999"
|
||||
@@ -785,10 +786,19 @@
|
||||
style="margin-left: 20px;"
|
||||
ng-model="ctrl.formValues.ReplicaCount"
|
||||
ng-disabled="!ctrl.supportScalableReplicaDeployment()"
|
||||
ng-change="ctrl.enforceReplicaCountMinimum()"
|
||||
ng-change="ctrl.onChangeVolumeRequestedSize()"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="kubernetesApplicationCreationForm['replica_count'].$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<ng-messages for="kubernetesApplicationCreationForm['replica_count'].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Instance count is required.</p>
|
||||
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Instance count must be greater than 0.</p>
|
||||
</ng-messages>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !replica count -->
|
||||
|
||||
<div
|
||||
|
||||
@@ -488,12 +488,6 @@ class KubernetesCreateApplicationController {
|
||||
return _.uniq(storageOptions).join(', ');
|
||||
}
|
||||
|
||||
enforceReplicaCountMinimum() {
|
||||
if (this.formValues.ReplicaCount === null) {
|
||||
this.formValues.ReplicaCount = 1;
|
||||
}
|
||||
}
|
||||
|
||||
resourceQuotaCapacityExceeded() {
|
||||
return !this.state.sliders.memory.max || !this.state.sliders.cpu.max;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -339,8 +339,8 @@ class KubernetesNodeController {
|
||||
|
||||
this.availableEffects = _.values(KubernetesNodeTaintEffects);
|
||||
this.formValues = KubernetesNodeConverter.nodeToFormValues(this.node);
|
||||
this.formValues.Labels = KubernetesNodeHelper.reorderLabels(this.formValues.Labels);
|
||||
this.formValues.Labels = KubernetesNodeHelper.computeUsedLabels(this.applications, this.formValues.Labels);
|
||||
this.formValues.Labels = KubernetesNodeHelper.reorderLabels(this.formValues.Labels);
|
||||
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@ import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelpe
|
||||
|
||||
class KubernetesConfigurationsController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, Notifications, KubernetesConfigurationService, KubernetesApplicationService) {
|
||||
constructor($async, $state, Notifications, KubernetesConfigurationService, KubernetesApplicationService, ModalService) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.Notifications = Notifications;
|
||||
this.KubernetesConfigurationService = KubernetesConfigurationService;
|
||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||
this.ModalService = ModalService;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.getConfigurations = this.getConfigurations.bind(this);
|
||||
@@ -56,7 +57,11 @@ class KubernetesConfigurationsController {
|
||||
}
|
||||
|
||||
removeAction(selectedItems) {
|
||||
return this.$async(this.removeActionAsync, selectedItems);
|
||||
this.ModalService.confirmDeletion('Do you want to remove the selected configuration(s)?', (confirmed) => {
|
||||
if (confirmed) {
|
||||
return this.$async(this.removeActionAsync, selectedItems);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getApplicationsAsync() {
|
||||
|
||||
@@ -25,6 +25,10 @@ class KubernetesCreateConfigurationController {
|
||||
this.state.alreadyExist = _.find(filteredConfigurations, (config) => config.Name === this.formValues.Name) !== undefined;
|
||||
}
|
||||
|
||||
onResourcePoolSelectionChange() {
|
||||
this.onChangeName();
|
||||
}
|
||||
|
||||
isFormValid() {
|
||||
const uniqueCheck = !this.state.alreadyExist && this.state.isDataValid;
|
||||
if (this.formValues.IsSimple) {
|
||||
|
||||
@@ -132,7 +132,53 @@
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Metrics
|
||||
Security
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
By default, all the users have access to the default namespace. Enable this option to set accesses on the default namespace.
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Restrict access to the default namespace
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" disabled /><i></i> </label>
|
||||
<span class="text-muted small" style="margin-left: 15px;">
|
||||
<i class="fa fa-user" aria-hidden="true"></i>
|
||||
This feature is available in <a href="https://www.portainer.io/business-upsell?from=k8s-setup-default" target="_blank"> Portainer Business Edition</a>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Resources and Metrics
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
By ENABLING resource over-commit, you are able to assign more resources to namespaces than is physically available in the cluster. This may lead to unexpected
|
||||
deployment failures if there is insufficient resource to service demand.
|
||||
<p style="margin-top: 2px;">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
By DISABLING resource over-commit (highly recommended), you are only able to assign resources to namespaces that are less (in aggregate) than the cluster total
|
||||
minus any system resource reservation.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Allow resource over-commit
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" checked disabled /><i></i> </label>
|
||||
<span class="text-muted small" style="margin-left: 15px;">
|
||||
<i class="fa fa-user" aria-hidden="true"></i>
|
||||
This feature is available in <a href="https://www.portainer.io/business-upsell?from=k8s-setup-overcommit" target="_blank"> Portainer Business Edition</a>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
|
||||
@@ -69,8 +69,14 @@
|
||||
</p>
|
||||
</span>
|
||||
<span class="col-sm-12 text-muted small" ng-show="ctrl.state.DeployType === ctrl.ManifestDeployTypes.KUBERNETES">
|
||||
You can get more information about Kubernetes file format in the
|
||||
<a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/" target="_blank">official documentation</a>.
|
||||
<p>
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This feature allows you to deploy any kind of Kubernetes resource in this environment (Deployment, Secret, ConfigMap...).
|
||||
</p>
|
||||
<p>
|
||||
You can get more information about Kubernetes file format in the
|
||||
<a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/" target="_blank">official documentation</a>.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
||||
@@ -129,6 +129,63 @@
|
||||
</div>
|
||||
<!-- #endregion -->
|
||||
|
||||
<!-- #region LOAD-BALANCERS -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Load balancers
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
You can set a quota on the amount of external load balancers that can be created inside this resource pool. Set this quota to 0 to effectively disable the use of
|
||||
load balancers in this resource pool.
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Load Balancer quota
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" disabled /><i></i> </label>
|
||||
<span class="text-muted small" style="margin-left: 15px;">
|
||||
<i class="fa fa-user" aria-hidden="true"></i>
|
||||
This feature is available in <a href="https://www.portainer.io/business-upsell?from=k8s-resourcepool-lbquota" target="_blank"> Portainer Business Edition</a>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- #endregion -->
|
||||
|
||||
<!-- #region STORAGES -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Storages
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Quotas can be set on each storage option to prevent users from exceeding a specific threshold when deploying applications. You can set a quota to 0 to effectively
|
||||
prevent the usage of a specific storage option inside this resource pool.
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-12 form-section-title">
|
||||
<i class="fa fa-route" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
standard
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Enable quota
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" disabled /><i></i> </label>
|
||||
<span class="text-muted small" style="margin-left: 15px;">
|
||||
<i class="fa fa-user" aria-hidden="true"></i>
|
||||
This feature is available in
|
||||
<a href="https://www.portainer.io/business-upsell?from=k8s-resourcepool-storagequota" target="_blank"> Portainer Business Edition</a>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- #endregion -->
|
||||
|
||||
<div ng-if="ctrl.state.canUseIngress">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Ingresses
|
||||
|
||||
@@ -250,6 +250,63 @@
|
||||
</div>
|
||||
<!-- #endregion -->
|
||||
</div>
|
||||
<!-- #region LOAD-BALANCERS -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Load balancers
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
You can set a quota on the amount of external load balancers that can be created inside this resource pool. Set this quota to 0 to effectively disable the use
|
||||
of load balancers in this resource pool.
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Load Balancer quota
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" disabled /><i></i> </label>
|
||||
<span class="text-muted small" style="margin-left: 15px;">
|
||||
<i class="fa fa-user" aria-hidden="true"></i>
|
||||
This feature is available in <a href="https://www.portainer.io/business-upsell?from=k8s-resourcepool-lbquota" target="_blank"> Portainer Business Edition</a>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- #endregion -->
|
||||
|
||||
<!-- #region LOAD-BALANCERS -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Storages
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Quotas can be set on each storage option to prevent users from exceeding a specific threshold when deploying applications. You can set a quota to 0 to
|
||||
effectively prevent the usage of a specific storage option inside this resource pool.
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-12 form-section-title">
|
||||
<i class="fa fa-route" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
standard
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Enable quota
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" disabled /><i></i> </label>
|
||||
<span class="text-muted small" style="margin-left: 15px;">
|
||||
<i class="fa fa-user" aria-hidden="true"></i>
|
||||
This feature is available in
|
||||
<a href="https://www.portainer.io/business-upsell?from=k8s-resourcepool-storagequota" target="_blank"> Portainer Business Edition</a>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- #endregion -->
|
||||
|
||||
<!-- actions -->
|
||||
<div ng-if="ctrl.isAdmin && ctrl.isEditable" class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
|
||||
@@ -392,6 +392,16 @@ angular.module('portainer.app', ['portainer.oauth']).config([
|
||||
},
|
||||
};
|
||||
|
||||
var roles = {
|
||||
name: 'portainer.roles',
|
||||
url: '/roles',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/roles/roles.html',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
$stateRegistryProvider.register(root);
|
||||
$stateRegistryProvider.register(endpointRoot);
|
||||
$stateRegistryProvider.register(portainer);
|
||||
@@ -422,6 +432,7 @@ angular.module('portainer.app', ['portainer.oauth']).config([
|
||||
$stateRegistryProvider.register(user);
|
||||
$stateRegistryProvider.register(teams);
|
||||
$stateRegistryProvider.register(team);
|
||||
$stateRegistryProvider.register(roles);
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@@ -28,6 +28,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Role
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-4">
|
||||
<span class="text-muted small">
|
||||
<i class="fa fa-user" aria-hidden="true"></i>
|
||||
This feature is available in <a href="https://www.portainer.io/business-upsell?from=k8s-rbac-access" target="_blank"> Portainer Business Edition</a>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- actions -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<div class="datatable">
|
||||
<div class="searchBar">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
<input type="text" class="searchInput" placeholder="Search..." ng-model-options="{ debounce: 300 }" />
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Endpoint
|
||||
</th>
|
||||
<th>
|
||||
Role
|
||||
</th>
|
||||
<th>Access origin</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="3" class="text-center text-muted">Select a user to show associated access and role</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
angular.module('portainer.app').component('accessViewerDatatable', {
|
||||
templateUrl: './accessViewerDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
dataset: '<',
|
||||
},
|
||||
});
|
||||
@@ -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 }"
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -66,6 +66,9 @@
|
||||
</td>
|
||||
<td>
|
||||
<a ui-sref="portainer.registries.registry.access({id: item.Id})" ng-if="$ctrl.accessManagement"> <i class="fa fa-users" aria-hidden="true"></i> Manage access </a>
|
||||
<span class="text-muted space-left" style="cursor: pointer;" data-toggle="tooltip" title="This feature is available in Portainer Business Edition">
|
||||
<i class="fa fa-search" aria-hidden="true"></i> Browse</span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<div class="datatable">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
|
||||
</div>
|
||||
<div class="searchBar">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
<input type="text" class="searchInput" placeholder="Search..." auto-focus ng-model-options="{ debounce: 300 }" />
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Name
|
||||
</th>
|
||||
<th>
|
||||
Description
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-muted">Endpoint administrator</td>
|
||||
<td class="text-muted">Full control of all resources in an endpoint</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">Helpdesk</td>
|
||||
<td class="text-muted">Read-only access of all resources in an endpoint</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">Read-only user</td>
|
||||
<td class="text-muted">Read-only access of assigned resources in an endpoint</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">Standard user</td>
|
||||
<td class="text-muted">Full control of assigned resources in an endpoint</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="footer">
|
||||
<div class="paginationControls">
|
||||
<form class="form-inline">
|
||||
<span class="limitSelector">
|
||||
<span style="margin-right: 5px;">
|
||||
Items per page
|
||||
</span>
|
||||
<select class="form-control">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</span>
|
||||
<dir-pagination-controls max-size="5"></dir-pagination-controls>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
angular.module('portainer.app').component('rolesDatatable', {
|
||||
templateUrl: './rolesDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
},
|
||||
});
|
||||
@@ -56,10 +56,10 @@ angular.module('portainer.app').factory('EndpointService', [
|
||||
return Endpoints.remove({ id: endpointID }).$promise;
|
||||
};
|
||||
|
||||
service.createLocalEndpoint = function () {
|
||||
service.createLocalEndpoint = function (name = 'local') {
|
||||
var deferred = $q.defer();
|
||||
|
||||
FileUploadService.createEndpoint('local', PortainerEndpointCreationTypes.LocalDockerEnvironment, '', '', 1, [], false)
|
||||
FileUploadService.createEndpoint(name, PortainerEndpointCreationTypes.LocalDockerEnvironment, '', '', 1, [], false)
|
||||
.then(function success(response) {
|
||||
deferred.resolve(response.data);
|
||||
})
|
||||
@@ -117,10 +117,10 @@ angular.module('portainer.app').factory('EndpointService', [
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.createLocalKubernetesEndpoint = function () {
|
||||
service.createLocalKubernetesEndpoint = function (name = 'local') {
|
||||
var deferred = $q.defer();
|
||||
|
||||
FileUploadService.createEndpoint('local', PortainerEndpointCreationTypes.LocalKubernetesEnvironment, '', '', 1, [], true, true, true)
|
||||
FileUploadService.createEndpoint(name, PortainerEndpointCreationTypes.LocalKubernetesEnvironment, '', '', 1, [], true, true, true)
|
||||
.then(function success(response) {
|
||||
deferred.resolve(response.data);
|
||||
})
|
||||
|
||||
@@ -19,6 +19,7 @@ angular
|
||||
) {
|
||||
$scope.state = {
|
||||
EnvironmentType: 'agent',
|
||||
PlatformType: 'linux',
|
||||
actionInProgress: false,
|
||||
deploymentTab: 0,
|
||||
allowCreateTag: Authentication.isAdmin(),
|
||||
@@ -56,8 +57,12 @@ angular
|
||||
};
|
||||
|
||||
$scope.copyAgentCommand = function () {
|
||||
if ($scope.state.deploymentTab === 2) {
|
||||
if ($scope.state.deploymentTab === 2 && $scope.state.PlatformType === 'linux') {
|
||||
clipboard.copyText('curl -L https://downloads.portainer.io/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent');
|
||||
} else if ($scope.state.deploymentTab === 2 && $scope.state.PlatformType === 'windows') {
|
||||
clipboard.copyText(
|
||||
'curl -L https://downloads.portainer.io/agent-stack-windows.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack-windows.yml portainer-agent'
|
||||
);
|
||||
} else if ($scope.state.deploymentTab === 1) {
|
||||
clipboard.copyText('curl -L https://downloads.portainer.io/portainer-agent-k8s-nodeport.yaml -o portainer-agent-k8s.yaml; kubectl apply -f portainer-agent-k8s.yaml');
|
||||
} else {
|
||||
@@ -84,40 +89,72 @@ angular
|
||||
$scope.availableTags = $scope.availableTags.concat(tag);
|
||||
$scope.formValues.TagIds = $scope.formValues.TagIds.concat(tag.Id);
|
||||
} catch (err) {
|
||||
Notifications.error('Failue', err, 'Unable to create tag');
|
||||
Notifications.error('Failure', err, 'Unable to create tag');
|
||||
}
|
||||
}
|
||||
|
||||
$scope.addDockerEndpoint = function () {
|
||||
if ($scope.formValues.ConnectSocket) {
|
||||
var endpointName = $scope.formValues.Name;
|
||||
$scope.state.actionInProgress = true;
|
||||
EndpointService.createLocalEndpoint(endpointName)
|
||||
.then(function success() {
|
||||
Notifications.success('Endpoint created', endpointName);
|
||||
$state.go('portainer.endpoints', {}, { reload: true });
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to create endpoint');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.actionInProgress = false;
|
||||
});
|
||||
} else {
|
||||
var name = $scope.formValues.Name;
|
||||
var URL = $filter('stripprotocol')($scope.formValues.URL);
|
||||
var publicURL = $scope.formValues.PublicURL === '' ? URL.split(':')[0] : $scope.formValues.PublicURL;
|
||||
var groupId = $scope.formValues.GroupId;
|
||||
var tagIds = $scope.formValues.TagIds;
|
||||
|
||||
var securityData = $scope.formValues.SecurityFormData;
|
||||
var TLS = securityData.TLS;
|
||||
var TLSMode = securityData.TLSMode;
|
||||
var TLSSkipVerify = TLS && (TLSMode === 'tls_client_noca' || TLSMode === 'tls_only');
|
||||
var TLSSkipClientVerify = TLS && (TLSMode === 'tls_ca' || TLSMode === 'tls_only');
|
||||
var TLSCAFile = TLSSkipVerify ? null : securityData.TLSCACert;
|
||||
var TLSCertFile = TLSSkipClientVerify ? null : securityData.TLSCert;
|
||||
var TLSKeyFile = TLSSkipClientVerify ? null : securityData.TLSKey;
|
||||
|
||||
addEndpoint(
|
||||
name,
|
||||
PortainerEndpointCreationTypes.LocalDockerEnvironment,
|
||||
URL,
|
||||
publicURL,
|
||||
groupId,
|
||||
tagIds,
|
||||
TLS,
|
||||
TLSSkipVerify,
|
||||
TLSSkipClientVerify,
|
||||
TLSCAFile,
|
||||
TLSCertFile,
|
||||
TLSKeyFile
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.addKubernetesEndpoint = function () {
|
||||
var name = $scope.formValues.Name;
|
||||
var URL = $filter('stripprotocol')($scope.formValues.URL);
|
||||
var publicURL = $scope.formValues.PublicURL === '' ? URL.split(':')[0] : $scope.formValues.PublicURL;
|
||||
var groupId = $scope.formValues.GroupId;
|
||||
var tagIds = $scope.formValues.TagIds;
|
||||
|
||||
var securityData = $scope.formValues.SecurityFormData;
|
||||
var TLS = securityData.TLS;
|
||||
var TLSMode = securityData.TLSMode;
|
||||
var TLSSkipVerify = TLS && (TLSMode === 'tls_client_noca' || TLSMode === 'tls_only');
|
||||
var TLSSkipClientVerify = TLS && (TLSMode === 'tls_ca' || TLSMode === 'tls_only');
|
||||
var TLSCAFile = TLSSkipVerify ? null : securityData.TLSCACert;
|
||||
var TLSCertFile = TLSSkipClientVerify ? null : securityData.TLSCert;
|
||||
var TLSKeyFile = TLSSkipClientVerify ? null : securityData.TLSKey;
|
||||
|
||||
addEndpoint(
|
||||
name,
|
||||
PortainerEndpointCreationTypes.LocalDockerEnvironment,
|
||||
URL,
|
||||
publicURL,
|
||||
groupId,
|
||||
tagIds,
|
||||
TLS,
|
||||
TLSSkipVerify,
|
||||
TLSSkipClientVerify,
|
||||
TLSCAFile,
|
||||
TLSCertFile,
|
||||
TLSKeyFile
|
||||
);
|
||||
$scope.state.actionInProgress = true;
|
||||
EndpointService.createLocalKubernetesEndpoint(name)
|
||||
.then(function success(result) {
|
||||
Notifications.success('Endpoint created', name);
|
||||
$state.go('portainer.endpoints.endpoint.kubernetesConfig', { id: result.Id });
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to create endpoint');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.actionInProgress = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.addAgentEndpoint = function () {
|
||||
|
||||
@@ -44,6 +44,16 @@
|
||||
<p>Directly connect to the Docker API</p>
|
||||
</label>
|
||||
</div>
|
||||
<div ng-click="resetEndpointURL()">
|
||||
<input type="radio" id="kubernetes_endpoint" ng-model="state.EnvironmentType" value="kubernetes" />
|
||||
<label for="kubernetes_endpoint">
|
||||
<div class="boxselector_header">
|
||||
<i class="fas fa-dharmachakra" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Kubernetes
|
||||
</div>
|
||||
<p>Local Kubernetes environment</p>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="azure_endpoint" ng-model="state.EnvironmentType" value="azure" />
|
||||
<label for="azure_endpoint">
|
||||
@@ -61,10 +71,18 @@
|
||||
Important notice
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
The Docker API must be exposed over TCP. You can find more information about how to expose the Docker API over TCP
|
||||
<a href="https://docs.docker.com/engine/security/https/" target="_blank">in the Docker documentation</a>.
|
||||
</span>
|
||||
<p class="col-sm-12 text-muted small">
|
||||
You can connect Portainer to a Docker environment via socket or via TCP. You can find more information about how to expose the Docker API over TCP
|
||||
<a href="https://docs.docker.com/engine/security/https/"> in the Docker documentation</a>.
|
||||
</p>
|
||||
|
||||
<p class="col-sm-12 text-muted small">
|
||||
When using the socket, ensure that you have started the Portainer container with the following Docker flag
|
||||
<code> -v "/var/run/docker.sock:/var/run/docker.sock" </code>
|
||||
(on Linux) or
|
||||
<code> -v \.\pipe\docker_engine:\.\pipe\docker_engine </code>
|
||||
(on Windows).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="state.EnvironmentType === 'agent'">
|
||||
@@ -74,24 +92,33 @@
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
Ensure that you have deployed the Portainer agent in your cluster first. Refer to the platform related command below to deploy it.
|
||||
<div class="input-group input-group-sm" style="margin-top: 10px; margin-bottom: 10px;">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<label class="btn btn-primary" ng-model="state.PlatformType" uib-btn-radio="'linux'"><i class="fab fa-linux" style="margin-right: 2px;"></i> Linux</label>
|
||||
<label class="btn btn-primary" ng-model="state.PlatformType" uib-btn-radio="'windows'"><i class="fab fa-windows" style="margin-right: 2px;"></i> Windows</label>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 10px;">
|
||||
<uib-tabset active="state.deploymentTab">
|
||||
<uib-tab index="0" heading="Kubernetes via load balancer">
|
||||
<uib-tab index="0" ng-if="state.PlatformType === 'linux'" heading="Kubernetes via load balancer">
|
||||
<code style="display: block; white-space: pre-wrap; padding: 16px 90px;"
|
||||
>curl -L https://downloads.portainer.io/portainer-agent-k8s-lb.yaml -o portainer-agent-k8s.yaml; kubectl apply -f portainer-agent-k8s.yaml</code
|
||||
>
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab index="1" heading="Kubernetes via node port">
|
||||
<uib-tab index="1" ng-if="state.PlatformType === 'linux'" heading="Kubernetes via node port">
|
||||
<code style="display: block; white-space: pre-wrap; padding: 16px 90px;"
|
||||
>curl -L https://downloads.portainer.io/portainer-agent-k8s-nodeport.yaml -o portainer-agent-k8s.yaml; kubectl apply -f portainer-agent-k8s.yaml</code
|
||||
>
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab index="2" heading="Docker Swarm">
|
||||
<code style="display: block; white-space: pre-wrap; padding: 16px 90px;"
|
||||
<code ng-if="state.PlatformType === 'linux'" style="display: block; white-space: pre-wrap; padding: 16px 90px;"
|
||||
>curl -L https://downloads.portainer.io/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent</code
|
||||
>
|
||||
<code ng-if="state.PlatformType === 'windows'" style="display: block; white-space: pre-wrap; padding: 16px 90px;"
|
||||
>curl -L https://downloads.portainer.io/agent-stack-windows.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack-windows.yml portainer-agent</code
|
||||
>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
<div style="margin-top: 10px;">
|
||||
@@ -118,6 +145,21 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="state.EnvironmentType === 'kubernetes'">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Important notice
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<p class="col-sm-12 text-muted small">
|
||||
This will allow you to manage the Kubernetes environment where Portainer is running.
|
||||
</p>
|
||||
|
||||
<p class="col-sm-12 text-muted small">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
In order to manage a remote Kubernetes environment, please use the Agent or Edge agent options.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="state.EnvironmentType === 'azure'">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Information
|
||||
@@ -167,8 +209,18 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- connect-via-socket-input -->
|
||||
<div ng-if="state.EnvironmentType === 'docker'">
|
||||
<div class="form-group" style="padding-left: 15px;">
|
||||
<label for="connect_socket" class="col-sm_12 control-label text-left">
|
||||
Connect via socket
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="formValues.ConnectSocket" /><i></i> </label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !connect-via-socket-input -->
|
||||
<!-- endpoint-url-input -->
|
||||
<div ng-if="state.EnvironmentType === 'docker' || state.EnvironmentType === 'agent'">
|
||||
<div ng-if="(state.EnvironmentType === 'docker' && !formValues.ConnectSocket) || state.EnvironmentType === 'agent'">
|
||||
<div class="form-group">
|
||||
<label for="endpoint_url" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Endpoint URL
|
||||
@@ -394,6 +446,17 @@
|
||||
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
|
||||
<span ng-show="state.actionInProgress">Creating endpoint...</span>
|
||||
</button>
|
||||
<button
|
||||
ng-if="state.EnvironmentType === 'kubernetes'"
|
||||
type="submit"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="state.actionInProgress || !endpointCreationForm.$valid"
|
||||
ng-click="addKubernetesEndpoint()"
|
||||
button-spinner="state.actionInProgress"
|
||||
>
|
||||
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
|
||||
<span ng-show="state.actionInProgress">Creating endpoint...</span>
|
||||
</button>
|
||||
<button
|
||||
ng-if="state.EnvironmentType === 'azure'"
|
||||
type="submit"
|
||||
|
||||
@@ -33,18 +33,26 @@
|
||||
<p>
|
||||
The agent will communicate with Portainer via <u>{{ edgeKeyDetails.instanceURL }}</u> and <u>tcp://{{ edgeKeyDetails.tunnelServerAddr }}</u>
|
||||
</p>
|
||||
<div class="input-group input-group-sm" style="margin-top: 10px; margin-bottom: 10px;">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<label class="btn btn-primary" ng-model="state.platformType" uib-btn-radio="'linux'"><i class="fab fa-linux" style="margin-right: 2px;"></i> Linux</label>
|
||||
<label class="btn btn-primary" ng-model="state.platformType" uib-btn-radio="'windows'"><i class="fab fa-windows" style="margin-right: 2px;"></i> Windows</label>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 10px;">
|
||||
<uib-tabset active="state.deploymentTab">
|
||||
<uib-tab index="0" heading="Kubernetes">
|
||||
<uib-tab index="0" ng-if="state.platformType === 'linux'" heading="Kubernetes">
|
||||
<code style="display: block; white-space: pre-wrap; padding: 16px 90px;"
|
||||
>curl https://downloads.portainer.io/portainer-edge-agent-setup.sh | sudo bash -s -- {{ randomEdgeID }} {{ endpoint.EdgeKey }}</code
|
||||
>
|
||||
</uib-tab>
|
||||
<uib-tab index="1" heading="Docker Swarm">
|
||||
<code style="display: block; white-space: pre-wrap; padding: 16px 90px;">{{ dockerCommands.swarm }}</code>
|
||||
<code ng-if="state.platformType === 'linux'" style="display: block; white-space: pre-wrap; padding: 16px 90px;">{{ dockerCommands.linuxSwarm }}</code>
|
||||
<code ng-if="state.platformType === 'windows'" style="display: block; white-space: pre-wrap; padding: 16px 90px;">{{ dockerCommands.windowsSwarm }}</code>
|
||||
</uib-tab>
|
||||
<uib-tab index="2" heading="Docker Standalone">
|
||||
<code style="display: block; white-space: pre-wrap; padding: 16px 90px;">{{ dockerCommands.standalone }}</code>
|
||||
<code ng-if="state.platformType === 'linux'" style="display: block; white-space: pre-wrap; padding: 16px 90px;">{{ dockerCommands.linuxStandalone }}</code>
|
||||
<code ng-if="state.platformType === 'windows'" style="display: block; white-space: pre-wrap; padding: 16px 90px;">{{ dockerCommands.windowsStandalone }}</code>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
<div style="margin-top: 10px;">
|
||||
@@ -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>
|
||||
|
||||
@@ -29,6 +29,7 @@ angular
|
||||
kubernetesEndpoint: false,
|
||||
agentEndpoint: false,
|
||||
edgeEndpoint: false,
|
||||
platformType: 'linux',
|
||||
allowCreate: Authentication.isAdmin(),
|
||||
availableEdgeAgentCheckinOptions: [
|
||||
{ key: 'Use default interval', value: 0 },
|
||||
@@ -55,15 +56,23 @@ angular
|
||||
};
|
||||
|
||||
$scope.copyEdgeAgentDeploymentCommand = function () {
|
||||
if ($scope.state.deploymentTab === 2) {
|
||||
if ($scope.state.deploymentTab === 2 && $scope.state.platformType === 'linux') {
|
||||
clipboard.copyText(
|
||||
'docker run -d -v /var/run/docker.sock:/var/run/docker.sock -v /var/lib/docker/volumes:/var/lib/docker/volumes -v /:/host --restart always -e EDGE=1 -e EDGE_ID=' +
|
||||
'docker run -d -v /var/run/docker.sock:/var/run/docker.sock -v /var/lib/docker/volumes:/var/lib/docker/volumes -v /:/host -v portainer_agent_data:/data --restart always -e EDGE=1 -e EDGE_ID=' +
|
||||
$scope.randomEdgeID +
|
||||
' -e EDGE_KEY=' +
|
||||
$scope.endpoint.EdgeKey +
|
||||
' -e CAP_HOST_MANAGEMENT=1 -v portainer_agent_data:/data --name portainer_edge_agent portainer/agent'
|
||||
' -e CAP_HOST_MANAGEMENT=1 --name portainer_edge_agent portainer/agent'
|
||||
);
|
||||
} else if ($scope.state.deploymentTab === 1) {
|
||||
} else if ($scope.state.deploymentTab === 2 && $scope.state.platformType === 'windows') {
|
||||
clipboard.copyText(
|
||||
'docker run -d --mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine --mount type=bind,src=C:\\ProgramData\\docker\\volumes,dst=C:\\ProgramData\\docker\\volumes --mount type=volume,src=portainer_agent_data,dst=C:\\data -e EDGE=1 -e EDGE_ID=' +
|
||||
$scope.randomEdgeID +
|
||||
' -e EDGE_KEY=' +
|
||||
$scope.endpoint.EdgeKey +
|
||||
' -e CAP_HOST_MANAGEMENT=1 --name portainer_edge_agent portainer/agent'
|
||||
);
|
||||
} else if ($scope.state.deploymentTab === 1 && $scope.state.platformType === 'linux') {
|
||||
clipboard.copyText(
|
||||
'docker network create --driver overlay portainer_agent_network; docker service create --name portainer_edge_agent --network portainer_agent_network -e AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent -e EDGE=1 -e EDGE_ID=' +
|
||||
$scope.randomEdgeID +
|
||||
@@ -71,6 +80,14 @@ angular
|
||||
$scope.endpoint.EdgeKey +
|
||||
" -e CAP_HOST_MANAGEMENT=1 --mode global --constraint 'node.platform.os == linux' --mount type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock --mount type=bind,src=//var/lib/docker/volumes,dst=/var/lib/docker/volumes --mount type=bind,src=//,dst=/host --mount type=volume,src=portainer_agent_data,dst=/data portainer/agent"
|
||||
);
|
||||
} else if ($scope.state.deploymentTab === 1 && $scope.state.platformType === 'windows') {
|
||||
clipboard.copyText(
|
||||
'docker network create --driver overlay portainer_edge_agent_network && docker service create --name portainer_edge_agent --network portainer_edge_agent_network -e AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent -e EDGE=1 -e EDGE_ID=' +
|
||||
$scope.randomEdgeID +
|
||||
' -e EDGE_KEY=' +
|
||||
$scope.endpoint.EdgeKey +
|
||||
' -e CAP_HOST_MANAGEMENT=1 --mode global --constraint node.platform.os==windows --mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine --mount type=bind,src=C:\\ProgramData\\docker\\volumes,dst=C:\\ProgramData\\docker\\volumes --mount type=volume,src=portainer_agent_data,dst=C:\\data portainer/agent'
|
||||
);
|
||||
} else {
|
||||
clipboard.copyText('curl https://downloads.portainer.io/portainer-edge-agent-setup.sh | bash -s -- ' + $scope.randomEdgeID + ' ' + $scope.endpoint.EdgeKey);
|
||||
}
|
||||
@@ -206,8 +223,10 @@ angular
|
||||
$scope.edgeKeyDetails = decodeEdgeKey(endpoint.EdgeKey);
|
||||
$scope.randomEdgeID = uuidv4();
|
||||
$scope.dockerCommands = {
|
||||
standalone: buildStandaloneCommand($scope.randomEdgeID, endpoint.EdgeKey),
|
||||
swarm: buildSwarmCommand($scope.randomEdgeID, endpoint.EdgeKey),
|
||||
linuxStandalone: buildLinuxStandaloneCommand($scope.randomEdgeID, endpoint.EdgeKey),
|
||||
windowsStandalone: buildWindowsStandaloneCommand($scope.randomEdgeID, endpoint.EdgeKey),
|
||||
linuxSwarm: buildLinuxSwarmCommand($scope.randomEdgeID, endpoint.EdgeKey),
|
||||
windowsSwarm: buildWindowsSwarmCommand($scope.randomEdgeID, endpoint.EdgeKey),
|
||||
};
|
||||
|
||||
const settings = data.settings;
|
||||
@@ -223,21 +242,36 @@ angular
|
||||
});
|
||||
}
|
||||
|
||||
function buildStandaloneCommand(edgeId, edgeKey) {
|
||||
return `docker run -d -v /var/run/docker.sock:/var/run/docker.sock \\
|
||||
function buildLinuxStandaloneCommand(edgeId, edgeKey) {
|
||||
return `docker run -d \\
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \\
|
||||
-v /var/lib/docker/volumes:/var/lib/docker/volumes \\
|
||||
-v /:/host \\
|
||||
-v portainer_agent_data:/data \\
|
||||
--restart always \\
|
||||
-e EDGE=1 \\
|
||||
-e EDGE_ID=${edgeId} \\
|
||||
-e EDGE_KEY=${edgeKey} \\
|
||||
-e CAP_HOST_MANAGEMENT=1 \\
|
||||
--name portainer_edge_agent \\localhost
|
||||
portainer/agent`;
|
||||
}
|
||||
|
||||
function buildWindowsStandaloneCommand(edgeId, edgeKey) {
|
||||
return `docker run -d \\
|
||||
--mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine \\
|
||||
--mount type=bind,src=C:\\ProgramData\\docker\\volumes,dst=C:\\ProgramData\\docker\\volumes \\
|
||||
--mount type=volume,src=portainer_agent_data,dst=C:\\data \\
|
||||
--restart always \\
|
||||
-e EDGE=1 \\
|
||||
-e EDGE_ID=${edgeId} \\
|
||||
-e EDGE_KEY=${edgeKey} \\
|
||||
-e CAP_HOST_MANAGEMENT=1 \\
|
||||
-v portainer_agent_data:/data \\
|
||||
--name portainer_edge_agent \\
|
||||
portainer/agent`;
|
||||
}
|
||||
|
||||
function buildSwarmCommand(edgeId, edgeKey) {
|
||||
function buildLinuxSwarmCommand(edgeId, edgeKey) {
|
||||
return `docker network create \\
|
||||
--driver overlay \\
|
||||
portainer_agent_network;
|
||||
@@ -259,5 +293,25 @@ docker service create \\
|
||||
portainer/agent`;
|
||||
}
|
||||
|
||||
function buildWindowsSwarmCommand(edgeId, edgeKey) {
|
||||
return `docker network create \\
|
||||
--driver overlay \\
|
||||
portainer_edge_agent_network && \\
|
||||
docker service create \\
|
||||
--name portainer_edge_agent \\
|
||||
--network portainer_edge_agent_network \\
|
||||
-e AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent \\
|
||||
-e EDGE=1 \\
|
||||
-e EDGE_ID=${edgeId} \\
|
||||
-e EDGE_KEY=${edgeKey} \\
|
||||
-e CAP_HOST_MANAGEMENT=1 \\
|
||||
--mode global \\
|
||||
--constraint node.platform.os==windows \\
|
||||
--mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine \\
|
||||
--mount type=bind,src=C:\\ProgramData\\docker\\volumes,dst=C:\\ProgramData\\docker\\volumes \\
|
||||
--mount type=volume,src=portainer_agent_data,dst=C:\\data \\
|
||||
portainer/agent`;
|
||||
}
|
||||
|
||||
initView();
|
||||
});
|
||||
|
||||
@@ -30,6 +30,17 @@
|
||||
<i class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
|
||||
</div>
|
||||
|
||||
<information-panel ng-if="isAdmin && endpoints.length === 0" title-text="Information">
|
||||
<span class="small text-muted">
|
||||
<p>
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
No environment available for management. Please head over the
|
||||
<a ui-sref="portainer.endpoints.new"> endpoints view </a>
|
||||
to add an endpoint.
|
||||
</p>
|
||||
</span>
|
||||
</information-panel>
|
||||
|
||||
<div class="row" ng-if="!state.connectingToEdgeEndpoint">
|
||||
<div class="col-sm-12">
|
||||
<endpoint-list
|
||||
|
||||
@@ -62,6 +62,9 @@
|
||||
<span ng-hide="ctrl.state.actionInProgress"><i class="fa fa-bolt" aria-hidden="true"></i> Connect</span>
|
||||
<span ng-show="ctrl.state.actionInProgress">Connecting...</span>
|
||||
</button>
|
||||
<button type="submit" class="btn btn-sm btn-default" ng-click="ctrl.skipEndpointCreation()" button-spinner="ctrl.state.actionInProgress">
|
||||
<span ng-hide="ctrl.state.actionInProgress"><i class="fa fa-share" aria-hidden="true"></i> Skip</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
|
||||
@@ -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)
|
||||
*/
|
||||
|
||||
56
app/portainer/views/roles/roles.html
Normal file
56
app/portainer/views/roles/roles.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<rd-header>
|
||||
<rd-header-title title-text="Roles">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="portainer.roles" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-sync" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Role management</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<information-panel title-text="Information">
|
||||
<span class="small">
|
||||
<p class="text-muted">
|
||||
<i class="fa fa-user" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This feature is available in <a href="https://www.portainer.io/business-upsell?from=k8s-rbac-roles" target="_blank">Portainer Business Edition</a>.
|
||||
</p>
|
||||
</span>
|
||||
</information-panel>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<roles-datatable title-text="Roles" title-icon="fa-file-code" table-key="roles" order-by="Name"></roles-datatable>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12" style="margin-bottom: 0px;">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-user-lock" title-text="Effective access viewer"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
User
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">
|
||||
No user available
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Access
|
||||
</div>
|
||||
<div>
|
||||
<div class="small text-muted" style="margin-bottom: 15px;">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Effective role for each endpoint will be displayed for the selected user
|
||||
</div>
|
||||
</div>
|
||||
<access-viewer-datatable table-key="access_viewer" order-by="EndpointName"> </access-viewer-datatable>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
@@ -366,6 +366,57 @@
|
||||
<!-- !group-search-settings -->
|
||||
</div>
|
||||
|
||||
<div ng-if="isOauthEnabled()">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Provider
|
||||
</div>
|
||||
<div class="form-group"></div>
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<div class="boxselector_wrapper">
|
||||
<div data-toggle="tooltip" title="This feature is available in Portainer Business Edition" style="color: #767676;">
|
||||
<input type="radio" id="microsoft" disabled />
|
||||
<label for="microsoft" style="cursor: pointer; border-color: #767676;">
|
||||
<div class="boxselector_header">
|
||||
<i class="fab fa-microsoft" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Microsoft
|
||||
</div>
|
||||
<p>Microsoft OAuth provider</p>
|
||||
</label>
|
||||
</div>
|
||||
<div data-toggle="tooltip" title="This feature is available in Portainer Business Edition" style="color: #767676;">
|
||||
<input type="radio" id="google" disabled />
|
||||
<label for="google" style="cursor: pointer; border-color: #767676;">
|
||||
<div class="boxselector_header">
|
||||
<i class="fab fa-google" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Google
|
||||
</div>
|
||||
<p>Google OAuth provider</p>
|
||||
</label>
|
||||
</div>
|
||||
<div data-toggle="tooltip" title="This feature is available in Portainer Business Edition" style="color: #767676;">
|
||||
<input type="radio" id="registry_auth" disabled />
|
||||
<label for="Github" style="cursor: pointer; border-color: #767676;">
|
||||
<div class="boxselector_header">
|
||||
<i class="fab fa-github" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Github
|
||||
</div>
|
||||
<p>Github OAuth provider</p>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="custom" ng-model="settings.AuthenticationMethod" ng-value="3" />
|
||||
<label for="custom">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-user-check" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Custom
|
||||
</div>
|
||||
<p>Custom OAuth provider</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<oauth-settings ng-if="isOauthEnabled()" settings="OAuthSettings" teams="teams"></oauth-settings>
|
||||
|
||||
<!-- actions -->
|
||||
|
||||
@@ -120,14 +120,32 @@
|
||||
($state.current.name === 'portainer.users' ||
|
||||
$state.current.name === 'portainer.users.user' ||
|
||||
$state.current.name === 'portainer.teams' ||
|
||||
$state.current.name === 'portainer.teams.team')
|
||||
$state.current.name === 'portainer.teams.team' ||
|
||||
$state.current.name === 'portainer.roles')
|
||||
"
|
||||
>
|
||||
<a ui-sref="portainer.teams" ui-sref-active="active">Teams</a>
|
||||
</div>
|
||||
<div
|
||||
class="sidebar-sublist"
|
||||
ng-if="
|
||||
toggle &&
|
||||
($state.current.name === 'portainer.users' ||
|
||||
$state.current.name === 'portainer.users.user' ||
|
||||
$state.current.name === 'portainer.teams' ||
|
||||
$state.current.name === 'portainer.teams.team' ||
|
||||
$state.current.name === 'portainer.roles')
|
||||
"
|
||||
>
|
||||
<a ui-sref="portainer.roles" ui-sref-active="active">Roles</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="isAdmin">
|
||||
<a ui-sref="portainer.endpoints" ui-sref-active="active">Endpoints <span class="menu-icon fa fa-plug fa-fw"></span></a>
|
||||
<a
|
||||
ui-sref="portainer.endpoints"
|
||||
ng-class="{ active: $state.current.name.includes('portainer.endpoints') && $state.current.name !== 'portainer.endpoints.endpoint.kubernetesConfig' }"
|
||||
>Endpoints <span class="menu-icon fa fa-plug fa-fw"></span
|
||||
></a>
|
||||
<div
|
||||
class="sidebar-sublist"
|
||||
ng-if="
|
||||
@@ -136,7 +154,6 @@
|
||||
$state.current.name === 'portainer.endpoints.endpoint' ||
|
||||
$state.current.name === 'portainer.endpoints.new' ||
|
||||
$state.current.name === 'portainer.endpoints.endpoint.access' ||
|
||||
$state.current.name === 'portainer.endpoints.endpoint.kubernetesConfig' ||
|
||||
$state.current.name === 'portainer.groups' ||
|
||||
$state.current.name === 'portainer.groups.group' ||
|
||||
$state.current.name === 'portainer.groups.group.access' ||
|
||||
@@ -154,7 +171,6 @@
|
||||
$state.current.name === 'portainer.endpoints.endpoint' ||
|
||||
$state.current.name === 'portainer.endpoints.new' ||
|
||||
$state.current.name === 'portainer.endpoints.endpoint.access' ||
|
||||
$state.current.name === 'portainer.endpoints.endpoint.kubernetesConfig' ||
|
||||
$state.current.name === 'portainer.groups' ||
|
||||
$state.current.name === 'portainer.groups.group' ||
|
||||
$state.current.name === 'portainer.groups.group.access' ||
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
});
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
7
build/download_kompose_binary.ps1
Normal file
7
build/download_kompose_binary.ps1
Normal 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"
|
||||
7
build/download_kubectl_binary.ps1
Normal file
7
build/download_kubectl_binary.ps1
Normal 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"
|
||||
@@ -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:
|
||||
|
||||
21
gruntfile.js
21
gruntfile.js
@@ -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
Reference in New Issue
Block a user