From 9af9b70f3ebdddadf3a196ce16dee6dd40f1abfb Mon Sep 17 00:00:00 2001 From: yi-portainer Date: Tue, 2 Feb 2021 17:54:02 +1300 Subject: [PATCH] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit e4605d990d06a6cb9a104ab3348e6ecac3c4c533 Author: yi-portainer Date: Tue Feb 2 17:42:57 2021 +1300 * update portainer version commit 768697157c9f724fb7aad0d028864febbb0235f9 Author: LP B Date: Tue Feb 2 05:00:19 2021 +0100 sec(app): remove unused and vulnerable dependencies (#4801) commit d3086da139b955a075261d37ee9559f3b3b5be80 Author: cong meng Date: Tue Feb 2 15:10:06 2021 +1300 fix(k8s) trigger port validation while changing protocol (ce#394) (#4804) Co-authored-by: Simon Meng commit 95894e80477a2f6fb105b9f4f6b4dbdc8613ae51 Author: cong meng Date: Tue Feb 2 15:03:11 2021 +1300 fix(k8s) parse empty configuration as empty string yaml instead of {} (ce#395) (#4805) Co-authored-by: Simon Meng commit 81de55fedd3227bf43d21bec527e9702dfaa118b Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com> Date: Tue Feb 2 11:12:40 2021 +1300 * fix missing kubectl download (#4802) commit 84827b8782b5aa6bdcc6a5a23b7a8449f2556151 Author: Steven Kang Date: Sun Jan 31 17:32:30 2021 +1300 feat(build): introducing buildx for Windows (#4792) * feat(build): introducing buildx for Windows * feat(build): re-ordered USER * feat(build): Fixed Typo * feat(build): fixed typo commit a71e71f481052b95eb1086d4f635bf8aff7d1c0d Author: Dmitry Salakhov Date: Mon Jan 25 19:16:53 2021 +0000 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 Co-authored-by: Simon Meng Co-authored-by: yi-portainer Co-authored-by: Steven Kang commit 83f4c5ec0bad7c4538ac4ccb3dfaef5d7e0fe560 Author: LP B Date: Mon Jan 25 02:43:54 2021 +0100 fix(k8s/app): remove advanced deployment panel from app details view (#4730) commit 41308d570d71de96e6984f23715bcd510155f019 Author: Maxime Bajeux Date: Mon Jan 25 02:14:35 2021 +0100 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 commit 46ff8a01bcbae5e0645cfa5be8655c767002f57e Author: Chaim Lev-Ari Date: Fri Jan 22 03:08:08 2021 +0200 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 commit 2b257d2785cf02c27f326246722132d1eb1f56f1 Author: yi-portainer Date: Thu Jan 21 00:02:22 2021 +1300 Squashed commit of the following 2.0.1 release fixes: commit f90d6b55d697a24f191ac715238a37b2c17f02ef Author: Chaim Lev-Ari 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 1b82b450d76cc68d635c7dba47ae303583df0919 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 b78d8048812ea610ddf9499c56f65896eeda3b64 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 380f106571330e778e2ef8790cc6f2a617643e97. Co-authored-by: Stéphane Busso commit 51b72c12f933d4252e03ec971ee98c7fe6e5af4c Author: Anthony Lapenna Date: Wed Dec 23 14:45:32 2020 +1300 fix(docker/stack-details): do not display editor tab for external stack (#4650) commit 58c04bdbe362f41bc78212e366200cc6f2661e29 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 a6320d522276d9ec9ca7416b0375d63b30b96ab2 Author: cong meng 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 Co-authored-by: Simon Meng Co-authored-by: Stéphane Busso commit da41dbb79a8451a23f826334d6c4ad9b86533470 Author: cong meng Date: Wed Jan 20 15:19:35 2021 +1300 fix(stack): stacks created via API are incorrectly marked as private with no owner (#3721) (#4725) Co-authored-by: Simon Meng commit 68d42617f2f139dc8e288c19f8024f35f89da3b5 Author: Maxime Bajeux Date: Wed Jan 20 01:02:18 2021 +0100 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 commit 8323e223095b278a6d6636342c87c8ed3faeb5e1 Author: Anthony McMahon <75223906+Anthony-Portainer@users.noreply.github.com> Date: Wed Jan 20 12:06:25 2021 +1300 Update issue templates Adding auto labelling to Bug Report (kind/bug, bug/unconfirmed) and Question (kind/question) commit 20d434117066a59776f86a2fb67277ec7651aac2 Author: Chaim Lev-Ari Date: Tue Jan 19 00:10:08 2021 +0200 fix(state): check validity of state (#4609) commit 832cafc933c8127b967c37a87fd37e8de556b806 Author: Chaim Lev-Ari Date: Mon Jan 18 02:59:57 2021 +0200 fix(registries): update password only when not empty (#4669) commit f3c537ac2cb14d8f9faf1416b61403726ce2a08c Author: cong meng Date: Mon Jan 18 13:02:16 2021 +1300 chore(build): bump Kompose version (#4473) (#4724) Co-authored-by: Simon Meng commit 958baf6283ea0e1b9455f2414e7a039621cd0b93 Author: Anthony McMahon <75223906+Anthony-Portainer@users.noreply.github.com> Date: Mon Jan 18 09:30:17 2021 +1300 Update README.md commit 08e392378e9fb5ede3950c55f02ff3f40ba86438 Author: Chaim Lev-Ari Date: Sun Jan 17 09:28:09 2021 +0200 chore(app): fail on angular components missing nginject (#4224) commit a2d9734b8b47f81338c437b0b8c674ed85976e78 Author: Alice Groux Date: Sun Jan 17 04:50:22 2021 +0100 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 commit 15aed9fc6fc506acef197d4b26ad1aaf80e372d3 Author: DarkAEther <30438425+DarkAEther@users.noreply.github.com> Date: Sun Jan 17 06:23:32 2021 +0530 feat(area/kubernetes): show shared access policy in volume details (#4707) commit 121d33538d203bc6a08e97d09c962c78db9a767d Author: Alice Groux Date: Fri Jan 15 02:51:36 2021 +0100 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 commit 7a03351df8c9017244da4062fa94812011ff5c54 Author: Olli Janatuinen Date: Thu Jan 14 23:05:33 2021 +0200 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 commit 0c2987893d6fa8991a700cd208d78796a9683e8a Author: Alice Groux Date: Thu Jan 14 03:04:44 2021 +0100 feat(app/images): in advanced mode, remove tooltip and add an information message (#4528) commit d1eddaa188e85bd0d0c8a20941fcd4ba392bf5b3 Author: Alice Groux Date: Thu Jan 14 00:24:56 2021 +0100 feat(app/network): rename restrict external acces to the network label and add a tooltip (#4514) commit d336ada3c23716f57c24241926a945bafcaced34 Author: Anthony Lapenna Date: Wed Jan 13 16:13:27 2021 +1300 feat(k8s/application): review application creation warning style (#4613) commit 839198fbff92eb4429fd3f6bbff906fd4a96d72a Author: Avadhut Tanugade <30384908+mrwhoknows55@users.noreply.github.com> Date: Wed Jan 13 04:49:18 2021 +0530 commit 486ffa5bbd424701b80210644a8f1133e482fc5e Author: Chaim Lev-Ari Date: Tue Jan 12 23:40:09 2021 +0200 chore(webpack): add source maps (#4471) * chore(webpack): add source maps * feat(build): fetch source maps for 3rd party libs commit 4cd468ce21bb584cac765c55a132a927274a3680 Author: Maxime Bajeux Date: Tue Jan 12 02:35:59 2021 +0100 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 commit cbd7fdc62ebd667cf3c6671c664e74da7743d911 Author: Chaim Lev-Ari Date: Tue Jan 12 01:38:49 2021 +0200 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 commit b9fe8009dd6b1351ac7bc458967baeed48d1ffe2 Author: DarkAEther <30438425+DarkAEther@users.noreply.github.com> Date: Mon Jan 11 08:05:19 2021 +0530 feat(image-details): Show labels in images datatable (#4287) * feat(images): show labels in images datatable * move labels to image details view commit 6a504e71345b4fb4fe2cd77ceaf4cdbca906a36c Author: Stéphane Busso Date: Mon Jan 11 14:44:15 2021 +1300 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 commit 51ba0876a5ce0b033a229a64423416aa1ed2eb0d Author: Alice Groux Date: Mon Jan 11 00:51:46 2021 +0100 feat(k8s/configuration): rename add ingress controller button and changed information text (#4540) commit 769e6a4c6c7a739df20353548044510617fd14a9 Author: Alice Groux Date: Sun Jan 10 23:30:31 2021 +0100 feat(k8s/configuration): add extra information panel when creating a sensitive configuration (#4541) commit 105d1ae5195545405f376cf8852d14649c9f5a7a Author: cong meng Date: Fri Jan 8 15:30:43 2021 +1300 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 commit cf508065ecb8bc0aeff572fa5367dd51d89d0744 Author: cong meng Date: Fri Jan 8 12:51:27 2021 +1300 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 commit eab828279e03d845fb273ef48b9743d2462e2180 Author: itsconquest Date: Fri Jan 8 12:46:57 2021 +1300 chore(project): exclude refactors (#4689) commit d5763a970b62f0303dc5f0ab850d99fff038ccc8 Author: cong meng Date: Fri Jan 8 12:45:06 2021 +1300 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 commit c9f68a4d8fc1ae5ef1dd4237adee7c3638581015 Author: cong meng Date: Fri Jan 8 11:55:42 2021 +1300 fix(kubernetes): removes kube client cache when edge proxy is removed (#4487) (#4574) Co-authored-by: Simon Meng commit 7848bcf2f496d2406aea11bf7543348ad2c14c74 Author: Alice Groux Date: Thu Jan 7 22:29:17 2021 +0100 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 commit b924347c5b532c8adeb974fc14ba8fa86155a27d Author: Stéphane Busso Date: Thu Jan 7 14:03:46 2021 +1300 Bump portainer version commit 9fbda9fb99a26daa8a6f79a251650992f30c9cb7 Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com> Date: Thu Jan 7 13:38:01 2021 +1300 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 Co-authored-by: Simon Meng Co-authored-by: Stéphane Busso * + 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 380f106571330e778e2ef8790cc6f2a617643e97. Co-authored-by: Stéphane Busso Co-authored-by: cong meng Co-authored-by: Simon Meng Co-authored-by: Stéphane Busso Co-authored-by: Anthony Lapenna commit 82f80627849b54d1eda4d2a0e871b627b42fecc0 Author: Anthony Lapenna Date: Wed Jan 6 11:31:05 2021 +1300 chore(github): update issue template commit 49982eb98aabdd8ccb988c5ebbeb526b1c287b0a Author: knittl Date: Tue Jan 5 20:49:50 2021 +0100 commit 4be3ac470f60b5aee651e6ed9a0271e372129a83 Merge: 7975ef79 a50ab51b Author: Stéphane Busso Date: Thu Dec 24 23:45:53 2020 +1300 Merge pull request #4658 from portainer/revert-4475-chore-ce-86-bump-kompose-version Revert "chore(build): bump Kompose version" commit a50ab51bef6ef673c3f4c11da80fc4dc5074222b Author: Stéphane Busso Date: Thu Dec 24 12:12:28 2020 +1300 Revert "chore(build): bump Kompose version (#4475)" This reverts commit 380f106571330e778e2ef8790cc6f2a617643e97. --- .github/ISSUE_TEMPLATE/Bug_report.md | 7 +- .github/ISSUE_TEMPLATE/Custom.md | 37 ++--- .github/ISSUE_TEMPLATE/Feature_request.md | 65 ++++---- .github/stale.yml | 1 + CONTRIBUTING.md | 20 +++ README.md | 5 +- api/cmd/portainer/main.go | 63 +++++--- api/exec/compose_wrapper.go | 132 ++++++++++++++++ api/exec/compose_wrapper_integration_test.go | 75 +++++++++ api/exec/compose_wrapper_test.go | 143 ++++++++++++++++++ api/exec/swarm_stack.go | 2 + api/exec/utils.go | 24 +++ api/exec/utils_test.go | 16 ++ api/go.mod | 1 + api/go.sum | 5 + .../handler/endpoints/endpoint_inspect.go | 3 +- api/http/handler/endpoints/endpoint_list.go | 4 +- api/http/handler/endpoints/handler.go | 1 + .../handler/registries/registry_update.go | 4 +- .../handler/stacks/create_compose_stack.go | 55 ++++--- api/http/handler/stacks/create_swarm_stack.go | 60 +++++--- api/http/handler/stacks/handler.go | 13 +- api/http/handler/stacks/stack_create.go | 15 +- api/http/handler/stacks/stack_delete.go | 1 + api/http/handler/stacks/stack_start.go | 2 +- api/http/handler/stacks/stack_stop.go | 9 +- api/http/handler/stacks/stack_update.go | 9 +- api/http/proxy/factory/docker_compose.go | 88 +++++++++++ .../proxy/factory/dockercompose/transport.go | 40 +++++ .../proxy/factory/kubernetes/transport.go | 73 +++++---- api/http/proxy/manager.go | 18 ++- api/http/server.go | 60 ++++---- api/internal/authorization/access_control.go | 15 ++ api/kubernetes/cli/client.go | 5 + api/libcompose/compose_stack.go | 10 +- api/portainer.go | 46 +++--- app/assets/css/app.css | 21 +++ .../containersDatatable.html | 96 +----------- .../containersDatatableController.js | 12 +- .../imageRegistry/porImageRegistry.html | 11 +- app/docker/models/image.js | 2 +- app/docker/models/imageDetails.js | 1 + app/docker/views/images/edit/image.html | 11 ++ .../views/networks/create/createnetwork.html | 3 +- .../views/services/create/createservice.html | 6 +- app/index.html | 2 +- .../applicationsStacksDatatable.html | 7 +- .../resourcePoolsDatatable.html | 2 +- .../kubernetesConfigurationData.html | 15 +- .../kubernetesConfigurationData.js | 1 + .../kubernetesConfigurationDataController.js | 36 ++++- app/kubernetes/converters/configMap.js | 53 ++++--- app/kubernetes/converters/configuration.js | 9 +- app/kubernetes/converters/daemonSet.js | 1 - app/kubernetes/converters/namespace.js | 4 +- app/kubernetes/converters/secret.js | 63 +++++--- app/kubernetes/converters/service.js | 2 +- app/kubernetes/helpers/application/index.js | 5 +- app/kubernetes/helpers/commonHelper.js | 6 +- app/kubernetes/helpers/configurationHelper.js | 33 ++++ .../models/application/formValues.js | 1 + app/kubernetes/models/config-map/models.js | 2 +- app/kubernetes/models/config-map/payloads.js | 2 + .../models/configuration/formvalues.js | 10 +- app/kubernetes/models/configuration/models.js | 1 + app/kubernetes/models/secret/models.js | 2 +- app/kubernetes/models/secret/payloads.js | 2 + app/kubernetes/pod/converter.js | 57 ++++++- app/kubernetes/pod/payloads/create.js | 45 ++++++ app/kubernetes/pod/service.js | 25 +++ app/kubernetes/rest/pod.js | 6 + .../templates/advancedDeploymentPanel.html | 11 ++ .../views/applications/applications.html | 14 +- .../applications/applicationsController.js | 2 + .../create/createApplication.html | 22 +-- .../create/createApplicationController.js | 30 +++- .../views/applications/edit/application.html | 12 +- .../edit/applicationController.js | 2 + .../views/configurations/configurations.html | 2 + .../configurationsController.js | 6 +- .../create/createConfiguration.html | 16 +- .../create/createConfigurationController.js | 8 +- .../configurations/edit/configuration.html | 7 +- .../edit/configurationController.js | 34 +++-- app/kubernetes/views/configure/configure.html | 4 +- .../volumes-storages-datatable/template.html | 11 +- app/kubernetes/views/volumes/edit/volume.html | 11 ++ .../views/volumes/edit/volumeController.js | 13 ++ app/kubernetes/views/volumes/volumes.html | 2 + .../views/volumes/volumesController.js | 17 ++- ...datatable-columns-visibility.controller.js | 7 + .../datatable-columns-visibility.html | 19 +++ .../datatable-columns-visibility/index.js | 12 ++ .../datatables/genericDatatableController.js | 6 + .../stacks-datatable/stacksDatatable.html | 27 +++- .../stacksDatatableController.js | 23 +++ app/portainer/helpers/stackHelper.js | 36 ++--- app/portainer/models/stack.js | 7 +- app/portainer/services/api/stackService.js | 20 +-- app/portainer/services/stateManager.js | 2 +- app/portainer/views/auth/auth.html | 73 +++++---- app/portainer/views/auth/authController.js | 11 +- .../stacks/create/createStackController.js | 11 +- .../views/stacks/create/createstack.html | 9 +- app/portainer/views/stacks/edit/stack.html | 23 ++- .../views/stacks/edit/stackController.js | 11 +- build/build_binary_azuredevops.ps1 | 24 --- build/build_binary_azuredevops.sh | 13 +- build/download_docker_binary.ps1 | 14 -- build/download_docker_binary.sh | 3 +- build/download_docker_compose_binary.sh | 18 +++ build/download_kompose_binary.ps1 | 8 - build/download_kompose_binary.sh | 9 +- build/download_kubectl_binary.ps1 | 8 - build/download_kubectl_binary.sh | 9 +- build/windows/Dockerfile | 24 +++ build/windows2016/nanoserver/Dockerfile | 15 -- gruntfile.js | 97 ++++++------ package.json | 8 +- webpack/webpack.common.js | 5 + webpack/webpack.develop.js | 1 + webpack/webpack.production.js | 1 + yarn.lock | 79 +++++++++- 123 files changed, 1870 insertions(+), 669 deletions(-) create mode 100644 api/exec/compose_wrapper.go create mode 100644 api/exec/compose_wrapper_integration_test.go create mode 100644 api/exec/compose_wrapper_test.go create mode 100644 api/exec/utils.go create mode 100644 api/exec/utils_test.go create mode 100644 api/http/proxy/factory/docker_compose.go create mode 100644 api/http/proxy/factory/dockercompose/transport.go create mode 100644 app/kubernetes/pod/payloads/create.js create mode 100644 app/kubernetes/templates/advancedDeploymentPanel.html create mode 100644 app/portainer/components/datatables/datatable-columns-visibility/datatable-columns-visibility.controller.js create mode 100644 app/portainer/components/datatables/datatable-columns-visibility/datatable-columns-visibility.html create mode 100644 app/portainer/components/datatables/datatable-columns-visibility/index.js delete mode 100755 build/build_binary_azuredevops.ps1 delete mode 100644 build/download_docker_binary.ps1 create mode 100755 build/download_docker_compose_binary.sh delete mode 100644 build/download_kompose_binary.ps1 delete mode 100644 build/download_kubectl_binary.ps1 create mode 100644 build/windows/Dockerfile delete mode 100644 build/windows2016/nanoserver/Dockerfile diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md index 3e9b69019..352cfec59 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -1,6 +1,10 @@ --- name: Bug report about: Create a bug report +title: '' +labels: bug/need-confirmation, kind/bug +assignees: '' + --- - -**Question**: -How can I deploy Portainer on... ? +--- +name: Question +about: Ask us a question about Portainer usage or deployment +title: '' +labels: '' +assignees: '' + +--- + + + +**Question**: +How can I deploy Portainer on... ? diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md index 6da5f4265..8c2bee587 100644 --- a/.github/ISSUE_TEMPLATE/Feature_request.md +++ b/.github/ISSUE_TEMPLATE/Feature_request.md @@ -1,31 +1,34 @@ ---- -name: Feature request -about: Suggest a feature/enhancement that should be added in Portainer - ---- - - - -**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: '' + +--- + + + +**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. diff --git a/.github/stale.yml b/.github/stale.yml index a5d534d01..fce5d3700 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -15,6 +15,7 @@ issues: - kind/question - kind/style - kind/workaround + - kind/refactor - bug/need-confirmation - bug/confirmed - status/discuss diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 537ae511f..622876590 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 . + +Find more detailed steps at . diff --git a/README.md b/README.md index 9da782e5a..18fefca5f 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,13 @@ Unlike the public demo, the playground sessions are deleted after 4 hours. Apart - [Deploy Portainer](https://www.portainer.io/installation/) - [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://documentation.portainer.io diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 5d83916be..229b0c0a5 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -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) diff --git a/api/exec/compose_wrapper.go b/api/exec/compose_wrapper.go new file mode 100644 index 000000000..5f91795fd --- /dev/null +++ b/api/exec/compose_wrapper.go @@ -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 +} diff --git a/api/exec/compose_wrapper_integration_test.go b/api/exec/compose_wrapper_integration_test.go new file mode 100644 index 000000000..766622614 --- /dev/null +++ b/api/exec/compose_wrapper_integration_test.go @@ -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) +} diff --git a/api/exec/compose_wrapper_test.go b/api/exec/compose_wrapper_test.go new file mode 100644 index 000000000..caee859ef --- /dev/null +++ b/api/exec/compose_wrapper_test.go @@ -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)) + } + }) + } +} diff --git a/api/exec/swarm_stack.go b/api/exec/swarm_stack.go index 31fb48836..cf59f7607 100644 --- a/api/exec/swarm_stack.go +++ b/api/exec/swarm_stack.go @@ -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 != "" { diff --git a/api/exec/utils.go b/api/exec/utils.go new file mode 100644 index 000000000..75a896f65 --- /dev/null +++ b/api/exec/utils.go @@ -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 +} diff --git a/api/exec/utils_test.go b/api/exec/utils_test.go new file mode 100644 index 000000000..38695488a --- /dev/null +++ b/api/exec/utils_test.go @@ -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") + } +} diff --git a/api/go.mod b/api/go.mod index fcee3b6a8..6a862d58a 100644 --- a/api/go.mod +++ b/api/go.mod @@ -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 diff --git a/api/go.sum b/api/go.sum index d7b7db557..6cdfdbb85 100644 --- a/api/go.sum +++ b/api/go.sum @@ -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= diff --git a/api/http/handler/endpoints/endpoint_inspect.go b/api/http/handler/endpoints/endpoint_inspect.go index 1411e93cb..a4248c87f 100644 --- a/api/http/handler/endpoints/endpoint_inspect.go +++ b/api/http/handler/endpoints/endpoint_inspect.go @@ -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) } diff --git a/api/http/handler/endpoints/endpoint_list.go b/api/http/handler/endpoints/endpoint_list.go index fe7c489ae..fbd3a595e 100644 --- a/api/http/handler/endpoints/endpoint_list.go +++ b/api/http/handler/endpoints/endpoint_list.go @@ -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)) diff --git a/api/http/handler/endpoints/handler.go b/api/http/handler/endpoints/handler.go index c004b5751..3dc8689d6 100644 --- a/api/http/handler/endpoints/handler.go +++ b/api/http/handler/endpoints/handler.go @@ -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. diff --git a/api/http/handler/registries/registry_update.go b/api/http/handler/registries/registry_update.go index e77dfb765..b4a166d73 100644 --- a/api/http/handler/registries/registry_update.go +++ b/api/http/handler/registries/registry_update.go @@ -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 } diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go index 6ceab991c..52fc2844c 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -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 diff --git a/api/http/handler/stacks/create_swarm_stack.go b/api/http/handler/stacks/create_swarm_stack.go index 0113e8a41..f7afbdeb5 100644 --- a/api/http/handler/stacks/create_swarm_stack.go +++ b/api/http/handler/stacks/create_swarm_stack.go @@ -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} diff --git a/api/http/handler/stacks/handler.go b/api/http/handler/stacks/handler.go index c706afbfd..caa537e2f 100644 --- a/api/http/handler/stacks/handler.go +++ b/api/http/handler/stacks/handler.go @@ -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 diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index c9f115ad1..a9fdf2f36 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -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} } diff --git a/api/http/handler/stacks/stack_delete.go b/api/http/handler/stacks/stack_delete.go index 6866c6809..eea3bd367 100644 --- a/api/http/handler/stacks/stack_delete.go +++ b/api/http/handler/stacks/stack_delete.go @@ -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) } diff --git a/api/http/handler/stacks/stack_start.go b/api/http/handler/stacks/stack_start.go index 298a11d42..4c129eed1 100644 --- a/api/http/handler/stacks/stack_start.go +++ b/api/http/handler/stacks/stack_start.go @@ -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" ) diff --git a/api/http/handler/stacks/stack_stop.go b/api/http/handler/stacks/stack_stop.go index ee2c13f32..48175e6b9 100644 --- a/api/http/handler/stacks/stack_stop.go +++ b/api/http/handler/stacks/stack_stop.go @@ -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 diff --git a/api/http/handler/stacks/stack_update.go b/api/http/handler/stacks/stack_update.go index df4178f8f..adc1ca792 100644 --- a/api/http/handler/stacks/stack_update.go +++ b/api/http/handler/stacks/stack_update.go @@ -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} diff --git a/api/http/proxy/factory/docker_compose.go b/api/http/proxy/factory/docker_compose.go new file mode 100644 index 000000000..7da8d898f --- /dev/null +++ b/api/http/proxy/factory/docker_compose.go @@ -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() + } +} diff --git a/api/http/proxy/factory/dockercompose/transport.go b/api/http/proxy/factory/dockercompose/transport.go new file mode 100644 index 000000000..b9be10e01 --- /dev/null +++ b/api/http/proxy/factory/dockercompose/transport.go @@ -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) +} diff --git a/api/http/proxy/factory/kubernetes/transport.go b/api/http/proxy/factory/kubernetes/transport.go index 4fbacf590..c1d0de13b 100644 --- a/api/http/proxy/factory/kubernetes/transport.go +++ b/api/http/proxy/factory/kubernetes/transport.go @@ -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 +} diff --git a/api/http/proxy/manager.go b/api/http/proxy/manager.go index e539d89c2..2013647d8 100644 --- a/api/http/proxy/manager.go +++ b/api/http/proxy/manager.go @@ -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 diff --git a/api/http/server.go b/api/http/server.go index 8f83529f1..35571d736 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -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 diff --git a/api/internal/authorization/access_control.go b/api/internal/authorization/access_control.go index 0e2d91ab7..eab0a3b16 100644 --- a/api/internal/authorization/access_control.go +++ b/api/internal/authorization/access_control.go @@ -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 { diff --git a/api/kubernetes/cli/client.go b/api/kubernetes/cli/client.go index 707bf3924..a268150c9 100644 --- a/api/kubernetes/cli/client.go +++ b/api/kubernetes/cli/client.go @@ -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) { diff --git a/api/libcompose/compose_stack.go b/api/libcompose/compose_stack.go index ec885b65b..4ac6ebdb9 100644 --- a/api/libcompose/compose_stack.go +++ b/api/libcompose/compose_stack.go @@ -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 { diff --git a/api/portainer.go b/api/portainer.go index 64197edef..00e1c32de 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -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.1" + APIVersion = "2.1.0" // 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 diff --git a/app/assets/css/app.css b/app/assets/css/app.css index 4f9ca5881..f813dbb2c 100644 --- a/app/assets/css/app.css +++ b/app/assets/css/app.css @@ -927,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; diff --git a/app/docker/components/datatables/containers-datatable/containersDatatable.html b/app/docker/components/datatables/containers-datatable/containersDatatable.html index 9ba136c4c..36835997d 100644 --- a/app/docker/components/datatables/containers-datatable/containersDatatable.html +++ b/app/docker/components/datatables/containers-datatable/containersDatatable.html @@ -4,100 +4,8 @@
{{ $ctrl.titleText }}
- - Columns - - + + Settings