Compare commits

..

93 Commits

Author SHA1 Message Date
xAt0mZ
f21b6c84d9 sec(app): remove unused and vulnerable dependencies 2021-02-01 17:56:17 +01:00
Yi Chen
afbd353808 Merge windows buildx to develop (#4796)
* feat(build): introducing buildx for Windows

* feat(build): re-ordered USER

* feat(build): Fixed Typo

* feat(build): fixed typo

Co-authored-by: ssbkang <skan070@gmail.com>
2021-01-31 17:46:45 +13:00
Dmitry Salakhov
a71e71f481 feat(compose): add docker-compose wrapper (#4713)
* feat(compose): add docker-compose wrapper

ce-187

* fix(compose): pick compose implementation upon startup

* Add static compose build for linux

* Fix wget

* Fix platofrm specific docker-compose download

* Keep amd64 architecture as download parameter

* Add tmp folder for docker-compose

* fix: line endings

* add proxy server

* logs

* Proxy

* Add lite transport for compose

* Fix local deployment

* refactor: pass proxyManager by ref

* fix: string conversion

* refactor: compose wrapper remove unused code

* fix: tests

* Add edge

* Fix merge issue

* refactor: remove unused code

* Move server to proxy implementation

* Cleanup wrapper and manager

* feat: pass max supported compose syntax version with each endpoint

* fix: pick compose syntax version

* fix: store wrapper version in portainer

* Get and show composeSyntaxMaxVersion at stack creation screen

* Get and show composeSyntaxMaxVersion at stack editor screen

* refactor: proxy server

* Fix used tmp

* Bump docker-compose to 1.28.0

* remove message for docker compose limitation

* fix: markup typo

* Rollback docker compose to 1.27.4

* * attempt to fix the windows build issue

* * attempt to debug grunt issue

* * use console log in grunt file

* fix: try to fix windows build by removing indirect deps from go.mod

* Remove tmp folder

* Remove builder stage

* feat(build/windows): add git for Docker Compose

* feat(build/windows): add git for Docker Compose

* feat(build/windows): add git for Docker Compose

* feat(build/windows): add git for Docker Compose

* feat(build/windows): add git for Docker Compose

* feat(build/windows): add git for Docker Compose - fixed verbose output

* refactor: renames

* fix(stack): get endpoint by EndpointProvider

* fix(stack): use margin to add space between line instead of using br tag

Co-authored-by: Stéphane Busso <stephane.busso@gmail.com>
Co-authored-by: Simon Meng <simon.meng@portainer.io>
Co-authored-by: yi-portainer <yi.chen@portainer.io>
Co-authored-by: Steven Kang <skan070@gmail.com>
2021-01-26 08:16:53 +13:00
LP B
83f4c5ec0b fix(k8s/app): remove advanced deployment panel from app details view (#4730) 2021-01-25 14:43:54 +13:00
Maxime Bajeux
41308d570d feat(configurations): Review UI/UX configurations (#4691)
* feat(configurations): Review UI/UX configurations

* feat(configurations): fix binary secret value

* fix(frontend): populate data between simple and advanced modes (#4503)

* fix(configuration): parseYaml before create configuration

* fix(configurations): change c to C in ConfigurationOwner

* fix(application): change configuration index to configuration key in the view

* fix(configuration): resolve problem in application create with configuration not overriden.

* fix(configuration): fix bad import in helper

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-01-25 14:14:35 +13:00
Chaim Lev-Ari
46ff8a01bc fix(kubernetes/pods): save note (#4675)
* feat(kubernetes/pods): introduce patch api

* feat(k8s/pods): pod converter

* feat(kubernetes/pods): introduce patch api

* feat(k8s/pod): add annotations only if needed

* fix(k8s/pod): replace class with factory function
2021-01-22 14:08:08 +13:00
yi-portainer
2b257d2785 Squashed commit of the following 2.0.1 release fixes:
commit f90d6b55d6
Author: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Date:   Wed Jan 13 00:56:19 2021 +0200

    feat(service): clear source volume when change type (#4627)

    * feat(service): clear source volume when change type

    * feat(service): init volume source to the correct value

commit 1b82b450d7
Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com>
Date:   Thu Jan 7 14:47:32 2021 +1300

    * bump the APIVersion to 2.0.1 (#4688)

commit b78d804881
Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com>
Date:   Wed Dec 30 23:03:43 2020 +1300

    Revert "chore(build): bump Kompose version (#4475)" (#4676)

    This reverts commit 380f106571.

    Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>

commit 51b72c12f9
Author: Anthony Lapenna <anthony.lapenna@portainer.io>
Date:   Wed Dec 23 14:45:32 2020 +1300

    fix(docker/stack-details): do not display editor tab for external stack (#4650)

commit 58c04bdbe3
Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com>
Date:   Tue Dec 22 13:47:11 2020 +1300

    + silently continue when downloading artifacts in windows (#4637)

commit a6320d5222
Author: cong meng <mcpacino@gmail.com>
Date:   Tue Dec 22 13:38:54 2020 +1300

    fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ce#180) (#4618)

    * fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ce#180)

    * fix(frontend) rephrase comments (#4629)

    Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>

    Co-authored-by: Simon Meng <simon.meng@portainer.io>
    Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>
2021-01-21 00:04:15 +13:00
cong meng
da41dbb79a fix(stack): stacks created via API are incorrectly marked as private with no owner (#3721) (#4725)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-01-20 15:19:35 +13:00
Maxime Bajeux
68d42617f2 feat(placement): Add a warning notification under the placement tab when an application cannot be scheduled on any node in the cluster (#4525)
* feat(placement): Add a warning notification under the placement tab when an application cannot be scheduled on any node in the cluster

* fix(applications): if there is at least one node the application can schedule on, then do not show the warning
2021-01-20 13:02:18 +13:00
Anthony McMahon
8323e22309 Update issue templates
Adding auto labelling to Bug Report (kind/bug, bug/unconfirmed) and Question (kind/question)
2021-01-20 12:06:25 +13:00
Chaim Lev-Ari
20d4341170 fix(state): check validity of state (#4609) 2021-01-19 11:10:08 +13:00
Chaim Lev-Ari
832cafc933 fix(registries): update password only when not empty (#4669) 2021-01-18 13:59:57 +13:00
cong meng
f3c537ac2c chore(build): bump Kompose version (#4473) (#4724)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-01-18 13:02:16 +13:00
Anthony McMahon
958baf6283 Update README.md 2021-01-18 09:30:17 +13:00
Chaim Lev-Ari
08e392378e chore(app): fail on angular components missing nginject (#4224) 2021-01-17 20:28:09 +13:00
Alice Groux
a2d9734b8b fix(k8s/datatables): reduce size of collapse/expand column for stacks datatable and storage datatable (#4511)
* fix(k8s/datatables): reduce size of collapse/expand column for stacks datatable and storage datatable

* fix(k8s/datatables): reduce size of expand/collapse column
2021-01-17 16:50:22 +13:00
DarkAEther
15aed9fc6f feat(area/kubernetes): show shared access policy in volume details (#4707) 2021-01-17 13:53:32 +13:00
Alice Groux
121d33538d fix(k8s/application): validate load balancer ports inputs (#4426)
* fix(k8s/application): validate load balancer ports inputs

* fix(k8s/application): allow user to only change the protocol on the first port mapping
2021-01-15 14:51:36 +13:00
Olli Janatuinen
7a03351df8 dep(api): Support Docker Stack 3.8 (#4333)
- Linux: Update Docker binary to version 19.03.13
- Windows: Update Docker binary to version 19.03.12
2021-01-15 10:05:33 +13:00
Alice Groux
0c2987893d feat(app/images): in advanced mode, remove tooltip and add an information message (#4528) 2021-01-14 15:04:44 +13:00
Alice Groux
d1eddaa188 feat(app/network): rename restrict external acces to the network label and add a tooltip (#4514) 2021-01-14 12:24:56 +13:00
Anthony Lapenna
d336ada3c2 feat(k8s/application): review application creation warning style (#4613) 2021-01-13 16:13:27 +13:00
Avadhut Tanugade
839198fbff #4424 style(stack-details): shift button position in stack details (#4439) 2021-01-13 12:19:18 +13:00
Chaim Lev-Ari
486ffa5bbd chore(webpack): add source maps (#4471)
* chore(webpack): add source maps

* feat(build): fetch source maps for 3rd party libs
2021-01-13 10:40:09 +13:00
Maxime Bajeux
4cd468ce21 Can't create kubernetes resources with a username longer than 63 characters (#4672)
* fix(kubernetes): truncate username when we create resource

* fix(k8s): remove forbidden characters in owner label
2021-01-12 14:35:59 +13:00
Chaim Lev-Ari
cbd7fdc62e feat(docker/stacks): introduce date info for stacks (#4660)
* feat(docker/stacks): add creation and update dates

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

* feat(docker/stacks): fix the no stacks message

* refactor(docker/stacks): make external stacks helpers more readable

* feat(docker/stacks): add updated and created by

* feat(docker/stacks): toggle updated column

* refactor(datatable): create column visibility component

Co-authored-by: alice groux <alice.grx@gmail.com>
2021-01-12 12:38:49 +13:00
DarkAEther
b9fe8009dd feat(image-details): Show labels in images datatable (#4287)
* feat(images): show labels in images datatable

* move labels to image details view
2021-01-11 15:35:19 +13:00
Stéphane Busso
6a504e7134 fix(settings): Use default setting if UserSessionTimeout not set (#4521)
* fix(settings): Use default settings if UserSessionTimeout not set

* Update UserSessionTimeout settings in database if set to empty string
2021-01-11 14:44:15 +13:00
Alice Groux
51ba0876a5 feat(k8s/configuration): rename add ingress controller button and changed information text (#4540) 2021-01-11 12:51:46 +13:00
Alice Groux
769e6a4c6c feat(k8s/configuration): add extra information panel when creating a sensitive configuration (#4541) 2021-01-11 11:30:31 +13:00
cong meng
105d1ae519 feat(frontend): de-emphasize internal login when OAuth is enabled (#3065) (#4565)
* feat(frontend): de-emphasize internal login when OAuth is enabled (#3065)

* feat(frontend): change the "Use internal authentication" style to be primary (#3065)

* feat(frontend): resize the login with "provider" button to use a 120% font size (#3065)

* feat(frontend): remove unused css for h1 tag (#3065)

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-01-08 15:30:43 +13:00
cong meng
cf508065ec fix(frontend): application edit page initializes the overridenKeyType of new added configuration key to NONE so that the user can select how to load it (#4548) (#4593)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-01-08 12:51:27 +13:00
itsconquest
eab828279e chore(project): exclude refactors (#4689) 2021-01-08 12:46:57 +13:00
cong meng
d5763a970b fix(frontend): Resource pool 'created' attribute is showing the time you view it at & not actual creation time (#4568) (#4599)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-01-08 12:45:06 +13:00
cong meng
c9f68a4d8f fix(kubernetes): removes kube client cache when edge proxy is removed (#4487) (#4574)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-01-08 11:55:42 +13:00
Alice Groux
7848bcf2f4 feat(k8s/resources-list-view): add advanced deployment panel to resources list view (#4516)
* feat(k8s/resources-list-view): add advanced deployment panel to applications view, configurations view and volumes view

* feat(k8s/resources-list-view): move advanced deployment into a template and use it everywhere
2021-01-08 10:29:17 +13:00
Stéphane Busso
b924347c5b Bump portainer version 2021-01-07 14:03:46 +13:00
Yi Chen
9fbda9fb99 Merge in release fixes to develop (#4687)
* fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ce#180) (#4618)

* fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ce#180)

* fix(frontend) rephrase comments (#4629)

Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>

Co-authored-by: Simon Meng <simon.meng@portainer.io>
Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>

* + silently continue when downloading artifacts in windows (#4637)

* fix(docker/stack-details): do not display editor tab for external stack (#4650)

* Revert "chore(build): bump Kompose version (#4475)" (#4676)

This reverts commit 380f106571.

Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>

Co-authored-by: cong meng <mcpacino@gmail.com>
Co-authored-by: Simon Meng <simon.meng@portainer.io>
Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>
Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>
2021-01-07 13:38:01 +13:00
Anthony Lapenna
82f8062784 chore(github): update issue template 2021-01-06 11:31:05 +13:00
knittl
49982eb98a #4411 docs: make build steps for local development more easily discoverable (#4412) 2021-01-06 08:49:50 +13:00
Stéphane Busso
4be3ac470f Merge pull request #4658 from portainer/revert-4475-chore-ce-86-bump-kompose-version
Revert "chore(build): bump Kompose version"
2020-12-24 23:45:53 +13:00
Stéphane Busso
a50ab51bef Revert "chore(build): bump Kompose version (#4475)"
This reverts commit 380f106571.
2020-12-24 12:12:28 +13:00
Yi Chen
7975ef796d Revert "feat(docker/stacks): add creation and update dates (#4418)" (#4606)
This reverts commit bd98b8956a.
2020-12-17 13:33:45 +13:00
xAt0mZ
f8b226a1ef fix(k8s/application): ability to remove naked pods (#4598) 2020-12-17 13:05:31 +13:00
cong meng
342a0d6d22 fix(k8s/application): transform username to be dns compliant (#4595) (#4601)
* fix(k8s/application): transform username to be dns compliant (#4595)

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

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

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

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

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

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

* refactor(services): remove ng-form

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

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

* feat(endpoint): minor UI update

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

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

* feat(endpoint): minor UI update

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

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

* feat(app/endpoint): fix typo

* feat(endpoint): minor UI update

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

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

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

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

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

* feat(ci): correctly pass in binary versions

* feat(ci): fix powershell errors

* feat(ci): fix cmdlet syntax

* feat(ci): fix typo

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

* feat(project): remove example text

* feat(project): add missing newlines

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

View File

@@ -1,6 +1,10 @@
---
name: Bug report
about: Create a bug report
title: ''
labels: bug/need-confirmation, kind/bug
assignees: ''
---
<!--
@@ -9,14 +13,14 @@ Thanks for reporting a bug for Portainer !
You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/.
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
Before opening a new issue, make sure that we do not have any duplicates
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 +31,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:**
@@ -40,6 +44,7 @@ You can see how [here](https://portainer.readthedocs.io/en/stable/faq.html#how-d
- Portainer version:
- Docker version (managed by Portainer):
- Kubernetes version (managed by Portainer):
- Platform (windows/linux):
- Command used to start Portainer (`docker run -p 9000:9000 portainer/portainer`):
- Browser:

View File

@@ -1,17 +1,20 @@
---
name: Question
about: Ask us a question about Portainer usage or deployment
---
<!--
You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/
Also, be sure to check our FAQ and documentation first: https://portainer.readthedocs.io
-->
**Question**:
How can I deploy Portainer on... ?
---
name: Question
about: Ask us a question about Portainer usage or deployment
title: ''
labels: ''
assignees: ''
---
<!--
You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
-->
**Question**:
How can I deploy Portainer on... ?

View File

@@ -1,31 +1,34 @@
---
name: Feature request
about: Suggest a feature/enhancement that should be added in Portainer
---
<!--
Thanks for opening a feature request for Portainer !
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/
Before opening a new issue, make sure that we do not have any duplicates
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
-->
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
---
name: Feature request
about: Suggest a feature/enhancement that should be added in Portainer
title: ''
labels: ''
assignees: ''
---
<!--
Thanks for opening a feature request for Portainer !
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
Before opening a new issue, make sure that we do not have any duplicates
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://documentation.portainer.io/
-->
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

3
.github/stale.yml vendored
View File

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

View File

@@ -74,3 +74,23 @@ Our contribution process is described below. Some of the steps can be visualized
The feature request process is similar to the bug report process but has an extra functional validation before the technical validation as well as a documentation validation before the testing phase.
![portainer_featurerequest_workflow](https://user-images.githubusercontent.com/5485061/45727229-5ad39f00-bbf5-11e8-9550-16ba66c50615.png)
## Build Portainer locally
Ensure you have Docker, Node.js, yarn, and Golang installed in the correct versions.
Install dependencies with yarn:
```sh
$ yarn
```
Then build and run the project:
```sh
$ yarn start
```
Portainer can now be accessed at <http://localhost:9000>.
Find more detailed steps at <https://documentation.portainer.io/contributing/instructions/>.

View File

@@ -10,7 +10,7 @@
**_Portainer_** is a lightweight management UI which allows you to **easily** manage your different Docker environments (Docker hosts or Swarm clusters).
**_Portainer_** is meant to be as **simple** to deploy as it is to use. It consists of a single container that can run on any Docker engine (can be deployed as Linux container or a Windows native container, supports other platforms too).
**_Portainer_** allows you to manage all your Docker resources (containers, images, volumes, networks and more) ! It is compatible with the _standalone Docker_ engine and with _Docker Swarm mode_.
**_Portainer_** allows you to manage all your Docker resources (containers, images, volumes, networks and more!) It is compatible with the _standalone Docker_ engine and with _Docker Swarm mode_.
## Demo
@@ -24,32 +24,39 @@ 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)
- [Building Portainer](https://documentation.portainer.io/contributing/instructions/)
## Getting help
For FORMAL Support, please purchase a support subscription from here: https://www.portainer.io/products-services/portainer-business-support/
For FORMAL Support, please purchase a support subscription from here: https://www.portainer.io/products/portainer-business
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/
For community support: You can find more information about Portainer's community support framework policy here: https://www.portainer.io/products/community-edition/customer-success
- Issues: https://github.com/portainer/portainer/issues
- FAQ: https://www.portainer.io/documentation/faqs/
- FAQ: https://documentation.portainer.io
- Slack (chat): https://portainer.io/slack/
## Reporting bugs and contributing
- Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new).
- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://www.portainer.io/documentation/how-to-contribute/) to build it locally and make a pull request. We need all the help we can get!
- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://documentation.portainer.io/contributing/instructions/) to build it locally and make a pull request. We need all the help we can get!
## Security
- Here at Portainer, we believe in [responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure) of security issues. If you have found a security issue, please report it to <security@portainer.io>.
## Privacy
**To make sure we focus our development effort in the right places we need to know which features get used most often. To give us this information we use [Matomo Analytics](https://matomo.org/), which is hosted in Germany and is fully GDPR compliant.**
When Portainer first starts, you are given the option to DISABLE analytics. If you **don't** choose to disable it, we collect anonymous usage as per [our privacy policy](https://www.portainer.io/documentation/in-app-analytics-and-privacy-policy/). **Please note**, there is no personally identifiable information sent or stored at any time and we only use the data to help us improve Portainer.
## Limitations
Portainer supports "Current - 2 docker versions only. Prior versions may operate, however these are not supported.

View File

@@ -6,7 +6,7 @@ import (
"strings"
"time"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt"
"github.com/portainer/portainer/api/chisel"
"github.com/portainer/portainer/api/cli"
@@ -17,6 +17,8 @@ import (
"github.com/portainer/portainer/api/git"
"github.com/portainer/portainer/api/http"
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/portainer/api/http/proxy"
kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/internal/snapshot"
"github.com/portainer/portainer/api/jwt"
"github.com/portainer/portainer/api/kubernetes"
@@ -71,7 +73,12 @@ func initDataStore(dataStorePath string, fileService portainer.FileService) port
return store
}
func initComposeStackManager(dataStorePath string, reverseTunnelService portainer.ReverseTunnelService) portainer.ComposeStackManager {
func initComposeStackManager(assetsPath string, dataStorePath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
composeWrapper := exec.NewComposeWrapper(assetsPath, proxyManager)
if composeWrapper != nil {
return composeWrapper
}
return libcompose.NewComposeStackManager(dataStorePath, reverseTunnelService)
}
@@ -89,6 +96,10 @@ func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error)
return nil, err
}
if settings.UserSessionTimeout == "" {
settings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
dataStore.Settings().UpdateSettings(settings)
}
jwtService, err := jwt.NewService(settings.UserSessionTimeout)
if err != nil {
return nil, err
@@ -380,8 +391,10 @@ func main() {
if err != nil {
log.Fatal(err)
}
kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager)
composeStackManager := initComposeStackManager(*flags.Data, reverseTunnelService)
composeStackManager := initComposeStackManager(*flags.Assets, *flags.Data, reverseTunnelService, proxyManager)
kubernetesDeployer := initKubernetesDeployer(*flags.Assets)
@@ -448,27 +461,29 @@ func main() {
}
var server portainer.Server = &http.Server{
ReverseTunnelService: reverseTunnelService,
Status: applicationStatus,
BindAddress: *flags.Addr,
AssetsPath: *flags.Assets,
DataStore: dataStore,
SwarmStackManager: swarmStackManager,
ComposeStackManager: composeStackManager,
KubernetesDeployer: kubernetesDeployer,
CryptoService: cryptoService,
JWTService: jwtService,
FileService: fileService,
LDAPService: ldapService,
OAuthService: oauthService,
GitService: gitService,
SignatureService: digitalSignatureService,
SnapshotService: snapshotService,
SSL: *flags.SSL,
SSLCert: *flags.SSLCert,
SSLKey: *flags.SSLKey,
DockerClientFactory: dockerClientFactory,
KubernetesClientFactory: kubernetesClientFactory,
ReverseTunnelService: reverseTunnelService,
Status: applicationStatus,
BindAddress: *flags.Addr,
AssetsPath: *flags.Assets,
DataStore: dataStore,
SwarmStackManager: swarmStackManager,
ComposeStackManager: composeStackManager,
KubernetesDeployer: kubernetesDeployer,
CryptoService: cryptoService,
JWTService: jwtService,
FileService: fileService,
LDAPService: ldapService,
OAuthService: oauthService,
GitService: gitService,
ProxyManager: proxyManager,
KubernetesTokenCacheManager: kubernetesTokenCacheManager,
SignatureService: digitalSignatureService,
SnapshotService: snapshotService,
SSL: *flags.SSL,
SSLCert: *flags.SSLCert,
SSLKey: *flags.SSLKey,
DockerClientFactory: dockerClientFactory,
KubernetesClientFactory: kubernetesClientFactory,
}
log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr)

132
api/exec/compose_wrapper.go Normal file
View File

@@ -0,0 +1,132 @@
package exec
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"path"
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy"
)
// ComposeWrapper is a wrapper for docker-compose binary
type ComposeWrapper struct {
binaryPath string
proxyManager *proxy.Manager
}
// NewComposeWrapper returns a docker-compose wrapper if corresponding binary present, otherwise nil
func NewComposeWrapper(binaryPath string, proxyManager *proxy.Manager) *ComposeWrapper {
if !IsBinaryPresent(programPath(binaryPath, "docker-compose")) {
return nil
}
return &ComposeWrapper{
binaryPath: binaryPath,
proxyManager: proxyManager,
}
}
// ComposeSyntaxMaxVersion returns the maximum supported version of the docker compose syntax
func (w *ComposeWrapper) ComposeSyntaxMaxVersion() string {
return portainer.ComposeSyntaxMaxVersion
}
// Up builds, (re)creates and starts containers in the background. Wraps `docker-compose up -d` command
func (w *ComposeWrapper) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
_, err := w.command([]string{"up", "-d"}, stack, endpoint)
return err
}
// Down stops and removes containers, networks, images, and volumes. Wraps `docker-compose down --remove-orphans` command
func (w *ComposeWrapper) Down(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
_, err := w.command([]string{"down", "--remove-orphans"}, stack, endpoint)
return err
}
func (w *ComposeWrapper) command(command []string, stack *portainer.Stack, endpoint *portainer.Endpoint) ([]byte, error) {
if endpoint == nil {
return nil, errors.New("cannot call a compose command on an empty endpoint")
}
program := programPath(w.binaryPath, "docker-compose")
options := setComposeFile(stack)
options = addProjectNameOption(options, stack)
options, err := addEnvFileOption(options, stack)
if err != nil {
return nil, err
}
if !(endpoint.URL == "" || strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://")) {
proxy, err := w.proxyManager.CreateComposeProxyServer(endpoint)
if err != nil {
return nil, err
}
defer proxy.Close()
options = append(options, "-H", fmt.Sprintf("http://127.0.0.1:%d", proxy.Port))
}
args := append(options, command...)
var stderr bytes.Buffer
cmd := exec.Command(program, args...)
cmd.Stderr = &stderr
out, err := cmd.Output()
if err != nil {
return out, errors.New(stderr.String())
}
return out, nil
}
func setComposeFile(stack *portainer.Stack) []string {
options := make([]string, 0)
if stack == nil || stack.EntryPoint == "" {
return options
}
composeFilePath := path.Join(stack.ProjectPath, stack.EntryPoint)
options = append(options, "-f", composeFilePath)
return options
}
func addProjectNameOption(options []string, stack *portainer.Stack) []string {
if stack == nil || stack.Name == "" {
return options
}
options = append(options, "-p", stack.Name)
return options
}
func addEnvFileOption(options []string, stack *portainer.Stack) ([]string, error) {
if stack == nil || stack.Env == nil || len(stack.Env) == 0 {
return options, nil
}
envFilePath := path.Join(stack.ProjectPath, "stack.env")
envfile, err := os.OpenFile(envFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return options, err
}
for _, v := range stack.Env {
envfile.WriteString(fmt.Sprintf("%s=%s\n", v.Name, v.Value))
}
envfile.Close()
options = append(options, "--env-file", envFilePath)
return options, nil
}

View File

@@ -0,0 +1,75 @@
// +build integration
package exec
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
portainer "github.com/portainer/portainer/api"
)
const composeFile = `version: "3.9"
services:
busybox:
image: "alpine:latest"
container_name: "compose_wrapper_test"`
const composedContainerName = "compose_wrapper_test"
func setup(t *testing.T) (*portainer.Stack, *portainer.Endpoint) {
dir := t.TempDir()
composeFileName := "compose_wrapper_test.yml"
f, _ := os.Create(filepath.Join(dir, composeFileName))
f.WriteString(composeFile)
stack := &portainer.Stack{
ProjectPath: dir,
EntryPoint: composeFileName,
Name: "project-name",
}
endpoint := &portainer.Endpoint{}
return stack, endpoint
}
func Test_UpAndDown(t *testing.T) {
stack, endpoint := setup(t)
w := NewComposeWrapper("", nil)
err := w.Up(stack, endpoint)
if err != nil {
t.Fatalf("Error calling docker-compose up: %s", err)
}
if containerExists(composedContainerName) == false {
t.Fatal("container should exist")
}
err = w.Down(stack, endpoint)
if err != nil {
t.Fatalf("Error calling docker-compose down: %s", err)
}
if containerExists(composedContainerName) {
t.Fatal("container should be removed")
}
}
func containerExists(contaierName string) bool {
cmd := exec.Command(osProgram("docker"), "ps", "-a", "-f", fmt.Sprintf("name=%s", contaierName))
out, err := cmd.Output()
if err != nil {
log.Fatalf("failed to list containers: %s", err)
}
return strings.Contains(string(out), contaierName)
}

View File

@@ -0,0 +1,143 @@
package exec
import (
"io/ioutil"
"os"
"path"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert"
)
func Test_setComposeFile(t *testing.T) {
tests := []struct {
name string
stack *portainer.Stack
expected []string
}{
{
name: "should return empty result if stack is missing",
stack: nil,
expected: []string{},
},
{
name: "should return empty result if stack don't have entrypoint",
stack: &portainer.Stack{},
expected: []string{},
},
{
name: "should allow file name and dir",
stack: &portainer.Stack{
ProjectPath: "dir",
EntryPoint: "file",
},
expected: []string{"-f", path.Join("dir", "file")},
},
{
name: "should allow file name only",
stack: &portainer.Stack{
EntryPoint: "file",
},
expected: []string{"-f", "file"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := setComposeFile(tt.stack)
assert.ElementsMatch(t, tt.expected, result)
})
}
}
func Test_addProjectNameOption(t *testing.T) {
tests := []struct {
name string
stack *portainer.Stack
expected []string
}{
{
name: "should not add project option if stack is missing",
stack: nil,
expected: []string{},
},
{
name: "should not add project option if stack doesn't have name",
stack: &portainer.Stack{},
expected: []string{},
},
{
name: "should add project name option if stack has a name",
stack: &portainer.Stack{
Name: "project-name",
},
expected: []string{"-p", "project-name"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
options := []string{"-a", "b"}
result := addProjectNameOption(options, tt.stack)
assert.ElementsMatch(t, append(options, tt.expected...), result)
})
}
}
func Test_addEnvFileOption(t *testing.T) {
dir := t.TempDir()
tests := []struct {
name string
stack *portainer.Stack
expected []string
expectedContent string
}{
{
name: "should not add env file option if stack is missing",
stack: nil,
expected: []string{},
},
{
name: "should not add env file option if stack doesn't have env variables",
stack: &portainer.Stack{},
expected: []string{},
},
{
name: "should not add env file option if stack's env variables are empty",
stack: &portainer.Stack{
ProjectPath: dir,
Env: []portainer.Pair{},
},
expected: []string{},
},
{
name: "should add env file option if stack has env variables",
stack: &portainer.Stack{
ProjectPath: dir,
Env: []portainer.Pair{
{Name: "var1", Value: "value1"},
{Name: "var2", Value: "value2"},
},
},
expected: []string{"--env-file", path.Join(dir, "stack.env")},
expectedContent: "var1=value1\nvar2=value2\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
options := []string{"-a", "b"}
result, _ := addEnvFileOption(options, tt.stack)
assert.ElementsMatch(t, append(options, tt.expected...), result)
if tt.expectedContent != "" {
f, _ := os.Open(path.Join(dir, "stack.env"))
content, _ := ioutil.ReadAll(f)
assert.Equal(t, tt.expectedContent, string(content))
}
})
}
}

View File

@@ -134,6 +134,8 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, dataPa
if !endpoint.TLSConfig.TLSSkipVerify {
args = append(args, "--tlsverify", "--tlscacert", endpoint.TLSConfig.TLSCACertPath)
} else {
args = append(args, "--tlscacert", "''")
}
if endpoint.TLSConfig.TLSCertPath != "" && endpoint.TLSConfig.TLSKeyPath != "" {

24
api/exec/utils.go Normal file
View File

@@ -0,0 +1,24 @@
package exec
import (
"os/exec"
"path/filepath"
"runtime"
)
func osProgram(program string) string {
if runtime.GOOS == "windows" {
program += ".exe"
}
return program
}
func programPath(rootPath, program string) string {
return filepath.Join(rootPath, osProgram(program))
}
// IsBinaryPresent returns true if corresponding program exists on PATH
func IsBinaryPresent(program string) bool {
_, err := exec.LookPath(program)
return err == nil
}

16
api/exec/utils_test.go Normal file
View File

@@ -0,0 +1,16 @@
package exec
import (
"testing"
)
func Test_isBinaryPresent(t *testing.T) {
if !IsBinaryPresent("docker") {
t.Error("expect docker binary to exist on the path")
}
if IsBinaryPresent("executable-with-this-name-should-not-exist") {
t.Error("expect binary with a random name to be missing on the path")
}
}

View File

@@ -28,6 +28,7 @@ require (
github.com/portainer/libcompose v0.5.3
github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33
github.com/stretchr/testify v1.6.1 // indirect
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 // indirect
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45

View File

@@ -262,12 +262,15 @@ github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
github.com/urfave/cli v1.21.0/go.mod h1:lxDj6qX9Q6lWQxIrbrT0nwecwUtRnhVZAJjJZrVUZZQ=
@@ -392,6 +395,8 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -6,7 +6,7 @@ import (
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
)
@@ -30,6 +30,7 @@ func (handler *Handler) endpointInspect(w http.ResponseWriter, r *http.Request)
}
hideFields(endpoint)
endpoint.ComposeSyntaxMaxVersion = handler.ComposeStackManager.ComposeSyntaxMaxVersion()
return response.JSON(w, endpoint)
}

View File

@@ -5,12 +5,11 @@ import (
"strconv"
"strings"
"github.com/portainer/portainer/api"
"github.com/portainer/libhttp/request"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
)
@@ -89,6 +88,7 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
for idx := range paginatedEndpoints {
hideFields(&paginatedEndpoints[idx])
paginatedEndpoints[idx].ComposeSyntaxMaxVersion = handler.ComposeStackManager.ComposeSyntaxMaxVersion()
}
w.Header().Set("X-Total-Count", strconv.Itoa(filteredEndpointCount))

View File

@@ -27,6 +27,7 @@ type Handler struct {
ProxyManager *proxy.Manager
ReverseTunnelService portainer.ReverseTunnelService
SnapshotService portainer.SnapshotService
ComposeStackManager portainer.ComposeStackManager
}
// NewHandler creates a handler to manage endpoint operations.

View File

@@ -7,7 +7,7 @@ import (
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
)
@@ -71,7 +71,7 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
registry.Username = *payload.Username
}
if payload.Password != nil {
if payload.Password != nil && *payload.Password != "" {
registry.Password = *payload.Password
}

View File

@@ -7,11 +7,12 @@ import (
"regexp"
"strconv"
"strings"
"time"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/security"
)
@@ -60,13 +61,14 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerComposeStack,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ComposeFileDefaultName,
Env: payload.Env,
Status: portainer.StackStatusActive,
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerComposeStack,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ComposeFileDefaultName,
Env: payload.Env,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
}
stackFolder := strconv.Itoa(int(stack.ID))
@@ -89,6 +91,8 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
stack.CreatedBy = config.user.Username
err = handler.DataStore.Stack().CreateStack(stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
@@ -146,13 +150,14 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerComposeStack,
EndpointID: endpoint.ID,
EntryPoint: payload.ComposeFilePathInRepository,
Env: payload.Env,
Status: portainer.StackStatusActive,
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerComposeStack,
EndpointID: endpoint.ID,
EntryPoint: payload.ComposeFilePathInRepository,
Env: payload.Env,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
}
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
@@ -185,6 +190,8 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
stack.CreatedBy = config.user.Username
err = handler.DataStore.Stack().CreateStack(stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
@@ -242,13 +249,14 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerComposeStack,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ComposeFileDefaultName,
Env: payload.Env,
Status: portainer.StackStatusActive,
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerComposeStack,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ComposeFileDefaultName,
Env: payload.Env,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
}
stackFolder := strconv.Itoa(int(stack.ID))
@@ -271,6 +279,8 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
stack.CreatedBy = config.user.Username
err = handler.DataStore.Stack().CreateStack(stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
@@ -347,7 +357,6 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig)
!isAdminOrEndpointAdmin {
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
if err != nil {
return err

View File

@@ -6,11 +6,12 @@ import (
"path"
"strconv"
"strings"
"time"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/security"
)
@@ -55,14 +56,15 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerSwarmStack,
SwarmID: payload.SwarmID,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ComposeFileDefaultName,
Env: payload.Env,
Status: portainer.StackStatusActive,
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerSwarmStack,
SwarmID: payload.SwarmID,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ComposeFileDefaultName,
Env: payload.Env,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
}
stackFolder := strconv.Itoa(int(stack.ID))
@@ -85,6 +87,8 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
stack.CreatedBy = config.user.Username
err = handler.DataStore.Stack().CreateStack(stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
@@ -145,14 +149,15 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerSwarmStack,
SwarmID: payload.SwarmID,
EndpointID: endpoint.ID,
EntryPoint: payload.ComposeFilePathInRepository,
Env: payload.Env,
Status: portainer.StackStatusActive,
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerSwarmStack,
SwarmID: payload.SwarmID,
EndpointID: endpoint.ID,
EntryPoint: payload.ComposeFilePathInRepository,
Env: payload.Env,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
}
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
@@ -185,6 +190,8 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
stack.CreatedBy = config.user.Username
err = handler.DataStore.Stack().CreateStack(stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
@@ -249,14 +256,15 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerSwarmStack,
SwarmID: payload.SwarmID,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ComposeFileDefaultName,
Env: payload.Env,
Status: portainer.StackStatusActive,
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerSwarmStack,
SwarmID: payload.SwarmID,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ComposeFileDefaultName,
Env: payload.Env,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
}
stackFolder := strconv.Itoa(int(stack.ID))
@@ -279,6 +287,8 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
stack.CreatedBy = config.user.Username
err = handler.DataStore.Stack().CreateStack(stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}

View File

@@ -7,7 +7,7 @@ import (
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
)
@@ -78,6 +78,17 @@ func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedR
return handler.userIsAdminOrEndpointAdmin(user, endpointID)
}
func (handler *Handler) userIsAdmin(userID portainer.UserID) (bool, error) {
user, err := handler.DataStore.User().User(userID)
if err != nil {
return false, err
}
isAdmin := user.Role == portainer.AdministratorRole
return isAdmin, nil
}
func (handler *Handler) userIsAdminOrEndpointAdmin(user *portainer.User, endpointID portainer.EndpointID) (bool, error) {
isAdmin := user.Role == portainer.AdministratorRole

View File

@@ -183,9 +183,20 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *port
}
func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *portainer.Stack, userID portainer.UserID) *httperror.HandlerError {
resourceControl := authorization.NewPrivateResourceControl(stack.Name, portainer.StackResourceControl, userID)
var resourceControl *portainer.ResourceControl
err := handler.DataStore.ResourceControl().CreateResourceControl(resourceControl)
isAdmin, err := handler.userIsAdmin(userID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err}
}
if isAdmin {
resourceControl = authorization.NewAdministratorsOnlyResourceControl(stack.Name, portainer.StackResourceControl)
} else {
resourceControl = authorization.NewPrivateResourceControl(stack.Name, portainer.StackResourceControl, userID)
}
err = handler.DataStore.ResourceControl().CreateResourceControl(resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist resource control inside the database", err}
}

View File

@@ -155,5 +155,6 @@ func (handler *Handler) deleteStack(stack *portainer.Stack, endpoint *portainer.
if stack.Type == portainer.DockerSwarmStack {
return handler.SwarmStackManager.Remove(stack, endpoint)
}
return handler.ComposeStackManager.Down(stack, endpoint)
}

View File

@@ -4,13 +4,13 @@ import (
"errors"
"net/http"
portainer "github.com/portainer/portainer/api"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
)

View File

@@ -4,15 +4,14 @@ import (
"errors"
"net/http"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
)
// POST request on /api/stacks/:id/stop

View File

@@ -4,12 +4,13 @@ import (
"errors"
"net/http"
"strconv"
"time"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
@@ -135,6 +136,9 @@ func (handler *Handler) updateComposeStack(r *http.Request, stack *portainer.Sta
return configErr
}
stack.UpdateDate = time.Now().Unix()
stack.UpdatedBy = config.user.Username
err = handler.deployComposeStack(config)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
@@ -163,6 +167,9 @@ func (handler *Handler) updateSwarmStack(r *http.Request, stack *portainer.Stack
return configErr
}
stack.UpdateDate = time.Now().Unix()
stack.UpdatedBy = config.user.Username
err = handler.deploySwarmStack(config)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}

View File

@@ -559,16 +559,18 @@ func (transport *Transport) executeGenericResourceDeletionOperation(request *htt
return response, err
}
resourceControl, err := transport.dataStore.ResourceControl().ResourceControlByResourceIDAndType(resourceIdentifierAttribute, resourceType)
if err != nil {
return response, err
}
if resourceControl != nil {
err = transport.dataStore.ResourceControl().DeleteResourceControl(resourceControl.ID)
if response.StatusCode == http.StatusNoContent || response.StatusCode == http.StatusOK {
resourceControl, err := transport.dataStore.ResourceControl().ResourceControlByResourceIDAndType(resourceIdentifierAttribute, resourceType)
if err != nil {
return response, err
}
if resourceControl != nil {
err = transport.dataStore.ResourceControl().DeleteResourceControl(resourceControl.ID)
if err != nil {
return response, err
}
}
}
return response, err

View File

@@ -0,0 +1,88 @@
package factory
import (
"fmt"
"log"
"net"
"net/http"
"net/url"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/proxy/factory/dockercompose"
)
// ProxyServer provide an extedned proxy with a local server to forward requests
type ProxyServer struct {
server *http.Server
Port int
}
func (factory *ProxyFactory) NewDockerComposeAgentProxy(endpoint *portainer.Endpoint) (*ProxyServer, error) {
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
return &ProxyServer{
Port: factory.reverseTunnelService.GetTunnelDetails(endpoint.ID).Port,
}, nil
}
endpointURL, err := url.Parse(endpoint.URL)
if err != nil {
return nil, err
}
endpointURL.Scheme = "http"
httpTransport := &http.Transport{}
if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify {
config, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
if err != nil {
return nil, err
}
httpTransport.TLSClientConfig = config
endpointURL.Scheme = "https"
}
proxy := newSingleHostReverseProxyWithHostHeader(endpointURL)
proxy.Transport = dockercompose.NewAgentTransport(factory.signatureService, httpTransport)
proxyServer := &ProxyServer{
&http.Server{
Handler: proxy,
},
0,
}
return proxyServer, proxyServer.start()
}
func (proxy *ProxyServer) start() error {
listener, err := net.Listen("tcp", ":0")
if err != nil {
return err
}
proxy.Port = listener.Addr().(*net.TCPAddr).Port
go func() {
proxyHost := fmt.Sprintf("127.0.0.1:%d", proxy.Port)
log.Printf("Starting Proxy server on %s...\n", proxyHost)
err := proxy.server.Serve(listener)
log.Printf("Exiting Proxy server %s\n", proxyHost)
if err != http.ErrServerClosed {
log.Printf("Proxy server %s exited with an error: %s\n", proxyHost, err)
}
}()
return nil
}
// Close shuts down the server
func (proxy *ProxyServer) Close() {
if proxy.server != nil {
proxy.server.Close()
}
}

View File

@@ -0,0 +1,40 @@
package dockercompose
import (
"net/http"
portainer "github.com/portainer/portainer/api"
)
type (
// AgentTransport is an http.Transport wrapper that adds custom http headers to communicate to an Agent
AgentTransport struct {
httpTransport *http.Transport
signatureService portainer.DigitalSignatureService
endpointIdentifier portainer.EndpointID
}
)
// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer agent
func NewAgentTransport(signatureService portainer.DigitalSignatureService, httpTransport *http.Transport) *AgentTransport {
transport := &AgentTransport{
httpTransport: httpTransport,
signatureService: signatureService,
}
return transport
}
// RoundTrip is the implementation of the the http.RoundTripper interface
func (transport *AgentTransport) RoundTrip(request *http.Request) (*http.Response, error) {
signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
if err != nil {
return nil, err
}
request.Header.Set(portainer.PortainerAgentPublicKeyHeader, transport.signatureService.EncodedPublicKey())
request.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
return transport.httpTransport.RoundTrip(request)
}

View File

@@ -3,6 +3,7 @@ package kubernetes
import (
"crypto/tls"
"fmt"
"log"
"net/http"
"github.com/portainer/portainer/api/http/security"
@@ -13,14 +14,16 @@ import (
type (
localTransport struct {
httpTransport *http.Transport
tokenManager *tokenManager
httpTransport *http.Transport
tokenManager *tokenManager
endpointIdentifier portainer.EndpointID
}
agentTransport struct {
httpTransport *http.Transport
tokenManager *tokenManager
signatureService portainer.DigitalSignatureService
httpTransport *http.Transport
tokenManager *tokenManager
signatureService portainer.DigitalSignatureService
endpointIdentifier portainer.EndpointID
}
edgeTransport struct {
@@ -50,21 +53,11 @@ func NewLocalTransport(tokenManager *tokenManager) (*localTransport, error) {
// RoundTrip is the implementation of the the http.RoundTripper interface
func (transport *localTransport) RoundTrip(request *http.Request) (*http.Response, error) {
tokenData, err := security.RetrieveTokenData(request)
token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier)
if err != nil {
return nil, err
}
var token string
if tokenData.Role == portainer.AdministratorRole {
token = transport.tokenManager.getAdminServiceAccountToken()
} else {
token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID))
if err != nil {
return nil, err
}
}
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
return transport.httpTransport.RoundTrip(request)
@@ -85,21 +78,11 @@ func NewAgentTransport(signatureService portainer.DigitalSignatureService, tlsCo
// RoundTrip is the implementation of the the http.RoundTripper interface
func (transport *agentTransport) RoundTrip(request *http.Request) (*http.Response, error) {
tokenData, err := security.RetrieveTokenData(request)
token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier)
if err != nil {
return nil, err
}
var token string
if tokenData.Role == portainer.AdministratorRole {
token = transport.tokenManager.getAdminServiceAccountToken()
} else {
token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID))
if err != nil {
return nil, err
}
}
request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
@@ -127,21 +110,11 @@ func NewEdgeTransport(reverseTunnelService portainer.ReverseTunnelService, endpo
// RoundTrip is the implementation of the the http.RoundTripper interface
func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response, error) {
tokenData, err := security.RetrieveTokenData(request)
token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier)
if err != nil {
return nil, err
}
var token string
if tokenData.Role == portainer.AdministratorRole {
token = transport.tokenManager.getAdminServiceAccountToken()
} else {
token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID))
if err != nil {
return nil, err
}
}
request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
response, err := transport.httpTransport.RoundTrip(request)
@@ -154,3 +127,27 @@ func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response
return response, err
}
func getRoundTripToken(
request *http.Request,
tokenManager *tokenManager,
endpointIdentifier portainer.EndpointID,
) (string, error) {
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return "", err
}
var token string
if tokenData.Role == portainer.AdministratorRole {
token = tokenManager.getAdminServiceAccountToken()
} else {
token, err = tokenManager.getUserServiceAccountToken(int(tokenData.ID))
if err != nil {
log.Printf("Failed retrieving service account token: %v", err)
return "", err
}
}
return token, nil
}

View File

@@ -1,6 +1,7 @@
package proxy
import (
"fmt"
"net/http"
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
@@ -21,6 +22,7 @@ type (
proxyFactory *factory.ProxyFactory
endpointProxies cmap.ConcurrentMap
legacyExtensionProxies cmap.ConcurrentMap
k8sClientFactory *cli.ClientFactory
}
)
@@ -29,6 +31,7 @@ func NewManager(dataStore portainer.DataStore, signatureService portainer.Digita
return &Manager{
endpointProxies: cmap.New(),
legacyExtensionProxies: cmap.New(),
k8sClientFactory: kubernetesClientFactory,
proxyFactory: factory.NewProxyFactory(dataStore, signatureService, tunnelService, clientFactory, kubernetesClientFactory, kubernetesTokenCacheManager),
}
}
@@ -41,13 +44,19 @@ func (manager *Manager) CreateAndRegisterEndpointProxy(endpoint *portainer.Endpo
return nil, err
}
manager.endpointProxies.Set(string(endpoint.ID), proxy)
manager.endpointProxies.Set(fmt.Sprint(endpoint.ID), proxy)
return proxy, nil
}
// CreateComposeProxyServer creates a new HTTP reverse proxy based on endpoint properties and and adds it to the registered proxies.
// It can also be used to create a new HTTP reverse proxy and replace an already registered proxy.
func (manager *Manager) CreateComposeProxyServer(endpoint *portainer.Endpoint) (*factory.ProxyServer, error) {
return manager.proxyFactory.NewDockerComposeAgentProxy(endpoint)
}
// GetEndpointProxy returns the proxy associated to a key
func (manager *Manager) GetEndpointProxy(endpoint *portainer.Endpoint) http.Handler {
proxy, ok := manager.endpointProxies.Get(string(endpoint.ID))
proxy, ok := manager.endpointProxies.Get(fmt.Sprint(endpoint.ID))
if !ok {
return nil
}
@@ -56,8 +65,11 @@ func (manager *Manager) GetEndpointProxy(endpoint *portainer.Endpoint) http.Hand
}
// DeleteEndpointProxy deletes the proxy associated to a key
// and cleans the k8s endpoint client cache. DeleteEndpointProxy
// is currently only called for edge connection clean up.
func (manager *Manager) DeleteEndpointProxy(endpoint *portainer.Endpoint) {
manager.endpointProxies.Remove(string(endpoint.ID))
manager.endpointProxies.Remove(fmt.Sprint(endpoint.ID))
manager.k8sClientFactory.RemoveKubeClient(endpoint)
}
// CreateLegacyExtensionProxy creates a new HTTP reverse proxy for a legacy extension and adds it to the registered proxies

View File

@@ -39,39 +39,41 @@ import (
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/kubernetes/cli"
)
// Server implements the portainer.Server interface
type Server struct {
BindAddress string
AssetsPath string
Status *portainer.Status
ReverseTunnelService portainer.ReverseTunnelService
ComposeStackManager portainer.ComposeStackManager
CryptoService portainer.CryptoService
SignatureService portainer.DigitalSignatureService
SnapshotService portainer.SnapshotService
FileService portainer.FileService
DataStore portainer.DataStore
GitService portainer.GitService
JWTService portainer.JWTService
LDAPService portainer.LDAPService
OAuthService portainer.OAuthService
SwarmStackManager portainer.SwarmStackManager
Handler *handler.Handler
SSL bool
SSLCert string
SSLKey string
DockerClientFactory *docker.ClientFactory
KubernetesClientFactory *cli.ClientFactory
KubernetesDeployer portainer.KubernetesDeployer
BindAddress string
AssetsPath string
Status *portainer.Status
ReverseTunnelService portainer.ReverseTunnelService
ComposeStackManager portainer.ComposeStackManager
CryptoService portainer.CryptoService
SignatureService portainer.DigitalSignatureService
SnapshotService portainer.SnapshotService
FileService portainer.FileService
DataStore portainer.DataStore
GitService portainer.GitService
JWTService portainer.JWTService
LDAPService portainer.LDAPService
OAuthService portainer.OAuthService
SwarmStackManager portainer.SwarmStackManager
ProxyManager *proxy.Manager
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
Handler *handler.Handler
SSL bool
SSLCert string
SSLKey string
DockerClientFactory *docker.ClientFactory
KubernetesClientFactory *cli.ClientFactory
KubernetesDeployer portainer.KubernetesDeployer
}
// Start starts the HTTP server
func (server *Server) Start() error {
kubernetesTokenCacheManager := kubernetes.NewTokenCacheManager()
proxyManager := proxy.NewManager(server.DataStore, server.SignatureService, server.ReverseTunnelService, server.DockerClientFactory, server.KubernetesClientFactory, kubernetesTokenCacheManager)
kubernetesTokenCacheManager := server.KubernetesTokenCacheManager
requestBouncer := security.NewRequestBouncer(server.DataStore, server.JWTService)
@@ -82,7 +84,7 @@ func (server *Server) Start() error {
authHandler.CryptoService = server.CryptoService
authHandler.JWTService = server.JWTService
authHandler.LDAPService = server.LDAPService
authHandler.ProxyManager = proxyManager
authHandler.ProxyManager = server.ProxyManager
authHandler.KubernetesTokenCacheManager = kubernetesTokenCacheManager
authHandler.OAuthService = server.OAuthService
@@ -116,10 +118,10 @@ func (server *Server) Start() error {
var endpointHandler = endpoints.NewHandler(requestBouncer)
endpointHandler.DataStore = server.DataStore
endpointHandler.FileService = server.FileService
endpointHandler.ProxyManager = proxyManager
endpointHandler.ProxyManager = server.ProxyManager
endpointHandler.SnapshotService = server.SnapshotService
endpointHandler.ProxyManager = proxyManager
endpointHandler.ReverseTunnelService = server.ReverseTunnelService
endpointHandler.ComposeStackManager = server.ComposeStackManager
var endpointEdgeHandler = endpointedge.NewHandler(requestBouncer)
endpointEdgeHandler.DataStore = server.DataStore
@@ -131,7 +133,7 @@ func (server *Server) Start() error {
var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer)
endpointProxyHandler.DataStore = server.DataStore
endpointProxyHandler.ProxyManager = proxyManager
endpointProxyHandler.ProxyManager = server.ProxyManager
endpointProxyHandler.ReverseTunnelService = server.ReverseTunnelService
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
@@ -141,7 +143,7 @@ func (server *Server) Start() error {
var registryHandler = registries.NewHandler(requestBouncer)
registryHandler.DataStore = server.DataStore
registryHandler.FileService = server.FileService
registryHandler.ProxyManager = proxyManager
registryHandler.ProxyManager = server.ProxyManager
var resourceControlHandler = resourcecontrols.NewHandler(requestBouncer)
resourceControlHandler.DataStore = server.DataStore

View File

@@ -6,6 +6,21 @@ import (
"github.com/portainer/portainer/api"
)
// NewAdministratorsOnlyResourceControl will create a new administrators only resource control associated to the resource specified by the
// identifier and type parameters.
func NewAdministratorsOnlyResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType) *portainer.ResourceControl {
return &portainer.ResourceControl{
Type: resourceType,
ResourceID: resourceIdentifier,
SubResourceIDs: []string{},
UserAccesses: []portainer.UserResourceAccess{},
TeamAccesses: []portainer.TeamResourceAccess{},
AdministratorsOnly: true,
Public: false,
System: false,
}
}
// NewPrivateResourceControl will create a new private resource control associated to the resource specified by the
// identifier and type parameters. It automatically assigns it to the user specified by the userID parameter.
func NewPrivateResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType, userID portainer.UserID) *portainer.ResourceControl {

View File

@@ -40,6 +40,11 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService, revers
}
}
// Remove the cached kube client so a new one can be created
func (factory *ClientFactory) RemoveKubeClient(endpoint *portainer.Endpoint) {
factory.endpointClients.Remove(strconv.Itoa(int(endpoint.ID)))
}
// GetKubeClient checks if an existing client is already registered for the endpoint and returns it if one is found.
// If no client is registered, it will create a new client, register it, and returns it.
func (factory *ClientFactory) GetKubeClient(endpoint *portainer.Endpoint) (portainer.KubeClient, error) {

View File

@@ -13,11 +13,12 @@ import (
"github.com/portainer/libcompose/lookup"
"github.com/portainer/libcompose/project"
"github.com/portainer/libcompose/project/options"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
)
const (
dockerClientVersion = "1.24"
dockerClientVersion = "1.24"
composeSyntaxMaxVersion = "2"
)
// ComposeStackManager represents a service for managing compose stacks.
@@ -58,6 +59,11 @@ func (manager *ComposeStackManager) createClient(endpoint *portainer.Endpoint) (
return client.NewDefaultFactory(clientOpts)
}
// ComposeSyntaxMaxVersion returns the maximum supported version of the docker compose syntax
func (manager *ComposeStackManager) ComposeSyntaxMaxVersion() string {
return composeSyntaxMaxVersion
}
// Up will deploy a compose stack (equivalent of docker-compose up)
func (manager *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {

View File

@@ -190,24 +190,25 @@ type (
// Endpoint represents a Docker endpoint with all the info required
// to connect to it
Endpoint struct {
ID EndpointID `json:"Id"`
Name string `json:"Name"`
Type EndpointType `json:"Type"`
URL string `json:"URL"`
GroupID EndpointGroupID `json:"GroupId"`
PublicURL string `json:"PublicURL"`
TLSConfig TLSConfiguration `json:"TLSConfig"`
Extensions []EndpointExtension `json:"Extensions"`
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"`
TagIDs []TagID `json:"TagIds"`
Status EndpointStatus `json:"Status"`
Snapshots []DockerSnapshot `json:"Snapshots"`
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
EdgeID string `json:"EdgeID,omitempty"`
EdgeKey string `json:"EdgeKey"`
EdgeCheckinInterval int `json:"EdgeCheckinInterval"`
Kubernetes KubernetesData `json:"Kubernetes"`
ID EndpointID `json:"Id"`
Name string `json:"Name"`
Type EndpointType `json:"Type"`
URL string `json:"URL"`
GroupID EndpointGroupID `json:"GroupId"`
PublicURL string `json:"PublicURL"`
TLSConfig TLSConfiguration `json:"TLSConfig"`
Extensions []EndpointExtension `json:"Extensions"`
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"`
TagIDs []TagID `json:"TagIds"`
Status EndpointStatus `json:"Status"`
Snapshots []DockerSnapshot `json:"Snapshots"`
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
EdgeID string `json:"EdgeID,omitempty"`
EdgeKey string `json:"EdgeKey"`
EdgeCheckinInterval int `json:"EdgeCheckinInterval"`
Kubernetes KubernetesData `json:"Kubernetes"`
ComposeSyntaxMaxVersion string `json:"ComposeSyntaxMaxVersion"`
// Deprecated fields
// Deprecated in DBVersion == 4
@@ -554,6 +555,10 @@ type (
Env []Pair `json:"Env"`
ResourceControl *ResourceControl `json:"ResourceControl"`
Status StackStatus `json:"Status"`
CreationDate int64
CreatedBy string
UpdateDate int64
UpdatedBy string
ProjectPath string
}
@@ -774,6 +779,7 @@ type (
// ComposeStackManager represents a service to manage Compose stacks
ComposeStackManager interface {
ComposeSyntaxMaxVersion() string
Up(stack *Stack, endpoint *Endpoint) error
Down(stack *Stack, endpoint *Endpoint) error
}
@@ -1119,9 +1125,11 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.0.0"
APIVersion = "2.0.1"
// DBVersion is the version number of the Portainer database
DBVersion = 25
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
ComposeSyntaxMaxVersion = "3.9"
// AssetsServerURL represents the URL of the Portainer asset server
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved

View File

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

View File

@@ -178,6 +178,11 @@ a[ng-click] {
word-break: break-word;
}
.widget .widget-body table.container-details-table > tbody > tr > td:first-child {
word-break: normal;
text-transform: uppercase;
}
.widget .widget-body table.description-table {
table-layout: fixed;
}
@@ -922,6 +927,27 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
z-index: 2;
}
.striketext:before,
.striketext:after {
background-color: #777777;
content: '';
display: inline-block;
height: 1px;
position: relative;
vertical-align: middle;
width: 50%;
}
.striketext:before {
right: 0.5em;
margin-left: -50%;
}
.striketext:after {
left: 0.5em;
margin-right: -50%;
}
/*bootbox override*/
.modal-open {
padding-right: 0 !important;
@@ -1037,3 +1063,7 @@ json-tree .branch-preview {
background-color: #337ab7;
}
/* !spinkit override */
.w-full {
width: 100%;
}

View File

@@ -4,100 +4,8 @@
<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 class="settings">
<span
class="setting"
ng-class="{ 'setting-active': $ctrl.columnVisibility.state.open }"
uib-dropdown
dropdown-append-to-body
auto-close="disabled"
is-open="$ctrl.columnVisibility.state.open"
>
<span uib-dropdown-toggle><i class="fa fa-columns space-right" aria-hidden="true"></i>Columns</span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader">
Show / Hide Columns
</div>
<div class="menuContent">
<div class="md-checkbox">
<input
id="col_vis_state"
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
type="checkbox"
ng-model="$ctrl.columnVisibility.columns.state.display"
/>
<label for="col_vis_state" ng-bind="$ctrl.columnVisibility.columns.state.label"></label>
</div>
<div class="md-checkbox">
<input
id="col_vis_actions"
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
type="checkbox"
ng-model="$ctrl.columnVisibility.columns.actions.display"
/>
<label for="col_vis_actions" ng-bind="$ctrl.columnVisibility.columns.actions.label"></label>
</div>
<div class="md-checkbox">
<input
id="col_vis_stack"
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
type="checkbox"
ng-model="$ctrl.columnVisibility.columns.stack.display"
/>
<label for="col_vis_stack" ng-bind="$ctrl.columnVisibility.columns.stack.label"></label>
</div>
<div class="md-checkbox">
<input
id="col_vis_image"
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
type="checkbox"
ng-model="$ctrl.columnVisibility.columns.image.display"
/>
<label for="col_vis_image" ng-bind="$ctrl.columnVisibility.columns.image.label"></label>
</div>
<div class="md-checkbox">
<input
id="col_vis_created"
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
type="checkbox"
ng-model="$ctrl.columnVisibility.columns.created.display"
/>
<label for="col_vis_created" ng-bind="$ctrl.columnVisibility.columns.created.label"></label>
</div>
<div class="md-checkbox" ng-if="$ctrl.showHostColumn">
<input
id="col_vis_host"
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
type="checkbox"
ng-model="$ctrl.columnVisibility.columns.host.display"
/>
<label for="col_vis_host" ng-bind="$ctrl.columnVisibility.columns.host.label"></label>
</div>
<div class="md-checkbox">
<input
id="col_vis_ports"
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
type="checkbox"
ng-model="$ctrl.columnVisibility.columns.ports.display"
/>
<label for="col_vis_ports" ng-bind="$ctrl.columnVisibility.columns.ports.label"></label>
</div>
<div class="md-checkbox">
<input
id="col_vis_ownership"
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
type="checkbox"
ng-model="$ctrl.columnVisibility.columns.ownership.display"
/>
<label for="col_vis_ownership" ng-bind="$ctrl.columnVisibility.columns.ownership.label"></label>
</div>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.columnVisibility.state.open = false;">Close</a>
</div>
</div>
</div>
</span>
<datatable-columns-visibility columns="$ctrl.columnVisibility.columns" on-change="($ctrl.onColumnVisibilityChange)"></datatable-columns-visibility>
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Settings</span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>

View File

@@ -36,9 +36,6 @@ angular.module('portainer.docker').controller('ContainersDatatableController', [
};
this.columnVisibility = {
state: {
open: false,
},
columns: {
state: {
label: 'State',
@@ -75,9 +72,11 @@ angular.module('portainer.docker').controller('ContainersDatatableController', [
},
};
this.onColumnVisibilityChange = function (columnVisibility) {
DatatableService.setColumnVisibilitySettings(this.tableKey, columnVisibility);
};
this.onColumnVisibilityChange = onColumnVisibilityChange.bind(this);
function onColumnVisibilityChange(columns) {
this.columnVisibility.columns = columns;
DatatableService.setColumnVisibilitySettings(this.tableKey, this.columnVisibility);
}
this.onSelectionChanged = function () {
this.updateSelectionState();
@@ -199,7 +198,6 @@ angular.module('portainer.docker').controller('ContainersDatatableController', [
var storedColumnVisibility = DatatableService.getColumnVisibilitySettings(this.tableKey);
if (storedColumnVisibility !== null) {
this.columnVisibility = storedColumnVisibility;
this.columnVisibility.state.open = false;
}
};
},

View File

@@ -36,10 +36,13 @@
<!-- don't use registry -->
<div ng-if="!$ctrl.model.UseRegistry">
<div class="form-group">
<label for="image_name" ng-class="$ctrl.labelClass" class="control-label text-left"
>Image
<portainer-tooltip position="bottom" message="Image and repository should be publicly available."></portainer-tooltip>
</label>
<span class="small">
<p class="text-muted" style="margin-left: 15px;">
<i class="fa fa-exclamation-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
When using advanced mode, image and repository <b>must be</b> publicly available.
</p>
</span>
<label for="image_name" ng-class="$ctrl.labelClass" class="control-label text-left">Image </label>
<div ng-class="$ctrl.inputClass">
<input type="text" class="form-control" ng-model="$ctrl.model.Image" name="image_name" placeholder="e.g. registry:port/myImage:myTag" required />
</div>

View File

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

View File

@@ -67,6 +67,7 @@
Actions
</label>
<div class="col-sm-11">
<button class="btn btn-primary btn-sm" type="button" ng-click="$ctrl.downloadLogs()" style="margin-left: 0;"><i class="fa fa-download"></i> Download logs</button>
<button
class="btn btn-primary btn-sm"
ng-click="$ctrl.copy()"

View File

@@ -1,8 +1,11 @@
import moment from 'moment';
import _ from 'lodash-es';
angular.module('portainer.docker').controller('LogViewerController', [
'clipboard',
function (clipboard) {
'Blob',
'FileSaver',
function (clipboard, Blob, FileSaver) {
this.state = {
availableSinceDatetime: [
{ desc: 'Last day', value: moment().subtract(1, 'days').format() },
@@ -43,5 +46,10 @@ angular.module('portainer.docker').controller('LogViewerController', [
this.state.selectedLines.splice(idx, 1);
}
};
this.downloadLogs = function () {
const data = new Blob([_.reduce(this.state.filteredLogs, (acc, log) => acc + '\n' + log, '')]);
FileSaver.saveAs(data, this.resourceName + '_logs.txt');
};
},
]);

View File

@@ -1,14 +1,18 @@
import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl';
function b64DecodeUnicode(str) {
return decodeURIComponent(
atob(str)
.split('')
.map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
})
.join('')
);
try {
return decodeURIComponent(
atob(str)
.split('')
.map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
})
.join('')
);
} catch (err) {
return atob(str);
}
}
export function ConfigViewModel(data) {

View File

@@ -4,7 +4,6 @@ export function ImageViewModel(data) {
this.Repository = data.Repository;
this.Created = data.Created;
this.Checked = false;
this.RepoTags = data.RepoTags;
if (!this.RepoTags && data.RepoDigests) {
this.RepoTags = [];
@@ -21,6 +20,7 @@ export function ImageViewModel(data) {
if (data.Portainer && data.Portainer.Agent && data.Portainer.Agent.NodeName) {
this.NodeName = data.Portainer.Agent.NodeName;
}
this.Labels = data.Labels;
}
export function ImageBuildModel(data) {

View File

@@ -16,4 +16,5 @@ export function ImageDetailsViewModel(data) {
this.ExposedPorts = data.ContainerConfig.ExposedPorts ? Object.keys(data.ContainerConfig.ExposedPorts) : [];
this.Volumes = data.ContainerConfig.Volumes ? Object.keys(data.ContainerConfig.Volumes) : [];
this.Env = data.ContainerConfig.Env ? data.ContainerConfig.Env : [];
this.Labels = data.ContainerConfig.Labels;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -128,6 +128,17 @@
<td>Build</td>
<td>Docker {{ image.DockerVersion }} on {{ image.Os }}, {{ image.Architecture }}</td>
</tr>
<tr ng-if="!(image.Labels | emptyobject)">
<td>Labels</td>
<td>
<table class="table table-bordered table-condensed">
<tr ng-repeat="(k, v) in image.Labels">
<td>{{ k }}</td>
<td>{{ v }}</td>
</tr>
</table>
</td>
</tr>
<tr ng-if="image.Author">
<td>Author</td>
<td>{{ image.Author }}</td>

View File

@@ -196,7 +196,8 @@
<div class="form-group" ng-hide="config.Driver === 'macvlan' && formValues.Macvlan.Scope === 'local'">
<div class="col-sm-12">
<label for="ownership" class="control-label text-left">
Restrict external access to the network
Isolated network
<portainer-tooltip position="bottom" message="An isolated network has no inbound or outbound communications."></portainer-tooltip>
</label>
<label name="ownership" class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="config.Internal" />

View File

@@ -520,6 +520,12 @@ angular.module('portainer.docker').controller('CreateServiceController', [
return true;
}
$scope.volumesAreValid = volumesAreValid;
function volumesAreValid() {
const volumes = $scope.formValues.Volumes;
return volumes.every((volume) => volume.Target && volume.Source);
}
$scope.create = function createService() {
var accessControlData = $scope.formValues.AccessControlData;

View File

@@ -123,7 +123,7 @@
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="state.actionInProgress || !formValues.RegistryModel.Image"
ng-disabled="state.actionInProgress || !formValues.RegistryModel.Image || !volumesAreValid()"
ng-click="create()"
button-spinner="state.actionInProgress"
>
@@ -298,16 +298,19 @@
<!-- 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>
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'volume'" ng-click="volume.Source = null">Volume</label>
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'bind'" ng-click="volume.Source = null">Bind</label>
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeVolume($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
@@ -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 value="">Select a volume</option>
</select>
</div>
<div class="small text-warning" ng-show="!volume.Source"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Source is required. </div>
</div>
<!-- !volume -->
<!-- bind -->
<div class="input-group input-group-sm col-sm-6" ng-if="volume.Type === 'bind'">
<span class="input-group-addon">host</span>
<input type="text" class="form-control" ng-model="volume.Source" placeholder="e.g. /path/on/host" />
<div class="input-group input-group-sm w-full">
<span class="input-group-addon">host</span>
<input type="text" class="form-control" ng-model="volume.Source" placeholder="e.g. /path/on/host" />
</div>
<div class="small text-warning" ng-show="!volume.Source"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Source is required. </div>
</div>
<!-- !bind -->
<!-- read-only -->
<div class="input-group input-group-sm col-sm-5" style="margin-left: 5px;">
<div class="input-group input-group-sm col-sm-5" style="margin-left: 5px; vertical-align: top;">
<div class="btn-group btn-group-sm">
<label class="btn btn-primary" ng-model="volume.ReadOnly" uib-btn-radio="false">Writable</label>
<label class="btn btn-primary" ng-model="volume.ReadOnly" uib-btn-radio="true">Read-only</label>

View File

@@ -42,12 +42,14 @@
<input
type="text"
class="form-control"
name=""
ng-model="mount.Source"
placeholder="e.g. /tmp/portainer/data"
ng-change="updateMount(service, mount)"
ng-disabled="isUpdating || (!isAdmin && !allowBindMounts && mount.Type === 'bind')"
ng-if="mount.Type === 'bind'"
/>
<div class="col-sm-12 small text-warning" ng-show="!mount.Source"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Source is required. </div>
</td>
<td>
<input
@@ -59,6 +61,7 @@
ng-disabled="isUpdating"
disable-authorization="DockerServiceUpdate"
/>
<div class="col-sm-12 small text-warning" ng-show="!mount.Target"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Target is required. </div>
</td>
<td authorization="DockerServiceUpdate">
<input type="checkbox" class="form-control" ng-model="mount.ReadOnly" ng-change="updateMount(service, mount)" ng-disabled="isUpdating" />
@@ -77,7 +80,9 @@
<rd-widget-footer authorization="DockerServiceUpdate">
<div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['ServiceMounts'])" ng-click="updateService(service)">Apply changes</button>
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!mountsAreValid() || !hasChanges(service, ['ServiceMounts'])" ng-click="updateService(service)">
Apply changes
</button>
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
</button>

View File

@@ -378,6 +378,12 @@ angular.module('portainer.docker').controller('ServiceController', [
return hasChanges;
};
$scope.mountsAreValid = mountsAreValid;
function mountsAreValid() {
const mounts = $scope.service.ServiceMounts;
return mounts.every((mount) => mount.Source && mount.Target);
}
function buildChanges(service) {
var config = ServiceHelper.serviceToConfig(service.Model);
config.Name = service.Name;

View File

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

View File

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

View File

@@ -51,14 +51,18 @@ angular.module('portainer.docker').controller('VolumeController', [
};
$scope.removeVolume = function removeVolume() {
VolumeService.remove($scope.volume)
.then(function success() {
Notifications.success('Volume successfully removed', $transition$.params().id);
$state.go('docker.volumes', {});
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove volume');
});
ModalService.confirmDeletion('Do you want to remove this volume?', (confirmed) => {
if (confirmed) {
VolumeService.remove($scope.volume)
.then(function success() {
Notifications.success('Volume successfully removed', $transition$.params().id);
$state.go('docker.volumes', {});
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove volume');
});
}
});
};
function getVolumeDataFromContainer(container, volumeId) {

View File

@@ -9,26 +9,31 @@ angular.module('portainer.docker').controller('VolumesController', [
'HttpRequestHelper',
'EndpointProvider',
'Authentication',
function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, EndpointProvider, Authentication) {
'ModalService',
function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, EndpointProvider, Authentication, ModalService) {
$scope.removeAction = function (selectedItems) {
var actionCount = selectedItems.length;
angular.forEach(selectedItems, function (volume) {
HttpRequestHelper.setPortainerAgentTargetHeader(volume.NodeName);
VolumeService.remove(volume)
.then(function success() {
Notifications.success('Volume successfully removed', volume.Id);
var index = $scope.volumes.indexOf(volume);
$scope.volumes.splice(index, 1);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove volume');
})
.finally(function final() {
--actionCount;
if (actionCount === 0) {
$state.reload();
}
ModalService.confirmDeletion('Do you want to remove the selected volume(s)?', (confirmed) => {
if (confirmed) {
var actionCount = selectedItems.length;
angular.forEach(selectedItems, function (volume) {
HttpRequestHelper.setPortainerAgentTargetHeader(volume.NodeName);
VolumeService.remove(volume)
.then(function success() {
Notifications.success('Volume successfully removed', volume.Id);
var index = $scope.volumes.indexOf(volume);
$scope.volumes.splice(index, 1);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove volume');
})
.finally(function final() {
--actionCount;
if (actionCount === 0) {
$state.reload();
}
});
});
}
});
};

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html
><html lang="en" ng-app="<%= name %>">
><html lang="en" ng-app="<%= name %>" ng-strict-di>
<head>
<meta charset="utf-8" />
<title>Portainer</title>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -71,7 +71,7 @@
<table class="table table-hover nowrap-cells">
<thead>
<tr>
<th>
<th style="width: 55px;">
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
@@ -142,8 +142,9 @@
</td>
</tr>
<tr dir-paginate-end ng-show="item.Expanded" ng-repeat="app in item.Applications" ng-style="{ background: item.Highlighted ? '#d5e8f3' : '#f5f5f5' }">
<td colspan="5">
<a ui-sref="kubernetes.applications.application({ name: app.Name, namespace: app.ResourcePool })" style="margin-left: 25px;">{{ app.Name }}</a>
<td></td>
<td colspan="4">
<a ui-sref="kubernetes.applications.application({ name: app.Name, namespace: app.ResourcePool })">{{ app.Name }}</a>
<span style="margin-left: 5px;" class="label label-primary image-tag" ng-if="!$ctrl.isSystemNamespace(app.ResourcePool) && $ctrl.isExternalApplication(app)"
>external</span
>

View File

@@ -118,7 +118,7 @@
<span style="margin-left: 5px;" class="label label-info image-tag" ng-if="$ctrl.isSystemNamespace(item)">system</span>
</td>
<td> <i class="fa {{ item.Quota ? 'fa-toggle-on' : 'fa-toggle-off' }}" aria-hidden="true" style="margin-right: 2px;"></i> {{ item.Quota ? 'Yes' : 'No' }} </td>
<td>{{ item.CreationDate | getisodate }} {{ item.Namespace.ResourcePoolOwner ? 'by ' + item.Namespace.ResourcePoolOwner : '' }}</td>
<td>{{ item.Namespace.CreationDate | getisodate }} {{ item.Namespace.ResourcePoolOwner ? 'by ' + item.Namespace.ResourcePoolOwner : '' }}</td>
<td ng-if="$ctrl.isAdmin">
<a ng-if="$ctrl.canManageAccess(item)" ui-sref="kubernetes.resourcePools.resourcePool.access({id: item.Namespace.Name})">
<i class="fa fa-users" aria-hidden="true"></i> Manage access

View File

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

View File

@@ -3,13 +3,13 @@
Data
</div>
<div class="form-group">
<div class="form-group" ng-if="$ctrl.isCreation">
<div class="col-sm-12">
<p>
<a class="small interactive" ng-if="$ctrl.formValues.IsSimple" ng-click="$ctrl.formValues.IsSimple = false">
<a class="small interactive" ng-if="$ctrl.formValues.IsSimple" ng-click="$ctrl.showAdvancedMode()">
<i class="fa fa-list-ol space-right" aria-hidden="true"></i> Advanced mode
</a>
<a class="small interactive" ng-if="!$ctrl.formValues.IsSimple" ng-click="$ctrl.formValues.IsSimple = true">
<a class="small interactive" ng-if="!$ctrl.formValues.IsSimple" ng-click="$ctrl.showSimpleMode()">
<i class="fa fa-edit space-right" aria-hidden="true"></i> Simple mode
</a>
</p>
@@ -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>
@@ -61,7 +61,7 @@
</div>
</div>
<div class="form-group" ng-if="$ctrl.formValues.IsSimple">
<div class="form-group" ng-if="$ctrl.formValues.IsSimple && !entry.IsBinary">
<label for="configuration_data_value_{{ index }}" class="col-sm-1 control-label text-left">Value</label>
<div class="col-sm-11">
<textarea
@@ -80,6 +80,13 @@
</div>
</div>
<div class="form-group" ng-if="$ctrl.formValues.IsSimple && entry.IsBinary">
<label for="configuration_data_value_{{ index }}" class="col-sm-1 control-label text-left">Value</label>
<div class="col-sm-11 control-label small text-muted text-left"
>Binary data <portainer-tooltip position="bottom" message="This key holds binary data and cannot be displayed."></portainer-tooltip
></div>
</div>
<div class="form-group" ng-if="$ctrl.formValues.IsSimple">
<div class="col-sm-1"></div>
<div class="col-sm-11">

View File

@@ -4,5 +4,6 @@ angular.module('portainer.kubernetes').component('kubernetesConfigurationData',
bindings: {
formValues: '=',
isValid: '=',
isCreation: '=',
},
});

View File

@@ -1,7 +1,10 @@
import angular from 'angular';
import _ from 'lodash-es';
import { KubernetesConfigurationFormValuesDataEntry } from 'Kubernetes/models/configuration/formvalues';
import chardet from 'chardet';
import { Base64 } from 'js-base64';
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
class KubernetesConfigurationDataController {
/* @ngInject */
@@ -12,6 +15,8 @@ class KubernetesConfigurationDataController {
this.editorUpdateAsync = this.editorUpdateAsync.bind(this);
this.onFileLoad = this.onFileLoad.bind(this);
this.onFileLoadAsync = this.onFileLoadAsync.bind(this);
this.showSimpleMode = this.showSimpleMode.bind(this);
this.showAdvancedMode = this.showAdvancedMode.bind(this);
}
onChangeKey() {
@@ -20,7 +25,7 @@ class KubernetesConfigurationDataController {
}
addEntry() {
this.formValues.Data.push(new KubernetesConfigurationFormValuesDataEntry());
this.formValues.Data.push(new KubernetesConfigurationFormValuesEntry());
}
removeEntry(index) {
@@ -37,9 +42,20 @@ class KubernetesConfigurationDataController {
}
async onFileLoadAsync(event) {
const entry = new KubernetesConfigurationFormValuesDataEntry();
const entry = new KubernetesConfigurationFormValuesEntry();
const encoding = chardet.detect(Buffer.from(event.target.result));
const decoder = new TextDecoder(encoding);
entry.Key = event.target.fileName;
entry.Value = event.target.result;
entry.IsBinary = KubernetesConfigurationHelper.isBinary(encoding);
if (!entry.IsBinary) {
entry.Value = decoder.decode(event.target.result);
} else {
const stringValue = decoder.decode(event.target.result);
entry.Value = Base64.encode(stringValue);
}
this.formValues.Data.push(entry);
this.onChangeKey();
}
@@ -53,10 +69,20 @@ class KubernetesConfigurationDataController {
const temporaryFileReader = new FileReader();
temporaryFileReader.fileName = file.name;
temporaryFileReader.onload = this.onFileLoad;
temporaryFileReader.readAsText(file);
temporaryFileReader.readAsArrayBuffer(file);
}
}
showSimpleMode() {
this.formValues.IsSimple = true;
this.formValues.Data = KubernetesConfigurationHelper.parseYaml(this.formValues);
}
showAdvancedMode() {
this.formValues.IsSimple = false;
this.formValues.DataYaml = KubernetesConfigurationHelper.parseData(this.formValues);
}
$onInit() {
this.state = {
duplicateKeys: {},

View File

@@ -16,6 +16,6 @@
<li class="sidebar-list">
<a ui-sref="kubernetes.cluster({endpointId: $ctrl.endpointId})" ui-sref-active="active">Cluster <span class="menu-icon fa fa-server fa-fw"></span></a>
<div class="sidebar-sublist" ng-if="$ctrl.adminAccess && ($ctrl.currentState === 'kubernetes.cluster' || $ctrl.currentState === 'portainer.endpoints.endpoint.kubernetesConfig')">
<a ui-sref="portainer.endpoints.endpoint.kubernetesConfig({id: $ctrl.endpointId})">Setup</a>
<a ui-sref="portainer.endpoints.endpoint.kubernetesConfig({id: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
</div>
</li>

View File

@@ -27,6 +27,7 @@ import KubernetesServiceConverter from 'Kubernetes/converters/service';
import KubernetesPersistentVolumeClaimConverter from 'Kubernetes/converters/persistentVolumeClaim';
import PortainerError from 'Portainer/error';
import { KubernetesIngressHelper } from 'Kubernetes/ingress/helper';
import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
function _apiPortsToPublishedPorts(pList, pRefs) {
const ports = _.map(pList, (item) => {
@@ -50,7 +51,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 +62,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 +119,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 +174,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 +218,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);
@@ -283,6 +295,8 @@ class KubernetesApplicationConverter {
}
static applicationFormValuesToApplication(formValues) {
formValues.ApplicationOwner = KubernetesCommonHelper.ownerToLabel(formValues.ApplicationOwner);
const claims = KubernetesPersistentVolumeClaimConverter.applicationFormValuesToVolumeClaims(formValues);
const rwx = _.find(claims, (item) => _.includes(item.StorageClass.AccessModes, 'RWX')) !== undefined;
@@ -310,7 +324,7 @@ class KubernetesApplicationConverter {
} else if (daemonSet) {
app = KubernetesDaemonSetConverter.applicationFormValuesToDaemonSet(formValues, claims);
} else {
throw new PortainerError('Unable to determine which association to use');
throw new PortainerError('Unable to determine which association to use to convert form');
}
let headlessService;

View File

@@ -1,8 +1,8 @@
import _ from 'lodash-es';
import YAML from 'yaml';
import { KubernetesConfigMap } from 'Kubernetes/models/config-map/models';
import { KubernetesConfigMapCreatePayload, KubernetesConfigMapUpdatePayload } from 'Kubernetes/models/config-map/payloads';
import { KubernetesPortainerConfigurationOwnerLabel } from 'Kubernetes/models/configuration/models';
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
class KubernetesConfigMapConverter {
/**
@@ -16,7 +16,23 @@ class KubernetesConfigMapConverter {
res.ConfigurationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : '';
res.CreationDate = data.metadata.creationTimestamp;
res.Yaml = yaml ? yaml.data : '';
res.Data = data.data;
res.Data = _.concat(
_.map(data.data, (value, key) => {
const entry = new KubernetesConfigurationFormValuesEntry();
entry.Key = key;
entry.Value = value;
return entry;
}),
_.map(data.binaryData, (value, key) => {
const entry = new KubernetesConfigurationFormValuesEntry();
entry.Key = key;
entry.Value = value;
entry.IsBinary = true;
return entry;
})
);
return res;
}
@@ -39,8 +55,16 @@ class KubernetesConfigMapConverter {
const res = new KubernetesConfigMapCreatePayload();
res.metadata.name = data.Name;
res.metadata.namespace = data.Namespace;
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = data.ConfigurationOwner;
res.data = data.Data;
const configurationOwner = _.truncate(data.ConfigurationOwner, { length: 63, omission: '' });
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = configurationOwner;
_.forEach(data.Data, (entry) => {
if (entry.IsBinary) {
res.binaryData[entry.Key] = entry.Value;
} else {
res.data[entry.Key] = entry.Value;
}
});
return res;
}
@@ -53,7 +77,13 @@ class KubernetesConfigMapConverter {
res.metadata.name = data.Name;
res.metadata.namespace = data.Namespace;
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = data.ConfigurationOwner;
res.data = data.Data;
_.forEach(data.Data, (entry) => {
if (entry.IsBinary) {
res.binaryData[entry.Key] = entry.Value;
} else {
res.data[entry.Key] = entry.Value;
}
});
return res;
}
@@ -63,18 +93,7 @@ class KubernetesConfigMapConverter {
res.Name = formValues.Name;
res.Namespace = formValues.ResourcePool.Namespace.Name;
res.ConfigurationOwner = formValues.ConfigurationOwner;
if (formValues.IsSimple) {
res.Data = _.reduce(
formValues.Data,
(acc, entry) => {
acc[entry.Key] = entry.Value;
return acc;
},
{}
);
} else {
res.Data = YAML.parse(formValues.DataYaml);
}
res.Data = formValues.Data;
return res;
}
}

View File

@@ -1,3 +1,4 @@
import _ from 'lodash-es';
import { KubernetesConfiguration, KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
class KubernetesConfigurationConverter {
@@ -9,7 +10,9 @@ class KubernetesConfigurationConverter {
res.Namespace = secret.Namespace;
res.CreationDate = secret.CreationDate;
res.Yaml = secret.Yaml;
res.Data = secret.Data;
_.forEach(secret.Data, (entry) => {
res.Data[entry.Key] = entry.Value;
});
res.ConfigurationOwner = secret.ConfigurationOwner;
return res;
}
@@ -22,7 +25,9 @@ class KubernetesConfigurationConverter {
res.Namespace = configMap.Namespace;
res.CreationDate = configMap.CreationDate;
res.Yaml = configMap.Yaml;
res.Data = configMap.Data;
_.forEach(configMap.Data, (entry) => {
res.Data[entry.Key] = entry.Value;
});
res.ConfigurationOwner = configMap.ConfigurationOwner;
return res;
}

View File

@@ -1,5 +1,4 @@
import * as JsonPatch from 'fast-json-patch';
import { KubernetesDaemonSet } from 'Kubernetes/models/daemon-set/models';
import { KubernetesDaemonSetCreatePayload } from 'Kubernetes/models/daemon-set/payloads';
import {

View File

@@ -1,3 +1,4 @@
import _ from 'lodash-es';
import { KubernetesNamespace } from 'Kubernetes/models/namespace/models';
import { KubernetesNamespaceCreatePayload } from 'Kubernetes/models/namespace/payloads';
import { KubernetesPortainerResourcePoolNameLabel, KubernetesPortainerResourcePoolOwnerLabel } from 'Kubernetes/models/resource-pool/models';
@@ -20,7 +21,8 @@ class KubernetesNamespaceConverter {
res.metadata.name = namespace.Name;
res.metadata.labels[KubernetesPortainerResourcePoolNameLabel] = namespace.ResourcePoolName;
if (namespace.ResourcePoolOwner) {
res.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] = namespace.ResourcePoolOwner;
const resourcePoolOwner = _.truncate(namespace.ResourcePoolOwner, { length: 63, omission: '' });
res.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] = resourcePoolOwner;
}
return res;
}

View File

@@ -1,16 +1,30 @@
import { KubernetesSecretCreatePayload, KubernetesSecretUpdatePayload } from 'Kubernetes/models/secret/payloads';
import { KubernetesApplicationSecret } from 'Kubernetes/models/secret/models';
import YAML from 'yaml';
import { KubernetesPortainerConfigurationDataAnnotation } from 'Kubernetes/models/configuration/models';
import _ from 'lodash-es';
import { KubernetesPortainerConfigurationOwnerLabel } from 'Kubernetes/models/configuration/models';
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
class KubernetesSecretConverter {
static createPayload(secret) {
const res = new KubernetesSecretCreatePayload();
res.metadata.name = secret.Name;
res.metadata.namespace = secret.Namespace;
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = secret.ConfigurationOwner;
res.stringData = secret.Data;
const configurationOwner = _.truncate(secret.ConfigurationOwner, { length: 63, omission: '' });
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = configurationOwner;
let annotation = '';
_.forEach(secret.Data, (entry) => {
if (entry.IsBinary) {
res.data[entry.Key] = entry.Value;
annotation += annotation !== '' ? '|' + entry.Key : entry.Key;
} else {
res.stringData[entry.Key] = entry.Value;
}
});
if (annotation !== '') {
res.metadata.annotations[KubernetesPortainerConfigurationDataAnnotation] = annotation;
}
return res;
}
@@ -18,7 +32,20 @@ class KubernetesSecretConverter {
const res = new KubernetesSecretUpdatePayload();
res.metadata.name = secret.Name;
res.metadata.namespace = secret.Namespace;
res.stringData = secret.Data;
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = secret.ConfigurationOwner;
let annotation = '';
_.forEach(secret.Data, (entry) => {
if (entry.IsBinary) {
res.data[entry.Key] = entry.Value;
annotation += annotation !== '' ? '|' + entry.Key : entry.Key;
} else {
res.stringData[entry.Key] = entry.Value;
}
});
if (annotation !== '') {
res.metadata.annotations[KubernetesPortainerConfigurationDataAnnotation] = annotation;
}
return res;
}
@@ -30,7 +57,21 @@ class KubernetesSecretConverter {
res.ConfigurationOwner = payload.metadata.labels ? payload.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : '';
res.CreationDate = payload.metadata.creationTimestamp;
res.Yaml = yaml ? yaml.data : '';
res.Data = payload.data;
res.Data = _.map(payload.data, (value, key) => {
const annotations = payload.metadata.annotations ? payload.metadata.annotations[KubernetesPortainerConfigurationDataAnnotation] : '';
const entry = new KubernetesConfigurationFormValuesEntry();
entry.Key = key;
entry.IsBinary = _.includes(annotations, entry.Key);
if (!entry.IsBinary) {
entry.Value = atob(value);
} else {
entry.Value = value;
}
return entry;
});
return res;
}
@@ -39,18 +80,7 @@ class KubernetesSecretConverter {
res.Name = formValues.Name;
res.Namespace = formValues.ResourcePool.Namespace.Name;
res.ConfigurationOwner = formValues.ConfigurationOwner;
if (formValues.IsSimple) {
res.Data = _.reduce(
formValues.Data,
(acc, entry) => {
acc[entry.Key] = entry.Value;
return acc;
},
{}
);
} else {
res.Data = YAML.parse(formValues.DataYaml);
}
res.Data = formValues.Data;
return res;
}
}

View File

@@ -69,7 +69,7 @@ class KubernetesServiceConverter {
payload.metadata.namespace = service.Namespace;
payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = service.StackName;
payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = service.ApplicationName;
payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = service.ApplicationOwner;
payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = service.Application;
payload.spec.ports = service.Ports;
payload.spec.selector.app = service.ApplicationName;
if (service.Headless) {

View File

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

View File

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

View File

@@ -3,14 +3,14 @@ import { KubernetesPortMapping, KubernetesPortMappingPort } from 'Kubernetes/mod
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
import {
KubernetesApplicationConfigurationFormValueOverridenKeyTypes,
KubernetesApplicationEnvironmentVariableFormValue,
KubernetesApplicationAutoScalerFormValue,
KubernetesApplicationConfigurationFormValue,
KubernetesApplicationConfigurationFormValueOverridenKey,
KubernetesApplicationConfigurationFormValueOverridenKeyTypes,
KubernetesApplicationEnvironmentVariableFormValue,
KubernetesApplicationPersistedFolderFormValue,
KubernetesApplicationPublishedPortFormValue,
KubernetesApplicationAutoScalerFormValue,
KubernetesApplicationPlacementFormValue,
KubernetesApplicationPublishedPortFormValue,
} from 'Kubernetes/models/application/formValues';
import {
KubernetesApplicationEnvConfigMapPayload,
@@ -23,13 +23,13 @@ import {
KubernetesApplicationVolumeSecretPayload,
} from 'Kubernetes/models/application/payloads';
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
import { KubernetesApplicationPlacementTypes, KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models';
import { KubernetesPodNodeAffinityNodeSelectorRequirementOperators, KubernetesPodAffinity } from 'Kubernetes/pod/models';
import { KubernetesApplicationDeploymentTypes, KubernetesApplicationPlacementTypes } from 'Kubernetes/models/application/models';
import { KubernetesPodAffinity, KubernetesPodNodeAffinityNodeSelectorRequirementOperators } from 'Kubernetes/pod/models';
import {
KubernetesNodeSelectorTermPayload,
KubernetesPreferredSchedulingTermPayload,
KubernetesPodNodeAffinityPayload,
KubernetesNodeSelectorRequirementPayload,
KubernetesNodeSelectorTermPayload,
KubernetesPodNodeAffinityPayload,
KubernetesPreferredSchedulingTermPayload,
} from 'Kubernetes/pod/payloads/affinities';
class KubernetesApplicationHelper {
@@ -38,8 +38,8 @@ class KubernetesApplicationHelper {
return !application.ApplicationOwner;
}
static associatePodsAndApplication(pods, app) {
return _.filter(pods, { Labels: app.spec.selector.matchLabels });
static associatePodsAndApplication(pods, selector) {
return _.filter(pods, ['metadata.labels', selector.matchLabels]);
}
static associateContainerPersistedFoldersAndConfigurations(app, containers) {
@@ -65,7 +65,7 @@ class KubernetesApplicationHelper {
}
static associateContainersAndApplication(app) {
if (!app.Pods) {
if (!app.Pods || app.Pods.length === 0) {
return [];
}
const containers = app.Pods[0].Containers;
@@ -175,7 +175,10 @@ class KubernetesApplicationHelper {
item.OverridenKeys = _.map(keys, (k) => {
const fvKey = new KubernetesApplicationConfigurationFormValueOverridenKey();
fvKey.Key = k.Key;
if (index < k.EnvCount) {
if (!k.Count) {
// !k.Count indicates k.Key is new added to the configuration and has not been loaded to the application yet
fvKey.Type = KubernetesApplicationConfigurationFormValueOverridenKeyTypes.NONE;
} else if (index < k.EnvCount) {
fvKey.Type = KubernetesApplicationConfigurationFormValueOverridenKeyTypes.ENVIRONMENT;
} else {
fvKey.Type = KubernetesApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM;

View File

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

View File

@@ -8,5 +8,13 @@ class KubernetesCommonHelper {
_.set(obj, path, value);
}
}
static ownerToLabel(owner) {
let label = _.replace(owner, /[^-A-Za-z0-9_.]/g, '.');
label = _.truncate(label, { length: 63, omission: '' });
label = _.replace(label, /^[-_.]*/g, '');
label = _.replace(label, /[-_.]*$/g, '');
return label;
}
}
export default KubernetesCommonHelper;

View File

@@ -1,5 +1,7 @@
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
import _ from 'lodash-es';
import YAML from 'yaml';
class KubernetesConfigurationHelper {
static getUsingApplications(config, applications) {
@@ -21,6 +23,10 @@ class KubernetesConfigurationHelper {
return _.startsWith(config.Name, 'default-token-');
}
static isBinary(encoding) {
return encoding !== '' && !_.includes(encoding, 'ISO') && !_.includes(encoding, 'UTF');
}
static setConfigurationUsed(config) {
config.Used = config.Applications && config.Applications.length !== 0;
}
@@ -32,6 +38,31 @@ class KubernetesConfigurationHelper {
});
}
static parseYaml(formValues) {
YAML.defaultOptions.customTags = ['binary'];
const data = _.map(YAML.parse(formValues.DataYaml), (value, key) => {
const entry = new KubernetesConfigurationFormValuesEntry();
entry.Key = key;
entry.Value = value;
const oldEntry = _.find(formValues.Data, { Key: entry.Key });
entry.IsBinary = oldEntry ? oldEntry.IsBinary : false;
return entry;
});
return data;
}
static parseData(formValues) {
const data = _.reduce(
formValues.Data,
(acc, entry) => {
acc[entry.Key] = entry.Value;
return acc;
},
{}
);
return YAML.stringify(data);
}
static isExternalConfiguration(configuration) {
return !configuration.ConfigurationOwner;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -35,6 +35,7 @@ export class KubernetesApplicationFormValues {
}
export const KubernetesApplicationConfigurationFormValueOverridenKeyTypes = Object.freeze({
NONE: 0,
ENVIRONMENT: 1,
FILESYSTEM: 2,
});

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