Compare commits

...

329 Commits

Author SHA1 Message Date
ArrisLee
4bf951f1ee oauth auto admin mapping 2021-06-30 00:22:44 +12:00
Hui
3c64fc7786 feat(oauth): update OAuth settings for auto admin population 2021-06-23 15:45:45 +12:00
zees-dev
8902bae7a4 removed redundant swagger comments as they are not applicable 2021-06-14 16:26:38 +12:00
zees-dev
08bcfa284c updated struct to utilise pointer 2021-06-14 16:24:48 +12:00
zees-dev
1345396c8c updated oauth tests 2021-06-14 16:24:48 +12:00
zees-dev
df4da34a63 merge conflct resolution to get project back to working state 2021-06-14 16:24:48 +12:00
zees-dev
e9675400ab udpated oauth team memebrship tests based on updated test store 2021-06-14 16:24:48 +12:00
zees-dev
49691de937 feat(memberships): automatic OAuth team memberships implementation
- fixed merge conflicts
2021-06-14 16:24:48 +12:00
Hui
ef2161abe9 change to EndpointCreationType (#426) 2021-06-14 14:46:08 +12:00
cong meng
c3f8ec2380 feat(k8s): Add the ability to display realtime node metrics in Kubernetes EE-130 (#359)
* feat(k8s): Add the ability to display realtime node metrics in Kubernetes EE-130

* feat(k8s) show observation timestamp instead of real timestamp EE-130

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-06-14 12:44:30 +12:00
Maxime Bajeux
06f62825ee fix(application): fix bad commit (#337) 2021-06-14 11:07:07 +12:00
Chaim Lev-Ari
4cf8082591 fix(app): parse response with null body (#256)
* fix(images): use async await

* fix(images): show correct error when failing import

* style(docker): add comment explaining change
2021-06-11 12:06:44 +12:00
Hui
96d1230461 feat(OAuth): Add SSO support for OAuth EE-390 (#381)
* feat(oauth): add sso, hide internal auth and logout options. (#355)

* feat(DB): Add new migration func for SSO settings EE-613

* feat(publicSettings): public settings response modification for OAuth SSO EE-608 (#357)

* feat(oauth): updated logout logic with logoutUrl. (#365)

* feat(oauth): update new token expiration for OAuth EE-612

* feat(oauth): add internal-auth view. (#358)

* feat(oauth): add internal-auth view.

* feat(oauth): removed unused method.

* feat(oauth): updated oauth settings model

* feat(oauth): updated #auth view with hide internal auth option (#369)

* HideInternalAuth logic update

* feat(oauth): HideInternalAuth logic update

* feat(oauth): internal auth view updates

* feat(oauth): internal auth login issue.

* set SSO to ON by default

* update migrator unit test

* set SSO to true by default for new instance

* prevent applying the SSO logout url to the initial admin user

* set HideInternalAuth to true by default

Co-authored-by: fhanportainer <79428273+fhanportainer@users.noreply.github.com>
Co-authored-by: Felix Han <felix.han@portainer.io>
2021-06-11 10:08:38 +12:00
Richard Wei
07e83d1e6e fix(frontend):fixes the kubernetes endpoint url not saved EE-825 (#397)
Co-authored-by: richard <richard@richards-iMac-Pro.local>
2021-06-11 09:35:58 +12:00
Chaim Lev-Ari
8b01f8681c fix(registry): handle quay registry (#258)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-06-10 12:26:38 +12:00
zees-dev
9c724d4a82 feat(kubernetes/summary): summary of k8s actions upon deploying/updating resources EE-436 (#399)
* feat(kubernetes/summary): summary panel component EE-811 (#388)

* initialised summary panel component, controller and binding

* summary component html, css, expanding-state persistance

* removed redundant scope variable

* - watching for formvalues scope changes
- 1-way data binding
- method descriptions and to-do
- objects setup for configuration output
- template setup to be re-used for summary of other k8s resources

* refactored to enable parallel development

* migrated k8s resource helper functions to their own respective files

* integrated action (create, update, delete) into html template

* configuration summary implementation (#390)

* feat(kubernetes/summary): kubernetes application creation and update summary EE-802 (#396)

* added application resources types

* - added support for oldFormValues to support complex app update summary
- additional app cpu and memory limits
- introduction of summary types - for service support

* initial implementation of app creation and update summary

* comments to applicationService methods to notify future devs to keep summary in sync

* updated comments to outline modified resources in k8s app edit

* pr bugfix to increase readability

* Feat kubernetes namespaces summary EE-801 (#398)

* feat(k8s/summary): show summary for namespace creation and update EE-436/EE-801

* feat(k8s/summary): show the correct article for summary EE-436/EE-801

* feat(k8s/summary): check formValue type in generateResourceSummaryList to avoid unnecessary calculation  EE-436/EE-801

* feat(k8s/summary): cleanup code EE-436/EE-801

* feat(k8s/summary): rename helps.js to helpers.js EE-436/EE-801

* updated comment to be more readable

Co-authored-by: Simon Meng <simon.meng@portainer.io>
Co-authored-by: zees-dev <dev.786zshan@gmail.com>

* feat(k8s/summary): hide summary section if nothing has changed EE-436

* bugfix: returning created resources after update

* fixed incorrect ingress summary bug

* fixed patch based bugs - displaying updates for resources properly

Co-authored-by: cong meng <mcpacino@gmail.com>
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-06-10 10:38:13 +12:00
fhanportainer
099e58d458 fix(aci): fixed aci with persistence or networking issue. (#275) 2021-06-10 01:34:31 +12:00
Alice Groux
a67331f1d4 fix(docker/stack): prevent stack duplication if name already used (#319)
* fix(docker/stack): prevent stack duplication if name already used

* fix(docker/stack): fix parenthesis error

* fix(docker/stack): fix condition for deploy stack button

* fix(docker/stack): add missing helper

* fix(stacks): show containers only for standalone

Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>
2021-06-08 14:45:35 +12:00
Alice Groux
55e52ee79a feat(k8s): truncate image name in tables (#262)
* feat(k8s): cut image name to 64 chars with truncate filter in all applications datatables

* feat(k8s): add full name on hovering over the image name

Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>
2021-06-04 17:18:02 +12:00
yi-portainer
c42756ae23 Merge branch 'release/2.4' into develop 2021-06-04 12:09:25 +12:00
dbuduev
dbcbef0953 feat(bolt): Add test scaffolding EE-872 (#407) 2021-06-04 11:45:01 +12:00
Hui
1d7ed11462 docs(api): document apis with swagger EE-155 (#326)
* document apis with swagger

* feat(api): introduce swagger

* feat(api): anottate api

* chore(api): tag endpoints

* chore(api): remove tags

* chore(api): add docs for oauth auth

* chore(api): document create endpoint api

* chore(api): document endpoint inspect and list

* chore(api): document endpoint update and snapshots

* docs(endpointgroups): document groups api

* docs(auth): document auth api

* chore(build): introduce a yarn script to build api docs

* docs(api): document auth

* docs(customtemplates): document customtemplates api

* docs(tags): document api

* docs(api): document the use of token

* docs(dockerhub): document dockerhub api

* docs(edgegroups): document edgegroups api

* docs(edgejobs): document api

* docs(edgestacks): doc api

* docs(http/upload): add security

* docs(api): document edge templates

* docs(edge): document edge jobs

* docs(endpointgroups): change description

* docs(endpoints): document missing apis

* docs(motd): doc api

* docs(registries): doc api

* docs(resourcecontrol): api doc

* docs(role): add swagger docs

* docs(settings): add swagger docs

* docs(api/status): add swagger docs

* docs(api/teammembership): add swagger docs

* docs(api/teams): add swagger docs

* docs(api/templates): add swagger docs

* docs(api/users): add swagger docs

* docs(api/webhooks): add swagger docs

* docs(api/webscokets): add swagger docs

* docs(api/stacks): swagger

* docs(api): fix missing apis

* docs(swagger): regen

* chore(build): remove docs from build

* docs(api): update tags

* docs(api): document tags

* docs(api): add description

* docs(api): rename jwt token

* docs(api): add info about types

* docs(api): document types

* docs(api): update request types annotation

* docs(api): doc registry and resource control

* chore(docs): add snippet

* docs(api): add description to role

* docs(api): add types for settings

* docs(status): add types

* style(swagger): remove documented code

* docs(http/upload): update docs with types

* docs(http/tags): add types

* docs(api/custom_templates): add types

* docs(api/teammembership): add types

* docs(http/teams): add types

* docs(http/stacks): add types

* docs(edge): add types to edgestack

* docs(http/teammembership): remove double returns

* docs(api/user): add types

* docs(http): fixes to make file built

* chore(snippets): add scope to swagger snippet

* chore(deps): install swag

* chore(swagger): remove handler

* docs(api): add description

* docs(api): ignore docs folder

* docs(api): add contributing guidelines

* docs(api): cleanup handler

* chore(deps): require swaggo

* fix(auth): fix typo

* fix(docs): make http ids pascal case

* feat(edge): add ids to http handlers

* fix(docs): add ids

* fix(docs): show correct api version

* chore(deps): remove swaggo dependency

* chore(docs): add install script for swag

* merge examples

* go.mod update

* merge validate rules

* remove empty example tag

* swagger anotation format

* swagger annotation update

* clean up go.mod

* update docs prebuild script

* Update porImageRegistry.html

* Update yamlInspector.html

* Update porImageRegistry.html

* Update package.json

* wording change

Co-authored-by: Chaim Lev-Ari <chiptus@users.noreply.github.com>
2021-06-04 09:37:23 +12:00
Alice Groux
100e8ebec2 feat(k8s/applications): reorder placement policies and select mandatory by default (#364) 2021-06-03 13:42:47 +02:00
Chaim Lev-Ari
8e0f681dd3 fix(docker/settings): fetch correct value for allow sysctl (#343)
* fix(docker/settings): fetch correct value for allow sysctl

* fix(endpoints): set sysctl setting for new endpoints
2021-06-03 11:36:50 +02:00
fhanportainer
e9e04cb61c fix(endpoint): skip tls for kube endpoints (#284) 2021-06-03 13:52:25 +12:00
Chaim Lev-Ari
8ab58bc959 chore(dev-build): custom portainer data folder (#255) 2021-06-03 13:35:31 +12:00
Chaim Lev-Ari
aacda65d8c fix(kube): replace remaining resource pool texts (#401) 2021-06-02 11:23:24 +12:00
Dmitry Salakhov
3634b5a10f feat: update docker version to 19.03 (#285)
tested with both linux and windows agents and edge agents
2021-06-02 10:38:53 +12:00
fhanportainer
afecc263a3 fix(templates): fixed type issue in custom template. (#391) 2021-06-02 09:29:31 +12:00
Dmitry Salakhov
d52e38a323 feat: update docker version to 19.03 (#286) 2021-06-02 09:25:06 +12:00
Hui
d6fce4931d use default resource pool (#302) 2021-06-01 17:07:18 +12:00
Chaim Lev-Ari
1437f79458 chore(dev): add debug config for vscode (#4756) (#271)
* chore(dev): add debug config for vscode
* chore(ide): move vscode configs to an example folder
2021-05-31 11:51:51 +12:00
Alice Groux
2fefff0536 feat(docker/service): update the information message about default location of secrets (#293) 2021-05-28 11:06:17 +12:00
fhanportainer
d8ff5a27df fix(template): fixed disabled deploy button EE-812 (#387) 2021-05-25 18:55:37 +02:00
cong meng
29de473e0e fix(frontend) Unable to retrieve namespaces error after updating Kube endpoint EE-775/EE-789 (#380)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-05-24 20:08:54 +12:00
dbuduev
4aa004e4e7 feat(git): Git clone improvements [EE-451] (#371) 2021-05-24 17:27:20 +12:00
cong meng
a6cab5f439 fix(rbac): clean namespace access policies EE-744 (#377)
* fix(rbac) override AccessPolicies by endpint group ID correctly instead by array index  EE-744

* fix(rbac) Iterate users and teams who are existing in NAP EE-744

* fix(rbac) Rename long func name to CleanNAPWithOverridePolicies EE-744

* fix(rbac) cleanup code EE-744

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-05-20 15:06:02 +12:00
Chaim Lev-Ari
aa43d2e7e8 fix(docker/services): create a service webhook (#5052) (#374) 2021-05-18 15:54:47 +12:00
cong meng
54ddb902e4 fix(rbac) Unable to remove an endpoint role from the user when the k8s endpoint is offline EE-736 (#375)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-05-17 11:15:05 +12:00
cong meng
efd973ff63 fix(ACI): At least one team or user should be specified when creating a restricted container in Azure ACI EE-578 (#329)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-05-17 11:06:36 +12:00
Chaim Lev-Ari
d64cc63a96 feat(k8s): UI: replace resourcepool with namespace EE-445 (#333) 2021-05-14 16:03:07 +12:00
yi-portainer
6aefdadc36 * update portainer version
(cherry picked from commit 93f763db1c)
2021-05-13 18:34:48 +12:00
yi-portainer
93f763db1c * update portainer version 2021-05-13 14:59:42 +12:00
cong meng
4837ab6a60 fix(rbac) User previously given access to namespace, can see it when they shouldn't EE-717 (#363)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-05-13 04:39:24 +02:00
dbuduev
81ea206f48 Revert "feat(git): Git clone improvements [EE-451] (#360)" (#366)
This reverts commit 08e3f6ac1a.
2021-05-12 10:44:09 +12:00
Dmitry Salakhov
be4454edc2 fix(namespace): update portainer-config when delete a namespace [EE-681] (#362) 2021-05-11 20:51:07 +12:00
dbuduev
08e3f6ac1a feat(git): Git clone improvements [EE-451] (#360)
* feat(git): update git checkout [EE-630] (#348)

* feat(git): Add Azure DevOps exception [EE-631] (#356)

* feat(git): refactoring git package (#631)

* feat(git): azure parse https url (#631)

* feat(git): unit-test refactoring (#631)

* feat(git): azure parse urls (#631)

* feat(git): extract azure module (#631)

* feat(git): azure service functions (#631)

* feat(git): azure, git refactoring and tests (#631)

* feat(archive): add unzip file tests (#631)

* feat(git): PR review changes (#631)

* feat(git): error handling updates (#631)

* feat(git): error handling updates (#631)

* feat(archive): test refactoring (#631)

Co-authored-by: Dennis Buduev <dennis.b@clubware.co.nz>

* feat(git) remove .git directory (#451)

* feat(git): return git clone error (#630)

Co-authored-by: Dennis Buduev <dennis.b@clubware.co.nz>
2021-05-11 14:57:22 +12:00
Dmitry Salakhov
a748857999 Fix(k8s): user cannot access k8s namespace [EE-629] (#353)
* fix: drop token cache when user updates config map

* tmp

* tmp

* fix: use same instance of token cache aross the app
2021-05-07 14:44:11 +12:00
cong meng
f98ca82bee fix(ACI): ACI UAC breaks when redeploying container with same name asone already existing EE-645 (#346)
* add container existence check

* modify response status code and err message

* return json instead of plain text for err msg

* Update api/http/proxy/factory/azure/containergroup.go

* Update api/http/proxy/factory/azure/containergroup.go

Co-authored-by: ArrisLee <arris_li@hotmail.com>
Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>
2021-05-05 20:11:43 +12:00
cong meng
86a7a7820f fix(log): ACI container create and delete is not logged in user activity UI EE-621 (#345)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-05-05 12:51:57 +12:00
zees-dev
432c2e7751 fix(access): homepage accessible by non-admin users (#344) 2021-05-05 11:53:14 +12:00
Alice Groux
b8c6c978b1 fix(docker/service): enable apply change button when user make changes on mounts section (#289) 2021-05-04 21:56:14 +12:00
cong meng
c7bac163c5 fix(uac): EE-173 Access control management via labels not fault tolerant (#314) 2021-05-04 12:40:44 +12:00
Hui
dc86024078 fix(stack): normalize stack name only for libcompose (#301)
* normalize stack name only for libcompose

* reformat and cleanup

Co-authored-by: Dmitry Salakhov <to@dimasalakhov.com>
2021-05-03 18:11:34 +02:00
fhanportainer
4444de1971 feat(log-viewer): add ansi color support for logs EE-558 (#331) 2021-05-03 13:56:01 +12:00
Hui
9bb90b0ff3 fix(application): can't update application with persisted data, after the storage option is disabled on cluster (#310)
* fix update application with persisted data

* revert

* revert

Co-authored-by: Maxime Bajeux <max.bajeux@gmail.com>
2021-05-03 12:26:53 +12:00
cong meng
5520585ac9 fix(k8s): Standard user & read-only user can see namespaces without access permissions EE-619 (#341)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-05-02 18:23:37 +12:00
cong meng
3ec649c6c6 feat(endpoint) Update the agent deployment command in the UI to use the new image for the agent EE-601 (#339)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-04-30 17:44:18 +12:00
Alice Groux
a1bdc99217 feat(docker/services): hide webhook interface (#320) 2021-04-30 15:01:41 +12:00
fhanportainer
769e885492 feat(kube/app): show image pull policy (#307) 2021-04-30 07:51:35 +12:00
Alice Groux
cb0fe58ef4 feat(app): sort environment variables (#290) 2021-04-30 07:11:56 +12:00
cong meng
4799a0a38d feat(home): EE-253 show node count on homepage (#321) 2021-04-29 16:30:34 +12:00
cong meng
7bcadb7dc8 feat(edge) Update the version of agent to 2.4.0 in agent deploy command on the adding edge screen EE-596 (#338)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-04-29 16:24:46 +12:00
Chaim Lev-Ari
13f921bf0d chore(deps): remove grunt-html2js and grunt-karma (#269)
fix [EE-171]
2021-04-29 15:17:30 +12:00
zees-dev
9887daaded feat(container-stats): introduce container block io stats chart (#327) 2021-04-29 14:43:45 +12:00
Hui
a57ab5ad72 #4374 feat(images): Add link to Docker Hub on container creation page (#4413) (#303)
Add a button next to the image field when creating a new container, which
takes the user to the Docker Hub search page for this image. Version
identifiers are trimmed from the image name to ensure that matching images
will be found.

Co-authored-by: knittl <knittl89+git@googlemail.com>
2021-04-29 14:08:51 +12:00
Alice Groux
341e0418e0 fix(docker/stack): update the content of code editor when switching custom template (#292) 2021-04-29 13:53:06 +12:00
Maxime Bajeux
b1d397e375 feat(k8s/container): realtime metrics (#297) 2021-04-29 13:11:18 +12:00
fhanportainer
f35979c7f5 chore(deps): install core-js@2 (#317) 2021-04-29 12:46:03 +12:00
zees-dev
27ad7d077f fix(customtemplate): Cannot create custom template from uploaded compose file EE-163 (#283) 2021-04-29 11:08:47 +12:00
Hui
5596a3bc99 feat(container): add sysctls setting in the container view EE-43 (#280) 2021-04-29 11:07:48 +12:00
fhanportainer
933177948d fix(snapshot): update snapshot interval (#309) 2021-04-29 09:54:50 +12:00
Dmitry Salakhov
ac4820da9f feat(logs): improved fatal log messages (#281) 2021-04-29 09:23:22 +12:00
cong meng
36d6df7885 fix(rbac): user in 2 teams with mix of endpoint admin and operator has perms of endpoint admin EE-587 (#335)
* fix(rbac) user in 2 teams with mix of endpoint admin and operator has perms of endpoint admin EE-587

* fix(rbac) add unit test for getKeyRole function EE-587

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-04-28 20:11:51 +12:00
Alice Groux
d97cd56d1f fix(app): app type button on every button with ngf-select (#291) 2021-04-28 17:44:19 +12:00
cong meng
cbc34bdd6d feat(edge): Show the status of the edge agent check-in on the home page dashboard EE-178 (#325) 2021-04-28 16:46:19 +12:00
fhanportainer
81759c4dfc fix(containers): fix layout in small screens (#313) 2021-04-28 16:10:22 +12:00
Chaim Lev-Ari
03d7246b4a fix(build): ignore chardet missing sourcemaps (#4760) (#268) 2021-04-28 13:03:00 +12:00
cong meng
9cc0d780fb fix(home): EE-83 redirect home if edge endpoint is down (#311) 2021-04-28 11:07:59 +12:00
Stéphane Busso
db15482adc fix(logging): Content is not set to [REDACTED] when creating or editing sensitive kube configs EE-580 (#330)
* fix stringData from secrets

* feat(useractivity): log secrets

Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>
2021-04-28 10:05:41 +12:00
Alice Groux
6c5ea68573 feat(docker/containers): display IP (#288) 2021-04-28 06:43:15 +12:00
Alice Groux
a0f2ab2a2d feat(k8s/ingress): create multiple ingress networks per k8s namespace with a differing config per ingress (#318) 2021-04-28 05:52:27 +12:00
fhanportainer
24d52ca395 feat(datatable): save text filters in session storage (#287) 2021-04-27 15:46:00 +02:00
Alice Groux
e66924271d feat(k8s/application): add the ability to redeploy external applications 2021-04-27 15:51:04 +12:00
fhanportainer
cb6fb3e47b fix(k8s/endpoint): update endpoint URL (#324) 2021-04-27 15:29:38 +12:00
Alice Groux
32b4b9c132 fix(k8s/application): display all environment variables in edition (#294) 2021-04-27 14:27:10 +12:00
cong meng
10a4d5a0e6 feat(k8s) EE-157 Better form validation for Configuration keys (#322)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-04-27 13:06:17 +12:00
Dmitry Salakhov
7d5641860a fix(docker/services): save the settings of the table for auto refresh (#282) 2021-04-27 12:51:12 +12:00
fhanportainer
e3ae899a5a fix(app/settings): EE-262 update link to template definition docs (#279) 2021-04-27 12:29:03 +12:00
Hui
53a89a173a feat(yaml-inspector): add button to expand/collapse yaml inspector 2021-04-27 12:28:28 +12:00
zees-dev
0cf9bd90c4 fix(service-details): clear volume source when changing type EE-143 (#306)
* fix(service-details): clear volume source when changing type

* fix(services): prevent adding volume without source and target

Co-authored-by: Chaim Lev-Ari <chiptus@users.noreply.github.com>
2021-04-23 20:04:32 +12:00
Hui
0295552a7a fix(stack): show correct error message 2021-04-21 14:47:45 +12:00
yi-portainer
c5488a8fc0 * update portainer version
(cherry picked from commit ef94b69718)
2021-04-16 14:03:12 +12:00
yi-portainer
ef94b69718 * update portainer version 2021-04-16 14:01:18 +12:00
Chaim Lev-Ari
8d53b5c60e fix(stacks): enable compose access to private registries (#264)
* fix(stacks): enable compose access to private registries

* chore(deps): update docker-wrapper lib

* update mod

* Update wrapper lib to rebased PR

* Update wrapper

Co-authored-by: Stéphane Busso <stephane.busso@gmail.com>
2021-04-16 13:14:20 +12:00
Chaim Lev-Ari
3d3bc9b692 fix(api): use docker-compose on windows (#273)
* feat(api): log compose initializtion

* feat(stacks): update docker-compose version

* feat(api): remove logs

* Update library

Co-authored-by: Stéphane Busso <stephane.busso@gmail.com>
2021-04-16 00:30:50 +02:00
cong meng
e6e5885fa2 Feat(rbac): Change migration for rbac operator role EE-226 (#266)
* feat(rbac): EE-226 set db version to 29 other than 30

* feat(rbac): EE-226  avoid a js warning

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-04-15 20:58:03 +12:00
Chaim Lev-Ari
99a372fb88 feat(useractivity): log user activity for write actions (#229)
* feat(useractivity): introduce backend for useractivity logging (#213)

* refactor(useractivity): move query and logs to base type

* feat(useractivity): cleanup user activity logs

* feat(useractivity): log an activity

* refactor(useractivity): create generic get logs function

* fix(api): hide unused function

* refactor(useractivity): create generic get logs function

* feat(useractivity): get user activity logs

* feat(http/ua): add http get logs handler

* refactor(http/ua): rename logs_list file

* feat(useractivity): fetch logs as csv

* feat(useractivity): save payload as bytes

* style(useractivity): doc the count parameter

* feat(useractivity): introduce UI for user activity logs (#220)

* feat(useractivity): add useractivity page

* feat(useractivity): get logs from server

* feat(useractivity): show logs in datatable

* fix(useractivity): save logs as csv

* feat(useractivity): show logs payload

* feat(useractivity): sort desc by default

* feat(useractivity): parse object

* fix(useractivity): expect base64 payload

* feat(useractivity): show message when missing logs

* feat(useractivity): log api (#215)

* feat(templates): log write methods

* refactor(useractivity): move middleware

* feat(dockerhub): log update docker settings

* feat(edgegroup): log write

* feat(edgejobs): log write request

* feat(useractivity): return bytes to user

* fix(customtemplates): set activity context

* feat(edgestacks): log activities

* feat(endpointgroup): log activities

* feat(endpoint): log write activities

* feat(licenses): log write activities

* feat(registries): log activitites

* feat(resource_control): log user activity

* feat(settings): log update

* feat(stacks): log activity

* feat(tags): log user activitiy

* feat(teammembership): log user activity

* feat(teams): log write activities

* feat(useractivity): get default context

* feat(http/upload): log upload tls

* feat(users): log user activities

* fix(settings): clean payload

* feat(webhook): log user activities

* feat(websocket): log activities

* feat(docker): log write activities

* refactor(useractivity): move log proxy

* feat(azure): log write activity

* refactor(kube): use basic transport for all transports

* feat(kube): log kube activity

* fix(useractivity): parse body

* refactor(kuberenetes): log requests only if success

* refactor(docker): log requests only if success

* refactor(azure): log requests only if success

* feat(gitlab): log activity

* feat(registries): log proxy request

Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>

* feat(activity-logs): save pagination limit

* feat(useractivity): remove config payload

* fix(docker): log request after success

* refactor(http): move copy body to utils

* feat(kuberentes): remove config values

* feat(useractivity): copy body before request

* fix(useractivity): fix column size

* feat(useractivity): filter json payloads

* refactor(useractivity): log with same logic

* fix(useractivity/csv): export same columns as datatable

* fix(useractivity): replace context with endpoint

* fix(user-activity): rename tables

* feat(endpoint): clear azure key

* feat(stacks): omit empty migrate values

* fix(stacks): add back import

* feat(endpoints): log update settings

* fix(registry): clear password value

* feat(registry): omit update empty value

* fix(registries): don't return from unauthorized azure request

* fix(useractivity): log any payload similar to json

* feat(useractivity): ignoer binary upload

* fix(useractivity): refresh user activity logs

* feat(useractivity): use [REDACTED] for cleared credential (#265)

* feat(docker/services): log force update service

* feat(useractivity): log username when available

* feat(webhooks): remove logging of execute

* refactor(http): replace redacted values

* style(kube): remove commented code

* feat(http/kube): proxy local requests

* feat(useractivity): log patch method

* fix(datatables): use unique filter id

* fix kube settings update

* fix: EE-527 set payload to [REDACTED] when update kube config

* refactor(http/k8s): rename proxy function

* EE-530: a dummy fix of exec activity log for a local kube setup

Co-authored-by: Dmitry Salakhov <to@dimasalakhov.com>
Co-authored-by: Hui <arris_li@hotmail.com>
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-04-15 20:37:29 +12:00
Dmitry Salakhov
37baabe134 EE-292: backup to and restore from s3 (#240)
* EE-384: add endpoint to set auto backup (#224)

* EE-383: add endpoint to fetch backup settings (#231)

* add get backup settings handler
* add api docs desc

* EE-382: restore from s3 (#233)

* EE-381: add GET backup status handler (#234)

* EE-385: Add S3 backup execute handler (#237)

* add s3 backup execute handler

* refactories inside `./api/backup/backup_scheduler.go` and `./api/backup/backup_scheduler.go`

* fix tests

* EE-375: added backup to S3 form

* EE-376: added restore from S3 form

* EE-377: Update Home screen to display last backup run status

* update backup service with back end endpoints.

* restart admin monitor during s3 restores

* use go 1.13

* go 1.13 compatibility

* EE-375: added cron-validator lib

* EE-375: using enum to compare form types

* EE-375: validate cron rule field

* try fix windows build

* EE-375 EE-376 backup and restore forms validation changes

* fix(autobackup): update autobackup settings validation rules (#260)

* fix(autobackup): automate backup to s3 fe update (#261)

* EE-292: fixed typo in property.

* EE-292: updated auto backup front end validation.

* EE-292: updated lib to validate cron rule in front end

* fix dependencies

* bumped libcompose version

Co-authored-by: Hui <arris_li@hotmail.com>
Co-authored-by: Felix Han <felix.han@portainer.io>
Co-authored-by: fhanportainer <79428273+fhanportainer@users.noreply.github.com>
2021-04-15 12:12:53 +12:00
Alice Groux
19b8117903 feat(k8s/configuration): rename add ingress button and changed information text (#139) 2021-04-15 11:00:14 +12:00
fhanportainer
2f3b64a742 fix(app): fixed browse button should not be showing for volumes in local endpoint (#232) 2021-04-15 10:59:11 +12:00
LP B
aec5356a0c fix(k8s/cluster): missing params in configureController (#270) 2021-04-15 10:51:27 +12:00
LP B
eca9e04c20 fix(k8s/resource-pool): unusable RP access management (#153)
* fix(k8s/resource-pool): unusable RP access management

* style(k8s): remove unused vars introduced by #194
2021-04-15 10:35:19 +12:00
cong meng
1b9a7d2f52 fix(registry): EE-387 can not browse proget registry (#236)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-04-13 15:34:59 +02:00
Hui
7666d32e97 EE-367: update liblicense version number (#248)
* update liblicense version number and minor test file fix

* revert test file fix
2021-04-13 16:12:33 +12:00
Maxime Bajeux
7d3790fc18 feat(custom-templates): switching a template to standalone makes it disappear in swarm mode (#219) 2021-04-13 13:14:50 +12:00
Chaim Lev-Ari
39a01cda29 fix(kube/config): show used key warning when needed (#254)
* feat(k8s/config): disable edit used config keys (#4754)

* feat(k8s/config): tag used data keys

* feat(k8s/config): disabled edit of used data keys

* fix(kube/config): show used key warning when needed (#4890)

fix [CE-469]
- recalculate duplcate keys when they are changed
- show used warning on duplicate keys
2021-04-13 02:18:28 +02:00
Alice Groux
915bb3ea78 feat(app) EndpointProvider fallback on URL EndpointID when no endpoint is selected (#239) 2021-04-13 12:10:56 +12:00
cong meng
aeadb5c375 fix(k8s): EE-354 Unable to use advanced deployment feature on agent and Edge agent endpoints (#194)
* fix(k8s): EE-354 Unable to use advanced deployment feature on agent and Edge agent endpoints

* fix(k8s): EE-354 enable advance deploy UI

* fix(k8s): EE-354 use the v2 version of agent api instead of v3

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-04-12 15:34:33 +02:00
Hui
e48b6940e7 fix(test): Use connection.DB (#253) 2021-04-12 12:49:46 +12:00
cong meng
6eb3dfd3c2 feat(ACI): EE-261 Add RBAC to ACI (#226)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-04-09 02:20:33 +02:00
cong meng
4682056058 Fix EE-471 Add createAdministratorFlow back and fix some typo and warnings (#242)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-04-09 09:11:37 +12:00
Chaim Lev-Ari
2fb60a29de style(proxy): fix function name (#243) 2021-04-09 09:02:32 +12:00
cong meng
edb05e6e00 feat(ACI): EE-273 add UAC to ACI (#222)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-04-08 10:46:04 +12:00
Chaim Lev-Ari
b8ecadb314 feat(useractivity): introduce auth logs (#203) 2021-04-07 16:54:07 +12:00
Dmitry Salakhov
e15b908983 Feat(backup): add the ability to backup and restore portainer from file [EE-279] (#204)
* EE-319: backup endpoint (#193)

* feat(backup):
* add an orbiter to block writes while backup
* add backup handler
* add an ability to tar.gz a dir
* add aes encryption support

* EE-320: restore endpoint (#196)

* feat(backup):
* add restore handler
* re-init system state after restore

* feat(backup): Update server to respect readonly lock (#199)

* feat(backup): EE-322 Add backup and restore screen (#198)

Co-authored-by: Simon Meng <simon.meng@portainer.io>

* name archive as portainer-backup_yyyy-mm-dd_hh-mm-ss

* backup custom templates and edge jobs

* restart http and proxy servers after restore to re-init internal state

* feat(backup): EE-322 hide password field if password protect toggle is off

* feat(backup): EE-322 add tooltip for password field of restore backup

* feat(backup): EE-322 wait for backend restart after restoring

* Shutdown background go-routines

* changed restore err message when cannot extract

* fix: symlinks are ignored from backups

* replace single admin check with a restartable monitor (#238)

* clean log

Co-authored-by: Maxime Bajeux <max.bajeux@gmail.com>
Co-authored-by: cong meng <mcpacino@gmail.com>
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-04-06 15:41:41 +12:00
cong meng
f9cf76234f feat(rbac): EE-226 Add a new RBAC "Operator" Role (#191)
* feat(rbac): EE-226 Add a new RBAC "Operator" Role

* feat(rbac): EE-226 prioritize Operator after EndpointAdmin and before Helpdesk

* feat(rbac): EE-226 access viewer shows incorrect effective role after introduce of Operator

* feat(rbac): EE-226 show roles order by priority other than name

* feat(rbac): EE-226 remove OperationK8sVolumeDetailsW authorization from operator role

* feat(rbac): EE-226 always increase bucket next sequence when create a role

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-04-06 11:34:54 +12:00
Chaim Lev-Ari
590755071f chore(deps): fix /x/sys version (#217)
closes [EE-429]
2021-04-05 23:14:11 +02:00
fhanportainer
6e8208cea8 fix(container): fixed pull latest image toggle missing on service update and container recreate modal (#235) 2021-04-01 11:01:08 +13:00
cong meng
0eec606ebe feat(authentication): EE-73 Rename all usernames to lowercase (#228)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-03-29 19:09:17 +02:00
fhanportainer
efd9c4c5e5 feat(app): EE-353 remove the version available check (#216)
* feat(app): EE-353 remove the version available check

* feat(app): EE-353 revert code indentation
2021-03-24 23:29:49 +01:00
cong meng
1c938516ee Feat(docker): relocate docker features security settings to be available per endpoint EE-131 (#209)
* feat(docker) EE-131 relocate the Docker features/security settings to be available per endpoint

* feat(docker) EE-131 allow endpoint admin role user to update endpoint settings

* feat(docker) EE-131 populate volume browsing authorizations to user endpoint authorizations when user toggle the setting of volume management for non-administrators

* feat(docker) EE-131 remove parameter volumeBrowsingAuthorizations from all DefaultEndpointAuthorizationsForxxx functions

* feat(docker) EE-131 fix a layout bug of the browse button

* feat(ACI): EE-273 move migrator of 27 into migrate_dbversion26.go

* feat(docker) EE-131 in container creation view, show the privileged mode toggle if cureent user is admin or endpoint admin

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-03-24 23:10:10 +01:00
Chaim Lev-Ari
65028ed96f feat(stacks): scope stack names to endpoint (#4520) (#212)
* refactor(stack): create unique name function

* refactor(stack): change stack resource control id

* feat(stacks): validate stack unique name in endpoint

* feat(stacks): prevent name collision with external stacks

* refactor(stacks): move resource id util

* refactor(stacks): supply resource id util with name and endpoint

* fix(docker): calculate swarm resource id

* feat(stack): prevent migration if stack name already exist

* feat(authorization): use stackutils
2021-03-24 16:40:25 +13:00
Maxime Bajeux
914476618d feat(configurations): Review UI/UX configurations (#184)
* feat(configurations): Review UI/UX configurations

* fix(configurations): remove duplicate lines

* fix(configurations): fix merge errors

* fix(configuration): parse empty configuration as empty string yaml instead of {}
2021-03-23 20:01:25 +01:00
Maxime Bajeux
ceda8b1975 fix(pods): import missing pod converter (#207) 2021-03-22 17:08:45 +01:00
Chaim Lev-Ari
78cf608990 feat(compose): add docker-compose wrapper (#161)
* 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>

* refactor(stacks): use compose library

* refactor(stacks): remove utils

* chore(deps): pin docker-compose-wrapper

* chore(build): simplify docker-compose build

* chore(build): remove ps compose script

* chore(deps): update docker-compose-wrapper

* fix(compose): close proxy after command

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-03-21 22:38:45 +01:00
cong meng
0df3909909 fix(docker/stack-details): do not display editor tab for external stack (ee-150) (#174)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-03-18 23:14:32 +01:00
Chaim Lev-Ari
55205efe44 chore(plop): use templates as in style guide (#197)
* chore(plop): use templates as in style guide

fix [CE-483]

* chore(plop): export component and add to module
2021-03-19 09:07:14 +13:00
Maxime Bajeux
fa124b4fbe feat(k8s/applications): exposed naked pods as applications (#162) 2021-03-16 22:23:57 +01:00
Maxime Bajeux
0eda4ff41d fix(configs): fix error with binary file (#164) 2021-03-12 23:26:11 +01:00
cong meng
b401ab5081 fix(registries): update password only when not empty (ee-138) (#175)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-03-12 22:27:41 +01:00
yi-portainer
0cf7e6f2eb * update version to 2.0.2 2021-03-12 10:48:50 +13:00
cong meng
0f67a71da2 fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ee-151) (#170)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-03-08 00:13:25 +01:00
Maxime Bajeux
6767d19c2c fix(k8s): Can't create kubernetes resources with a username longer than 63 characters (#172) 2021-03-05 18:44:37 +01:00
cong meng
89af2b71e4 fix(frontend): revalidate configuration name when change resource pool (ee-85) (#158)
* fix(frontend): revalidate configuration name when change resource pool (ee-85)

* fix(front-end): EE-85 fixed the issue with the form validation in the Configuration creation view

Co-authored-by: Simon Meng <simon.meng@portainer.io>
Co-authored-by: Felix Han <felix.han@portainer.io>
2021-03-04 12:04:48 +01:00
cong meng
3809ce1546 fix(k8s/application): transform username to be dns compliant (ee#123) (#136)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-03-04 12:12:55 +13:00
cong meng
3398cbf279 fix(k8s): unable to apply a note to a pod (ee-128) (#177)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-03-03 22:37:15 +01:00
Chaim Lev-Ari
27870be241 feat(k8s/advanced-deployment): update extra information message when kubernetes type is selected (#4542) (#138)
Co-authored-by: Alice Groux <alice.grx@gmail.com>
2021-03-03 20:15:47 +01:00
Alice Groux
c4ffaa4da2 feat(applications/ports-mapping): load balancer link expand only if item length > 1 (#148) 2021-03-03 19:51:53 +01:00
Alice Groux
14550db3b5 feat(k8s/application): validate load balancer ports inputs (#152) 2021-03-03 19:26:22 +01:00
Chaim Lev-Ari
92d5eba499 feat(service): clear source volume when change type (#4627) (#171)
* feat(service): clear source volume when change type

* feat(service): init volume source to the correct value
2021-03-03 15:47:49 +01:00
cong meng
31dcf031f3 chore(webpack): add source maps (EE-37) (#167)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-03-02 21:46:13 +01:00
Alice Groux
61c7379312 feat(docker/network): rename restrict external access to the network label (#141) 2021-03-02 11:30:53 +01:00
Alice Groux
ff08fffdce feat(k8s/advanced-deployment): introduce advanced deployment panel to each resource list view (#181) 2021-03-01 18:19:11 +01:00
Maxime Bajeux
aa9cb52575 fix(frontend): add a key to existing used configuration won't throw error anymore on using-app edition (#154)
Application edit page initializes the overridenKeyType of new added configuration key to NONE so that the user can select how to load it
2021-03-01 17:57:31 +01:00
Maxime Bajeux
5bb43432b0 fix(frontend): override configuration keys disappear (#186) 2021-03-01 16:15:08 +01:00
cong meng
b17d296783 fix(frontend): Resource pool 'created' attribute is showing the time you view it at not actual creation time (EE-90) (#166)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-03-01 15:56:21 +01:00
Alice Groux
006d19cd63 feat(app/images): in advanced mode, remove tooltip and add information message (#150) 2021-03-01 15:39:27 +01:00
Alice Groux
a0001305cc feat(app/logs): add download button on logs views (#151) 2021-02-28 21:14:22 +01:00
cong meng
e71fc3bb0e fix(service): set volume source to the correct value (ee-132) (#178)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-02-28 20:19:50 +01:00
Alice Groux
766e865bd2 feat(k8s/configuration): save the owner when updating the configuration (#142) 2021-02-27 23:44:00 +01:00
Dmitry Salakhov
880755125b feat(volumes): show volume access policy (#176) 2021-02-27 11:28:52 +01:00
Alice Groux
dc19061c97 feat(k8s/sidebar): accessing cluster steup not expand endpoint sidebar (#149) 2021-02-26 23:20:26 +01:00
Alice Groux
d04cf56f37 fix(k8s/node): sort labels (#146) 2021-02-26 22:57:15 +01:00
Alice Groux
85cc619dc6 feat(k8s/configuration): add extra information panel when creating a sensitive configuration (#143) 2021-02-26 22:36:15 +01:00
Alice Groux
1b432ad1af feat(k8s/application): refreshing yaml panel doesn't change the selected panel (#147) 2021-02-26 22:05:38 +01:00
Alice Groux
62fe32fe31 feat(app/endpoint): start Portainer without endpoint (#180) 2021-02-26 18:05:30 +01:00
Alice Groux
4ee52a5062 feat(k8s/datatables): reduce size of collapse/expand column for stacks datatable and storage datatable (#145) 2021-02-26 12:14:45 +01:00
Alice Groux
1d50068003 feat(k8s/configuration): rename create entry file button (#144) 2021-02-26 11:57:37 +01:00
Maxime Bajeux
4ceae3a43c fix(frontend): cannnot access configuration details view containing binary data (#163) 2021-02-26 11:35:45 +01:00
Alice Groux
e78756ccfa feat(docker/volumes): add confirmation modal before deleting volumes (#140) 2021-02-26 03:05:57 +01:00
cong meng
d618d05ee1 fix(stack): stacks created via API are incorrectly marked as private with no owner (ee#74) (#156)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-02-26 01:16:18 +01:00
Chaim Lev-Ari
401a471748 feat(k8s/application): review application creation warning style (#4613) (#159)
Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>
2021-02-25 16:54:20 +01:00
cong meng
74f3fb0ba2 feat(login): de-emphasize internal login when oauth enabled (ee-70) (#155)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-02-25 16:19:48 +01:00
cong meng
0c20e788f2 fix(style): shift button position in stack details (ee-135) (#168)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-02-25 16:10:01 +01:00
Chaim Lev-Ari
1cbda51517 feat(image-details): Show labels in images datatable (#4287) (#137)
* feat(images): show labels in images datatable

* move labels to image details view

Co-authored-by: DarkAEther <30438425+DarkAEther@users.noreply.github.com>
2021-02-25 16:04:49 +01:00
Chaim Lev-Ari
924bfdee2a feat(docker/stacks): introduce date info for stacks (#182)
* 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-02-25 15:59:38 +01:00
cong meng
7549ae2c11 fix(state): check validity of state (ee-77) (#157)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-02-23 20:10:01 +01:00
Chaim Lev-Ari
fd0c6ea868 #4470 fix(stack): fix a display issue with the stack editor tab. (#4543) (#160)
Co-authored-by: aravind-korada <70788131+aravind-korada@users.noreply.github.com>
2021-02-23 20:04:44 +01:00
yi-portainer
c3f82f51c9 * update version to 2.0.1
(cherry picked from commit 5a784906db76f01461430489bba19ede71aefb93)
2021-02-22 19:10:13 +13:00
Yi Chen
2d6c96a89d * revert docker linux to versioon 18.09.3 (#188)
(cherry picked from commit 37c9dc7d497b4e1a440de14f6998f6616faa417f)
2021-02-22 16:01:07 +13:00
itsconquest
86c378b561 fix(build): fix kubectl download script (#187) 2021-02-19 14:31:43 +13:00
Stéphane Busso
c8f18adfc3 Fix(build): docker compose binary download partial backport (#185) 2021-02-17 09:01:48 +13:00
Steven Kang
a761412bd9 feat(build): introduce buildx (#173)
* feat(build): introduce buildx

* feat(build): excluded compose v3

* feat(build): excluded compose v3

* feat(build): revert back the Docker binary for Windows

* * fix liblicense url override

Co-authored-by: yi-portainer <yi.chen@portainer.io>
2021-02-15 09:47:14 +13:00
Stéphane Busso
b0f7dee463 fix(license): default license server url (#135)
* fix default license server url

* Add azure scripts

* Add ldflags to crossplatform builder
2021-01-26 10:01:18 +13:00
itsconquest
7690a1c894 feat(testing): add missing docker rbac asserts (#133) 2020-12-09 14:08:53 +13:00
itsconquest
461bc9b931 Feat cypress rbac tests (#132)
* feat(testing): bump cypress version

* feat(testing): bring changes from intial PR + helpers

* feat(testing): add kubernetes rbac asserts

* feat(testing): cleanup from previous changes

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2020-12-09 12:17:07 +13:00
Chaim Lev-Ari
06fe256f40 fix(app): remove ghost pages (#131)
* fix(app): remove timeout

* style(app): remove comments

* fix(app): remove async from hook which cause blink

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2020-12-03 17:42:14 +13:00
Yi Chen
92d597608e fix(RBAC) adding/removing teams into namespace causing error (#129)
* * handle teams been added or removed in the resource pool
* do not delete role bindings but just remove the user subject

* * fix missing rolemap

* * revert the role bindings changes (not the cause of the issue)

* * fix token cache cleaning endpoint tokens
2020-12-02 20:38:09 +13:00
Stéphane Busso
5e8e6d2821 chore(license): Update liblicense (#130) 2020-12-02 14:43:31 +13:00
Stéphane Busso
d46844fa7c Override license server (#128) 2020-12-02 09:38:52 +13:00
Yi Chen
f6824ce11c - remove rbac debug statements (#126) 2020-12-01 22:37:13 +13:00
Stéphane Busso
5f9ece92ae fix(board): Set license validation every day fixes#117 (#47) 2020-12-01 22:36:35 +13:00
Anthony Lapenna
674d20bfb9 feat(docker/dashboard): wrap dashboard elements in div (#127) 2020-12-01 22:27:36 +13:00
cong meng
50bd34632d #220 fix(frontend): reenable license cache with 30s timeout and move license checking out of router hook (#124)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2020-12-01 20:40:23 +13:00
Stéphane Busso
e316a5ebe1 fix(license): Fix license expiration inconsistency with displayed date (#111)
* fix(license): Fix license expiration  inconsistency with displayed date

* Fix inconsistent expiration

* Use liblicense expiration compute

* wip

* Use db for expiresAt in license detailed view

* Fix date differences
2020-12-01 17:39:37 +13:00
Yi Chen
db9a1826e5 * fix nil user or team access in edge endpoint (#125) 2020-12-01 15:27:26 +13:00
Yi Chen
02b1ccd521 fix(RBAC) remove role/cluster role bindings when user is deleted (#120)
* * partially ignore errors during user deletion
* collect all errors during user deletion
* remove role/cluster role bindings when empty

* + update resource pool access endpoint
* remove bindings when user is removed from resource pool
* remove token cache when user is added to the resource pool

* - remove delete tokens endpoint
* use actual TriggerUserAuthUpdate

* * fix comments

* * improve error returns
2020-12-01 11:45:49 +13:00
Yi Chen
d4929f06f8 fix(RBAC) refresh user token when operating on endpoints, namespaces, users, teams and memberships (#117)
* * refresh user auth when operating endpoint, team, user and membership

* + adding delete token endpoint
* remove tokens when auth config map is changed

* feat(rbac): add warning messages in the UI

* feat(endpoint): update access warnings

* * fix delete tokens api url

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
2020-11-30 21:15:52 +13:00
Anthony Lapenna
6a31ef1f12 feat(endpoint): update agent instructions (#119)
* feat(endpoint): update agent instructions

* feat(endpoint): update Edge script name
2020-11-30 18:51:37 +13:00
Stéphane Busso
fc5b5368f1 fix(settings): Fix portainer fail to start when missing settings (#123) 2020-11-30 18:39:29 +13:00
Anthony Lapenna
e3b38d0b0a fix(docker/resourcecontrol): fix an issue with Docker resource deletion (#121) 2020-11-30 17:07:46 +13:00
Yi Chen
05cd7094a5 fix(RBAC): authorize advanced deployment (#116)
* * removed authorization in stack deployment, will let k8s handling it

* * removed unused import

* + OperationK8sApplicationsAdvancedDeploymentRW for user
* check namespace authorization in k8s stack deployment endpoint

* - remove OperationK8sApplicationsAdvancedDeploymentRW from user
2020-11-30 13:02:05 +13:00
Maxime Bajeux
5cdcbbc604 fix(rbac): In the application details view, for a non admin user there is a link to the node details view (#118) 2020-11-27 14:34:16 +13:00
Maxime Bajeux
f717cf3eda fix(rbac): Errors thrown in cluster view for standard & readonly users (#114)
* fix(rbac): Errors thrown in cluster view for standard & readonly users

* Revert "fix(rbac): Errors thrown in cluster view for standard & readonly users"

This reverts commit 400016314c3c0f9b22880ede10e3b2d2897c0363.

* * block the loading of nodes and endpoints if not authorized with K8sClusterNodeR

Co-authored-by: yi-portainer <yi.chen@portainer.io>
2020-11-27 14:13:05 +13:00
Maxime Bajeux
7254703449 fix(rbac): Endpoint admin cannot access the cluster setup view (#112)
* fix(rbac): Endpoint admin cannot access the cluster setup view

* * allow endpoint admin to update k8s cluster setup in endpoint

* * make sure a user token is issued first

* fix(rbac): allow admin to update cluster setup

Co-authored-by: yi-portainer <yi.chen@portainer.io>
2020-11-27 14:12:46 +13:00
Maxime Bajeux
8fed4181ed fix(rbac): Error thrown in the node details view for the helpdesk user (#113) 2020-11-26 22:20:52 +13:00
Anthony Lapenna
249b8762f8 fix(k8s/resourcepool): fix missing YAML for resource pool (#109) 2020-11-26 22:16:10 +13:00
cong meng
93f112672e feat(frontend/k8s): add a confirmation modal before deleting one or more application or configuration (#104)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2020-11-26 22:15:44 +13:00
Maxime Bajeux
414e62503b fix(rbac): forbidden view access (#101)
* fix(rbac): Not enforcing on backend for resource creation, application edit and console log operations of users that this should be prevented for

* + k8s access user namespaces policy
+ debug logs
* fix multiple authorization calculation issues

* * use endpoint role rather than user role for calculating authorizations

* * fix namespace role binding

* * check user authorization in k8s pod exec

* * fix some of the logging messages

Co-authored-by: yi-portainer <yi.chen@portainer.io>
2020-11-26 11:30:36 +13:00
Chaim Lev-Ari
9a16af37af fix(router): block route if license is invalid (#90)
* feat(router): add transition guard for init route

* feat(router): check if license is valid between routes

* style(app): change order of config and run

* feat(bouncer): block non admins from using without license

* style(bouncer): add comment about license validation
2020-11-26 09:35:40 +13:00
Chaim Lev-Ari
9dbe6d9474 feat(license): count standalone nodes (#102)
* feat(license): count standalone nodes

* refactor(http/status): return maximum
2020-11-26 09:33:54 +13:00
cong meng
ec327411b7 fix(frontend): add the url for buy/renew license button (#110)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2020-11-26 09:05:38 +13:00
Stéphane Busso
ab796b6896 chore(license): update license package to manage expiration date (#108) 2020-11-25 17:42:11 +13:00
xAt0mZ
cb90635016 fix(endpoint): hide kubernetes edge/agent instructions for windows (#107)
* fix(endpoint): hide kubernetes edge/agent instructions for windows

* feat(endpoint): minor UI update

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
2020-11-25 08:26:56 +13:00
xAt0mZ
2e2d635f6e fix(k8s/application): transform username to be dns compliant (#106) 2020-11-24 14:10:47 +13:00
Alice Groux
fe66252df7 fix(k8s/storageclass): hide disabled storage options for standard users and readonly users (#105) 2020-11-24 14:00:06 +13:00
Yi Chen
8f66414be9 Remove the cache of kcli with edge proxy (#103)
* * removes kube client cache when edge proxy is removed

* + added logging when failed retrieving k8s service account token

* * take out reusable code
2020-11-24 13:26:15 +13:00
cong meng
2378d4cc9d fix(frontend): show failing placement details for endpoint-admin and helpdesk users (#100)
* fix(frontend): show failing placement details for endpoint-admin and helpdesk users

* fix(frontend): add excludeAuthorization directive to determine endpoint-admin and helpdesk users

* fix(k8s/rbac): add OperationK8sApplicationErrorDetailsR authorization for endpoint-admin and helpdesk users

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2020-11-23 21:51:22 +13:00
Chaim Lev-Ari
44fa68407d fix(licenses): prevent removal of last valid license (#89)
* fix(licenses): prevent removal of last valid license

* * add back the logic that prevent the last license been removed, whether valid or not.

* Revert "* add back the logic that prevent the last license been removed, whether valid or not."

This reverts commit 389b5f8985bf543821cab02ad3252d75ef46ccee.

Co-authored-by: yi-portainer <yi.chen@portainer.io>
2020-11-21 16:36:50 +13:00
Stéphane Busso
428ac54b08 fix(license): better error message when login with no valid license (#99)
* fix(license): better error message when login with no valid license

* add authenticateOAuth
2020-11-21 08:37:48 +13:00
xAt0mZ
911898371b feat(k8s/applications): disable advanced deployment on agent / edge agent endpoints (#95) 2020-11-20 15:57:57 +13:00
Stéphane Busso
d41676ec02 fix(license): update liblicense with invalid message when login 2020-11-20 15:50:56 +13:00
Stéphane Busso
0dacb828b8 fix(license): License Expire Message (#98) 2020-11-20 15:37:23 +13:00
Stéphane Busso
4897f3a87c fix(portainer): Remove the version update notifier on the sidebar in BE (#96) 2020-11-20 15:36:55 +13:00
Alice Groux
9dcc76b218 fix(k8s/application): improve ux for instance count input (#93)
* fix(k8s/application): improve ux for instance count input in creation/edition application

* fix(k8s/application): improve validation on persisted folder size and instance count for isolated applications

* feat(k8s/application): minor UI update

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
2020-11-20 15:27:40 +13:00
Stéphane Busso
faa04c188b feat(bolt/backup): backup and restore db for migration and edition upgrades (#87)
* refactor backup

Update upgrade texts

* Restore Failed Upgrade to EE to initial CE version

* Store version before upgrading

* Check rollback command line

* Fix version display

* Update template url only for CE 1.xx

* Fix comments

* revert go modules

* remove duplicate migration

* remove unused files
2020-11-20 12:40:01 +13:00
Stéphane Busso
7da5336158 fix(license): remove license cache (#94) 2020-11-20 12:18:37 +13:00
Alice Groux
563c1405d5 fix(k8s/node): hide taints section for helpdesk users if no taints available (#92) 2020-11-20 09:26:21 +13:00
Maxime Bajeux
597397abfc fix(resource-pool): Invalid resource pool display for a resource pool with a load balancer quota set to 0 (#91) 2020-11-20 09:25:53 +13:00
Chaim Lev-Ari
febc77933f fix(datatable): remove width from company column (#88)
* fix(datatable): remove width from company column

* fix(licenses): limit size of small cols
2020-11-20 09:20:26 +13:00
Maxime Bajeux
2460dfe6dc fix(team): deleting a team throws error object not found in database (#85) 2020-11-19 19:34:04 +13:00
Chaim Lev-Ari
58f8b2aaef feat(licenses): show nodes count in license info (#77)
* feat(licenses): show nodes count in license info

* add bold to node count

* add red color to node when over usage

Co-authored-by: Stéphane Busso <stephane.busso@gmail.com>
2020-11-19 19:32:35 +13:00
Yi Chen
5829da5560 * update license check url (#86) 2020-11-19 13:35:31 +13:00
Stéphane Busso
60e7875889 feat(bolt): add log packaget (#82) 2020-11-19 11:17:57 +13:00
Yi Chen
7d9454eed5 Revert "Setup rollbar (#53)" (#75)
This reverts commit aeeee84530.
2020-11-19 11:17:37 +13:00
Alice Groux
0e489aa898 fix(k8s/cluster): update right access to cluster resource panel (#81) 2020-11-19 11:16:20 +13:00
Maxime Bajeux
98c0d53541 fix(application): Publishing over load balancer should be prevented when editing an application in an exhausted resource pool (#74)
* fix(application): Publishing over load balancer should be prevented when editing an application in an exhausted resource pool

* fix(application): can't edit the load balancer ports associated to an application that was deployed whilst the quota was not exhausted
2020-11-19 11:14:53 +13:00
Stéphane Busso
3a6b6cc7a3 feat(bolt): extract services to new file (#83) 2020-11-19 11:09:21 +13:00
Stéphane Busso
59446e1853 fix(portainer): fix couple of comments (#84) 2020-11-19 11:08:37 +13:00
Alice Groux
4971e8ce14 fix(k8s/resource-pool): endpoint admin can edit quota (#80) 2020-11-19 11:03:13 +13:00
Chaim Lev-Ari
0e7577b696 feat(kube/resourcepools): show load balancer usage (#79) 2020-11-19 10:59:58 +13:00
Chaim Lev-Ari
ff480aa226 feat(license): update license exp warning message (#78) 2020-11-19 09:11:09 +13:00
Chaim Lev-Ari
32c3467c18 fix(licenses): use clipboard library (#76)
* fix(licenses): use clipboard library

* Fix copy text

Co-authored-by: Stéphane Busso <stephane.busso@gmail.com>
2020-11-19 08:40:22 +13:00
Stéphane Busso
be46cc52f2 Adding no-cache headers to GET requests (#73) 2020-11-18 13:27:01 +13:00
xAt0mZ
bbcb6b29c1 fix(registry): disable browse for non browsable urls (#72) 2020-11-18 11:33:41 +13:00
Maxime Bajeux
1b2f3ded58 fix(resource-reservation): Invalid total CPU count in multiple places (#59)
* fix(resource-reservation): Invalid total CPU count in multiple places

* fix(ldap): apply default filters on search (#60)

* fix(k8s/resourcepool): fix CPU count in resource pool creation

* fix(k8s/resourcepool): round CPU count to one decimal

* fix(app): possibly unhandled rejections

* chorse(deps): update liblicense (#62)

* fix(licenses): show message when license is invalid (#66)

* fix(licenses): show message when license is invalid

* fix(licenses): align icon

* feat(license): prevent removal of all licenses (#65)

* feat(license): prevent removal of all licenses

* fix(license): skip caching of info if all licenses failed

* fix(ldap): clear settings when moving from custom to openldap (#64)

* fix(ldap): parse both lower and upper case domain (#63)

* fix(ldap): use a specific openldap settings (#67)

* feat(ldap): disable anonymous mode in openldap (#68)

* fix(k8s/application): persisted folders restriction (#69)

* fix(build/kompose): bump Kompose version (#70)

* fix(resource-reservation): Invalid total CPU count in multiple places

* fix(resource-reservation): Invalid total CPU count in multiple places

Co-authored-by: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Co-authored-by: alice groux <alice.grx@gmail.com>
Co-authored-by: xAt0mZ <baron_l@epitech.eu>
Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>
Co-authored-by: xAt0mZ <xAt0mZ@users.noreply.github.com>
2020-11-18 10:34:55 +13:00
Stéphane Busso
d8e9849bb2 Revert "Adding no-cache headers to GET requests"
This reverts commit 8a8c38ef91.
2020-11-18 09:58:12 +13:00
Stéphane Busso
8a8c38ef91 Adding no-cache headers to GET requests 2020-11-18 09:54:49 +13:00
Maxime Bajeux
16c0838124 fix(quota): Assigning a LB quota on RP without quota is not working (#71) 2020-11-18 08:54:19 +13:00
Anthony Lapenna
b7ed564108 fix(build/kompose): bump Kompose version (#70) 2020-11-17 11:40:13 +13:00
xAt0mZ
a549b7408a fix(k8s/application): persisted folders restriction (#69) 2020-11-17 08:12:35 +13:00
Chaim Lev-Ari
51f15603da feat(ldap): disable anonymous mode in openldap (#68) 2020-11-16 22:05:10 +13:00
Chaim Lev-Ari
b11499dee1 fix(ldap): use a specific openldap settings (#67) 2020-11-16 20:49:32 +13:00
Chaim Lev-Ari
b405cbedf5 fix(ldap): parse both lower and upper case domain (#63) 2020-11-16 10:39:25 +13:00
Chaim Lev-Ari
65c51eef3c fix(ldap): clear settings when moving from custom to openldap (#64) 2020-11-16 10:39:05 +13:00
Chaim Lev-Ari
513a5a7f8c feat(license): prevent removal of all licenses (#65)
* feat(license): prevent removal of all licenses

* fix(license): skip caching of info if all licenses failed
2020-11-16 10:38:17 +13:00
Chaim Lev-Ari
a6f80eb246 fix(licenses): show message when license is invalid (#66)
* fix(licenses): show message when license is invalid

* fix(licenses): align icon
2020-11-16 10:35:04 +13:00
Chaim Lev-Ari
856922f25c chorse(deps): update liblicense (#62) 2020-11-15 22:16:22 +13:00
Anthony Lapenna
10b853b699 Merge pull request #61 from portainer/fix/AB/146-non-catched-errors
fix(app): possibly unhandled rejections
2020-11-13 11:57:33 +13:00
Anthony Lapenna
c140ea3451 Merge pull request #58 from portainer/fix/AB/132-invalid-cpu-count-resource-pool
fix(k8s/resourcepool): fix CPU count in resource pool creation
2020-11-13 09:18:12 +13:00
xAt0mZ
01e2442409 fix(app): possibly unhandled rejections 2020-11-12 17:46:24 +01:00
alice groux
62808822c6 fix(k8s/resourcepool): round CPU count to one decimal 2020-11-12 14:56:30 +01:00
alice groux
9df9a645b0 fix(k8s/resourcepool): fix CPU count in resource pool creation 2020-11-12 09:57:01 +01:00
Chaim Lev-Ari
e9d5f44c85 fix(ldap): apply default filters on search (#60) 2020-11-12 17:31:12 +13:00
xAt0mZ
9a83f19a4e fix(k8s/resource-pool): restricted LB quota edit visibiliy to the same as other quotas (#56) 2020-11-12 09:34:04 +13:00
Chaim Lev-Ari
dc437084f2 feat(ldap): show groups in a better format (#55)
* feat(ldap): show list of groups

* feat(ldap): show only the cn part of the username

* fix(ldap): rename group search button
2020-11-12 09:33:24 +13:00
Anthony Lapenna
085ee043d9 Merge pull request #57 from portainer/fix/AB/143-resource-assignment-no-quota
style(k8s): rename to resourceOverCommitEnabled
2020-11-12 09:33:14 +13:00
xAt0mZ
ac7e7b015b style(k8s): rename to resourceOverCommitEnabled 2020-11-11 19:21:19 +01:00
Stéphane Busso
aeeee84530 Setup rollbar (#53) 2020-11-11 22:28:21 +13:00
Chaim Lev-Ari
0de11465d0 fix(containers): allow bind mounts and privileged mode for admins (#50)
* fix(containers): allow bind mounts for admins

* fix(container-create): allow priviliged mode for endpoint admin
2020-11-11 18:39:56 +13:00
Chaim Lev-Ari
99adb8a3d6 feat(ldap/search): return unique users (#52) 2020-11-11 18:37:52 +13:00
Chaim Lev-Ari
da36dbd37b fix(licenses-datatable): allow copy of license key (#51) 2020-11-11 12:06:54 +13:00
yi-portainer
707f1b9041 * support flattening in webpack 2020-11-10 14:38:59 +13:00
yi-portainer
097674ebca - remove CI download progress report 2020-11-06 21:55:08 +00:00
Yi Chen
6d446dbafd (fix) unable to delete endpoint && endpoint table cannot select all (#48)
* * fix selected items in generic datatable

* * fix _.union issue
2020-11-07 09:30:38 +13:00
yi-portainer
4cbadb5ed1 - remove debugging statements 2020-11-06 09:32:03 +00:00
yi-portainer
117290e960 + debug build environment 2020-11-05 23:43:13 +00:00
portainer-ci
1d4c2c9078 Merge branch 'ce-develop' into develop 2020-11-05 08:23:41 +00:00
yi-portainer
3c0b33265b * fix missing injections 2020-11-05 01:08:24 +00:00
Anthony Lapenna
ceb6cfb83e Merge pull request #46 from portainer/fixAB120-edge-windows
fix(endpoint): fix invalid Edge agent deployment command for Windows
2020-11-05 09:45:50 +13:00
Anthony Lapenna
819db2dbc0 Merge pull request #45 from portainer/fixAB119-agent-windows
fix(endpoint): fix invalid Windows agent deployment command
2020-11-05 09:45:40 +13:00
Anthony Lapenna
81483aeec1 fix(endpoint): fix invalid Edge agent deployment command for Windows 2020-11-05 07:35:26 +13:00
Anthony Lapenna
5f9e342960 feat(endpoint): fix invalid Windows agent deployment command 2020-11-05 07:17:55 +13:00
Anthony Lapenna
c158c52837 fix(endpoint): fix invalid Windows agent deployment command 2020-11-05 07:15:28 +13:00
Anthony Lapenna
9e2e1810ce fix(endpoint): fix invalid Windows agent deployment command 2020-11-05 07:12:52 +13:00
Anthony Lapenna
72cf5d8ede feat(css): update sidebar color and dashboard items color (#44) 2020-11-04 21:30:43 +13:00
Chaim Lev-Ari
cec0ef17e0 feat(licenses): sync between portainer and license-server (#41)
* feat(license): add sync service

* feat(licenses): check license server

* chore(deps): update liblicense

* feat(license): revoke license if invalid

* feat(license): log revokation

* - removed retry logics
- removed license sync logging
* revert liblicense version

* - remove not used field

Co-authored-by: yi-portainer <yi.chen@portainer.io>
2020-11-04 14:07:45 +13:00
xAt0mZ
7e768a54d5 feat(k8s/resource-pool): storage quotas (#26)
* feat(k8s/resource-pool): add storage quota create/edit

* feat(kubernetes): persistent volume claim size validation on app create/edit

* feat(k8s/volume): quota validation on volume expansion

* fix(k8s/application): remove resource limitation message when then is no resource limitation but volume quota

* style(k8s/application): remove HTML layout debug string

* feat(k8s/resource-pool): remove warning message on storage quota reduction

* fix(k8s/application): available size on storage quota is now properly computed on init

* fix(k8s/application): 'flagged for removal' bindings are not considered free space anymore

* feat(k8s/application): allow users to use existing available volumes when quotas are exhausted

* feat(k8s/resource-pool): storage quota usage bar in edit view

* fix(k8s/resource-pool): create RP enable quota by default

* refactor(k8s): move all volume related units to base 10 instead of base 2 (remive i suffix)

* fix(k8s/application): visual issues caused by latency in computation

* feat(k8s/resource-pool): allow standard users to see storage quota usage

* feat(k8s/volume): show max available size on volume expand

* style(k8s/application): exhausted storage quota message

* fix(k8s/application): remove persisted folders entries when selecting RP with all exhausted storage quotas and no available volumes

* style(k8s/application): file format after rebase

* fix(k8s/application): evaluate quota onInit for app edit

* chore(grunt): add prod watch grunt rule and config

* fix(k8s/application): display 'no storages' message on all restricted quotas

* refactor(k8s/volumes): unify volume parsing

* refactor(app): proper prod watch + enforce parseInt radix
2020-11-04 14:07:21 +13:00
Alice Groux
c302364dd7 feat(app/endpoint): edge deployment for windows (#43)
* feat(app/endpoint): edge deployment for windows
* feat(endpoint-details): update Edge deployment commands

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
2020-11-04 11:20:03 +13:00
Alice Groux
3376f730a2 feat(app/endpoint): add deployment instructions for windows (#42)
* feat(app/endpoint): add deployment instructions for windows
* feat(endpoint-creation): minor UI update

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
2020-11-04 11:16:52 +13:00
Yi Chen
2247d8c3a2 (feat)k8s/RBAC: Provide Portainer RBAC functionality for Kubernetes endpoints (#35)
* + endpoint and namespace level authorizations
+ user namespace authorization API
+ k8s client setup service account with k8s roles and policies by portainer role
* User authorization changes refresh token cache
* rbac authorizes k8s requests
* CE to EE migrator to include new authorizations

* code clean up
* comments

* * merge in the RestrictDefaultNamespace changes

* - remove unnecessary check for default namespace

* + updates namespace access policies when generating token

* * updates namespace access policies when querying the user namespace endpoint

* + k8s rule in rbac.go for endpoint access test
+ missing k8s cluster rules for different roles

* feat(rbac): update kube rbac

* feat(rbac): use the authorization directive

* feat(rbac): Update namespace access policies when user/team is deleted

* refactor(app): use new angular-multi-select capabilities

* feat(rbac): fix authorizations

* feat(rbac): fix userAccessPolicies update bug

* feat(rbac): add W applications authorizations

* feat(rbac): add application details W authorizations

* feat(rbac): add configurations W autohorizations

* feat(rbac): add configuration details W authorizations

* feat(rbac): add volumes W authorizations

* feat(rbac): add volume details W authorizations

* feat(rbac): add componentstatus to portainer-view role and add cluster/node authorizations

* fix(rbac): disable application note for non authorized user

* fix(rbac): add endpoints list and components status to portainer-basic

* fix(rbac): allow user to access default namespace when restrict default namespace isn't activated

* fix(rbac): remove default namespace from useraccesspolicies when restrict default namespace isn't activated

* fix(rbac): change some things

* fix(rbac): allow standard user to access container console

* - removed unused parameter

* fix(rbac): fix team authorizations

Co-authored-by: Maxime Bajeux <max.bajeux@gmail.com>
Co-authored-by: xAt0mZ <baron_l@epitech.eu>
2020-11-03 22:08:09 +13:00
Chaim Lev-Ari
0e7cb4cb42 feat(stacks): prevent name collision with external stacks (#16)
* feat(stacks): check for name collision within external stacks

* feat(stacks): check for name collisions

* feat(stacks): check for running stacks

* feat(stacks): change name collision message

* feat(stack): check for existing services only on swarm

* fix(http): supply docker factory to handler

* feat(stacks): look at all containers
2020-11-03 15:50:18 +13:00
Chaim Lev-Ari
812c0b34ea feat(ldap): simplify ldap configuration (#15)
* feat(ldap): simplify ldap configuration

refactor(auth): move ldap settings to a component

feat(ldap): add username style autofill

feat(ldap): customs for ad

feat(app): introduce box selector

refactor(auth-settings): use box selector

feat(ldap): style changes

refactor(ldap): move connectivity check button to a component

refactor(settings): move ldap security settings to a component

refactor(ldap): move user search to component

refactor(ldap): move group search to component

style(ldap): remove comment

refactor(auth-settings): move auto-user-toggle to component

feat(ldap): provide methods to search for users and groups

refactor(ldap): move group/user settings into component

refactor(ldap): provide labels for components

refactor(ldap): separate custom and ad settings

fix(ldap): search for users

feat(ldap): search users

feat(ldap): complete password if missing

feat(ldap): search for users

feat(ldap): show a list of users

feat(ldap): get user uid

feat(ldap): search groups without password

feat(groups): show group results

feat(ldap): add display types

feat(ldap): search for groups

refactor(ldap): clean code

fix(ldap): sort users table

fix(ldap): show settings by type

feat(ldap): parse values from basedn

feat(ldap): parse values

feat(app): emit on change event from box-selector

feat(ldap): user search filter

feat(ldap): search username attribute

feat(ldap): remove format around search filter

feat(ldap): ad group search

refactor(ldap): move dn builder to component

feat(ldap): use base dn builder for group search

feat(ldap): search for ad groups

refactor(ldap): replace domain root object

feat(ldap): openldap settings

refactor(ldap): delete empty controllers

feat(ldap): remove warning on wrong group filter

feat(ldap): clear username and pass if not AD

feat(ldap): clear basedn when switch from openldap to ad

feat(ldap): clear ldap settings when switich from ldap to ad

feat(ldap): set dn only if there are values

feat(ldap): support more cases of domains

feat(ldap): parse openldap domain correctly

refactor(ldap): move server type check

feat(ldap): move entries

feat(ldap): show username format

style(ldap): remove comments

feat(ldap): clear group filter when no groups

refactor(ldap): replace generic payload

feat(ldap): allow the user to test login

feat(ldap): add test login to custom and open ldap settings

feat(ldap): style fixes

fix(ldap): style fix

fix(ldap): style fixes

refactor(ldap): move components to module

feat(ldap): add group entries

feat(ldap): add borders around each group entry

feat(ldap): parse user filter

feat(ldap): add/remove group

feat(ldap): set ad anonymous mode to false

feat(ldap): add group name

feat(ldap): fix parentheses

feat(ldap): separate between each search config

fix(ldap): fix parsing of group dn

feat(ldap): style fixes

feat(ldap): remove of change of filter

refactor(ldap): remove user display style

feat(ldap): rename group entries field

refactor(auth): move auto user provision

refactor(ldap): refactor box selector

feat(ldap): move ad settings to be a global setting

style(ldap): remove comments

feat(ldap): add auto user toggle

refactor(auth/ad): rename ad component

fix(auth/ad): fix the use of a certificate

refactor(ldap): rename components

fix(ldap): show user and group search

fix(ldap): design group settings

feat(ldap): search users and groups

feat(ldap): add margins

refactor(ldap): separate ldap and ad settings

refactor(auth): use central check for auth method

feat(ldap): clear margins

feat(ldap): add port if missing

feat(ldap): fix ad name

fix(ldap): rename fields

feat(ldap): add domain root field

feat(auth/ad): remove domain root field

feat(ldap): rename base dn to root domain

feat(ldap/openldap): get suffix

feat(ldap/open): change base filter

fix(ldap): align

feat(db): introduce migration for ldap server type

refactor(ldap): move service to ldap module

refactor(ldap): sync between client and server constants

fix(ldap): use post for check

style(ldap): fix handler comments

fix(ldap): check for errors

style(ldap): fix tyop

fix(ldap): check equality

style(ldap): add comments

fix(ldap): allow anonymous mode

fix(ldap): show errors on search users

feat(lasp): use custom settings for each server

fix(ldap): supply default group filter

fix(ldap): show domain suffix in new settings

fix(ldap): replace icon with text

refactor(components): remove box-selector-wrapper

* fix(ldap): enable test when form is valid

* fix(ldap): add port if missing
2020-11-03 15:26:28 +13:00
Maxime Bajeux
162b32a47b feat(namespace): Set quota for Load Balancer usage per Resource Pool (#27)
* feat(namespace): Set quota for Load Balancer usage per Resource Pool

* feat(namespace): UX changes

* feat(namespace): add load balancers usage progress bar

* feat(namespace): minor changes

* feat(resource pool): Minor ux changes

* feat(resource-pool): wording changes

* feat(applications): set correct publishing type when ingresses are refresh

* feat(applications): fix load balancers quota overflow bug

* feat(applications): fix load balancers quota overflow bug
2020-11-03 15:23:51 +13:00
Chaim Lev-Ari
3b670c1f54 feat(db): add flag to rollback to ce edition (#39)
* feat(db): add flag to rollback to ce edition

* refactor(db): make backup of db

* style(api): remove comments

* refactor(db): export backup function

Co-authored-by: yi-portainer <yi.chen@portainer.io>
2020-11-03 14:05:42 +13:00
portainer-ci
3448a23033 Merge branch 'ce-develop' into develop 2020-11-02 23:59:35 +00:00
Maxime Bajeux
82297ba990 feat(resource-pool): Provide a means for an admin to allow/disallow resource over-commit (#33)
* feat(resource-pool): change resource over commit implementation

* fix(resource-pool): hide resource reservation gauges when resources are set to unlimited both

* feat(resource-pool): renaming and hide switch when resource over commit is disabled

* feat(k8s/resource-pools): minor UI update

* fix(resource-pool): fix resource quota validation on resource pool details

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
2020-11-03 10:52:44 +13:00
Alice Groux
a23daeb5cf Feat/ab/106 business theme (#36)
* feat(app): change sidebar background color and font color for business edition

* feat(app/sidebar): add edition version

* feat(app/sidebar): get the edition version

* feat(app/sidebar): change color of selected item for business edition
2020-11-03 10:51:15 +13:00
Chaim Lev-Ari
15ce12e7b7 feat(license): introduce license management (#31)
* feat(license): add liblicense dep

* feat(license): add bolt license service

* feat(license): introduce license service

* feat(license): validate license before adding

* feat(license): aggregate info after changing of licenses

* feat(http): implement http handlers

* feat(license-management): introduce license service

* feat(licenses): introduce empty view

* feat(license-management): add datatable

* feat(licenses): show license info

* fix(license): inject services

* feat(licenses): add buttons to buy/renew license

* feat(licenses): introduce add license route

* feat(licenses): add license form

* feat(license): datatable

* feat(license): show more details about license

* refactor(license): rename components name

* feat(licenses): show expiration date

* feat(license): introduce init license route

* feat(license): validate license

* feat(license): save licenses

* feat(bouncer): check if license is valid on restricted

* feat(bouncer): remove license check on api

* feat(home): add node warning

* feat(licenses): remove license

* feat(licenses): listen to info changes

* feat(license): show license expiration message

* feat(license): block regular users from licenses view

* feat(license): prevent removing of last license

* fix(license): show message when failed delete

* feat(license): remove trial license when applying oneoff

* feat(license): hide the number of nodes for trial

* feat(auth): disable login if license is invalid

* feat(licenses): add confirmation before removal of license

* feat(nodes): count nodes in env

* feat(license): show message if nodes exceed allowed

* feat(deps): update liblicense

* feat(licenses): show validation errors

* feat(license): use information panel for node info

* fix(license): reload license data on remove

* fix(license): always send list of failed keys

* fix(license): rename buttons

* feat(license): replace icon

* feat(license): add link to licenses page in add license

* fix(licenses): show green valid icon

* fix(licenses): rename expires at

* fix(licenses): rename Attach to add

* fix(licenses): show license type label

* feat(license): aggregate revoked info

* chore(deps): update liblicense

* fix(license): remove space

* fix(sidebar): align icon

* fix(license): change info layout

* feat(license): aggregate only valid licenses

* fix(licenses): move add license to a new line

* style(license): remove console

* refactor(license): move license line to component

* feat(license): check server validation

* fix(licenses): check form validation before submit

* feat(licenses): send only invalid licenses

* fix(license):  hide panels when not needed

* feat(licnese): receive a single license on init

* refactor(header): move header to module

* feat(license): move license panel to header

* fix(header): set min height

* fix(home): show node warning only if subscription

* feat(licenses): minor UI updates

* feat(licenses): minor UI update

* feat(licenses-datatable): add copy button

* fix(licenses-datatable): show date without hours

* feat(license): show expiration message

* fix(users): get user info only on restriced access

* fix(license): clear check for single license

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
2020-11-02 19:10:57 +13:00
Chaim Lev-Ari
9591e1012c feat(auth): support a list of LDAP urls (#9)
* feat(ldap): move urls to url

* feat(ldap): test a few connections

* feat(ldap): update urls

* feat(settings-auth): support array of ldap urls

* feat(settings-auth): support list of urls

* feat(auth): add explanation about server urls

* feat(bolt): add url to urls only if needed

* fix(settings): add nil guards

* fix(settings): set inital value for ldap urls

* feat(settings): prevent the deletion of the first url

* feat(core/settings): minor UI update

* feat(authentication): check that ldap settings are valid

* feat(bolt): create migration for settings

* fix(settings): add wrapping

* feat(ldap): disable submit button only on ldap

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
2020-11-02 11:39:25 +13:00
Chaim Lev-Ari
b357cb54f0 fix(kuberentes): disable rbac check for kuberentes (#38) 2020-10-28 23:13:51 +13:00
Chaim Lev-Ari
d645b4ce6d fix(rbac): add user portainer authorization from ce (#37)
* fix(rbac): add user portainer authorization from ce

* fix(bolt): remove unneeded property
2020-10-28 16:49:30 +13:00
portainer-ci
7b576bcd1d Merge branch 'ce-develop' into develop 2020-10-27 23:59:36 +00:00
Chaim Lev-Ari
c23d2a33da feat(rbac): protect templates deployment (#34)
* feat(templates): show templates link

* feat(templates): protect deploying of templates

* feat(templates): allow fetching of templates to any user

* feat(rbac): allow template file fetching
2020-10-27 20:33:49 +13:00
Chaim Lev-Ari
41eb89cdb1 fix(docker): check for endpoint access auth (#32)
* fix(docker): check for endpoint access auth

* fix(rbac): load user authorizations

* fix(volumes): hide browse button when not agent
2020-10-22 16:07:43 +13:00
portainer-ci
fe9964a405 Merge branch 'ce-develop' into develop 2020-10-20 23:59:58 +00:00
portainer-ci
211def5b51 Merge branch 'ce-develop' into develop 2020-10-19 23:59:32 +00:00
portainer-ci
454a39f83d Merge branch 'ce-develop' into develop 2020-10-16 23:59:34 +00:00
Maxime Bajeux
1f26bc6e8b feat(namespace): Hide Default Namespace for non-admins (#25)
* feat(namespace): Hide Default Namespace for non-admins

* feat(namespace): fix expected behavior when turning on the setting

* feat(resourcePool): Handle when user doesn't have access to any resource pool

* Update app/kubernetes/views/applications/create/createApplication.html

* Update app/kubernetes/views/configurations/create/createConfiguration.html

* Update app/kubernetes/views/applications/create/createApplication.html

* Update app/kubernetes/views/configurations/create/createConfiguration.html

Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>
2020-10-15 14:02:29 +13:00
portainer-ci
96c6fbf1ed Merge branch 'ce-develop' into develop 2020-10-12 23:59:33 +00:00
Chaim Lev-Ari
336399d482 fix(db): set edition on first run (#30) 2020-10-09 09:29:48 +13:00
Chaim Lev-Ari
8dba19694a feat(roles-management): integrate rbac extension (#6)
* refactor(rbac): move client extension code

* feat(app): remove checks for extension

* feat(rbac): remove checks for extensions

* feat(extensions): remove reference to rbac extensions

* feat(roles): add changes from codebase before removal of rbac

* refactor(security): remove rbac service

* refactor(security): use AdminAccess as an alias

* fix(access): rename policies type

* style(security): add comment about Aliasing AdminAccess to RestrictedAccess

* feat(bolt): add auth migration from ce to ee

* feat(stacks): use authorized access to stop/start stacks

* fix(bolt): supply right params to migrator

* feat(rbac): get authorization on client side
2020-10-07 23:21:14 +13:00
portainer-ci
1a57f656e8 Merge branch 'ce-develop' into develop 2020-10-04 23:59:29 +00:00
Chaim Lev-Ari
6fa16ff49b fix(registry): inject services (#29)
* fix(registry): inject services

* chore(app): add angularjs strict mode
2020-10-05 11:00:59 +13:00
portainer-ci
d84dd8643f Merge branch 'ce-develop' into develop 2020-09-30 23:59:30 +00:00
portainer-ci
4a5bb7c761 Merge branch 'ce-develop' into develop 2020-09-28 23:59:31 +00:00
portainer-ci
4c6f10fc41 Merge branch 'ce-develop' into develop 2020-09-24 23:59:35 +00:00
portainer-ci
ce7400fa9d Merge branch 'ce-develop' into develop 2020-09-23 23:59:46 +00:00
portainer-ci
2271aed31f Merge branch 'ce-develop' into develop 2020-09-21 23:59:33 +00:00
Chaim Lev-Ari
66375454a7 feat(edition): change from BE to EE (#24) 2020-09-15 19:37:43 +12:00
xAt0mZ
c6a8eba1e8 fix(rest): remove timeouts for all REST services (#23) 2020-09-10 16:04:56 +12:00
Chaim Lev-Ari
92872435c4 feat(registry): integrate RM extension (#4)
* refactor(registries): move to portainer

* feat(registries): show browse link

* feat(registry): move registry extension code

* fix(registry): revert files

* refactor(registry): use component

* refactor(registry): replace $scope with this

* refactor(registry): use async await

* refactor(registry): rename and extract

* refactor(registry): rename progression-modal files

* refactor(registry): replace view with component

* refactor(registry): replace with component

* style(regirstries): sort handler keys

* feat(registry): force the recreation of a proxy client

* fix(registry): ignore 404 tags
2020-09-08 19:35:29 +12:00
Chaim Lev-Ari
5459c5cc5b feat(bolt): handle migrations from ce to ee (#22)
* feat(db): add edition value to db

* feat(bolt): handle migrations from ce to ee

* refactor(bolt): merge if branches

* refactor(bolt): rename migration function

* feat(bolt): change migration message

* feat(bolt): add edition to migration messages

* feat(bolt): add log tags

* feat(portainer): add edition

* feat(db): set initial db version

* feat(bolt): cache current version

* refactor(portainer): remove current edition const
2020-09-07 22:15:38 +12:00
Anthony Lapenna
81703dfd0b feat(resource-pools): Provide a means for an admin to allow/disallow resource over-commit 2020-09-01 10:18:15 +12:00
Anthony Lapenna
8b3119cf83 feat(k8s/resource-pool): minor UI update 2020-09-01 10:17:26 +12:00
Anthony Lapenna
e87f2f12ec feat(oauth): add oauth providers 2020-09-01 08:59:21 +12:00
Anthony Lapenna
49730157d8 fix(k8s/applications): fix an issue with daemonset in 0/0 state 2020-09-01 08:45:49 +12:00
Anthony Lapenna
c2075fee29 fix(k8s/volumes): fix an issue with the system volume filter not working 2020-09-01 08:45:29 +12:00
Anthony Lapenna
7f329bb484 feat(analytics): change matomo site id and url 2020-09-01 08:44:51 +12:00
Anthony Lapenna
65bdc2ed6f feat(portainer-ee): Use a different source for MOTD 2020-09-01 08:42:58 +12:00
Anthony Lapenna
38c1c72d38 fix(k8s/applications): fix an issue with daemonset in 0/0 state 2020-08-31 16:59:10 +12:00
Anthony Lapenna
8eb432b0b0 fix(k8s/volumes): fix an issue with the system volume filter not working 2020-08-31 12:54:22 +12:00
Maxime Bajeux
5437d9db7c feat(resource-pools): Provide a means for an admin to allow/disallow resource over-commit 2020-08-30 23:22:28 +02:00
Chaim Lev-Ari
8850bc3dcd feat(analytics): change matomo site id and url 2020-08-30 15:27:31 +03:00
Anthony Lapenna
3b02596704 Merge pull request #10 from portainer/ee-pulldog
feat(build/pulldog): review pulldog configuration
2020-08-28 15:25:59 +12:00
Maxime Bajeux
18c1425b8e feat(portainer-ee): Use a different source for MOTD 2020-08-24 18:34:29 +02:00
Chaim Lev-Ari
957dd43aa3 feat(oauth): add oauth providers 2020-08-18 11:24:33 +03:00
Anthony Lapenna
2972dbeafb feat(build/pulldog): review pulldog configuration 2020-08-18 12:36:01 +12:00
920 changed files with 48773 additions and 13683 deletions

View File

@@ -5,7 +5,8 @@
"@babel/preset-env",
{
"modules": false,
"useBuiltIns": "entry"
"useBuiltIns": "entry",
"corejs": "2"
}
]
]

7
.gitignore vendored
View File

@@ -8,3 +8,10 @@ api/cmd/portainer/portainer*
**/.vscode/tasks.json
.eslintcache
.idea
test/e2e/cypress/screenshots
*.db
*.log
__debug_bin
api/docs
.env

View File

@@ -0,0 +1,19 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceRoot}/api/cmd/portainer/main.go",
"cwd": "${workspaceRoot}",
"env": {},
"showLog": true,
"args": ["--data", "${env:HOME}/portainer-data", "--assets", "${workspaceRoot}/dist"]
}
]
}

View File

@@ -21,11 +21,11 @@
"description": "Dummy Angularjs Component",
"body": [
"import angular from 'angular';",
"import ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}Controller from './${TM_FILENAME_BASE}Controller'",
"import controller from './${TM_FILENAME_BASE}Controller'",
"",
"angular.module('portainer.${TM_DIRECTORY/.*\\/app\\/([^\\/]*)(\\/.*)?$/$1/}').component('$TM_FILENAME_BASE', {",
" templateUrl: './$TM_FILENAME_BASE.html',",
" controller: ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}Controller,",
" controller,",
"});",
""
]
@@ -44,25 +44,6 @@
],
"description": "Dummy ES6+ controller"
},
"Model": {
"scope": "javascript",
"prefix": "mymodel",
"description": "Dummy ES6+ model",
"body": [
"/**",
" * $1 Model",
" */",
"const _$1 = Object.freeze({",
" $0",
"});",
"",
"export class $1 {",
" constructor() {",
" Object.assign(this, JSON.parse(JSON.stringify(_$1)));",
" }",
"}"
]
},
"Service": {
"scope": "javascript",
"prefix": "myservice",
@@ -158,5 +139,29 @@
"export default $1;",
"angular.module('portainer.${TM_DIRECTORY/.*\\/app\\/([^\\/]*)(\\/.*)?$/$1/}').service('$1', $1);"
]
},
"swagger-api-doc": {
"prefix": "swapi",
"scope": "go",
"description": "Snippet for a api doc",
"body": [
"// @id ",
"// @summary ",
"// @description ",
"// @description **Access policy**: ",
"// @tags ",
"// @security jwt",
"// @accept json",
"// @produce json",
"// @param id path int true \"identifier\"",
"// @param body body Object true \"details\"",
"// @success 200 {object} portainer. \"Success\"",
"// @success 204 \"Success\"",
"// @failure 400 \"Invalid request\"",
"// @failure 403 \"Permission denied\"",
"// @failure 404 \" not found\"",
"// @failure 500 \"Server error\"",
"// @router /{id} [get]"
]
}
}

View File

@@ -74,3 +74,56 @@ 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/>.
## Adding api docs
When adding a new resource (or a route handler), we should add a new tag to api/http/handler/handler.go#L136 like this:
```
// @tag.name <Name of resource>
// @tag.description a short description
```
When adding a new route to an existing handler use the following as a template (you can use `swapi` snippet if you're using vscode):
```
// @id
// @summary
// @description
// @description **Access policy**:
// @tags
// @security jwt
// @accept json
// @produce json
// @param id path int true "identifier"
// @param body body Object true "details"
// @success 200 {object} portainer. "Success"
// @success 204 "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"
// @failure 404 " not found"
// @failure 500 "Server error"
// @router /{id} [get]
```
explanation about each line can be found (here)[https://github.com/swaggo/swag#api-operation]

View File

@@ -0,0 +1,69 @@
package adminmonitor
import (
"context"
"log"
"time"
portainer "github.com/portainer/portainer/api"
)
var logFatalf = log.Fatalf
type Monitor struct {
timeout time.Duration
datastore portainer.DataStore
shutdownCtx context.Context
cancellationFunc context.CancelFunc
}
// New creates a monitor that when started will wait for the timeout duration and then shutdown the application unless it has been initialized.
func New(timeout time.Duration, datastore portainer.DataStore, shutdownCtx context.Context) *Monitor {
return &Monitor{
timeout: timeout,
datastore: datastore,
shutdownCtx: shutdownCtx,
}
}
// Starts starts the monitor. Active monitor could be stopped or shuttted down by cancelling the shutdown context.
func (m *Monitor) Start() {
cancellationCtx, cancellationFunc := context.WithCancel(context.Background())
m.cancellationFunc = cancellationFunc
go func() {
log.Println("[DEBUG] [internal,init] [message: start initialization monitor ]")
select {
case <-time.After(m.timeout):
initialized, err := m.WasInitialized()
if err != nil {
logFatalf("failed getting admin user: %s", err)
}
if !initialized {
logFatalf("[FATAL] [internal,init] No administrator account was created in %f mins. Shutting down the Portainer instance for security reasons", m.timeout.Minutes())
}
case <-cancellationCtx.Done():
log.Println("[DEBUG] [internal,init] [message: canceling initialization monitor]")
case <-m.shutdownCtx.Done():
log.Println("[DEBUG] [internal,init] [message: shutting down initialization monitor]")
}
}()
}
// Stop stops monitor. Safe to call even if monitor wasn't started.
func (m *Monitor) Stop() {
if m.cancellationFunc == nil {
return
}
m.cancellationFunc()
m.cancellationFunc = nil
}
// WasInitialized is a system initialization check
func (m *Monitor) WasInitialized() (bool, error) {
users, err := m.datastore.User().UsersByRole(portainer.AdministratorRole)
if err != nil {
return false, err
}
return len(users) > 0, nil
}

View File

@@ -0,0 +1,50 @@
package adminmonitor
import (
"context"
"testing"
"time"
portainer "github.com/portainer/portainer/api"
i "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert"
)
func Test_stopWithoutStarting(t *testing.T) {
monitor := New(1*time.Minute, nil, nil)
monitor.Stop()
}
func Test_stopCouldBeCalledMultipleTimes(t *testing.T) {
monitor := New(1*time.Minute, nil, nil)
monitor.Stop()
monitor.Stop()
}
func Test_canStopStartedMonitor(t *testing.T) {
monitor := New(1*time.Minute, nil, context.Background())
monitor.Start()
assert.NotNil(t, monitor.cancellationFunc, "cancellation function is missing in started monitor")
monitor.Stop()
assert.Nil(t, monitor.cancellationFunc, "cancellation function should absent in stopped monitor")
}
func Test_start_shouldFatalAfterTimeout_ifNotInitialized(t *testing.T) {
timeout := 10 * time.Millisecond
datastore := i.NewDatastore(i.WithUsers([]portainer.User{}))
var fataled bool
origLogFatalf := logFatalf
logFatalf = func(s string, v ...interface{}) { fataled = true }
defer func() {
logFatalf = origLogFatalf
}()
monitor := New(timeout, datastore, context.Background())
monitor.Start()
<-time.After(2 * timeout)
assert.True(t, fataled, "monitor should been timeout and fatal")
}

53
api/api-description.md Normal file
View File

@@ -0,0 +1,53 @@
Portainer API is an HTTP API served by Portainer. It is used by the Portainer UI and everything you can do with the UI can be done using the HTTP API.
Examples are available at https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8
You can find out more about Portainer at [http://portainer.io](http://portainer.io) and get some support on [Slack](http://portainer.io/slack/).
# Authentication
Most of the API endpoints require to be authenticated as well as some level of authorization to be used.
Portainer API uses JSON Web Token to manage authentication and thus requires you to provide a token in the **Authorization** header of each request
with the **Bearer** authentication mechanism.
Example:
```
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsImV4cCI6MTQ5OTM3NjE1NH0.NJ6vE8FY1WG6jsRQzfMqeatJ4vh2TWAeeYfDhP71YEE
```
# Security
Each API endpoint has an associated access policy, it is documented in the description of each endpoint.
Different access policies are available:
- Public access
- Authenticated access
- Restricted access
- Administrator access
### Public access
No authentication is required to access the endpoints with this access policy.
### Authenticated access
Authentication is required to access the endpoints with this access policy.
### Restricted access
Authentication is required to access the endpoints with this access policy.
Extra-checks might be added to ensure access to the resource is granted. Returned data might also be filtered.
### Administrator access
Authentication as well as an administrator role are required to access the endpoints with this access policy.
# Execute Docker requests
Portainer **DO NOT** expose specific endpoints to manage your Docker resources (create a container, remove a volume, etc...).
Instead, it acts as a reverse-proxy to the Docker HTTP API. This means that you can execute Docker requests **via** the Portainer HTTP API.
To do so, you can use the `/endpoints/{id}/docker` Portainer API endpoint (which is not documented below due to Swagger limitations). This endpoint has a restricted access policy so you still need to be authenticated to be able to query this endpoint. Any query on this endpoint will be proxied to the Docker API of the associated endpoint (requests and responses objects are the same as documented in the Docker API).
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).

119
api/archive/targz.go Normal file
View File

@@ -0,0 +1,119 @@
package archive
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// TarGzDir creates a tar.gz archive and returns it's path.
// abosolutePath should be an absolute path to a directory.
// Archive name will be <directoryName>.tar.gz and will be placed next to the directory.
func TarGzDir(absolutePath string) (string, error) {
targzPath := filepath.Join(absolutePath, fmt.Sprintf("%s.tar.gz", filepath.Base(absolutePath)))
outFile, err := os.Create(targzPath)
if err != nil {
return "", err
}
defer outFile.Close()
zipWriter := gzip.NewWriter(outFile)
defer zipWriter.Close()
tarWriter := tar.NewWriter(zipWriter)
defer tarWriter.Close()
err = filepath.Walk(absolutePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if path == targzPath {
return nil // skip archive file
}
pathInArchive := filepath.Clean(strings.TrimPrefix(path, absolutePath))
if pathInArchive == "" {
return nil // skip root dir
}
return addToArchive(tarWriter, pathInArchive, path, info)
})
return targzPath, err
}
func addToArchive(tarWriter *tar.Writer, pathInArchive string, path string, info os.FileInfo) error {
header, err := tar.FileInfoHeader(info, info.Name())
if err != nil {
return err
}
header.Name = pathInArchive // use relative paths in archive
err = tarWriter.WriteHeader(header)
if err != nil {
return err
}
if info.IsDir() {
return nil
}
file, err := os.Open(path)
if err != nil {
return err
}
_, err = io.Copy(tarWriter, file)
return err
}
// ExtractTarGz reads a .tar.gz archive from the reader and extracts it into outputDirPath directory
func ExtractTarGz(r io.Reader, outputDirPath string) error {
zipReader, err := gzip.NewReader(r)
if err != nil {
return err
}
defer zipReader.Close()
tarReader := tar.NewReader(zipReader)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
switch header.Typeflag {
case tar.TypeDir:
// skip, dir will be created with a file
case tar.TypeReg:
p := filepath.Clean(filepath.Join(outputDirPath, header.Name))
if err := os.MkdirAll(filepath.Dir(p), 0744); err != nil {
return fmt.Errorf("Failed to extract dir %s", filepath.Dir(p))
}
outFile, err := os.Create(p)
if err != nil {
return fmt.Errorf("Failed to create file %s", header.Name)
}
if _, err := io.Copy(outFile, tarReader); err != nil {
return fmt.Errorf("Failed to extract file %s", header.Name)
}
outFile.Close()
default:
return fmt.Errorf("Tar: uknown type: %v in %s",
header.Typeflag,
header.Name)
}
}
return nil
}

98
api/archive/targz_test.go Normal file
View File

@@ -0,0 +1,98 @@
package archive
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func listFiles(dir string) []string {
items := make([]string, 0)
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if path == dir {
return nil
}
items = append(items, path)
return nil
})
return items
}
func Test_shouldCreateArhive(t *testing.T) {
tmpdir, _ := ioutil.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
content := []byte("content")
ioutil.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
ioutil.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
ioutil.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
gzPath, err := TarGzDir(tmpdir)
assert.Nil(t, err)
assert.Equal(t, filepath.Join(tmpdir, fmt.Sprintf("%s.tar.gz", filepath.Base(tmpdir))), gzPath)
extractionDir, _ := ioutil.TempDir("", "extract")
defer os.RemoveAll(extractionDir)
cmd := exec.Command("tar", "-xzf", gzPath, "-C", extractionDir)
err = cmd.Run()
if err != nil {
t.Fatal("Failed to extract archive: ", err)
}
extractedFiles := listFiles(extractionDir)
wasExtracted := func(p string) {
fullpath := path.Join(extractionDir, p)
assert.Contains(t, extractedFiles, fullpath)
copyContent, _ := ioutil.ReadFile(fullpath)
assert.Equal(t, content, copyContent)
}
wasExtracted("outer")
wasExtracted("dir/inner")
wasExtracted("dir/.dotfile")
}
func Test_shouldCreateArhiveXXXXX(t *testing.T) {
tmpdir, _ := ioutil.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
content := []byte("content")
ioutil.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
ioutil.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
ioutil.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
gzPath, err := TarGzDir(tmpdir)
assert.Nil(t, err)
assert.Equal(t, filepath.Join(tmpdir, fmt.Sprintf("%s.tar.gz", filepath.Base(tmpdir))), gzPath)
extractionDir, _ := ioutil.TempDir("", "extract")
defer os.RemoveAll(extractionDir)
r, _ := os.Open(gzPath)
ExtractTarGz(r, extractionDir)
if err != nil {
t.Fatal("Failed to extract archive: ", err)
}
extractedFiles := listFiles(extractionDir)
wasExtracted := func(p string) {
fullpath := path.Join(extractionDir, p)
assert.Contains(t, extractedFiles, fullpath)
copyContent, _ := ioutil.ReadFile(fullpath)
assert.Equal(t, content, copyContent)
}
wasExtracted("outer")
wasExtracted("dir/inner")
wasExtracted("dir/.dotfile")
}

BIN
api/archive/testdata/sample_archive.zip vendored Normal file

Binary file not shown.

View File

@@ -3,10 +3,13 @@ package archive
import (
"archive/zip"
"bytes"
"fmt"
"github.com/pkg/errors"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
// UnzipArchive will unzip an archive from bytes into the dest destination folder on disk
@@ -52,3 +55,60 @@ func extractFileFromArchive(file *zip.File, dest string) error {
return outFile.Close()
}
// UnzipFile will decompress a zip archive, moving all files and folders
// within the zip file (parameter 1) to an output directory (parameter 2).
func UnzipFile(src string, dest string) error {
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer r.Close()
for _, f := range r.File {
p := filepath.Join(dest, f.Name)
// Check for ZipSlip. More Info: http://bit.ly/2MsjAWE
if !strings.HasPrefix(p, filepath.Clean(dest)+string(os.PathSeparator)) {
return fmt.Errorf("%s: illegal file path", p)
}
if f.FileInfo().IsDir() {
// Make Folder
os.MkdirAll(p, os.ModePerm)
continue
}
err = unzipFile(f, p)
if err != nil {
return err
}
}
return nil
}
func unzipFile(f *zip.File, p string) error {
// Make File
if err := os.MkdirAll(filepath.Dir(p), os.ModePerm); err != nil {
return errors.Wrapf(err, "unzipFile: can't make a path %s", p)
}
outFile, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return errors.Wrapf(err, "unzipFile: can't create file %s", p)
}
defer outFile.Close()
rc, err := f.Open()
if err != nil {
return errors.Wrapf(err, "unzipFile: can't open zip file %s in the archive", f.Name)
}
defer rc.Close()
_, err = io.Copy(outFile, rc)
if err != nil {
return errors.Wrapf(err, "unzipFile: can't copy an archived file content")
}
return nil
}

32
api/archive/zip_test.go Normal file
View File

@@ -0,0 +1,32 @@
package archive
import (
"github.com/stretchr/testify/assert"
"io/ioutil"
"os"
"path/filepath"
"testing"
)
func TestUnzipFile(t *testing.T) {
dir, err := ioutil.TempDir("", "unzip-test-")
assert.NoError(t, err)
defer os.RemoveAll(dir)
/*
Archive structure.
├── 0
│ ├── 1
│ │ └── 2.txt
│ └── 1.txt
└── 0.txt
*/
err = UnzipFile("./testdata/sample_archive.zip", dir)
assert.NoError(t, err)
archiveDir := dir + "/sample_archive"
assert.FileExists(t, filepath.Join(archiveDir, "0.txt"))
assert.FileExists(t, filepath.Join(archiveDir, "0", "1.txt"))
assert.FileExists(t, filepath.Join(archiveDir, "0", "1", "2.txt"))
}

112
api/backup/backup.go Normal file
View File

@@ -0,0 +1,112 @@
package backup
import (
"fmt"
"log"
"os"
"path/filepath"
"time"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/archive"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/offlinegate"
"github.com/portainer/portainer/api/s3"
)
const rwxr__r__ os.FileMode = 0744
var filesToBackup = []string{"compose", "config.json", "custom_templates", "edge_jobs", "edge_stacks", "extensions", "portainer.key", "portainer.pub", "tls"}
func BackupToS3(settings portainer.S3BackupSettings, gate *offlinegate.OfflineGate, datastore portainer.DataStore, filestorePath string) error {
archivePath, err := CreateBackupArchive(settings.Password, gate, datastore, filestorePath)
if err != nil {
log.Printf("[ERROR] failed to backup: %s \n", err)
return err
}
archiveReader, err := os.Open(archivePath)
if err != nil {
log.Println("[ERROR] failed to open backup file")
return err
}
defer os.RemoveAll(filepath.Dir(archivePath))
archiveName := fmt.Sprintf("portainer-backup_%s", filepath.Base(archivePath))
s3session, err := s3.NewSession(settings.Region, settings.AccessKeyID, settings.SecretAccessKey)
if err != nil {
log.Printf("[ERROR] %s \n", err)
return err
}
if err := s3.Upload(s3session, archiveReader, settings.BucketName, archiveName); err != nil {
log.Printf("[ERROR] failed to upload backup to S3: %s \n", err)
return err
}
return nil
}
// Creates a tar.gz system archive and encrypts it if password is not empty. Returns a path to the archive file.
func CreateBackupArchive(password string, gate *offlinegate.OfflineGate, datastore portainer.DataStore, filestorePath string) (string, error) {
unlock := gate.Lock()
defer unlock()
backupDirPath := filepath.Join(filestorePath, "backup", time.Now().Format("2006-01-02_15-04-05"))
if err := os.MkdirAll(backupDirPath, rwxr__r__); err != nil {
return "", errors.Wrap(err, "Failed to create backup dir")
}
if err := backupDb(backupDirPath, datastore); err != nil {
return "", errors.Wrap(err, "Failed to backup database")
}
for _, filename := range filesToBackup {
err := copyPath(filepath.Join(filestorePath, filename), backupDirPath)
if err != nil {
return "", errors.Wrap(err, "Failed to create backup file")
}
}
archivePath, err := archive.TarGzDir(backupDirPath)
if err != nil {
return "", errors.Wrap(err, "Failed to make an archive")
}
if password != "" {
archivePath, err = encrypt(archivePath, password)
if err != nil {
return "", errors.Wrap(err, "Failed to encrypt backup with the password")
}
}
return archivePath, nil
}
func backupDb(backupDirPath string, datastore portainer.DataStore) error {
backupWriter, err := os.Create(filepath.Join(backupDirPath, "portainer.db"))
if err != nil {
return err
}
if err = datastore.BackupTo(backupWriter); err != nil {
return err
}
return backupWriter.Close()
}
func encrypt(path string, passphrase string) (string, error) {
in, err := os.Open(path)
if err != nil {
return "", err
}
defer in.Close()
outFileName := fmt.Sprintf("%s.encrypted", path)
out, err := os.Create(outFileName)
if err != nil {
return "", err
}
err = crypto.AesEncrypt(in, out, []byte(passphrase))
return outFileName, err
}

View File

@@ -0,0 +1,118 @@
package backup
import (
"context"
"log"
"time"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/offlinegate"
"github.com/robfig/cron/v3"
)
// BackupScheduler orchestrates S3 settings and active backup cron jobs
type BackupScheduler struct {
cronmanager *cron.Cron
s3backupService portainer.S3BackupService
gate *offlinegate.OfflineGate
datastore portainer.DataStore
filestorePath string
}
func NewBackupScheduler(offlineGate *offlinegate.OfflineGate, datastore portainer.DataStore, filestorePath string) *BackupScheduler {
crontab := cron.New(cron.WithChain(cron.Recover(cron.DefaultLogger)))
s3backupService := datastore.S3Backup()
return &BackupScheduler{
cronmanager: crontab,
s3backupService: s3backupService,
gate: offlineGate,
datastore: datastore,
filestorePath: filestorePath,
}
}
// Start fetches latest backup settings and starts cron job if configured
func (s *BackupScheduler) Start() error {
s.cronmanager.Start()
settings, err := s.s3backupService.GetSettings()
if err != nil {
return errors.Wrap(err, "failed to fetch settings")
}
if canBeScheduled(settings) {
return s.startJob(settings)
}
return nil
}
// Stop stops the scheduler if it is running; otherwise it does nothing.
// A context is returned so the caller can wait for running jobs to complete.
func (s *BackupScheduler) Stop() context.Context {
if s.cronmanager != nil {
log.Println("[DEBUG] Stopping backup scheduler")
return s.cronmanager.Stop()
}
return nil
}
// Update updates stored S3 backup settings and orchestrates cron jobs.
// When scheduler has an active cron job, then it shuts it down.
// When a provided settings has a cron, then starts a new cron job.
// When ever current cron is being shut down, last cron error going to be dropped.
func (s *BackupScheduler) Update(settings portainer.S3BackupSettings) error {
if err := s.s3backupService.UpdateSettings(settings); err != nil {
return errors.Wrap(err, "failed to update settings")
}
if err := s.stopJobs(); err != nil {
return errors.Wrap(err, "failed to stop current cronjob")
}
if canBeScheduled(settings) {
return s.startJob(settings)
}
return nil
}
// stops current backup cron job and drops last cron error if any
func (s *BackupScheduler) stopJobs() error {
// stopping all cron jobs as there should be only one (c)
for _, job := range s.cronmanager.Entries() {
s.cronmanager.Remove(job.ID)
}
return s.s3backupService.DropStatus()
}
func (s *BackupScheduler) startJob(settings portainer.S3BackupSettings) error {
_, err := s.cronmanager.AddFunc(settings.CronRule, s.backup(settings))
if err != nil {
return errors.Wrap(err, "failed to start a new backup cron job")
}
return nil
}
func canBeScheduled(s portainer.S3BackupSettings) bool {
return s.AccessKeyID != "" && s.SecretAccessKey != "" && s.Region != "" && s.BucketName != "" && s.CronRule != ""
}
func (s *BackupScheduler) backup(settings portainer.S3BackupSettings) func() {
return func() {
err := BackupToS3(settings, s.gate, s.datastore, s.filestorePath)
status := portainer.S3BackupStatus{
Failed: err != nil,
Timestamp: time.Now(),
}
if err = s.s3backupService.UpdateStatus(status); err != nil {
log.Printf("[ERROR] failed to update status of last scheduled backup. Status: %+v . Err: %s \n", status, err)
}
}
}

View File

@@ -0,0 +1,112 @@
package backup
import (
"testing"
"time"
portainer "github.com/portainer/portainer/api"
i "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert"
)
func newScheduler(status *portainer.S3BackupStatus, settings *portainer.S3BackupSettings) *BackupScheduler {
scheduler := NewBackupScheduler(nil, i.NewDatastore(i.WithS3BackupService(status, settings)), "")
scheduler.Start()
return scheduler
}
func settings(cronRule string,
accessKeyID string,
secretAccessKey string,
region string,
bucketName string) *portainer.S3BackupSettings {
return &portainer.S3BackupSettings{
CronRule: cronRule,
AccessKeyID: accessKeyID,
SecretAccessKey: secretAccessKey,
Region: region,
BucketName: bucketName,
}
}
func Test_startWithoutCron_shouldNotStartAJob(t *testing.T) {
scheduler := newScheduler(&portainer.S3BackupStatus{}, &portainer.S3BackupSettings{})
defer scheduler.Stop()
jobs := scheduler.cronmanager.Entries()
assert.Len(t, jobs, 0, "should have empty job list")
}
func Test_startWitACron_shouldAlsoStartAJob(t *testing.T) {
scheduler := newScheduler(&portainer.S3BackupStatus{}, settings("*/10 * * * *", "id", "key", "region", "bucket"))
defer scheduler.Stop()
jobs := scheduler.cronmanager.Entries()
assert.Len(t, jobs, 1, "should have 1 active job")
}
func Test_update_shouldDropStatus(t *testing.T) {
storedStatus := &portainer.S3BackupStatus{Failed: true, Timestamp: time.Now().Add(-time.Hour)}
scheduler := newScheduler(storedStatus, &portainer.S3BackupSettings{})
defer scheduler.Stop()
scheduler.Update(*settings("*/10 * * * *", "id", "key", "region", "bucket"))
assert.Equal(t, portainer.S3BackupStatus{}, *storedStatus, "stasus should be dropped")
}
func Test_update_shouldUpdateSettings(t *testing.T) {
storedSettings := &portainer.S3BackupSettings{}
scheduler := newScheduler(&portainer.S3BackupStatus{}, storedSettings)
defer scheduler.Stop()
newSettings := settings("", "id2", "key2", "region2", "bucket2")
scheduler.Update(*newSettings)
assert.EqualValues(t, *storedSettings, *newSettings, "updated settings should match stored settings")
}
func Test_updateWithCron_shouldStartAJob(t *testing.T) {
scheduler := newScheduler(&portainer.S3BackupStatus{}, &portainer.S3BackupSettings{})
defer scheduler.Stop()
jobs := scheduler.cronmanager.Entries()
assert.Len(t, jobs, 0, "should have empty job list upon startup")
scheduler.Update(*settings("*/10 * * * *", "id", "key", "region", "bucket"))
jobs = scheduler.cronmanager.Entries()
assert.Len(t, jobs, 1, "should have 1 active job")
}
func Test_updateWithoutCron_shouldStopActiveJob(t *testing.T) {
scheduler := newScheduler(&portainer.S3BackupStatus{}, &portainer.S3BackupSettings{})
defer scheduler.Stop()
scheduler.Update(*settings("*/10 * * * *", "id", "key", "region", "bucket"))
jobs := scheduler.cronmanager.Entries()
assert.Len(t, jobs, 1, "should have 1 active job")
scheduler.Update(*settings("", "id2", "key2", "region2", "bucket2"))
jobs = scheduler.cronmanager.Entries()
assert.Len(t, jobs, 0, "should have no active jobs")
}
func Test_updateWithACron_shouldStopActiveJob_andStartNewJob(t *testing.T) {
scheduler := newScheduler(&portainer.S3BackupStatus{}, &portainer.S3BackupSettings{})
defer scheduler.Stop()
scheduler.Update(*settings("*/10 * * * *", "id", "key", "region", "bucket"))
jobs := scheduler.cronmanager.Entries()
assert.Len(t, jobs, 1, "should have 1 active job")
initJobId := jobs[0].ID
scheduler.Update(*settings("*/10 * * * *", "id", "key", "region", "bucket"))
jobs = scheduler.cronmanager.Entries()
assert.Len(t, jobs, 1, "should have 1 active job")
assert.NotEqual(t, initJobId, jobs[0].ID, "new job should have a diffent id")
}

68
api/backup/copy.go Normal file
View File

@@ -0,0 +1,68 @@
package backup
import (
"errors"
"io"
"os"
"path/filepath"
"strings"
)
func copyPath(path string, toDir string) error {
info, err := os.Stat(path)
if err != nil && errors.Is(err, os.ErrNotExist) {
// skip copy if file does not exist
return nil
}
if !info.IsDir() {
destination := filepath.Join(toDir, info.Name())
return copyFile(path, destination)
}
return copyDir(path, toDir)
}
func copyDir(fromDir, toDir string) error {
cleanedSourcePath := filepath.Clean(fromDir)
parentDirectory := filepath.Dir(cleanedSourcePath)
err := filepath.Walk(cleanedSourcePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
destination := filepath.Join(toDir, strings.TrimPrefix(path, parentDirectory))
if info.IsDir() {
return nil // skip directory creations
}
if info.Mode()&os.ModeSymlink != 0 { // entry is a symlink
return nil // don't copy symlinks
}
return copyFile(path, destination)
})
return err
}
// copies regular a file from src to dst
func copyFile(src, dst string) error {
from, err := os.Open(src)
if err != nil {
return err
}
defer from.Close()
// has to include 'execute' bit, otherwise fails. MkdirAll follows `mkdir -m` restrictions
if err := os.MkdirAll(filepath.Dir(dst), 0744); err != nil {
return err
}
to, err := os.Create(dst)
if err != nil {
return err
}
defer to.Close()
_, err = io.Copy(to, from)
return err
}

104
api/backup/copy_test.go Normal file
View File

@@ -0,0 +1,104 @@
package backup
import (
"io/ioutil"
"os"
"path"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func listFiles(dir string) []string {
items := make([]string, 0)
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if path == dir {
return nil
}
items = append(items, path)
return nil
})
return items
}
func contains(t *testing.T, list []string, path string) {
assert.Contains(t, list, path)
copyContent, _ := ioutil.ReadFile(path)
assert.Equal(t, "content\n", string(copyContent))
}
func Test_copyFile_returnsError_whenSourceDoesNotExist(t *testing.T) {
tmpdir, _ := ioutil.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
err := copyFile("does-not-exist", tmpdir)
assert.NotNil(t, err)
}
func Test_copyFile_shouldMakeAbackup(t *testing.T) {
tmpdir, _ := ioutil.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
content := []byte("content")
ioutil.WriteFile(path.Join(tmpdir, "origin"), content, 0600)
err := copyFile(path.Join(tmpdir, "origin"), path.Join(tmpdir, "copy"))
assert.Nil(t, err)
copyContent, _ := ioutil.ReadFile(path.Join(tmpdir, "copy"))
assert.Equal(t, content, copyContent)
}
func Test_copyDir_shouldCopyAllFilesAndDirectories(t *testing.T) {
destination, _ := ioutil.TempDir("", "destination")
defer os.RemoveAll(destination)
err := copyDir("./test_assets/copy_test", destination)
assert.Nil(t, err)
createdFiles := listFiles(destination)
contains(t, createdFiles, filepath.Join(destination, "copy_test", "outer"))
contains(t, createdFiles, filepath.Join(destination, "copy_test", "dir", ".dotfile"))
contains(t, createdFiles, filepath.Join(destination, "copy_test", "dir", "inner"))
}
func Test_backupPath_shouldSkipWhenNotExist(t *testing.T) {
tmpdir, _ := ioutil.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
err := copyPath("does-not-exists", tmpdir)
assert.Nil(t, err)
assert.Empty(t, listFiles(tmpdir))
}
func Test_backupPath_shouldCopyFile(t *testing.T) {
tmpdir, _ := ioutil.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
content := []byte("content")
ioutil.WriteFile(path.Join(tmpdir, "file"), content, 0600)
os.MkdirAll(path.Join(tmpdir, "backup"), 0700)
err := copyPath(path.Join(tmpdir, "file"), path.Join(tmpdir, "backup"))
assert.Nil(t, err)
copyContent, err := ioutil.ReadFile(path.Join(tmpdir, "backup", "file"))
assert.Nil(t, err)
assert.Equal(t, content, copyContent)
}
func Test_backupPath_shouldCopyDir(t *testing.T) {
destination, _ := ioutil.TempDir("", "destination")
defer os.RemoveAll(destination)
err := copyPath("./test_assets/copy_test", destination)
assert.Nil(t, err)
createdFiles := listFiles(destination)
contains(t, createdFiles, filepath.Join(destination, "copy_test", "outer"))
contains(t, createdFiles, filepath.Join(destination, "copy_test", "dir", ".dotfile"))
contains(t, createdFiles, filepath.Join(destination, "copy_test", "dir", "inner"))
}

68
api/backup/restore.go Normal file
View File

@@ -0,0 +1,68 @@
package backup
import (
"context"
"io"
"os"
"path/filepath"
"time"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/archive"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/offlinegate"
)
var filesToRestore = append(filesToBackup, "portainer.db")
// Restores system state from backup archive, will trigger system shutdown, when finished.
func RestoreArchive(archive io.Reader, password string, filestorePath string, gate *offlinegate.OfflineGate, datastore portainer.DataStore, shutdownTrigger context.CancelFunc) error {
var err error
if password != "" {
archive, err = decrypt(archive, password)
if err != nil {
return errors.Wrap(err, "failed to decrypt the archive")
}
}
restorePath := filepath.Join(filestorePath, "restore", time.Now().Format("20060102150405"))
defer os.RemoveAll(filepath.Dir(restorePath))
err = extractArchive(archive, restorePath)
if err != nil {
return errors.Wrap(err, "cannot extract files from the archive. Please ensure the password is correct and try again")
}
unlock := gate.Lock()
defer unlock()
if err = datastore.Close(); err != nil {
return errors.Wrap(err, "Failed to stop db")
}
if err = restoreFiles(restorePath, filestorePath); err != nil {
return errors.Wrap(err, "failed to restore the system state")
}
shutdownTrigger()
return nil
}
func decrypt(r io.Reader, password string) (io.Reader, error) {
return crypto.AesDecrypt(r, []byte(password))
}
func extractArchive(r io.Reader, destinationDirPath string) error {
return archive.ExtractTarGz(r, destinationDirPath)
}
func restoreFiles(srcDir string, destinationDir string) error {
for _, filename := range filesToRestore {
err := copyPath(filepath.Join(srcDir, filename), destinationDir)
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1 @@
content

View File

@@ -0,0 +1 @@
content

View File

@@ -0,0 +1 @@
content

181
api/bolt/backup.go Normal file
View File

@@ -0,0 +1,181 @@
package bolt
import (
"fmt"
"io/ioutil"
"os"
"path"
"time"
portainer "github.com/portainer/portainer/api"
plog "github.com/portainer/portainer/api/bolt/log"
)
var backupDefaults = struct {
backupDir string
editions []string
databaseFileName string
}{
"backups",
[]string{"CE", "BE", "EE"},
databaseFileName,
}
var backupLog = plog.NewScopedLog("bolt, backup")
//
// Backup Helpers
//
// createBackupFolders create initial folders for backups
func (store *Store) createBackupFolders() {
for _, e := range backupDefaults.editions {
p := path.Join(store.path, backupDefaults.backupDir, e)
if exists, _ := store.fileService.FileExists(p); !exists {
err := os.MkdirAll(p, 0700)
if err != nil {
backupLog.Error("Error while creating backup folders", err)
}
}
}
}
func (store *Store) databasePath() string {
return path.Join(store.path, databaseFileName)
}
func (store *Store) editionBackupDir(edition portainer.SoftwareEdition) string {
return path.Join(store.path, backupDefaults.backupDir, edition.GetEditionLabel())
}
func (store *Store) copyDBFile(from string, to string) error {
backupLog.Info(fmt.Sprintf("Copying db file from %s to %s", from, to))
err := store.fileService.Copy(from, to, true)
if err != nil {
backupLog.Error("Failed", err)
}
return err
}
// BackupOptions provide a helper to inject backup options
type BackupOptions struct {
Edition portainer.SoftwareEdition
Version int
BackupDir string
BackupFileName string
BackupPath string
}
func (store *Store) setupOptions(options *BackupOptions) *BackupOptions {
if options == nil {
options = &BackupOptions{}
}
if options.Edition == 0 {
options.Edition = store.edition()
}
if options.Version == 0 {
options.Version, _ = store.version()
}
if options.BackupDir == "" {
options.BackupDir = store.editionBackupDir(options.Edition)
}
if options.BackupFileName == "" {
options.BackupFileName = fmt.Sprintf("%s.%s.%s", backupDefaults.databaseFileName, fmt.Sprintf("%03d", options.Version), time.Now().Format("20060102150405"))
}
if options.BackupPath == "" {
options.BackupPath = path.Join(options.BackupDir, options.BackupFileName)
}
return options
}
func (store *Store) listEditionBackups(edition portainer.SoftwareEdition) ([]string, error) {
var fileNames = []string{}
files, err := ioutil.ReadDir(store.editionBackupDir(edition))
if err != nil {
backupLog.Error("Error while retrieving backup files", err)
return fileNames, err
}
for _, f := range files {
fileNames = append(fileNames, f.Name())
}
return fileNames, nil
}
func (store *Store) lastestEditionBackup() (string, error) {
edition := store.edition()
files, err := store.listEditionBackups(edition)
if err != nil {
backupLog.Error("Error while retrieving backup files", err)
return "", err
}
if len(files) == 0 {
return "", nil
}
return files[len(files)-1], nil
}
// BackupWithOptions backup current database with options
func (store *Store) BackupWithOptions(options *BackupOptions) (string, error) {
backupLog.Info("creating db backup")
store.createBackupFolders()
options = store.setupOptions(options)
return options.BackupPath, store.copyDBFile(store.databasePath(), options.BackupPath)
}
// Backup current database with default options
func (store *Store) Backup() (string, error) {
return store.BackupWithOptions(nil)
}
// RestoreWithOptions previously saved backup for the current Edition with options
// Restore strategies:
// - default: restore latest from current edition
// - restore a specific
func (store *Store) RestoreWithOptions(options *BackupOptions) error {
// Check if backup file exist before restoring
options = store.setupOptions(options)
_, err := os.Stat(options.BackupPath)
if os.IsNotExist(err) {
backupLog.Error(fmt.Sprintf("Backup file to restore does not exist %s", options.BackupPath), err)
return err
}
err = store.Close()
if err != nil {
backupLog.Error("Error while closing store before restore", err)
return err
}
backupLog.Info("Restoring db backup")
err = store.copyDBFile(options.BackupPath, store.databasePath())
if err != nil {
return err
}
return store.Open()
}
// Restore previously saved backup for the current Edition with default options
func (store *Store) Restore() error {
var options = &BackupOptions{}
var err error
options.BackupFileName, err = store.lastestEditionBackup()
if err != nil {
return err
}
return store.RestoreWithOptions(options)
}

118
api/bolt/backup_test.go Normal file
View File

@@ -0,0 +1,118 @@
package bolt
import (
"fmt"
"log"
"testing"
portainer "github.com/portainer/portainer/api"
)
func TestCreateBackupFolders(t *testing.T) {
store := NewTestStore(portainer.PortainerEE, portainer.DBVersionEE, false)
if exists, _ := store.fileService.FileExists("tmp/backups"); exists {
t.Error("Expect backups folder to not exist")
}
store.createBackupFolders()
if exists, _ := store.fileService.FileExists("tmp/backups"); !exists {
t.Error("Expect backups folder to exist")
}
store.createBackupFolders()
store.Close()
teardown()
}
func TestStoreCreation(t *testing.T) {
store := NewTestStore(portainer.PortainerEE, portainer.DBVersionEE, false)
if store == nil {
t.Error("Expect to create a store")
}
if store.edition() != portainer.PortainerEE {
t.Error("Expect to get EE Edition")
}
version, err := store.version()
if err != nil {
log.Fatal(err)
}
if version != portainer.DBVersionEE {
t.Error("Expect to get EE DBVersion")
}
store.Close()
teardown()
}
func TestBackup(t *testing.T) {
tests := []struct {
edition portainer.SoftwareEdition
version int
}{
{edition: portainer.PortainerCE, version: portainer.DBVersion},
{edition: portainer.PortainerEE, version: portainer.DBVersionEE},
}
for _, tc := range tests {
backupFileName := fmt.Sprintf("tmp/backups/%s/portainer.db.%03d.*", tc.edition.GetEditionLabel(), tc.version)
t.Run(fmt.Sprintf("Backup should create %s", backupFileName), func(t *testing.T) {
store := NewTestStore(tc.edition, tc.version, false)
store.Backup()
if !isFileExist(backupFileName) {
t.Errorf("Expect backup file to be created %s", backupFileName)
}
store.Close()
})
}
t.Run("BackupWithOption should create a name specific backup", func(t *testing.T) {
edition := portainer.PortainerCE
version := portainer.DBVersion
store := NewTestStore(edition, version, false)
store.BackupWithOptions(&BackupOptions{
BackupFileName: beforePortainerUpgradeToEEBackup,
Edition: portainer.PortainerCE,
})
backupFileName := fmt.Sprintf("tmp/backups/%s/%s", edition.GetEditionLabel(), beforePortainerUpgradeToEEBackup)
if !isFileExist(backupFileName) {
t.Errorf("Expect backup file to be created %s", backupFileName)
}
store.Close()
})
teardown()
}
// TODO restore / backup failed test cases
func TestRestore(t *testing.T) {
editions := []portainer.SoftwareEdition{portainer.PortainerCE, portainer.PortainerEE}
var currentVersion = 0
for i, e := range editions {
editionLabel := e.GetEditionLabel()
currentVersion = 10 ^ i + 1
store := NewTestStore(e, currentVersion, false)
t.Run(fmt.Sprintf("Basic Restore for %s", editionLabel), func(t *testing.T) {
store.Backup()
updateVersion(store, currentVersion+1)
testVersion(store, currentVersion+1, t)
store.Restore()
testVersion(store, currentVersion, t)
})
t.Run(fmt.Sprintf("Basic Restore After Multiple Backup for %s", editionLabel), func(t *testing.T) {
currentVersion = currentVersion + 5
updateVersion(store, currentVersion)
store.Backup()
updateVersion(store, currentVersion+2)
testVersion(store, currentVersion+2, t)
store.Restore()
testVersion(store, currentVersion, t)
})
store.Close()
}
teardown()
}

View File

@@ -0,0 +1,73 @@
package bolttest
import (
"io/ioutil"
"log"
"os"
"github.com/pkg/errors"
"github.com/portainer/portainer/api/bolt"
"github.com/portainer/portainer/api/filesystem"
)
var errTempDir = errors.New("can't create a temp dir")
func MustNewTestStore(init bool) (*bolt.Store, func()) {
store, teardown, err := NewTestStore(init)
if err != nil {
if !errors.Is(err, errTempDir) {
teardown()
}
log.Fatal(err)
}
return store, teardown
}
func NewTestStore(init bool) (*bolt.Store, func(), error) {
// Creates unique temp directory in a concurrency friendly manner.
dataStorePath, err := ioutil.TempDir("", "boltdb")
if err != nil {
return nil, nil, errors.Wrap(errTempDir, err.Error())
}
fileService, err := filesystem.NewService(dataStorePath, "")
if err != nil {
return nil, nil, err
}
store, err := bolt.NewStore(dataStorePath, fileService)
if err != nil {
return nil, nil, err
}
err = store.Open()
if err != nil {
return nil, nil, err
}
if init {
err = store.Init()
if err != nil {
return nil, nil, err
}
}
teardown := func() {
teardown(store, dataStorePath)
}
return store, teardown, nil
}
func teardown(store *bolt.Store, dataStorePath string) {
err := store.Close()
if err != nil {
log.Fatalln(err)
}
err = os.RemoveAll(dataStorePath)
if err != nil {
log.Fatalln(err)
}
}

View File

@@ -2,7 +2,7 @@ package customtemplate
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
@@ -13,18 +13,18 @@ const (
// Service represents a service for managing custom template data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -32,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) CustomTemplates() ([]portainer.CustomTemplate, error) {
var customTemplates = make([]portainer.CustomTemplate, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -56,7 +56,7 @@ func (service *Service) CustomTemplate(ID portainer.CustomTemplateID) (*portaine
var customTemplate portainer.CustomTemplate
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &customTemplate)
err := internal.GetObject(service.connection, BucketName, identifier, &customTemplate)
if err != nil {
return nil, err
}
@@ -67,18 +67,18 @@ func (service *Service) CustomTemplate(ID portainer.CustomTemplateID) (*portaine
// UpdateCustomTemplate updates an custom template.
func (service *Service) UpdateCustomTemplate(ID portainer.CustomTemplateID, customTemplate *portainer.CustomTemplate) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, customTemplate)
return internal.UpdateObject(service.connection, BucketName, identifier, customTemplate)
}
// DeleteCustomTemplate deletes an custom template.
func (service *Service) DeleteCustomTemplate(ID portainer.CustomTemplateID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}
// CreateCustomTemplate assign an ID to a new custom template and saves it.
func (service *Service) CreateCustomTemplate(customTemplate *portainer.CustomTemplate) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data, err := internal.MarshalObject(customTemplate)
@@ -92,5 +92,5 @@ func (service *Service) CreateCustomTemplate(customTemplate *portainer.CustomTem
// GetNextIdentifier returns the next identifier for a custom template.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
return internal.GetNextIdentifier(service.connection, BucketName)
}

View File

@@ -1,12 +1,17 @@
package bolt
import (
"log"
"io"
"path"
"time"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/portainer/portainer/api/bolt/license"
"github.com/portainer/portainer/api/bolt/s3backup"
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/customtemplate"
"github.com/portainer/portainer/api/bolt/dockerhub"
"github.com/portainer/portainer/api/bolt/edgegroup"
@@ -15,9 +20,7 @@ import (
"github.com/portainer/portainer/api/bolt/endpoint"
"github.com/portainer/portainer/api/bolt/endpointgroup"
"github.com/portainer/portainer/api/bolt/endpointrelation"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/extension"
"github.com/portainer/portainer/api/bolt/migrator"
"github.com/portainer/portainer/api/bolt/registry"
"github.com/portainer/portainer/api/bolt/resourcecontrol"
"github.com/portainer/portainer/api/bolt/role"
@@ -31,10 +34,9 @@ import (
"github.com/portainer/portainer/api/bolt/user"
"github.com/portainer/portainer/api/bolt/version"
"github.com/portainer/portainer/api/bolt/webhook"
"github.com/portainer/portainer/api/internal/authorization"
)
const (
var (
databaseFileName = "portainer.db"
)
@@ -42,7 +44,7 @@ const (
// BoltDB as the storage system.
type Store struct {
path string
db *bolt.DB
connection *internal.DbConnection
isNew bool
fileService portainer.FileService
CustomTemplateService *customtemplate.Service
@@ -54,9 +56,11 @@ type Store struct {
EndpointService *endpoint.Service
EndpointRelationService *endpointrelation.Service
ExtensionService *extension.Service
LicenseService *license.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
RoleService *role.Service
S3BackupService *s3backup.Service
ScheduleService *schedule.Service
SettingsService *settings.Service
StackService *stack.Service
@@ -69,12 +73,29 @@ type Store struct {
WebhookService *webhook.Service
}
func (store *Store) version() (int, error) {
version, err := store.VersionService.DBVersion()
if err == errors.ErrObjectNotFound {
version = 0
}
return version, err
}
func (store *Store) edition() portainer.SoftwareEdition {
edition, err := store.VersionService.Edition()
if err == errors.ErrObjectNotFound {
edition = portainer.PortainerCE
}
return edition
}
// NewStore initializes a new Store and the associated services
func NewStore(storePath string, fileService portainer.FileService) (*Store, error) {
store := &Store{
path: storePath,
fileService: fileService,
isNew: true,
connection: &internal.DbConnection{},
}
databasePath := path.Join(storePath, databaseFileName)
@@ -97,15 +118,15 @@ func (store *Store) Open() error {
if err != nil {
return err
}
store.db = db
store.connection.DB = db
return store.initServices()
}
// Close closes the BoltDB database.
func (store *Store) Close() error {
if store.db != nil {
return store.db.Close()
if store.connection.DB != nil {
return store.connection.Close()
}
return nil
}
@@ -116,286 +137,11 @@ func (store *Store) IsNew() bool {
return store.isNew
}
// MigrateData automatically migrate the data based on the DBVersion.
// This process is only triggered on an existing database, not if the database was just created.
func (store *Store) MigrateData() error {
if store.isNew {
return store.VersionService.StoreDBVersion(portainer.DBVersion)
}
version, err := store.VersionService.DBVersion()
if err == errors.ErrObjectNotFound {
version = 0
} else if err != nil {
// BackupTo backs up db to a provided writer.
// It does hot backup and doesn't block other database reads and writes
func (store *Store) BackupTo(w io.Writer) error {
return store.connection.View(func(tx *bolt.Tx) error {
_, err := tx.WriteTo(w)
return err
}
if version < portainer.DBVersion {
migratorParams := &migrator.Parameters{
DB: store.db,
DatabaseVersion: version,
EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService,
EndpointRelationService: store.EndpointRelationService,
ExtensionService: store.ExtensionService,
RegistryService: store.RegistryService,
ResourceControlService: store.ResourceControlService,
RoleService: store.RoleService,
ScheduleService: store.ScheduleService,
SettingsService: store.SettingsService,
StackService: store.StackService,
TagService: store.TagService,
TeamMembershipService: store.TeamMembershipService,
UserService: store.UserService,
VersionService: store.VersionService,
FileService: store.fileService,
AuthorizationService: authorization.NewService(store),
}
migrator := migrator.NewMigrator(migratorParams)
log.Printf("Migrating database from version %v to %v.\n", version, portainer.DBVersion)
err = migrator.Migrate()
if err != nil {
log.Printf("An error occurred during database migration: %s\n", err)
return err
}
}
return nil
}
func (store *Store) initServices() error {
authorizationsetService, err := role.NewService(store.db)
if err != nil {
return err
}
store.RoleService = authorizationsetService
customTemplateService, err := customtemplate.NewService(store.db)
if err != nil {
return err
}
store.CustomTemplateService = customTemplateService
dockerhubService, err := dockerhub.NewService(store.db)
if err != nil {
return err
}
store.DockerHubService = dockerhubService
edgeStackService, err := edgestack.NewService(store.db)
if err != nil {
return err
}
store.EdgeStackService = edgeStackService
edgeGroupService, err := edgegroup.NewService(store.db)
if err != nil {
return err
}
store.EdgeGroupService = edgeGroupService
edgeJobService, err := edgejob.NewService(store.db)
if err != nil {
return err
}
store.EdgeJobService = edgeJobService
endpointgroupService, err := endpointgroup.NewService(store.db)
if err != nil {
return err
}
store.EndpointGroupService = endpointgroupService
endpointService, err := endpoint.NewService(store.db)
if err != nil {
return err
}
store.EndpointService = endpointService
endpointRelationService, err := endpointrelation.NewService(store.db)
if err != nil {
return err
}
store.EndpointRelationService = endpointRelationService
extensionService, err := extension.NewService(store.db)
if err != nil {
return err
}
store.ExtensionService = extensionService
registryService, err := registry.NewService(store.db)
if err != nil {
return err
}
store.RegistryService = registryService
resourcecontrolService, err := resourcecontrol.NewService(store.db)
if err != nil {
return err
}
store.ResourceControlService = resourcecontrolService
settingsService, err := settings.NewService(store.db)
if err != nil {
return err
}
store.SettingsService = settingsService
stackService, err := stack.NewService(store.db)
if err != nil {
return err
}
store.StackService = stackService
tagService, err := tag.NewService(store.db)
if err != nil {
return err
}
store.TagService = tagService
teammembershipService, err := teammembership.NewService(store.db)
if err != nil {
return err
}
store.TeamMembershipService = teammembershipService
teamService, err := team.NewService(store.db)
if err != nil {
return err
}
store.TeamService = teamService
tunnelServerService, err := tunnelserver.NewService(store.db)
if err != nil {
return err
}
store.TunnelServerService = tunnelServerService
userService, err := user.NewService(store.db)
if err != nil {
return err
}
store.UserService = userService
versionService, err := version.NewService(store.db)
if err != nil {
return err
}
store.VersionService = versionService
webhookService, err := webhook.NewService(store.db)
if err != nil {
return err
}
store.WebhookService = webhookService
scheduleService, err := schedule.NewService(store.db)
if err != nil {
return err
}
store.ScheduleService = scheduleService
return nil
}
// CustomTemplate gives access to the CustomTemplate data management layer
func (store *Store) CustomTemplate() portainer.CustomTemplateService {
return store.CustomTemplateService
}
// DockerHub gives access to the DockerHub data management layer
func (store *Store) DockerHub() portainer.DockerHubService {
return store.DockerHubService
}
// EdgeGroup gives access to the EdgeGroup data management layer
func (store *Store) EdgeGroup() portainer.EdgeGroupService {
return store.EdgeGroupService
}
// EdgeJob gives access to the EdgeJob data management layer
func (store *Store) EdgeJob() portainer.EdgeJobService {
return store.EdgeJobService
}
// EdgeStack gives access to the EdgeStack data management layer
func (store *Store) EdgeStack() portainer.EdgeStackService {
return store.EdgeStackService
}
// Endpoint gives access to the Endpoint data management layer
func (store *Store) Endpoint() portainer.EndpointService {
return store.EndpointService
}
// EndpointGroup gives access to the EndpointGroup data management layer
func (store *Store) EndpointGroup() portainer.EndpointGroupService {
return store.EndpointGroupService
}
// EndpointRelation gives access to the EndpointRelation data management layer
func (store *Store) EndpointRelation() portainer.EndpointRelationService {
return store.EndpointRelationService
}
// Registry gives access to the Registry data management layer
func (store *Store) Registry() portainer.RegistryService {
return store.RegistryService
}
// ResourceControl gives access to the ResourceControl data management layer
func (store *Store) ResourceControl() portainer.ResourceControlService {
return store.ResourceControlService
}
// Role gives access to the Role data management layer
func (store *Store) Role() portainer.RoleService {
return store.RoleService
}
// Settings gives access to the Settings data management layer
func (store *Store) Settings() portainer.SettingsService {
return store.SettingsService
}
// Stack gives access to the Stack data management layer
func (store *Store) Stack() portainer.StackService {
return store.StackService
}
// Tag gives access to the Tag data management layer
func (store *Store) Tag() portainer.TagService {
return store.TagService
}
// TeamMembership gives access to the TeamMembership data management layer
func (store *Store) TeamMembership() portainer.TeamMembershipService {
return store.TeamMembershipService
}
// Team gives access to the Team data management layer
func (store *Store) Team() portainer.TeamService {
return store.TeamService
}
// TunnelServer gives access to the TunnelServer data management layer
func (store *Store) TunnelServer() portainer.TunnelServerService {
return store.TunnelServerService
}
// User gives access to the User data management layer
func (store *Store) User() portainer.UserService {
return store.UserService
}
// Version gives access to the Version data management layer
func (store *Store) Version() portainer.VersionService {
return store.VersionService
}
// Webhook gives access to the Webhook data management layer
func (store *Store) Webhook() portainer.WebhookService {
return store.WebhookService
})
}

View File

@@ -1,10 +1,8 @@
package dockerhub
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
)
const (
@@ -15,18 +13,18 @@ const (
// Service represents a service for managing Dockerhub data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -34,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) DockerHub() (*portainer.DockerHub, error) {
var dockerhub portainer.DockerHub
err := internal.GetObject(service.db, BucketName, []byte(dockerHubKey), &dockerhub)
err := internal.GetObject(service.connection, BucketName, []byte(dockerHubKey), &dockerhub)
if err != nil {
return nil, err
}
@@ -44,5 +42,5 @@ func (service *Service) DockerHub() (*portainer.DockerHub, error) {
// UpdateDockerHub updates a DockerHub object.
func (service *Service) UpdateDockerHub(dockerhub *portainer.DockerHub) error {
return internal.UpdateObject(service.db, BucketName, []byte(dockerHubKey), dockerhub)
return internal.UpdateObject(service.connection, BucketName, []byte(dockerHubKey), dockerhub)
}

View File

@@ -2,7 +2,7 @@ package edgegroup
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
@@ -13,18 +13,18 @@ const (
// Service represents a service for managing Edge group data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -32,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) EdgeGroups() ([]portainer.EdgeGroup, error) {
var groups = make([]portainer.EdgeGroup, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -56,7 +56,7 @@ func (service *Service) EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGrou
var group portainer.EdgeGroup
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &group)
err := internal.GetObject(service.connection, BucketName, identifier, &group)
if err != nil {
return nil, err
}
@@ -67,18 +67,18 @@ func (service *Service) EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGrou
// UpdateEdgeGroup updates an Edge group.
func (service *Service) UpdateEdgeGroup(ID portainer.EdgeGroupID, group *portainer.EdgeGroup) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, group)
return internal.UpdateObject(service.connection, BucketName, identifier, group)
}
// DeleteEdgeGroup deletes an Edge group.
func (service *Service) DeleteEdgeGroup(ID portainer.EdgeGroupID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}
// CreateEdgeGroup assign an ID to a new Edge group and saves it.
func (service *Service) CreateEdgeGroup(group *portainer.EdgeGroup) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()

View File

@@ -2,7 +2,7 @@ package edgejob
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
@@ -13,18 +13,18 @@ const (
// Service represents a service for managing edge jobs data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -32,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) EdgeJobs() ([]portainer.EdgeJob, error) {
var edgeJobs = make([]portainer.EdgeJob, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -56,7 +56,7 @@ func (service *Service) EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, err
var edgeJob portainer.EdgeJob
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &edgeJob)
err := internal.GetObject(service.connection, BucketName, identifier, &edgeJob)
if err != nil {
return nil, err
}
@@ -66,7 +66,7 @@ func (service *Service) EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, err
// CreateEdgeJob creates a new Edge job
func (service *Service) CreateEdgeJob(edgeJob *portainer.EdgeJob) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
if edgeJob.ID == 0 {
@@ -86,16 +86,16 @@ func (service *Service) CreateEdgeJob(edgeJob *portainer.EdgeJob) error {
// UpdateEdgeJob updates an Edge job by ID
func (service *Service) UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, edgeJob)
return internal.UpdateObject(service.connection, BucketName, identifier, edgeJob)
}
// DeleteEdgeJob deletes an Edge job
func (service *Service) DeleteEdgeJob(ID portainer.EdgeJobID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}
// GetNextIdentifier returns the next identifier for an endpoint.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
return internal.GetNextIdentifier(service.connection, BucketName)
}

View File

@@ -2,7 +2,7 @@ package edgestack
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
@@ -13,18 +13,18 @@ const (
// Service represents a service for managing Edge stack data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -32,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) EdgeStacks() ([]portainer.EdgeStack, error) {
var stacks = make([]portainer.EdgeStack, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -56,7 +56,7 @@ func (service *Service) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStac
var stack portainer.EdgeStack
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &stack)
err := internal.GetObject(service.connection, BucketName, identifier, &stack)
if err != nil {
return nil, err
}
@@ -66,7 +66,7 @@ func (service *Service) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStac
// CreateEdgeStack assign an ID to a new Edge stack and saves it.
func (service *Service) CreateEdgeStack(edgeStack *portainer.EdgeStack) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
if edgeStack.ID == 0 {
@@ -86,16 +86,16 @@ func (service *Service) CreateEdgeStack(edgeStack *portainer.EdgeStack) error {
// UpdateEdgeStack updates an Edge stack.
func (service *Service) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, edgeStack)
return internal.UpdateObject(service.connection, BucketName, identifier, edgeStack)
}
// DeleteEdgeStack deletes an Edge stack.
func (service *Service) DeleteEdgeStack(ID portainer.EdgeStackID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}
// GetNextIdentifier returns the next identifier for an endpoint.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
return internal.GetNextIdentifier(service.connection, BucketName)
}

View File

@@ -2,7 +2,7 @@ package endpoint
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
@@ -13,18 +13,18 @@ const (
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -33,7 +33,7 @@ func (service *Service) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint,
var endpoint portainer.Endpoint
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &endpoint)
err := internal.GetObject(service.connection, BucketName, identifier, &endpoint)
if err != nil {
return nil, err
}
@@ -44,20 +44,20 @@ func (service *Service) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint,
// UpdateEndpoint updates an endpoint.
func (service *Service) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, endpoint)
return internal.UpdateObject(service.connection, BucketName, identifier, endpoint)
}
// DeleteEndpoint deletes an endpoint.
func (service *Service) DeleteEndpoint(ID portainer.EndpointID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}
// Endpoints return an array containing all the endpoints.
func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
var endpoints = make([]portainer.Endpoint, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -78,7 +78,7 @@ func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
// CreateEndpoint assign an ID to a new endpoint and saves it.
func (service *Service) CreateEndpoint(endpoint *portainer.Endpoint) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
// We manually manage sequences for endpoints
@@ -98,12 +98,12 @@ func (service *Service) CreateEndpoint(endpoint *portainer.Endpoint) error {
// GetNextIdentifier returns the next identifier for an endpoint.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
return internal.GetNextIdentifier(service.connection, BucketName)
}
// Synchronize creates, updates and deletes endpoints inside a single transaction.
func (service *Service) Synchronize(toCreate, toUpdate, toDelete []*portainer.Endpoint) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
for _, endpoint := range toCreate {

View File

@@ -1,7 +1,7 @@
package endpointgroup
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
@@ -14,18 +14,18 @@ const (
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -34,7 +34,7 @@ func (service *Service) EndpointGroup(ID portainer.EndpointGroupID) (*portainer.
var endpointGroup portainer.EndpointGroup
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &endpointGroup)
err := internal.GetObject(service.connection, BucketName, identifier, &endpointGroup)
if err != nil {
return nil, err
}
@@ -45,20 +45,20 @@ func (service *Service) EndpointGroup(ID portainer.EndpointGroupID) (*portainer.
// UpdateEndpointGroup updates an endpoint group.
func (service *Service) UpdateEndpointGroup(ID portainer.EndpointGroupID, endpointGroup *portainer.EndpointGroup) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, endpointGroup)
return internal.UpdateObject(service.connection, BucketName, identifier, endpointGroup)
}
// DeleteEndpointGroup deletes an endpoint group.
func (service *Service) DeleteEndpointGroup(ID portainer.EndpointGroupID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}
// EndpointGroups return an array containing all the endpoint groups.
func (service *Service) EndpointGroups() ([]portainer.EndpointGroup, error) {
var endpointGroups = make([]portainer.EndpointGroup, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -79,7 +79,7 @@ func (service *Service) EndpointGroups() ([]portainer.EndpointGroup, error) {
// CreateEndpointGroup assign an ID to a new endpoint group and saves it.
func (service *Service) CreateEndpointGroup(endpointGroup *portainer.EndpointGroup) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()

View File

@@ -13,18 +13,18 @@ const (
// Service represents a service for managing endpoint relation data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -33,7 +33,7 @@ func (service *Service) EndpointRelation(endpointID portainer.EndpointID) (*port
var endpointRelation portainer.EndpointRelation
identifier := internal.Itob(int(endpointID))
err := internal.GetObject(service.db, BucketName, identifier, &endpointRelation)
err := internal.GetObject(service.connection, BucketName, identifier, &endpointRelation)
if err != nil {
return nil, err
}
@@ -43,7 +43,7 @@ func (service *Service) EndpointRelation(endpointID portainer.EndpointID) (*port
// CreateEndpointRelation saves endpointRelation
func (service *Service) CreateEndpointRelation(endpointRelation *portainer.EndpointRelation) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data, err := internal.MarshalObject(endpointRelation)
@@ -58,11 +58,11 @@ func (service *Service) CreateEndpointRelation(endpointRelation *portainer.Endpo
// UpdateEndpointRelation updates an Endpoint relation object
func (service *Service) UpdateEndpointRelation(EndpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error {
identifier := internal.Itob(int(EndpointID))
return internal.UpdateObject(service.db, BucketName, identifier, endpointRelation)
return internal.UpdateObject(service.connection, BucketName, identifier, endpointRelation)
}
// DeleteEndpointRelation deletes an Endpoint relation object
func (service *Service) DeleteEndpointRelation(EndpointID portainer.EndpointID) error {
identifier := internal.Itob(int(EndpointID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}

View File

@@ -4,4 +4,5 @@ import "errors"
var (
ErrObjectNotFound = errors.New("Object not found inside the database")
ErrMigrationToCE = errors.New("DB is already on CE edition")
)

View File

@@ -1,7 +1,7 @@
package extension
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
@@ -14,18 +14,18 @@ const (
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -34,7 +34,7 @@ func (service *Service) Extension(ID portainer.ExtensionID) (*portainer.Extensio
var extension portainer.Extension
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &extension)
err := internal.GetObject(service.connection, BucketName, identifier, &extension)
if err != nil {
return nil, err
}
@@ -46,7 +46,7 @@ func (service *Service) Extension(ID portainer.ExtensionID) (*portainer.Extensio
func (service *Service) Extensions() ([]portainer.Extension, error) {
var extensions = make([]portainer.Extension, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -67,7 +67,7 @@ func (service *Service) Extensions() ([]portainer.Extension, error) {
// Persist persists a extension inside the database.
func (service *Service) Persist(extension *portainer.Extension) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data, err := internal.MarshalObject(extension)
@@ -82,5 +82,5 @@ func (service *Service) Persist(extension *portainer.Extension) error {
// DeleteExtension deletes a Extension.
func (service *Service) DeleteExtension(ID portainer.ExtensionID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}

115
api/bolt/helpers_test.go Normal file
View File

@@ -0,0 +1,115 @@
package bolt
import (
"fmt"
"log"
"math/rand"
"os"
"path"
"path/filepath"
"testing"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
)
var (
dataStorePath string
testBackupPath string
)
func init() {
rand.Seed(time.Now().UnixNano())
databaseFileName = fmt.Sprintf("portainer-%08d.db", rand.Intn(100000000))
pwd, err := os.Getwd()
if err != nil {
log.Println(err)
}
dataStorePath = path.Join(pwd, "tmp")
testBackupPath = path.Join(dataStorePath, "backups")
teardown()
}
func NewTestStore(edition portainer.SoftwareEdition, version int, init bool) *Store {
fileService, err := filesystem.NewService(dataStorePath, "")
if err != nil {
log.Fatal(err)
}
store, err := NewStore(dataStorePath, fileService)
if err != nil {
log.Fatal(err)
}
err = store.Open()
if err != nil {
log.Fatal(err)
}
if init {
err = store.Init()
if err != nil {
log.Fatal(err)
}
}
err = store.VersionService.StoreEdition(edition)
if err != nil {
log.Fatal(err)
}
err = store.VersionService.StoreDBVersion(version)
if err != nil {
log.Fatal(err)
}
return store
}
func teardown() {
err := os.RemoveAll(testBackupPath)
if err != nil {
log.Fatalln(err)
}
files, err := filepath.Glob(path.Join(dataStorePath, "portainer-*.*"))
if err != nil {
log.Fatalln(err)
}
for _, f := range files {
if err := os.Remove(f); err != nil {
log.Fatalln(err)
}
}
}
func isFileExist(path string) bool {
matches, err := filepath.Glob(path)
if err != nil {
return false
}
return len(matches) > 0
}
func updateVersion(store *Store, v int) {
err := store.VersionService.StoreDBVersion(v)
if err != nil {
log.Fatal(err)
}
}
func testVersion(store *Store, versionWant int, t *testing.T) {
if v, _ := store.version(); v != versionWant {
t.Errorf("Expect store version to be %d but was %d", versionWant, v)
}
}
func testEdition(store *Store, editionWant portainer.SoftwareEdition, t *testing.T) {
if e := store.edition(); e != editionWant {
t.Errorf("Expect store edition to be %s but was %s", editionWant.GetEditionLabel(), e.GetEditionLabel())
}
}

View File

@@ -33,6 +33,7 @@ func (store *Store) Init() error {
AnonymousMode: true,
AutoCreateUsers: true,
TLSConfig: portainer.TLSConfiguration{},
URLs: []string{},
SearchSettings: []portainer.LDAPSearchSettings{
portainer.LDAPSearchSettings{},
},
@@ -40,18 +41,14 @@ func (store *Store) Init() error {
portainer.LDAPGroupSearchSettings{},
},
},
OAuthSettings: portainer.OAuthSettings{},
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowVolumeBrowserForRegularUsers: false,
AllowHostNamespaceForRegularUsers: true,
AllowDeviceMappingForRegularUsers: true,
AllowStackManagementForRegularUsers: true,
AllowContainerCapabilitiesForRegularUsers: true,
EnableHostManagementFeatures: false,
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
TemplatesURL: portainer.DefaultTemplatesURL,
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
OAuthSettings: portainer.OAuthSettings{
TeamMemberships: portainer.TeamMemberships{
OAuthClaimMappings: make([]portainer.OAuthClaimMappings, 0),
},
},
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
TemplatesURL: portainer.DefaultTemplatesURL,
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
}
err = store.SettingsService.UpdateSettings(defaultSettings)
@@ -99,5 +96,17 @@ func (store *Store) Init() error {
}
}
roles, err := store.RoleService.Roles()
if err != nil {
return err
}
if len(roles) == 0 {
err := store.RoleService.CreateOrUpdatePredefinedRoles()
if err != nil {
return err
}
}
return nil
}

View File

@@ -7,6 +7,10 @@ import (
"github.com/portainer/portainer/api/bolt/errors"
)
type DbConnection struct {
*bolt.DB
}
// Itob returns an 8-byte big endian representation of v.
// This function is typically used for encoding integer IDs to byte slices
// so that they can be used as BoltDB keys.
@@ -17,8 +21,8 @@ func Itob(v int) []byte {
}
// CreateBucket is a generic function used to create a bucket inside a bolt database.
func CreateBucket(db *bolt.DB, bucketName string) error {
return db.Update(func(tx *bolt.Tx) error {
func CreateBucket(connection *DbConnection, bucketName string) error {
return connection.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(bucketName))
if err != nil {
return err
@@ -28,10 +32,10 @@ func CreateBucket(db *bolt.DB, bucketName string) error {
}
// GetObject is a generic function used to retrieve an unmarshalled object from a bolt database.
func GetObject(db *bolt.DB, bucketName string, key []byte, object interface{}) error {
func GetObject(connection *DbConnection, bucketName string, key []byte, object interface{}) error {
var data []byte
err := db.View(func(tx *bolt.Tx) error {
err := connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
value := bucket.Get(key)
@@ -52,8 +56,8 @@ func GetObject(db *bolt.DB, bucketName string, key []byte, object interface{}) e
}
// UpdateObject is a generic function used to update an object inside a bolt database.
func UpdateObject(db *bolt.DB, bucketName string, key []byte, object interface{}) error {
return db.Update(func(tx *bolt.Tx) error {
func UpdateObject(connection *DbConnection, bucketName string, key []byte, object interface{}) error {
return connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
data, err := MarshalObject(object)
@@ -71,18 +75,18 @@ func UpdateObject(db *bolt.DB, bucketName string, key []byte, object interface{}
}
// DeleteObject is a generic function used to delete an object inside a bolt database.
func DeleteObject(db *bolt.DB, bucketName string, key []byte) error {
return db.Update(func(tx *bolt.Tx) error {
func DeleteObject(connection *DbConnection, bucketName string, key []byte) error {
return connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
return bucket.Delete(key)
})
}
// GetNextIdentifier is a generic function that returns the specified bucket identifier incremented by 1.
func GetNextIdentifier(db *bolt.DB, bucketName string) int {
func GetNextIdentifier(connection *DbConnection, bucketName string) int {
var identifier int
db.Update(func(tx *bolt.Tx) error {
connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
id, err := bucket.NextSequence()
if err != nil {

View File

@@ -0,0 +1,92 @@
package license
import (
"github.com/portainer/liblicense"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "license"
)
// Service represents a service for managing endpoint data.
type Service struct {
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
}
// License returns a license by licenseKey
func (service *Service) License(licenseKey string) (*liblicense.PortainerLicense, error) {
var license liblicense.PortainerLicense
identifier := []byte(licenseKey)
err := internal.GetObject(service.connection, BucketName, identifier, &license)
if err != nil {
return nil, err
}
return &license, nil
}
// Licenses return an array containing all the licenses.
func (service *Service) Licenses() ([]liblicense.PortainerLicense, error) {
var licenses = make([]liblicense.PortainerLicense, 0)
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var license liblicense.PortainerLicense
err := internal.UnmarshalObject(v, &license)
if err != nil {
return err
}
licenses = append(licenses, license)
}
return nil
})
return licenses, err
}
// AddLicense persists a license inside the database.
func (service *Service) AddLicense(licenseKey string, license *liblicense.PortainerLicense) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data, err := internal.MarshalObject(license)
if err != nil {
return err
}
return bucket.Put([]byte(licenseKey), data)
})
}
// UpdateLicense updates a license.
func (service *Service) UpdateLicense(licenseKey string, license *liblicense.PortainerLicense) error {
identifier := []byte(licenseKey)
return internal.UpdateObject(service.connection, BucketName, identifier, license)
}
// DeleteLicense deletes a License.
func (service *Service) DeleteLicense(licenseKey string) error {
identifier := []byte(licenseKey)
return internal.DeleteObject(service.connection, BucketName, identifier)
}

41
api/bolt/log/log.go Normal file
View File

@@ -0,0 +1,41 @@
package log
import (
"fmt"
"log"
)
const (
INFO = "INFO"
ERROR = "ERROR"
DEBUG = "DEBUG"
FATAL = "FATAL"
)
type ScopedLog struct {
scope string
}
func NewScopedLog(scope string) *ScopedLog {
return &ScopedLog{scope: scope}
}
func (slog *ScopedLog) print(kind string, message string) {
log.Printf("[%s] [%s] %s", kind, slog.scope, message)
}
func (slog *ScopedLog) Debug(message string) {
slog.print(DEBUG, fmt.Sprintf("[message: %s]", message))
}
func (slog *ScopedLog) Info(message string) {
slog.print(INFO, fmt.Sprintf("[message: %s]", message))
}
func (slog *ScopedLog) Error(message string, err error) {
slog.print(ERROR, fmt.Sprintf("[message: %s] [error: %s]", message, err))
}
func (slog *ScopedLog) NotImplemented(method string) {
log.Fatalf("[%s] [%s] [%s]", FATAL, slog.scope, fmt.Sprintf("%s is not yet implemented", method))
}

1
api/bolt/log/log.test.go Normal file
View File

@@ -0,0 +1 @@
package log

207
api/bolt/migrate_data.go Normal file
View File

@@ -0,0 +1,207 @@
package bolt
import (
"fmt"
portainer "github.com/portainer/portainer/api"
errors "github.com/portainer/portainer/api/bolt/errors"
plog "github.com/portainer/portainer/api/bolt/log"
"github.com/portainer/portainer/api/bolt/migrator"
"github.com/portainer/portainer/api/cli"
"github.com/portainer/portainer/api/internal/authorization"
)
const beforePortainerUpgradeToEEBackup = "portainer.db.before-EE-upgrade"
var migrateLog = plog.NewScopedLog("bolt, migrate")
// FailSafeMigrate backup and restore DB if migration fail
func (store *Store) FailSafeMigrate(migrator *migrator.Migrator, version int) error {
defer func() {
if err := recover(); err != nil {
migrateLog.Info(fmt.Sprintf("Error during migration, recovering [%v]", err))
store.Restore()
}
}()
return migrator.Migrate(version)
}
// MigrateData automatically migrate the data based on the DBVersion.
// This process is only triggered on an existing database, not if the database was just created.
// if force is true, then migrate regardless.
func (store *Store) MigrateData(force bool) error {
// 0 if DB is new then we don't need to migrate any data and just set version and edition to latest EE
if store.isNew && !force {
err := store.VersionService.StoreDBVersion(portainer.DBVersionEE)
if err != nil {
return err
}
err = store.VersionService.StoreEdition(portainer.PortainerEE)
if err != nil {
return err
}
return nil
}
migrator, err := store.newMigrator()
if err != nil {
return err
}
if migrator.Edition() == portainer.PortainerCE {
// backup before migrating
store.BackupWithOptions(&BackupOptions{
BackupFileName: beforePortainerUpgradeToEEBackup,
Edition: portainer.PortainerCE,
})
store.VersionService.StorePreviousDBVersion(migrator.Version())
// 1 We need to migrate DB to latest CE version
if migrator.Version() < portainer.DBVersion {
store.Backup()
err = store.FailSafeMigrate(migrator, portainer.DBVersion)
if err != nil {
store.Restore()
migrateLog.Error("An error occurred while migrating CE database to latest version", err)
return err
}
}
}
if portainer.Edition == portainer.PortainerEE {
// 2 if DB is CE Edition we need to upgrade settings to EE
if migrator.Edition() < portainer.PortainerEE {
err = migrator.UpgradeToEE()
if err != nil {
migrateLog.Error("An error occurred while upgrading database to EE", err)
store.RollbackFailedUpgradeToEE()
return err
}
}
// 3 if DB is EE Edition we need to migrate to latest version of EE
if migrator.Edition() == portainer.PortainerEE && migrator.Version() < portainer.DBVersionEE {
store.Backup()
err = store.FailSafeMigrate(migrator, portainer.DBVersionEE)
if err != nil {
migrateLog.Error("An error occurred while migrating EE database to latest version", err)
store.Restore()
return err
}
}
}
return nil
}
// RollbackFailedUpgradeToEE down migrate to previous version
func (store *Store) RollbackFailedUpgradeToEE() error {
return store.RestoreWithOptions(&BackupOptions{
BackupFileName: beforePortainerUpgradeToEEBackup,
Edition: portainer.PortainerCE,
})
}
// RollbackToCE rollbacks the store to the current ce version
func (store *Store) RollbackToCE() error {
migrator, err := store.newMigrator()
if err != nil {
return err
}
migrateLog.Info(fmt.Sprintf("Current Software Edition: %s", migrator.Edition().GetEditionLabel()))
migrateLog.Info(fmt.Sprintf("Current DB Version: %d", migrator.Version()))
if migrator.Edition() == portainer.PortainerCE {
return errors.ErrMigrationToCE
}
previousVersion, err := store.VersionService.PreviousDBVersion()
if err != nil {
migrateLog.Error("An Error occurred with retrieving previous DB version", err)
return err
}
confirmed, err := cli.Confirm(fmt.Sprintf("Are you sure you want to rollback your database to %d?", previousVersion))
if err != nil || !confirmed {
return err
}
if previousVersion < 25 {
migrator.DowngradeSettingsFrom25()
}
err = store.VersionService.StoreDBVersion(previousVersion)
if err != nil {
migrateLog.Error(fmt.Sprintf("An Error occurred with rolling back to CE Edition, DB Version %d", previousVersion), err)
return err
}
err = store.VersionService.StoreEdition(portainer.PortainerCE)
if err != nil {
migrateLog.Error(fmt.Sprintf("An Error occurred with rolling back to CE Edition, DB Version %d", previousVersion), err)
return err
}
migrateLog.Info(fmt.Sprintf("Rolled back to CE Edition, DB Version %d", previousVersion))
return nil
}
func (store *Store) newMigrator() (*migrator.Migrator, error) {
version, err := store.version()
if err != nil {
return nil, err
}
edition := store.edition()
params := &migrator.Parameters{
DB: store.connection.DB,
DatabaseVersion: version,
CurrentEdition: edition,
EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService,
EndpointRelationService: store.EndpointRelationService,
ExtensionService: store.ExtensionService,
RegistryService: store.RegistryService,
ResourceControlService: store.ResourceControlService,
RoleService: store.RoleService,
ScheduleService: store.ScheduleService,
SettingsService: store.SettingsService,
StackService: store.StackService,
TagService: store.TagService,
TeamMembershipService: store.TeamMembershipService,
UserService: store.UserService,
VersionService: store.VersionService,
FileService: store.fileService,
AuthorizationService: authorization.NewService(store),
}
return migrator.NewMigrator(params), nil
}
// RollbackVersion down migrate to previous version
func (store *Store) RollbackVersion(version int) error {
// TODO
backupLog.NotImplemented("RollbackVersion")
return nil
}
// RollbackEdition downgrade to previous edition
func (store *Store) RollbackEdition(edition portainer.SoftwareEdition) error {
// TODO
backupLog.NotImplemented("RollbackEdition")
// Change Edition
// Migrate Services
// Restore Latest
return nil
}

View File

@@ -0,0 +1,99 @@
package bolt
import (
"fmt"
"log"
"testing"
"github.com/boltdb/bolt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
)
// New Database should be EE and DBVersion
//
func TestMigrateData(t *testing.T) {
var store *Store
t.Run("MigrateData for New Store", func(t *testing.T) {
fileService, err := filesystem.NewService(dataStorePath, "")
if err != nil {
log.Fatal(err)
}
store, err := NewStore(dataStorePath, fileService)
if err != nil {
log.Fatal(err)
}
err = store.Open()
if err != nil {
log.Fatal(err)
}
err = store.Init()
if err != nil {
log.Fatal(err)
}
store.MigrateData(false)
testVersion(store, portainer.DBVersionEE, t)
testEdition(store, portainer.PortainerEE, t)
store.Close()
})
tests := []struct {
edition portainer.SoftwareEdition
version int
expectedVersion int
}{
{edition: portainer.PortainerCE, version: 5, expectedVersion: portainer.DBVersionEE},
{edition: portainer.PortainerCE, version: 21, expectedVersion: portainer.DBVersionEE},
}
for _, tc := range tests {
store = NewTestStore(tc.edition, tc.version, true)
t.Run(fmt.Sprintf("MigrateData for %s version %d", tc.edition.GetEditionLabel(), tc.version), func(t *testing.T) {
store.MigrateData(false)
testVersion(store, tc.expectedVersion, t)
testEdition(store, portainer.PortainerEE, t)
})
t.Run(fmt.Sprintf("Restoring DB after migrateData for %s version %d", tc.edition.GetEditionLabel(), tc.version), func(t *testing.T) {
store.RollbackToCE()
testVersion(store, tc.version, t)
testEdition(store, tc.edition, t)
})
store.Close()
}
t.Run("Error in MigrateData should restore backup before MigrateData", func(t *testing.T) {
version := 21
store = NewTestStore(portainer.PortainerCE, version, true)
deleteBucket(store.connection.DB, "settings")
store.MigrateData(false)
testVersion(store, version, t)
testEdition(store, portainer.PortainerCE, t)
store.Close()
})
teardown()
}
func deleteBucket(db *bolt.DB, bucketName string) {
db.Update(func(tx *bolt.Tx) error {
log.Printf("Delete bucket %s\n", bucketName)
err := tx.DeleteBucket([]byte(bucketName))
if err != nil {
log.Println(err)
}
return err
})
}

View File

@@ -0,0 +1,15 @@
package migrator
// DowngradeSettingsFrom25 downgrade template settings for portainer v1.2
func (migrator *Migrator) DowngradeSettingsFrom25() error {
legacySettings, err := migrator.settingsService.Settings()
if err != nil {
return err
}
legacySettings.TemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates-1.20.0.json"
err = migrator.settingsService.UpdateSettings(legacySettings)
return err
}

View File

@@ -0,0 +1,308 @@
package migrator
import (
"log"
portainer "github.com/portainer/portainer/api"
)
// MigrateCE checks the database version and migrate the existing data to the most recent data model.
func (m *Migrator) MigrateCE() error {
// Portainer < 1.12
if m.currentDBVersion < 1 {
err := m.updateAdminUserToDBVersion1()
if err != nil {
return err
}
}
// Portainer 1.12.x
if m.currentDBVersion < 2 {
err := m.updateResourceControlsToDBVersion2()
if err != nil {
return err
}
err = m.updateEndpointsToDBVersion2()
if err != nil {
return err
}
}
// Portainer 1.13.x
if m.currentDBVersion < 3 {
err := m.updateSettingsToDBVersion3()
if err != nil {
return err
}
}
// Portainer 1.14.0
if m.currentDBVersion < 4 {
err := m.updateEndpointsToDBVersion4()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1235
if m.currentDBVersion < 5 {
err := m.updateSettingsToVersion5()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1236
if m.currentDBVersion < 6 {
err := m.updateSettingsToVersion6()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1449
if m.currentDBVersion < 7 {
err := m.updateSettingsToVersion7()
if err != nil {
return err
}
}
if m.currentDBVersion < 8 {
err := m.updateEndpointsToVersion8()
if err != nil {
return err
}
}
// https: //github.com/portainer/portainer/issues/1396
if m.currentDBVersion < 9 {
err := m.updateEndpointsToVersion9()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/461
if m.currentDBVersion < 10 {
err := m.updateEndpointsToVersion10()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1906
if m.currentDBVersion < 11 {
err := m.updateEndpointsToVersion11()
if err != nil {
return err
}
}
// Portainer 1.18.0
if m.currentDBVersion < 12 {
err := m.updateEndpointsToVersion12()
if err != nil {
return err
}
err = m.updateEndpointGroupsToVersion12()
if err != nil {
return err
}
err = m.updateStacksToVersion12()
if err != nil {
return err
}
}
// Portainer 1.19.0
if m.currentDBVersion < 13 {
err := m.updateSettingsToVersion13()
if err != nil {
return err
}
}
// Portainer 1.19.2
if m.currentDBVersion < 14 {
err := m.updateResourceControlsToDBVersion14()
if err != nil {
return err
}
}
// Portainer 1.20.0
if m.currentDBVersion < 15 {
err := m.updateSettingsToDBVersion15()
if err != nil {
return err
}
err = m.updateTemplatesToVersion15()
if err != nil {
return err
}
}
if m.currentDBVersion < 16 {
err := m.updateSettingsToDBVersion16()
if err != nil {
return err
}
}
// Portainer 1.20.1
if m.currentDBVersion < 17 {
err := m.updateExtensionsToDBVersion17()
if err != nil {
return err
}
}
// Portainer 1.21.0
if m.currentDBVersion < 18 {
err := m.updateUsersToDBVersion18()
if err != nil {
return err
}
err = m.updateEndpointsToDBVersion18()
if err != nil {
return err
}
err = m.updateEndpointGroupsToDBVersion18()
if err != nil {
return err
}
err = m.updateRegistriesToDBVersion18()
if err != nil {
return err
}
}
// Portainer 1.22.0
if m.currentDBVersion < 19 {
err := m.updateSettingsToDBVersion19()
if err != nil {
return err
}
}
// Portainer 1.22.1
if m.currentDBVersion < 20 {
err := m.updateUsersToDBVersion20()
if err != nil {
return err
}
err = m.updateSettingsToDBVersion20()
if err != nil {
return err
}
err = m.updateSchedulesToDBVersion20()
if err != nil {
return err
}
}
// Portainer 1.23.0
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
if m.currentDBVersion < 22 {
err := m.updateResourceControlsToDBVersion22()
if err != nil {
return err
}
err = m.updateUsersAndRolesToDBVersion22()
if err != nil {
return err
}
}
// Portainer 1.24.0
if m.currentDBVersion < 23 {
err := m.updateTagsToDBVersion23()
if err != nil {
return err
}
err = m.updateEndpointsAndEndpointGroupsToDBVersion23()
if err != nil {
return err
}
}
// Portainer 1.24.1
if m.currentDBVersion < 24 {
err := m.updateSettingsToDB24()
if err != nil {
return err
}
}
// Portainer 2.0.0
if m.currentDBVersion < 25 {
err := m.updateSettingsToDB25()
if err != nil {
return err
}
err = m.updateStacksToDB24()
if err != nil {
return err
}
}
// Portainer 2.1.0
if m.currentDBVersion < 26 {
err := m.updateEndpointSettingsToDB26()
if err != nil {
return err
}
err = m.updateRbacRolesToDB26()
if err != nil {
return err
}
}
// Portainer 2.2.0
if m.currentDBVersion < 27 {
err := m.updateStackResourceControlToDB27()
if err != nil {
return err
}
}
// Portainer EE-2.4.0
if m.currentDBVersion < 28 {
err := m.updateUsersAndRolesToDBVersion28()
if err != nil {
return err
}
}
// Portainer EE-2.4.0
if m.currentDBVersion < 29 {
err := m.updateRbacRolesToDB29()
if err != nil {
return err
}
}
// Portainer EE-2.7.0
if m.currentDBVersion < 31 {
err := m.updateSettingsToDB31()
if err != nil {
return err
}
}
log.Println("Update DB version to ", portainer.DBVersion)
return m.versionService.StoreDBVersion(portainer.DBVersion)
}

View File

@@ -29,11 +29,6 @@ func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
return err
}
settings, err := m.settingsService.Settings()
if err != nil {
return err
}
for _, user := range legacyUsers {
user.PortainerAuthorizations = authorization.DefaultPortainerAuthorizations()
err = m.userService.UpdateUser(user.ID, &user)
@@ -56,7 +51,7 @@ func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
return err
}
helpDeskRole.Priority = 2
helpDeskRole.Authorizations = authorization.DefaultEndpointAuthorizationsForHelpDeskRole(settings.AllowVolumeBrowserForRegularUsers)
helpDeskRole.Authorizations = authorization.DefaultEndpointAuthorizationsForHelpDeskRole()
err = m.roleService.UpdateRole(helpDeskRole.ID, helpDeskRole)
@@ -65,7 +60,7 @@ func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
return err
}
standardUserRole.Priority = 3
standardUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForStandardUserRole(settings.AllowVolumeBrowserForRegularUsers)
standardUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForStandardUserRole()
err = m.roleService.UpdateRole(standardUserRole.ID, standardUserRole)
@@ -74,7 +69,7 @@ func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
return err
}
readOnlyUserRole.Priority = 4
readOnlyUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForReadOnlyUserRole(settings.AllowVolumeBrowserForRegularUsers)
readOnlyUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForReadOnlyUserRole()
err = m.roleService.UpdateRole(readOnlyUserRole.ID, readOnlyUserRole)
if err != nil {

View File

@@ -0,0 +1,76 @@
package migrator
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/authorization"
)
func (m *Migrator) updateEndpointSettingsToDB26() error {
settings, err := m.settingsService.Settings()
if err != nil {
return err
}
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for i := range endpoints {
endpoint := endpoints[i]
securitySettings := portainer.EndpointSecuritySettings{}
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment ||
endpoint.Type == portainer.AgentOnDockerEnvironment ||
endpoint.Type == portainer.DockerEnvironment {
securitySettings = portainer.EndpointSecuritySettings{
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
AllowContainerCapabilitiesForRegularUsers: settings.AllowContainerCapabilitiesForRegularUsers,
AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers,
AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers,
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
AllowStackManagementForRegularUsers: settings.AllowStackManagementForRegularUsers,
}
if endpoint.Type == portainer.AgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
securitySettings.AllowVolumeBrowserForRegularUsers = settings.AllowVolumeBrowserForRegularUsers
securitySettings.EnableHostManagementFeatures = settings.EnableHostManagementFeatures
}
}
endpoint.SecuritySettings = securitySettings
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) updateRbacRolesToDB26() error {
defaultAuthorizationsOfRoles := map[portainer.RoleID]portainer.Authorizations{
portainer.RoleIDEndpointAdmin: authorization.DefaultEndpointAuthorizationsForEndpointAdministratorRole(),
portainer.RoleIDHelpdesk: authorization.DefaultEndpointAuthorizationsForHelpDeskRole(),
portainer.RoleIDStandardUser: authorization.DefaultEndpointAuthorizationsForStandardUserRole(),
portainer.RoleIDReadonly: authorization.DefaultEndpointAuthorizationsForReadOnlyUserRole(),
}
for roleID, defaultAuthorizations := range defaultAuthorizationsOfRoles {
role, err := m.roleService.Role(roleID)
if err != nil {
return err
}
role.Authorizations = defaultAuthorizations
err = m.roleService.UpdateRole(role.ID, role)
if err != nil {
return err
}
}
return m.authorizationService.UpdateUsersAuthorizations()
}

View File

@@ -0,0 +1,43 @@
package migrator
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
)
func (m *Migrator) updateStackResourceControlToDB27() error {
resourceControls, err := m.resourceControlService.ResourceControls()
if err != nil {
return err
}
for _, resource := range resourceControls {
if resource.Type != portainer.StackResourceControl {
continue
}
stackName := resource.ResourceID
if err != nil {
return err
}
stack, err := m.stackService.StackByName(stackName)
if err != nil {
if err == errors.ErrObjectNotFound {
continue
}
return err
}
resource.ResourceID = fmt.Sprintf("%d_%s", stack.EndpointID, stack.Name)
err = m.resourceControlService.UpdateResourceControl(resource.ID, &resource)
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,10 @@
package migrator
func (m *Migrator) updateUsersAndRolesToDBVersion28() error {
err := m.roleService.CreateOrUpdatePredefinedRoles()
if err != nil {
return err
}
return m.authorizationService.UpdateUsersAuthorizations()
}

View File

@@ -0,0 +1,31 @@
package migrator
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/authorization"
)
func (m *Migrator) updateRbacRolesToDB29() error {
defaultAuthorizationsOfRoles := map[portainer.RoleID]portainer.Authorizations{
portainer.RoleIDEndpointAdmin: authorization.DefaultEndpointAuthorizationsForEndpointAdministratorRole(),
portainer.RoleIDHelpdesk: authorization.DefaultEndpointAuthorizationsForHelpDeskRole(),
portainer.RoleIDOperator: authorization.DefaultEndpointAuthorizationsForOperatorRole(),
portainer.RoleIDStandardUser: authorization.DefaultEndpointAuthorizationsForStandardUserRole(),
portainer.RoleIDReadonly: authorization.DefaultEndpointAuthorizationsForReadOnlyUserRole(),
}
for roleID, defaultAuthorizations := range defaultAuthorizationsOfRoles {
role, err := m.roleService.Role(roleID)
if err != nil {
return err
}
role.Authorizations = defaultAuthorizations
err = m.roleService.UpdateRole(role.ID, role)
if err != nil {
return err
}
}
return m.authorizationService.UpdateUsersAuthorizations()
}

View File

@@ -0,0 +1,12 @@
package migrator
func (m *Migrator) updateSettingsToDB31() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.OAuthSettings.SSO = false
legacySettings.OAuthSettings.HideInternalAuth = false
legacySettings.OAuthSettings.LogoutURI = ""
return m.settingsService.UpdateSettings(legacySettings)
}

View File

@@ -0,0 +1,67 @@
package migrator
import (
"os"
"testing"
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api/bolt/settings"
)
var (
testingDBStorePath string
testingDBFileName string
dummyLogoURL string
dbConn *bolt.DB
settingsService *settings.Service
)
func setup() error {
testingDBStorePath, _ = os.Getwd()
testingDBFileName = "portainer-ee-mig-30.db"
dummyLogoURL = "example.com"
var err error
dbConn, err = initTestingDBConn(testingDBStorePath, testingDBFileName)
if err != nil {
return err
}
dummySettingsObj := map[string]interface{}{
"LogoURL": dummyLogoURL,
}
settingsService, err = initTestingSettingsService(dbConn, dummySettingsObj)
if err != nil {
return err
}
return nil
}
func TestUpdateSettingsToDB31(t *testing.T) {
if err := setup(); err != nil {
t.Errorf("failed to complete testing setups, err: %v", err)
}
defer dbConn.Close()
defer os.Remove(testingDBFileName)
m := &Migrator{
db: dbConn,
settingsService: settingsService,
}
if err := m.updateSettingsToDB31(); err != nil {
t.Errorf("failed to update settings: %v", err)
}
updatedSettings, err := m.settingsService.Settings()
if err != nil {
t.Errorf("failed to retrieve the updated settings: %v", err)
}
if updatedSettings.LogoURL != dummyLogoURL {
t.Errorf("unexpected value changes in the updated settings, want LogoURL value: %s, got LogoURL value: %s", dummyLogoURL, updatedSettings.LogoURL)
}
if updatedSettings.OAuthSettings.SSO != false {
t.Errorf("unexpected default OAuth SSO setting, want: false, got: %t", updatedSettings.OAuthSettings.SSO)
}
if updatedSettings.OAuthSettings.HideInternalAuth != false {
t.Errorf("unexpected default OAuth HideInternalAuth setting, want: false, got: %t", updatedSettings.OAuthSettings.HideInternalAuth)
}
if updatedSettings.OAuthSettings.LogoutURI != "" {
t.Errorf("unexpected default OAuth HideInternalAuth setting, want:, got: %s", updatedSettings.OAuthSettings.LogoutURI)
}
}

View File

@@ -0,0 +1,38 @@
package migrator
import (
"path"
"time"
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/portainer/portainer/api/bolt/settings"
)
// initTestingDBConn creates a raw bolt DB connection
// for unit testing usage only since using NewStore will cause cycle import inside migrator pkg
func initTestingDBConn(storePath, fileName string) (*bolt.DB, error) {
databasePath := path.Join(storePath, fileName)
dbConn, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second})
if err != nil {
return nil, err
}
return dbConn, nil
}
// initTestingDBConn creates a settings service with raw bolt DB connection
// for unit testing usage only since using NewStore will cause cycle import inside migrator pkg
func initTestingSettingsService(dbConn *bolt.DB, preSetObj map[string]interface{}) (*settings.Service, error) {
internalDBConn := &internal.DbConnection{
DB: dbConn,
}
settingsService, err := settings.NewService(internalDBConn)
if err != nil {
return nil, err
}
//insert a obj
if err := internal.UpdateObject(internalDBConn, "settings", []byte("SETTINGS"), preSetObj); err != nil {
return nil, err
}
return settingsService, nil
}

View File

@@ -1,12 +1,15 @@
package migrator
import (
"fmt"
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/endpoint"
"github.com/portainer/portainer/api/bolt/endpointgroup"
"github.com/portainer/portainer/api/bolt/endpointrelation"
"github.com/portainer/portainer/api/bolt/extension"
plog "github.com/portainer/portainer/api/bolt/log"
"github.com/portainer/portainer/api/bolt/registry"
"github.com/portainer/portainer/api/bolt/resourcecontrol"
"github.com/portainer/portainer/api/bolt/role"
@@ -20,11 +23,15 @@ import (
"github.com/portainer/portainer/api/internal/authorization"
)
var migrateLog = plog.NewScopedLog("bolt, migrate")
type (
// Migrator defines a service to migrate data after a Portainer version update.
Migrator struct {
currentDBVersion int
db *bolt.DB
db *bolt.DB
currentDBVersion int
currentEdition portainer.SoftwareEdition
endpointGroupService *endpointgroup.Service
endpointService *endpoint.Service
endpointRelationService *endpointrelation.Service
@@ -45,8 +52,10 @@ type (
// Parameters represents the required parameters to create a new Migrator instance.
Parameters struct {
DB *bolt.DB
DatabaseVersion int
DB *bolt.DB
DatabaseVersion int
CurrentEdition portainer.SoftwareEdition
EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service
EndpointRelationService *endpointrelation.Service
@@ -71,6 +80,7 @@ func NewMigrator(parameters *Parameters) *Migrator {
return &Migrator{
db: parameters.DB,
currentDBVersion: parameters.DatabaseVersion,
currentEdition: parameters.CurrentEdition,
endpointGroupService: parameters.EndpointGroupService,
endpointService: parameters.EndpointService,
endpointRelationService: parameters.EndpointRelationService,
@@ -90,257 +100,43 @@ func NewMigrator(parameters *Parameters) *Migrator {
}
}
// Migrate checks the database version and migrate the existing data to the most recent data model.
func (m *Migrator) Migrate() error {
// Portainer < 1.12
if m.currentDBVersion < 1 {
err := m.updateAdminUserToDBVersion1()
if err != nil {
return err
}
}
// Portainer 1.12.x
if m.currentDBVersion < 2 {
err := m.updateResourceControlsToDBVersion2()
if err != nil {
return err
}
err = m.updateEndpointsToDBVersion2()
if err != nil {
return err
}
}
// Portainer 1.13.x
if m.currentDBVersion < 3 {
err := m.updateSettingsToDBVersion3()
if err != nil {
return err
}
}
// Portainer 1.14.0
if m.currentDBVersion < 4 {
err := m.updateEndpointsToDBVersion4()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1235
if m.currentDBVersion < 5 {
err := m.updateSettingsToVersion5()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1236
if m.currentDBVersion < 6 {
err := m.updateSettingsToVersion6()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1449
if m.currentDBVersion < 7 {
err := m.updateSettingsToVersion7()
if err != nil {
return err
}
}
if m.currentDBVersion < 8 {
err := m.updateEndpointsToVersion8()
if err != nil {
return err
}
}
// https: //github.com/portainer/portainer/issues/1396
if m.currentDBVersion < 9 {
err := m.updateEndpointsToVersion9()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/461
if m.currentDBVersion < 10 {
err := m.updateEndpointsToVersion10()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1906
if m.currentDBVersion < 11 {
err := m.updateEndpointsToVersion11()
if err != nil {
return err
}
}
// Portainer 1.18.0
if m.currentDBVersion < 12 {
err := m.updateEndpointsToVersion12()
if err != nil {
return err
}
err = m.updateEndpointGroupsToVersion12()
if err != nil {
return err
}
err = m.updateStacksToVersion12()
if err != nil {
return err
}
}
// Portainer 1.19.0
if m.currentDBVersion < 13 {
err := m.updateSettingsToVersion13()
if err != nil {
return err
}
}
// Portainer 1.19.2
if m.currentDBVersion < 14 {
err := m.updateResourceControlsToDBVersion14()
if err != nil {
return err
}
}
// Portainer 1.20.0
if m.currentDBVersion < 15 {
err := m.updateSettingsToDBVersion15()
if err != nil {
return err
}
err = m.updateTemplatesToVersion15()
if err != nil {
return err
}
}
if m.currentDBVersion < 16 {
err := m.updateSettingsToDBVersion16()
if err != nil {
return err
}
}
// Portainer 1.20.1
if m.currentDBVersion < 17 {
err := m.updateExtensionsToDBVersion17()
if err != nil {
return err
}
}
// Portainer 1.21.0
if m.currentDBVersion < 18 {
err := m.updateUsersToDBVersion18()
if err != nil {
return err
}
err = m.updateEndpointsToDBVersion18()
if err != nil {
return err
}
err = m.updateEndpointGroupsToDBVersion18()
if err != nil {
return err
}
err = m.updateRegistriesToDBVersion18()
if err != nil {
return err
}
}
// Portainer 1.22.0
if m.currentDBVersion < 19 {
err := m.updateSettingsToDBVersion19()
if err != nil {
return err
}
}
// Portainer 1.22.1
if m.currentDBVersion < 20 {
err := m.updateUsersToDBVersion20()
if err != nil {
return err
}
err = m.updateSettingsToDBVersion20()
if err != nil {
return err
}
err = m.updateSchedulesToDBVersion20()
if err != nil {
return err
}
}
// Portainer 1.23.0
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
if m.currentDBVersion < 22 {
err := m.updateResourceControlsToDBVersion22()
if err != nil {
return err
}
err = m.updateUsersAndRolesToDBVersion22()
if err != nil {
return err
}
}
// Portainer 1.24.0
if m.currentDBVersion < 23 {
err := m.updateTagsToDBVersion23()
if err != nil {
return err
}
err = m.updateEndpointsAndEndpointGroupsToDBVersion23()
if err != nil {
return err
}
}
// Portainer 1.24.1
if m.currentDBVersion < 24 {
err := m.updateSettingsToDB24()
if err != nil {
return err
}
}
// Portainer 2.0.0
if m.currentDBVersion < 25 {
err := m.updateSettingsToDB25()
if err != nil {
return err
}
err = m.updateStacksToDB24()
if err != nil {
return err
}
}
return m.versionService.StoreDBVersion(portainer.DBVersion)
// Version exposes version of database
func (migrator *Migrator) Version() int {
return migrator.currentDBVersion
}
// Edition exposes edition of portainer
func (migrator *Migrator) Edition() portainer.SoftwareEdition {
return migrator.currentEdition
}
// Migrate helper to upgrade DB
func (migrator *Migrator) Migrate(version int) error {
migrateLog.Info(fmt.Sprintf("Migrating %s database from version %d to %d.", migrator.Edition().GetEditionLabel(), migrator.currentDBVersion, version))
// TODO : run backup before migration and restore if failed
err := migrator.MigrateCE() //CE
if err != nil {
migrateLog.Error("An error occurred during database migration", err)
return err
}
migrator.versionService.StoreDBVersion(version)
migrator.currentDBVersion = version
return nil
}
// RollbackVersion rolls back the db to version
func (migrator *Migrator) RollbackVersion(version int) error {
err := migrator.versionService.StoreDBVersion(version) // portainer.DBVersion
return err
}
// RollbackEdition rolls back the db to portainer CE
func (migrator *Migrator) RollbackEdition(edition portainer.SoftwareEdition) error {
err := migrator.versionService.StoreEdition(portainer.PortainerCE)
return err
}

View File

@@ -0,0 +1,4 @@
package migrator
// test CE version is always upgraded to latest version of CE
// test EE version is always upgraded to latest version of EE

View File

@@ -0,0 +1,228 @@
package migrator
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/authorization"
)
// UpgradeToEE will migrate the db from latest ce version to latest ee version
// Latest version is v25 on 06/11/2020
func (m *Migrator) UpgradeToEE() error {
migrateLog.Info(fmt.Sprintf("Migrating CE database version %d to EE database version %d.", m.Version(), portainer.DBVersion))
migrateLog.Info("Updating LDAP settings to EE")
err := m.updateSettingsToEE()
if err != nil {
return err
}
migrateLog.Info("Updating user roles to EE")
err = m.updateUserRolesToEE()
if err != nil {
return err
}
migrateLog.Info("Updating role authorizations to EE")
err = m.updateRoleAuthorizationsToEE()
if err != nil {
return err
}
migrateLog.Info("Updating user authorizations")
err = m.authorizationService.UpdateUsersAuthorizations()
if err != nil {
return err
}
migrateLog.Info(fmt.Sprintf("Setting db version to %d", portainer.DBVersionEE))
err = m.versionService.StoreDBVersion(portainer.DBVersionEE)
if err != nil {
return err
}
migrateLog.Info(fmt.Sprintf("Setting edition to %s", portainer.PortainerEE.GetEditionLabel()))
err = m.versionService.StoreEdition(portainer.PortainerEE)
if err != nil {
return err
}
m.currentDBVersion = portainer.DBVersionEE
m.currentEdition = portainer.PortainerEE
return nil
}
func (m *Migrator) updateSettingsToEE() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.LDAPSettings.URLs = []string{}
url := legacySettings.LDAPSettings.URL
if url != "" {
legacySettings.LDAPSettings.URLs = append(legacySettings.LDAPSettings.URLs, url)
}
legacySettings.LDAPSettings.ServerType = portainer.LDAPServerCustom
return m.settingsService.UpdateSettings(legacySettings)
}
// Updating role authorizations because of the new policies in Kube RBAC
func (m *Migrator) updateRoleAuthorizationsToEE() error {
migrateLog.Debug("Retriving settings")
migrateLog.Debug("Updating Endpoint Admin Role")
endpointAdministratorRole, err := m.roleService.Role(portainer.RoleID(1))
if err != nil {
return err
}
endpointAdministratorRole.Priority = 1
endpointAdministratorRole.Authorizations = authorization.DefaultEndpointAuthorizationsForEndpointAdministratorRole()
err = m.roleService.UpdateRole(endpointAdministratorRole.ID, endpointAdministratorRole)
migrateLog.Debug("Updating Help Desk Role")
helpDeskRole, err := m.roleService.Role(portainer.RoleID(2))
if err != nil {
return err
}
helpDeskRole.Priority = 2
helpDeskRole.Authorizations = authorization.DefaultEndpointAuthorizationsForHelpDeskRole()
err = m.roleService.UpdateRole(helpDeskRole.ID, helpDeskRole)
migrateLog.Debug("Updating Standard User Role")
standardUserRole, err := m.roleService.Role(portainer.RoleID(3))
if err != nil {
return err
}
standardUserRole.Priority = 3
standardUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForStandardUserRole()
err = m.roleService.UpdateRole(standardUserRole.ID, standardUserRole)
migrateLog.Debug("Updating Read Only User Role")
readOnlyUserRole, err := m.roleService.Role(portainer.RoleID(4))
if err != nil {
return err
}
readOnlyUserRole.Priority = 4
readOnlyUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForReadOnlyUserRole()
err = m.roleService.UpdateRole(readOnlyUserRole.ID, readOnlyUserRole)
if err != nil {
return err
}
return nil
}
// If RBAC extension wasn't installed before, update all users in endpoints and
// endpoint groups to have read only access.
func (m *Migrator) updateUserRolesToEE() error {
err := m.updateUserAuthorizationToEE()
if err != nil {
return err
}
migrateLog.Debug("Retriving extension info")
extensions, err := m.extensionService.Extensions()
for _, extension := range extensions {
if extension.ID == 3 && extension.Enabled {
migrateLog.Info("RBAC extensions were enabled before; Skip updating User Roles")
return nil
}
}
migrateLog.Debug("Retriving endpoint groups")
endpointGroups, err := m.endpointGroupService.EndpointGroups()
if err != nil {
return err
}
for _, endpointGroup := range endpointGroups {
migrateLog.Debug(fmt.Sprintf("Updating user policies for endpoint group %v", endpointGroup.ID))
for key := range endpointGroup.UserAccessPolicies {
updateUserAccessPolicyToReadOnlyRole(endpointGroup.UserAccessPolicies, key)
}
for key := range endpointGroup.TeamAccessPolicies {
updateTeamAccessPolicyToReadOnlyRole(endpointGroup.TeamAccessPolicies, key)
}
err := m.endpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
if err != nil {
return err
}
}
migrateLog.Debug("Retriving endpoints")
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range endpoints {
migrateLog.Debug(fmt.Sprintf("Updating user policies for endpoint %v", endpoint.ID))
for key := range endpoint.UserAccessPolicies {
updateUserAccessPolicyToReadOnlyRole(endpoint.UserAccessPolicies, key)
}
for key := range endpoint.TeamAccessPolicies {
updateTeamAccessPolicyToReadOnlyRole(endpoint.TeamAccessPolicies, key)
}
err := m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) updateUserAuthorizationToEE() error {
legacyUsers, err := m.userService.Users()
if err != nil {
return err
}
for _, user := range legacyUsers {
user.PortainerAuthorizations = authorization.DefaultPortainerAuthorizations()
err = m.userService.UpdateUser(user.ID, &user)
if err != nil {
return err
}
}
return nil
}
func updateUserAccessPolicyToNoRole(policies portainer.UserAccessPolicies, key portainer.UserID) {
tmp := policies[key]
tmp.RoleID = 0
policies[key] = tmp
}
func updateTeamAccessPolicyToNoRole(policies portainer.TeamAccessPolicies, key portainer.TeamID) {
tmp := policies[key]
tmp.RoleID = 0
policies[key] = tmp
}
func updateUserAccessPolicyToReadOnlyRole(policies portainer.UserAccessPolicies, key portainer.UserID) {
tmp := policies[key]
tmp.RoleID = 4
policies[key] = tmp
}
func updateTeamAccessPolicyToReadOnlyRole(policies portainer.TeamAccessPolicies, key portainer.TeamID) {
tmp := policies[key]
tmp.RoleID = 4
policies[key] = tmp
}

View File

@@ -1,7 +1,7 @@
package registry
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
@@ -14,18 +14,18 @@ const (
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -34,7 +34,7 @@ func (service *Service) Registry(ID portainer.RegistryID) (*portainer.Registry,
var registry portainer.Registry
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &registry)
err := internal.GetObject(service.connection, BucketName, identifier, &registry)
if err != nil {
return nil, err
}
@@ -46,7 +46,7 @@ func (service *Service) Registry(ID portainer.RegistryID) (*portainer.Registry,
func (service *Service) Registries() ([]portainer.Registry, error) {
var registries = make([]portainer.Registry, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -67,7 +67,7 @@ func (service *Service) Registries() ([]portainer.Registry, error) {
// CreateRegistry creates a new registry.
func (service *Service) CreateRegistry(registry *portainer.Registry) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
@@ -85,11 +85,11 @@ func (service *Service) CreateRegistry(registry *portainer.Registry) error {
// UpdateRegistry updates an registry.
func (service *Service) UpdateRegistry(ID portainer.RegistryID, registry *portainer.Registry) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, registry)
return internal.UpdateObject(service.connection, BucketName, identifier, registry)
}
// DeleteRegistry deletes an registry.
func (service *Service) DeleteRegistry(ID portainer.RegistryID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}

View File

@@ -1,7 +1,7 @@
package resourcecontrol
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
@@ -14,18 +14,18 @@ const (
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -34,7 +34,7 @@ func (service *Service) ResourceControl(ID portainer.ResourceControlID) (*portai
var resourceControl portainer.ResourceControl
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &resourceControl)
err := internal.GetObject(service.connection, BucketName, identifier, &resourceControl)
if err != nil {
return nil, err
}
@@ -48,7 +48,7 @@ func (service *Service) ResourceControl(ID portainer.ResourceControlID) (*portai
func (service *Service) ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
var resourceControl *portainer.ResourceControl
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -82,7 +82,7 @@ func (service *Service) ResourceControlByResourceIDAndType(resourceID string, re
func (service *Service) ResourceControls() ([]portainer.ResourceControl, error) {
var rcs = make([]portainer.ResourceControl, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -103,7 +103,7 @@ func (service *Service) ResourceControls() ([]portainer.ResourceControl, error)
// CreateResourceControl creates a new ResourceControl object
func (service *Service) CreateResourceControl(resourceControl *portainer.ResourceControl) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
@@ -121,11 +121,11 @@ func (service *Service) CreateResourceControl(resourceControl *portainer.Resourc
// UpdateResourceControl saves a ResourceControl object.
func (service *Service) UpdateResourceControl(ID portainer.ResourceControlID, resourceControl *portainer.ResourceControl) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, resourceControl)
return internal.UpdateObject(service.connection, BucketName, identifier, resourceControl)
}
// DeleteResourceControl deletes a ResourceControl object by ID
func (service *Service) DeleteResourceControl(ID portainer.ResourceControlID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}

View File

@@ -0,0 +1,68 @@
package role
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/internal/authorization"
)
// CreateOrUpdatePredefinedRoles update the predefined roles. Create one if it does not exist yet.
func (service *Service) CreateOrUpdatePredefinedRoles() error {
predefinedRoles := map[portainer.RoleID]*portainer.Role{
portainer.RoleIDEndpointAdmin: &portainer.Role{
Name: "Endpoint administrator",
Description: "Full control of all resources in an endpoint",
ID: portainer.RoleIDEndpointAdmin,
Priority: 1,
Authorizations: authorization.DefaultEndpointAuthorizationsForEndpointAdministratorRole(),
},
portainer.RoleIDOperator: &portainer.Role{
Name: "Operator",
Description: "Operational control of all existing resources in an endpoint",
ID: portainer.RoleIDOperator,
Priority: 2,
Authorizations: authorization.DefaultEndpointAuthorizationsForOperatorRole(),
},
portainer.RoleIDHelpdesk: &portainer.Role{
Name: "Helpdesk",
Description: "Read-only access of all resources in an endpoint",
ID: portainer.RoleIDHelpdesk,
Priority: 3,
Authorizations: authorization.DefaultEndpointAuthorizationsForHelpDeskRole(),
},
portainer.RoleIDStandardUser: &portainer.Role{
Name: "Standard user",
Description: "Full control of assigned resources in an endpoint",
ID: portainer.RoleIDStandardUser,
Priority: 4,
Authorizations: authorization.DefaultEndpointAuthorizationsForStandardUserRole(),
},
portainer.RoleIDReadonly: &portainer.Role{
Name: "Read-only user",
Description: "Read-only access of assigned resources in an endpoint",
ID: portainer.RoleIDReadonly,
Priority: 5,
Authorizations: authorization.DefaultEndpointAuthorizationsForReadOnlyUserRole(),
},
}
for roleID, predefinedRole := range predefinedRoles {
_, err := service.Role(roleID)
if err == errors.ErrObjectNotFound {
err := service.CreateRole(predefinedRole)
if err != nil {
return err
}
} else if err != nil {
return err
} else {
err = service.UpdateRole(predefinedRole.ID, predefinedRole)
if err != nil {
return err
}
}
}
return nil
}

View File

@@ -1,7 +1,7 @@
package role
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
@@ -14,18 +14,18 @@ const (
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -34,7 +34,7 @@ func (service *Service) Role(ID portainer.RoleID) (*portainer.Role, error) {
var set portainer.Role
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &set)
err := internal.GetObject(service.connection, BucketName, identifier, &set)
if err != nil {
return nil, err
}
@@ -46,7 +46,7 @@ func (service *Service) Role(ID portainer.RoleID) (*portainer.Role, error) {
func (service *Service) Roles() ([]portainer.Role, error) {
var sets = make([]portainer.Role, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -67,11 +67,13 @@ func (service *Service) Roles() ([]portainer.Role, error) {
// CreateRole creates a new Role.
func (service *Service) CreateRole(role *portainer.Role) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
role.ID = portainer.RoleID(id)
if role.ID == 0 {
role.ID = portainer.RoleID(id)
}
data, err := internal.MarshalObject(role)
if err != nil {
@@ -85,5 +87,5 @@ func (service *Service) CreateRole(role *portainer.Role) error {
// UpdateRole updates a role.
func (service *Service) UpdateRole(ID portainer.RoleID, role *portainer.Role) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, role)
return internal.UpdateObject(service.connection, BucketName, identifier, role)
}

View File

@@ -0,0 +1,66 @@
package s3backup
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/internal"
)
const (
bucketName = "s3backup"
statusKey = "lastRunStatus"
settingsKey = "settings"
)
type Service struct {
connection *internal.DbConnection
}
// NewService creates a new service and ensures corresponding bucket exist
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, bucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
}
// GetStatus returns the status of the last scheduled backup run
func (s *Service) GetStatus() (portainer.S3BackupStatus, error) {
var status portainer.S3BackupStatus
err := internal.GetObject(s.connection, bucketName, []byte(statusKey), &status)
if err == errors.ErrObjectNotFound {
return status, nil
}
return status, err
}
// DropStatus deletes the status of the last sheduled backup run
func (s *Service) DropStatus() error {
return internal.DeleteObject(s.connection, bucketName, []byte(statusKey))
}
// UpdateStatus upserts a status of the last scheduled backup run
func (s *Service) UpdateStatus(status portainer.S3BackupStatus) error {
return internal.UpdateObject(s.connection, bucketName, []byte(statusKey), status)
}
// UpdateSettings updates stored s3 backup settings
func (s *Service) UpdateSettings(settings portainer.S3BackupSettings) error {
return internal.UpdateObject(s.connection, bucketName, []byte(settingsKey), settings)
}
// GetSettings returns stored s3 backup settings
func (s *Service) GetSettings() (portainer.S3BackupSettings, error) {
var settings portainer.S3BackupSettings
err := internal.GetObject(s.connection, bucketName, []byte(settingsKey), &settings)
if err == errors.ErrObjectNotFound {
return settings, nil
}
return settings, err
}

View File

@@ -1,7 +1,7 @@
package schedule
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
@@ -14,18 +14,18 @@ const (
// Service represents a service for managing schedule data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -34,7 +34,7 @@ func (service *Service) Schedule(ID portainer.ScheduleID) (*portainer.Schedule,
var schedule portainer.Schedule
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &schedule)
err := internal.GetObject(service.connection, BucketName, identifier, &schedule)
if err != nil {
return nil, err
}
@@ -45,20 +45,20 @@ func (service *Service) Schedule(ID portainer.ScheduleID) (*portainer.Schedule,
// UpdateSchedule updates a schedule.
func (service *Service) UpdateSchedule(ID portainer.ScheduleID, schedule *portainer.Schedule) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, schedule)
return internal.UpdateObject(service.connection, BucketName, identifier, schedule)
}
// DeleteSchedule deletes a schedule.
func (service *Service) DeleteSchedule(ID portainer.ScheduleID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}
// Schedules return a array containing all the schedules.
func (service *Service) Schedules() ([]portainer.Schedule, error) {
var schedules = make([]portainer.Schedule, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -82,7 +82,7 @@ func (service *Service) Schedules() ([]portainer.Schedule, error) {
func (service *Service) SchedulesByJobType(jobType portainer.JobType) ([]portainer.Schedule, error) {
var schedules = make([]portainer.Schedule, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -105,7 +105,7 @@ func (service *Service) SchedulesByJobType(jobType portainer.JobType) ([]portain
// CreateSchedule assign an ID to a new schedule and saves it.
func (service *Service) CreateSchedule(schedule *portainer.Schedule) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
// We manually manage sequences for schedules
@@ -125,5 +125,5 @@ func (service *Service) CreateSchedule(schedule *portainer.Schedule) error {
// GetNextIdentifier returns the next identifier for a schedule.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
return internal.GetNextIdentifier(service.connection, BucketName)
}

287
api/bolt/services.go Normal file
View File

@@ -0,0 +1,287 @@
package bolt
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/customtemplate"
"github.com/portainer/portainer/api/bolt/dockerhub"
"github.com/portainer/portainer/api/bolt/edgegroup"
"github.com/portainer/portainer/api/bolt/edgejob"
"github.com/portainer/portainer/api/bolt/edgestack"
"github.com/portainer/portainer/api/bolt/endpoint"
"github.com/portainer/portainer/api/bolt/endpointgroup"
"github.com/portainer/portainer/api/bolt/endpointrelation"
"github.com/portainer/portainer/api/bolt/extension"
"github.com/portainer/portainer/api/bolt/license"
"github.com/portainer/portainer/api/bolt/registry"
"github.com/portainer/portainer/api/bolt/resourcecontrol"
"github.com/portainer/portainer/api/bolt/role"
"github.com/portainer/portainer/api/bolt/s3backup"
"github.com/portainer/portainer/api/bolt/schedule"
"github.com/portainer/portainer/api/bolt/settings"
"github.com/portainer/portainer/api/bolt/stack"
"github.com/portainer/portainer/api/bolt/tag"
"github.com/portainer/portainer/api/bolt/team"
"github.com/portainer/portainer/api/bolt/teammembership"
"github.com/portainer/portainer/api/bolt/tunnelserver"
"github.com/portainer/portainer/api/bolt/user"
"github.com/portainer/portainer/api/bolt/version"
"github.com/portainer/portainer/api/bolt/webhook"
)
func (store *Store) initServices() error {
authorizationsetService, err := role.NewService(store.connection)
if err != nil {
return err
}
store.RoleService = authorizationsetService
customTemplateService, err := customtemplate.NewService(store.connection)
if err != nil {
return err
}
store.CustomTemplateService = customTemplateService
dockerhubService, err := dockerhub.NewService(store.connection)
if err != nil {
return err
}
store.DockerHubService = dockerhubService
edgeStackService, err := edgestack.NewService(store.connection)
if err != nil {
return err
}
store.EdgeStackService = edgeStackService
edgeGroupService, err := edgegroup.NewService(store.connection)
if err != nil {
return err
}
store.EdgeGroupService = edgeGroupService
edgeJobService, err := edgejob.NewService(store.connection)
if err != nil {
return err
}
store.EdgeJobService = edgeJobService
endpointgroupService, err := endpointgroup.NewService(store.connection)
if err != nil {
return err
}
store.EndpointGroupService = endpointgroupService
endpointService, err := endpoint.NewService(store.connection)
if err != nil {
return err
}
store.EndpointService = endpointService
endpointRelationService, err := endpointrelation.NewService(store.connection)
if err != nil {
return err
}
store.EndpointRelationService = endpointRelationService
extensionService, err := extension.NewService(store.connection)
if err != nil {
return err
}
store.ExtensionService = extensionService
licenseService, err := license.NewService(store.connection)
if err != nil {
return err
}
store.LicenseService = licenseService
registryService, err := registry.NewService(store.connection)
if err != nil {
return err
}
store.RegistryService = registryService
resourcecontrolService, err := resourcecontrol.NewService(store.connection)
if err != nil {
return err
}
store.ResourceControlService = resourcecontrolService
s3backupService, err := s3backup.NewService(store.connection)
if err != nil {
return nil
}
store.S3BackupService = s3backupService
settingsService, err := settings.NewService(store.connection)
if err != nil {
return err
}
store.SettingsService = settingsService
stackService, err := stack.NewService(store.connection)
if err != nil {
return err
}
store.StackService = stackService
tagService, err := tag.NewService(store.connection)
if err != nil {
return err
}
store.TagService = tagService
teammembershipService, err := teammembership.NewService(store.connection)
if err != nil {
return err
}
store.TeamMembershipService = teammembershipService
teamService, err := team.NewService(store.connection)
if err != nil {
return err
}
store.TeamService = teamService
tunnelServerService, err := tunnelserver.NewService(store.connection)
if err != nil {
return err
}
store.TunnelServerService = tunnelServerService
userService, err := user.NewService(store.connection)
if err != nil {
return err
}
store.UserService = userService
versionService, err := version.NewService(store.connection)
if err != nil {
return err
}
store.VersionService = versionService
webhookService, err := webhook.NewService(store.connection)
if err != nil {
return err
}
store.WebhookService = webhookService
scheduleService, err := schedule.NewService(store.connection)
if err != nil {
return err
}
store.ScheduleService = scheduleService
return nil
}
// CustomTemplate gives access to the CustomTemplate data management layer
func (store *Store) CustomTemplate() portainer.CustomTemplateService {
return store.CustomTemplateService
}
// DockerHub gives access to the DockerHub data management layer
func (store *Store) DockerHub() portainer.DockerHubService {
return store.DockerHubService
}
// EdgeGroup gives access to the EdgeGroup data management layer
func (store *Store) EdgeGroup() portainer.EdgeGroupService {
return store.EdgeGroupService
}
// EdgeJob gives access to the EdgeJob data management layer
func (store *Store) EdgeJob() portainer.EdgeJobService {
return store.EdgeJobService
}
// EdgeStack gives access to the EdgeStack data management layer
func (store *Store) EdgeStack() portainer.EdgeStackService {
return store.EdgeStackService
}
// Endpoint gives access to the Endpoint data management layer
func (store *Store) Endpoint() portainer.EndpointService {
return store.EndpointService
}
// EndpointGroup gives access to the EndpointGroup data management layer
func (store *Store) EndpointGroup() portainer.EndpointGroupService {
return store.EndpointGroupService
}
// EndpointRelation gives access to the EndpointRelation data management layer
func (store *Store) EndpointRelation() portainer.EndpointRelationService {
return store.EndpointRelationService
}
// License provides access to the License data management layer
func (store *Store) License() portainer.LicenseRepository {
return store.LicenseService
}
// Registry gives access to the Registry data management layer
func (store *Store) Registry() portainer.RegistryService {
return store.RegistryService
}
// ResourceControl gives access to the ResourceControl data management layer
func (store *Store) ResourceControl() portainer.ResourceControlService {
return store.ResourceControlService
}
// Role gives access to the Role data management layer
func (store *Store) Role() portainer.RoleService {
return store.RoleService
}
// S3Backup gives access to S3 backup settings and status
func (store *Store) S3Backup() portainer.S3BackupService {
return store.S3BackupService
}
// Settings gives access to the Settings data management layer
func (store *Store) Settings() portainer.SettingsService {
return store.SettingsService
}
// Stack gives access to the Stack data management layer
func (store *Store) Stack() portainer.StackService {
return store.StackService
}
// Tag gives access to the Tag data management layer
func (store *Store) Tag() portainer.TagService {
return store.TagService
}
// TeamMembership gives access to the TeamMembership data management layer
func (store *Store) TeamMembership() portainer.TeamMembershipService {
return store.TeamMembershipService
}
// Team gives access to the Team data management layer
func (store *Store) Team() portainer.TeamService {
return store.TeamService
}
// TunnelServer gives access to the TunnelServer data management layer
func (store *Store) TunnelServer() portainer.TunnelServerService {
return store.TunnelServerService
}
// User gives access to the User data management layer
func (store *Store) User() portainer.UserService {
return store.UserService
}
// Version gives access to the Version data management layer
func (store *Store) Version() portainer.VersionService {
return store.VersionService
}
// Webhook gives access to the Webhook data management layer
func (store *Store) Webhook() portainer.WebhookService {
return store.WebhookService
}

View File

@@ -1,10 +1,8 @@
package settings
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
)
const (
@@ -15,18 +13,18 @@ const (
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -34,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) Settings() (*portainer.Settings, error) {
var settings portainer.Settings
err := internal.GetObject(service.db, BucketName, []byte(settingsKey), &settings)
err := internal.GetObject(service.connection, BucketName, []byte(settingsKey), &settings)
if err != nil {
return nil, err
}
@@ -44,5 +42,5 @@ func (service *Service) Settings() (*portainer.Settings, error) {
// UpdateSettings persists a Settings object.
func (service *Service) UpdateSettings(settings *portainer.Settings) error {
return internal.UpdateObject(service.db, BucketName, []byte(settingsKey), settings)
return internal.UpdateObject(service.connection, BucketName, []byte(settingsKey), settings)
}

View File

@@ -1,7 +1,7 @@
package stack
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/internal"
@@ -15,18 +15,18 @@ const (
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -35,7 +35,7 @@ func (service *Service) Stack(ID portainer.StackID) (*portainer.Stack, error) {
var stack portainer.Stack
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &stack)
err := internal.GetObject(service.connection, BucketName, identifier, &stack)
if err != nil {
return nil, err
}
@@ -47,7 +47,7 @@ func (service *Service) Stack(ID portainer.StackID) (*portainer.Stack, error) {
func (service *Service) StackByName(name string) (*portainer.Stack, error) {
var stack *portainer.Stack
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -78,7 +78,7 @@ func (service *Service) StackByName(name string) (*portainer.Stack, error) {
func (service *Service) Stacks() ([]portainer.Stack, error) {
var stacks = make([]portainer.Stack, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -99,12 +99,12 @@ func (service *Service) Stacks() ([]portainer.Stack, error) {
// GetNextIdentifier returns the next identifier for a stack.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
return internal.GetNextIdentifier(service.connection, BucketName)
}
// CreateStack creates a new stack.
func (service *Service) CreateStack(stack *portainer.Stack) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
// We manually manage sequences for stacks
@@ -125,11 +125,11 @@ func (service *Service) CreateStack(stack *portainer.Stack) error {
// UpdateStack updates a stack.
func (service *Service) UpdateStack(ID portainer.StackID, stack *portainer.Stack) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, stack)
return internal.UpdateObject(service.connection, BucketName, identifier, stack)
}
// DeleteStack deletes a stack.
func (service *Service) DeleteStack(ID portainer.StackID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}

View File

@@ -1,7 +1,7 @@
package tag
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
@@ -14,18 +14,18 @@ const (
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -33,7 +33,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) Tags() ([]portainer.Tag, error) {
var tags = make([]portainer.Tag, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -57,7 +57,7 @@ func (service *Service) Tag(ID portainer.TagID) (*portainer.Tag, error) {
var tag portainer.Tag
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &tag)
err := internal.GetObject(service.connection, BucketName, identifier, &tag)
if err != nil {
return nil, err
}
@@ -67,7 +67,7 @@ func (service *Service) Tag(ID portainer.TagID) (*portainer.Tag, error) {
// CreateTag creates a new tag.
func (service *Service) CreateTag(tag *portainer.Tag) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
@@ -85,11 +85,11 @@ func (service *Service) CreateTag(tag *portainer.Tag) error {
// UpdateTag updates a tag.
func (service *Service) UpdateTag(ID portainer.TagID, tag *portainer.Tag) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, tag)
return internal.UpdateObject(service.connection, BucketName, identifier, tag)
}
// DeleteTag deletes a tag.
func (service *Service) DeleteTag(ID portainer.TagID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}

View File

@@ -1,11 +1,11 @@
package team
import (
"github.com/portainer/portainer/api"
"github.com/boltdb/bolt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
"strings"
)
const (
@@ -15,18 +15,18 @@ const (
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -35,7 +35,7 @@ func (service *Service) Team(ID portainer.TeamID) (*portainer.Team, error) {
var team portainer.Team
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &team)
err := internal.GetObject(service.connection, BucketName, identifier, &team)
if err != nil {
return nil, err
}
@@ -47,7 +47,7 @@ func (service *Service) Team(ID portainer.TeamID) (*portainer.Team, error) {
func (service *Service) TeamByName(name string) (*portainer.Team, error) {
var team *portainer.Team
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -58,7 +58,7 @@ func (service *Service) TeamByName(name string) (*portainer.Team, error) {
return err
}
if t.Name == name {
if strings.EqualFold(t.Name, name) {
team = &t
break
}
@@ -78,7 +78,7 @@ func (service *Service) TeamByName(name string) (*portainer.Team, error) {
func (service *Service) Teams() ([]portainer.Team, error) {
var teams = make([]portainer.Team, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -100,12 +100,12 @@ func (service *Service) Teams() ([]portainer.Team, error) {
// UpdateTeam saves a Team.
func (service *Service) UpdateTeam(ID portainer.TeamID, team *portainer.Team) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, team)
return internal.UpdateObject(service.connection, BucketName, identifier, team)
}
// CreateTeam creates a new Team.
func (service *Service) CreateTeam(team *portainer.Team) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
@@ -123,5 +123,5 @@ func (service *Service) CreateTeam(team *portainer.Team) error {
// DeleteTeam deletes a Team.
func (service *Service) DeleteTeam(ID portainer.TeamID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}

View File

@@ -1,7 +1,7 @@
package teammembership
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
@@ -14,18 +14,18 @@ const (
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -34,7 +34,7 @@ func (service *Service) TeamMembership(ID portainer.TeamMembershipID) (*portaine
var membership portainer.TeamMembership
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &membership)
err := internal.GetObject(service.connection, BucketName, identifier, &membership)
if err != nil {
return nil, err
}
@@ -46,7 +46,7 @@ func (service *Service) TeamMembership(ID portainer.TeamMembershipID) (*portaine
func (service *Service) TeamMemberships() ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -69,7 +69,7 @@ func (service *Service) TeamMemberships() ([]portainer.TeamMembership, error) {
func (service *Service) TeamMembershipsByUserID(userID portainer.UserID) ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -95,7 +95,7 @@ func (service *Service) TeamMembershipsByUserID(userID portainer.UserID) ([]port
func (service *Service) TeamMembershipsByTeamID(teamID portainer.TeamID) ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -120,12 +120,12 @@ func (service *Service) TeamMembershipsByTeamID(teamID portainer.TeamID) ([]port
// UpdateTeamMembership saves a TeamMembership object.
func (service *Service) UpdateTeamMembership(ID portainer.TeamMembershipID, membership *portainer.TeamMembership) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, membership)
return internal.UpdateObject(service.connection, BucketName, identifier, membership)
}
// CreateTeamMembership creates a new TeamMembership object.
func (service *Service) CreateTeamMembership(membership *portainer.TeamMembership) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
@@ -143,12 +143,12 @@ func (service *Service) CreateTeamMembership(membership *portainer.TeamMembershi
// DeleteTeamMembership deletes a TeamMembership object.
func (service *Service) DeleteTeamMembership(ID portainer.TeamMembershipID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}
// DeleteTeamMembershipByUserID deletes all the TeamMembership object associated to a UserID.
func (service *Service) DeleteTeamMembershipByUserID(userID portainer.UserID) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -173,7 +173,7 @@ func (service *Service) DeleteTeamMembershipByUserID(userID portainer.UserID) er
// DeleteTeamMembershipByTeamID deletes all the TeamMembership object associated to a TeamID.
func (service *Service) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()

View File

@@ -1,10 +1,8 @@
package tunnelserver
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
)
const (
@@ -15,18 +13,18 @@ const (
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -34,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) Info() (*portainer.TunnelServerInfo, error) {
var info portainer.TunnelServerInfo
err := internal.GetObject(service.db, BucketName, []byte(infoKey), &info)
err := internal.GetObject(service.connection, BucketName, []byte(infoKey), &info)
if err != nil {
return nil, err
}
@@ -44,5 +42,5 @@ func (service *Service) Info() (*portainer.TunnelServerInfo, error) {
// UpdateInfo persists a TunnelServerInfo object.
func (service *Service) UpdateInfo(settings *portainer.TunnelServerInfo) error {
return internal.UpdateObject(service.db, BucketName, []byte(infoKey), settings)
return internal.UpdateObject(service.connection, BucketName, []byte(infoKey), settings)
}

View File

@@ -1,11 +1,11 @@
package user
import (
"github.com/portainer/portainer/api"
"github.com/boltdb/bolt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
"strings"
)
const (
@@ -15,18 +15,18 @@ const (
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -35,7 +35,7 @@ func (service *Service) User(ID portainer.UserID) (*portainer.User, error) {
var user portainer.User
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &user)
err := internal.GetObject(service.connection, BucketName, identifier, &user)
if err != nil {
return nil, err
}
@@ -47,7 +47,9 @@ func (service *Service) User(ID portainer.UserID) (*portainer.User, error) {
func (service *Service) UserByUsername(username string) (*portainer.User, error) {
var user *portainer.User
err := service.db.View(func(tx *bolt.Tx) error {
username = strings.ToLower(username)
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -58,7 +60,7 @@ func (service *Service) UserByUsername(username string) (*portainer.User, error)
return err
}
if u.Username == username {
if strings.EqualFold(u.Username, username) {
user = &u
break
}
@@ -77,7 +79,7 @@ func (service *Service) UserByUsername(username string) (*portainer.User, error)
func (service *Service) Users() ([]portainer.User, error) {
var users = make([]portainer.User, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -99,7 +101,7 @@ func (service *Service) Users() ([]portainer.User, error) {
// UsersByRole return an array containing all the users with the specified role.
func (service *Service) UsersByRole(role portainer.UserRole) ([]portainer.User, error) {
var users = make([]portainer.User, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -123,16 +125,18 @@ func (service *Service) UsersByRole(role portainer.UserRole) ([]portainer.User,
// UpdateUser saves a user.
func (service *Service) UpdateUser(ID portainer.UserID, user *portainer.User) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, user)
user.Username = strings.ToLower(user.Username)
return internal.UpdateObject(service.connection, BucketName, identifier, user)
}
// CreateUser creates a new user.
func (service *Service) CreateUser(user *portainer.User) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
user.ID = portainer.UserID(id)
user.Username = strings.ToLower(user.Username)
data, err := internal.MarshalObject(user)
if err != nil {
@@ -146,5 +150,5 @@ func (service *Service) CreateUser(user *portainer.User) error {
// DeleteUser deletes a user.
func (service *Service) DeleteUser(ID portainer.UserID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}

View File

@@ -4,76 +4,109 @@ import (
"strconv"
"github.com/boltdb/bolt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "version"
versionKey = "DB_VERSION"
instanceKey = "INSTANCE_ID"
BucketName = "version"
versionKey = "DB_VERSION"
previousVersionKey = "PREVIOUS_DB_VERSION"
instanceKey = "INSTANCE_ID"
editionKey = "EDITION"
)
// Service represents a service to manage stored versions.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
// DBVersion retrieves the stored database version.
func (service *Service) DBVersion() (int, error) {
var data []byte
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
value := bucket.Get([]byte(versionKey))
if value == nil {
return errors.ErrObjectNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
// Edition retrieves the stored portainer edition.
func (service *Service) Edition() (portainer.SoftwareEdition, error) {
editionData, err := service.getKey(editionKey)
if err != nil {
return 0, err
}
return strconv.Atoi(string(data))
edition, err := strconv.Atoi(string(editionData))
if err != nil {
return 0, err
}
return portainer.SoftwareEdition(edition), nil
}
// StoreEdition store the portainer edition.
func (service *Service) StoreEdition(edition portainer.SoftwareEdition) error {
return service.setKey(editionKey, strconv.Itoa(int(edition)))
}
// PreviousDBVersion retrieves the stored database version.
func (service *Service) PreviousDBVersion() (int, error) {
version, err := service.getKey(previousVersionKey)
if err != nil {
return 0, err
}
return strconv.Atoi(string(version))
}
// DBVersion retrieves the stored database version.
func (service *Service) DBVersion() (int, error) {
version, err := service.getKey(versionKey)
if err != nil {
return 0, err
}
return strconv.Atoi(string(version))
}
// StorePreviousDBVersion store the database version.
func (service *Service) StorePreviousDBVersion(version int) error {
return service.setKey(previousVersionKey, strconv.Itoa(version))
}
// StoreDBVersion store the database version.
func (service *Service) StoreDBVersion(version int) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data := []byte(strconv.Itoa(version))
return bucket.Put([]byte(versionKey), data)
})
return service.setKey(versionKey, strconv.Itoa(version))
}
// InstanceID retrieves the stored instance ID.
func (service *Service) InstanceID() (string, error) {
instanceID, err := service.getKey(instanceKey)
if err != nil {
return "", err
}
return string(instanceID), nil
}
// StoreInstanceID store the instance ID.
func (service *Service) StoreInstanceID(ID string) error {
return service.setKey(instanceKey, ID)
}
func (service *Service) getKey(key string) ([]byte, error) {
var data []byte
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
value := bucket.Get([]byte(instanceKey))
value := bucket.Get([]byte(key))
if value == nil {
return errors.ErrObjectNotFound
}
@@ -83,19 +116,19 @@ func (service *Service) InstanceID() (string, error) {
return nil
})
if err != nil {
return "", err
return nil, err
}
return string(data), nil
return data, nil
}
// StoreInstanceID store the instance ID.
func (service *Service) StoreInstanceID(ID string) error {
return service.db.Update(func(tx *bolt.Tx) error {
func (service *Service) setKey(key string, value string) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data := []byte(ID)
return bucket.Put([]byte(instanceKey), data)
data := []byte(value)
return bucket.Put([]byte(key), data)
})
}

View File

@@ -1,7 +1,7 @@
package webhook
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/internal"
@@ -15,18 +15,18 @@ const (
// Service represents a service for managing webhook data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -34,7 +34,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) Webhooks() ([]portainer.Webhook, error) {
var webhooks = make([]portainer.Webhook, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -58,7 +58,7 @@ func (service *Service) Webhook(ID portainer.WebhookID) (*portainer.Webhook, err
var webhook portainer.Webhook
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &webhook)
err := internal.GetObject(service.connection, BucketName, identifier, &webhook)
if err != nil {
return nil, err
}
@@ -70,7 +70,7 @@ func (service *Service) Webhook(ID portainer.WebhookID) (*portainer.Webhook, err
func (service *Service) WebhookByResourceID(ID string) (*portainer.Webhook, error) {
var webhook *portainer.Webhook
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -101,7 +101,7 @@ func (service *Service) WebhookByResourceID(ID string) (*portainer.Webhook, erro
func (service *Service) WebhookByToken(token string) (*portainer.Webhook, error) {
var webhook *portainer.Webhook
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -131,12 +131,12 @@ func (service *Service) WebhookByToken(token string) (*portainer.Webhook, error)
// DeleteWebhook deletes a webhook.
func (service *Service) DeleteWebhook(ID portainer.WebhookID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}
// CreateWebhook assign an ID to a new webhook and saves it.
func (service *Service) CreateWebhook(webhook *portainer.Webhook) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()

View File

@@ -1,6 +1,7 @@
package chisel
import (
"context"
"fmt"
"log"
"strconv"
@@ -9,7 +10,7 @@ import (
"github.com/dchest/uniuri"
chserver "github.com/jpillora/chisel/server"
cmap "github.com/orcaman/concurrent-map"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
)
@@ -29,13 +30,15 @@ type Service struct {
dataStore portainer.DataStore
snapshotService portainer.SnapshotService
chiselServer *chserver.Server
shutdownCtx context.Context
}
// NewService returns a pointer to a new instance of Service
func NewService(dataStore portainer.DataStore) *Service {
func NewService(dataStore portainer.DataStore, shutdownCtx context.Context) *Service {
return &Service{
tunnelDetailsMap: cmap.New(),
dataStore: dataStore,
shutdownCtx: shutdownCtx,
}
}
@@ -83,6 +86,11 @@ func (service *Service) StartTunnelServer(addr, port string, snapshotService por
return nil
}
// StopTunnelServer stops tunnel http server
func (service *Service) StopTunnelServer() error {
return service.chiselServer.Close()
}
func (service *Service) retrievePrivateKeySeed() (string, error) {
var serverInfo *portainer.TunnelServerInfo
@@ -108,13 +116,16 @@ func (service *Service) retrievePrivateKeySeed() (string, error) {
func (service *Service) startTunnelVerificationLoop() {
log.Printf("[DEBUG] [chisel, monitoring] [check_interval_seconds: %f] [message: starting tunnel management process]", tunnelCleanupInterval.Seconds())
ticker := time.NewTicker(tunnelCleanupInterval)
stopSignal := make(chan struct{})
for {
select {
case <-ticker.C:
service.checkTunnels()
case <-stopSignal:
case <-service.shutdownCtx.Done():
log.Println("[DEBUG] Shutting down tunnel service")
if err := service.StopTunnelServer(); err != nil {
log.Printf("Stopped tunnel service: %s", err)
}
ticker.Stop()
return
}

View File

@@ -5,7 +5,7 @@ import (
"log"
"time"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"os"
"path/filepath"
@@ -42,6 +42,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(),
TLSKey: kingpin.Flag("tlskey", "Path to the TLS key").Default(defaultTLSKeyPath).String(),
RollbackToCE: kingpin.Flag("rollback-to-ce", "Rollback the database store to CE").Bool(),
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL").Default(defaultSSL).Bool(),
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").Default(defaultSSLCertPath).String(),
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").Default(defaultSSLKeyPath).String(),

24
api/cli/confirm.go Normal file
View File

@@ -0,0 +1,24 @@
package cli
import (
"bufio"
"log"
"os"
"strings"
)
// Confirm starts a rollback db cli application
func Confirm(message string) (bool, error) {
log.Printf("%s [y/N]", message)
reader := bufio.NewReader(os.Stdin)
answer, err := reader.ReadString('\n')
if err != nil {
return false, err
}
answer = strings.Replace(answer, "\n", "", -1)
answer = strings.ToLower(answer)
return answer == "y" || answer == "yes", nil
}

View File

@@ -1,86 +1,119 @@
package main
import (
"context"
"log"
"os"
"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"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/exec"
"github.com/portainer/portainer/api/filesystem"
"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/authorization"
"github.com/portainer/portainer/api/internal/edge"
"github.com/portainer/portainer/api/internal/snapshot"
"github.com/portainer/portainer/api/jwt"
"github.com/portainer/portainer/api/kubernetes"
kubecli "github.com/portainer/portainer/api/kubernetes/cli"
"github.com/portainer/portainer/api/ldap"
"github.com/portainer/portainer/api/libcompose"
"github.com/portainer/portainer/api/license"
"github.com/portainer/portainer/api/oauth"
"github.com/portainer/portainer/api/useractivity"
)
func initCLI() *portainer.CLIFlags {
var cliService portainer.CLIService = &cli.Service{}
flags, err := cliService.ParseFlags(portainer.APIVersion)
if err != nil {
log.Fatal(err)
log.Fatalf("failed parsing flags: %s", err)
}
err = cliService.ValidateFlags(flags)
if err != nil {
log.Fatal(err)
log.Fatalf("failed validating flags:%s", err)
}
return flags
}
func initUserActivityStore(dataStorePath string) portainer.UserActivityStore {
store, err := useractivity.NewUserActivityStore(dataStorePath)
if err != nil {
log.Fatalf("Failed initalizing user activity store: %s", err)
}
return store
}
func initFileService(dataStorePath string) portainer.FileService {
fileService, err := filesystem.NewService(dataStorePath, "")
if err != nil {
log.Fatal(err)
log.Fatalf("failed creating file service: %s", err)
}
return fileService
}
func initDataStore(dataStorePath string, fileService portainer.FileService) portainer.DataStore {
func initDataStore(dataStorePath string, rollback bool, fileService portainer.FileService) portainer.DataStore {
store, err := bolt.NewStore(dataStorePath, fileService)
if err != nil {
log.Fatal(err)
log.Fatalf("failed creating data store: %s", err)
}
err = store.Open()
if err != nil {
log.Fatal(err)
log.Fatalf("failed opening store: %s", err)
}
err = store.Init()
if err != nil {
log.Fatal(err)
log.Fatalf("failed initializing data store: %s", err)
}
err = store.MigrateData()
if rollback {
err := store.RollbackToCE()
if err != nil {
log.Fatalf("failed rolling back to CE: %s", err)
}
log.Println("Exiting rollback")
os.Exit(0)
return nil
}
err = store.MigrateData(false)
if err != nil {
log.Fatal(err)
log.Fatalf("failed migration: %s", err)
}
return store
}
func initComposeStackManager(dataStorePath string, reverseTunnelService portainer.ReverseTunnelService) portainer.ComposeStackManager {
return libcompose.NewComposeStackManager(dataStorePath, reverseTunnelService)
func initComposeStackManager(assetsPath string, dataStorePath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
composeWrapper, err := exec.NewComposeStackManager(assetsPath, dataStorePath, proxyManager)
if err != nil {
log.Printf("[INFO] [main,compose] [message: falling-back to libcompose] [error: %s]", err)
return libcompose.NewComposeStackManager(dataStorePath, reverseTunnelService)
}
return composeWrapper
}
func initSwarmStackManager(assetsPath string, dataStorePath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) (portainer.SwarmStackManager, error) {
return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService, reverseTunnelService)
}
func initKubernetesDeployer(assetsPath string) portainer.KubernetesDeployer {
return exec.NewKubernetesDeployer(assetsPath)
func initKubernetesDeployer(dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, assetsPath string) portainer.KubernetesDeployer {
return exec.NewKubernetesDeployer(dataStore, reverseTunnelService, signatureService, assetsPath)
}
func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error) {
@@ -89,7 +122,11 @@ func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error)
return nil, err
}
jwtService, err := jwt.NewService(settings.UserSessionTimeout)
userSessionTimeout := settings.UserSessionTimeout
if userSessionTimeout == "" {
userSessionTimeout = portainer.DefaultUserSessionTimeout
}
jwtService, err := jwt.NewService(userSessionTimeout)
if err != nil {
return nil, err
}
@@ -124,11 +161,11 @@ func initKubernetesClientFactory(signatureService portainer.DigitalSignatureServ
return kubecli.NewClientFactory(signatureService, reverseTunnelService, instanceID)
}
func initSnapshotService(snapshotInterval string, dataStore portainer.DataStore, dockerClientFactory *docker.ClientFactory, kubernetesClientFactory *kubecli.ClientFactory) (portainer.SnapshotService, error) {
func initSnapshotService(snapshotInterval string, dataStore portainer.DataStore, dockerClientFactory *docker.ClientFactory, kubernetesClientFactory *kubecli.ClientFactory, shutdownCtx context.Context) (portainer.SnapshotService, error) {
dockerSnapshotter := docker.NewSnapshotter(dockerClientFactory)
kubernetesSnapshotter := kubernetes.NewSnapshotter(kubernetesClientFactory)
snapshotService, err := snapshot.NewService(snapshotInterval, dataStore, dockerSnapshotter, kubernetesSnapshotter)
snapshotService, err := snapshot.NewService(snapshotInterval, dataStore, dockerSnapshotter, kubernetesSnapshotter, shutdownCtx)
if err != nil {
return nil, err
}
@@ -136,21 +173,6 @@ func initSnapshotService(snapshotInterval string, dataStore portainer.DataStore,
return snapshotService, nil
}
func loadEdgeJobsFromDatabase(dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService) error {
edgeJobs, err := dataStore.EdgeJob().EdgeJobs()
if err != nil {
return err
}
for _, edgeJob := range edgeJobs {
for endpointID := range edgeJob.Endpoints {
reverseTunnelService.AddEdgeJob(endpointID, &edgeJob)
}
}
return nil
}
func initStatus(flags *portainer.CLIFlags) *portainer.Status {
return &portainer.Status{
Version: portainer.APIVersion,
@@ -167,6 +189,8 @@ func updateSettingsFromFlags(dataStore portainer.DataStore, flags *portainer.CLI
settings.SnapshotInterval = *flags.SnapshotInterval
settings.EnableEdgeComputeFeatures = *flags.EnableEdgeComputeFeatures
settings.EnableTelemetry = true
settings.OAuthSettings.SSO = true
settings.OAuthSettings.HideInternalAuth = true
if *flags.Templates != "" {
settings.TemplatesURL = *flags.Templates
@@ -199,7 +223,7 @@ func generateAndStoreKeyPair(fileService portainer.FileService, signatureService
func initKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
existingKeyPair, err := fileService.KeyPairFilesExist()
if err != nil {
log.Fatal(err)
log.Fatalf("failed checking for existing key pair: %s", err)
}
if existingKeyPair {
@@ -237,6 +261,19 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore portainer.Dat
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.DockerSnapshot{},
Kubernetes: portainer.KubernetesDefault(),
SecuritySettings: portainer.EndpointSecuritySettings{
AllowVolumeBrowserForRegularUsers: false,
EnableHostManagementFeatures: false,
AllowSysctlSettingForRegularUsers: true,
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowHostNamespaceForRegularUsers: true,
AllowContainerCapabilitiesForRegularUsers: true,
AllowDeviceMappingForRegularUsers: true,
AllowStackManagementForRegularUsers: true,
},
}
if strings.HasPrefix(endpoint.URL, "tcp://") {
@@ -286,6 +323,19 @@ func createUnsecuredEndpoint(endpointURL string, dataStore portainer.DataStore,
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.DockerSnapshot{},
Kubernetes: portainer.KubernetesDefault(),
SecuritySettings: portainer.EndpointSecuritySettings{
AllowVolumeBrowserForRegularUsers: false,
EnableHostManagementFeatures: false,
AllowSysctlSettingForRegularUsers: true,
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowHostNamespaceForRegularUsers: true,
AllowContainerCapabilitiesForRegularUsers: true,
AllowDeviceMappingForRegularUsers: true,
AllowStackManagementForRegularUsers: true,
},
}
err := snapshotService.SnapshotEndpoint(endpoint)
@@ -317,32 +367,21 @@ func initEndpoint(flags *portainer.CLIFlags, dataStore portainer.DataStore, snap
return createUnsecuredEndpoint(*flags.EndpointURL, dataStore, snapshotService)
}
func terminateIfNoAdminCreated(dataStore portainer.DataStore) {
timer1 := time.NewTimer(5 * time.Minute)
<-timer1.C
users, err := dataStore.User().UsersByRole(portainer.AdministratorRole)
if err != nil {
log.Fatal(err)
}
if len(users) == 0 {
log.Fatal("No administrator account was created after 5 min. Shutting down the Portainer instance for security reasons.")
return
}
}
func main() {
flags := initCLI()
func buildServer(flags *portainer.CLIFlags) portainer.Server {
shutdownCtx, shutdownTrigger := context.WithCancel(context.Background())
fileService := initFileService(*flags.Data)
dataStore := initDataStore(*flags.Data, fileService)
defer dataStore.Close()
dataStore := initDataStore(*flags.Data, *flags.RollbackToCE, fileService)
jwtService, err := initJWTService(dataStore)
if err != nil {
log.Fatal(err)
log.Fatalf("failed initializing JWT service: %s", err)
}
licenseService := license.NewService(dataStore.License(), shutdownCtx)
if err = licenseService.Init(); err != nil {
log.Fatalf("failed initializing license service: %s", err)
}
ldapService := initLDAPService()
@@ -357,62 +396,70 @@ func main() {
err = initKeyPair(fileService, digitalSignatureService)
if err != nil {
log.Fatal(err)
log.Fatalf("failed initializing key pair: %s", err)
}
reverseTunnelService := chisel.NewService(dataStore)
reverseTunnelService := chisel.NewService(dataStore, shutdownCtx)
instanceID, err := dataStore.Version().InstanceID()
if err != nil {
log.Fatal(err)
log.Fatalf("failed to get datastore version: %s", err)
}
dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
kubernetesClientFactory := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, instanceID)
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory)
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx)
if err != nil {
log.Fatal(err)
log.Fatalf("failed initializing snapshot service: %s", err)
}
snapshotService.Start()
authorizationService := authorization.NewService(dataStore)
authorizationService.K8sClientFactory = kubernetesClientFactory
swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService, reverseTunnelService)
if err != nil {
log.Fatal(err)
log.Fatalf("failed initializing swarm stack manager: %s", err)
}
kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()
composeStackManager := initComposeStackManager(*flags.Data, reverseTunnelService)
userActivityStore := initUserActivityStore(*flags.Data)
kubernetesDeployer := initKubernetesDeployer(*flags.Assets)
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, authorizationService, userActivityStore)
composeStackManager := initComposeStackManager(*flags.Assets, *flags.Data, reverseTunnelService, proxyManager)
kubernetesDeployer := initKubernetesDeployer(dataStore, reverseTunnelService, digitalSignatureService, *flags.Assets)
if dataStore.IsNew() {
err = updateSettingsFromFlags(dataStore, flags)
if err != nil {
log.Fatal(err)
log.Fatalf("failed updating settings from flags: %s", err)
}
}
err = loadEdgeJobsFromDatabase(dataStore, reverseTunnelService)
err = edge.LoadEdgeJobs(dataStore, reverseTunnelService)
if err != nil {
log.Fatal(err)
log.Fatalf("failed loading edge jobs from database: %s", err)
}
applicationStatus := initStatus(flags)
err = initEndpoint(flags, dataStore, snapshotService)
if err != nil {
log.Fatal(err)
log.Fatalf("failed initializing endpoint: %s", err)
}
adminPasswordHash := ""
if *flags.AdminPasswordFile != "" {
content, err := fileService.GetFileContent(*flags.AdminPasswordFile)
if err != nil {
log.Fatal(err)
log.Fatalf("failed getting admin password file: %s", err)
}
adminPasswordHash, err = cryptoService.Hash(strings.TrimSuffix(string(content), "\n"))
if err != nil {
log.Fatal(err)
log.Fatalf("failed hashing admin password: %s", err)
}
} else if *flags.AdminPassword != "" {
adminPasswordHash = *flags.AdminPassword
@@ -421,59 +468,75 @@ func main() {
if adminPasswordHash != "" {
users, err := dataStore.User().UsersByRole(portainer.AdministratorRole)
if err != nil {
log.Fatal(err)
log.Fatalf("failed getting admin user: %s", err)
}
if len(users) == 0 {
log.Println("Created admin user with the given password.")
user := &portainer.User{
Username: "admin",
Role: portainer.AdministratorRole,
Password: adminPasswordHash,
Username: "admin",
Role: portainer.AdministratorRole,
Password: adminPasswordHash,
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
}
err := dataStore.User().CreateUser(user)
if err != nil {
log.Fatal(err)
log.Fatalf("failed creating admin user: %s", err)
}
} else {
log.Println("Instance already has an administrator user defined. Skipping admin password related flags.")
}
}
go terminateIfNoAdminCreated(dataStore)
err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService)
if err != nil {
log.Fatal(err)
log.Fatalf("failed starting tunnel server: %s", err)
}
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,
}
log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr)
err = server.Start()
err = licenseService.Start()
if err != nil {
log.Fatal(err)
log.Fatalf("failed starting license service: %s", err)
}
return &http.Server{
AuthorizationService: authorizationService,
ReverseTunnelService: reverseTunnelService,
Status: applicationStatus,
BindAddress: *flags.Addr,
AssetsPath: *flags.Assets,
DataStore: dataStore,
LicenseService: licenseService,
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,
UserActivityStore: userActivityStore,
KubernetesClientFactory: kubernetesClientFactory,
ShutdownCtx: shutdownCtx,
ShutdownTrigger: shutdownTrigger,
}
}
func main() {
flags := initCLI()
for {
server := buildServer(flags)
log.Printf("Starting Portainer %s on %s\n", portainer.APIVersion, *flags.Addr)
err := server.Start()
log.Printf("Http server exited: %s\n", err)
}
}

70
api/crypto/aes.go Normal file
View File

@@ -0,0 +1,70 @@
package crypto
import (
"crypto/aes"
"crypto/cipher"
"io"
"golang.org/x/crypto/scrypt"
)
// NOTE: has to go with what is considered to be a simplistic in that it omits any
// authentication of the encrypted data.
// Person with better knowledge is welcomed to improve it.
// sourced from https://golang.org/src/crypto/cipher/example_test.go
var emptySalt []byte = make([]byte, 0, 0)
// AesEncrypt reads from input, encrypts with AES-256 and writes to the output.
// passphrase is used to generate an encryption key.
func AesEncrypt(input io.Reader, output io.Writer, passphrase []byte) error {
// making a 32 bytes key that would correspond to AES-256
// don't necessarily need a salt, so just kept in empty
key, err := scrypt.Key(passphrase, emptySalt, 32768, 8, 1, 32)
if err != nil {
return err
}
block, err := aes.NewCipher(key)
if err != nil {
return err
}
// If the key is unique for each ciphertext, then it's ok to use a zero
// IV.
var iv [aes.BlockSize]byte
stream := cipher.NewOFB(block, iv[:])
writer := &cipher.StreamWriter{S: stream, W: output}
// Copy the input to the output, encrypting as we go.
if _, err := io.Copy(writer, input); err != nil {
return err
}
return nil
}
// AesDecrypt reads from input, decrypts with AES-256 and returns the reader to a read decrypted content from.
// passphrase is used to generate an encryption key.
func AesDecrypt(input io.Reader, passphrase []byte) (io.Reader, error) {
// making a 32 bytes key that would correspond to AES-256
// don't necessarily need a salt, so just kept in empty
key, err := scrypt.Key(passphrase, emptySalt, 32768, 8, 1, 32)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
// If the key is unique for each ciphertext, then it's ok to use a zero
// IV.
var iv [aes.BlockSize]byte
stream := cipher.NewOFB(block, iv[:])
reader := &cipher.StreamReader{S: stream, R: input}
return reader, nil
}

131
api/crypto/aes_test.go Normal file
View File

@@ -0,0 +1,131 @@
package crypto
import (
"io"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
tmpdir, _ := ioutil.TempDir("", "encrypt")
defer os.RemoveAll(tmpdir)
var (
originFilePath = filepath.Join(tmpdir, "origin")
encryptedFilePath = filepath.Join(tmpdir, "encrypted")
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
)
content := []byte("content")
ioutil.WriteFile(originFilePath, content, 0600)
originFile, _ := os.Open(originFilePath)
defer originFile.Close()
encryptedFileWriter, _ := os.Create(encryptedFilePath)
defer encryptedFileWriter.Close()
err := AesEncrypt(originFile, encryptedFileWriter, []byte("passphrase"))
assert.Nil(t, err, "Failed to encrypt a file")
encryptedContent, err := ioutil.ReadFile(encryptedFilePath)
assert.Nil(t, err, "Couldn't read encrypted file")
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
encryptedFileReader, _ := os.Open(encryptedFilePath)
defer encryptedFileReader.Close()
decryptedFileWriter, _ := os.Create(decryptedFilePath)
defer decryptedFileWriter.Close()
decryptedReader, err := AesDecrypt(encryptedFileReader, []byte("passphrase"))
assert.Nil(t, err, "Failed to decrypt file")
io.Copy(decryptedFileWriter, decryptedReader)
decryptedContent, _ := ioutil.ReadFile(decryptedFilePath)
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
}
func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
tmpdir, _ := ioutil.TempDir("", "encrypt")
defer os.RemoveAll(tmpdir)
var (
originFilePath = filepath.Join(tmpdir, "origin")
encryptedFilePath = filepath.Join(tmpdir, "encrypted")
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
)
content := []byte("content")
ioutil.WriteFile(originFilePath, content, 0600)
originFile, _ := os.Open(originFilePath)
defer originFile.Close()
encryptedFileWriter, _ := os.Create(encryptedFilePath)
defer encryptedFileWriter.Close()
err := AesEncrypt(originFile, encryptedFileWriter, []byte(""))
assert.Nil(t, err, "Failed to encrypt a file")
encryptedContent, err := ioutil.ReadFile(encryptedFilePath)
assert.Nil(t, err, "Couldn't read encrypted file")
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
encryptedFileReader, _ := os.Open(encryptedFilePath)
defer encryptedFileReader.Close()
decryptedFileWriter, _ := os.Create(decryptedFilePath)
defer decryptedFileWriter.Close()
decryptedReader, err := AesDecrypt(encryptedFileReader, []byte(""))
assert.Nil(t, err, "Failed to decrypt file")
io.Copy(decryptedFileWriter, decryptedReader)
decryptedContent, _ := ioutil.ReadFile(decryptedFilePath)
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
}
func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T) {
tmpdir, _ := ioutil.TempDir("", "encrypt")
defer os.RemoveAll(tmpdir)
var (
originFilePath = filepath.Join(tmpdir, "origin")
encryptedFilePath = filepath.Join(tmpdir, "encrypted")
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
)
content := []byte("content")
ioutil.WriteFile(originFilePath, content, 0600)
originFile, _ := os.Open(originFilePath)
defer originFile.Close()
encryptedFileWriter, _ := os.Create(encryptedFilePath)
defer encryptedFileWriter.Close()
err := AesEncrypt(originFile, encryptedFileWriter, []byte("passphrase"))
assert.Nil(t, err, "Failed to encrypt a file")
encryptedContent, err := ioutil.ReadFile(encryptedFilePath)
assert.Nil(t, err, "Couldn't read encrypted file")
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
encryptedFileReader, _ := os.Open(encryptedFilePath)
defer encryptedFileReader.Close()
decryptedFileWriter, _ := os.Create(decryptedFilePath)
defer decryptedFileWriter.Close()
decryptedReader, err := AesDecrypt(encryptedFileReader, []byte("garbage"))
assert.Nil(t, err, "Should allow to decrypt with wrong passphrase")
io.Copy(decryptedFileWriter, decryptedReader)
decryptedContent, _ := ioutil.ReadFile(decryptedFilePath)
assert.NotEqual(t, content, decryptedContent, "Original and decrypted content should NOT match")
}

3
api/dev.sh Executable file
View File

@@ -0,0 +1,3 @@
#! /bin/sh
go run -v -ldflags="-X github.com/portainer/liblicense.LicenseServerBaseURL=http://localhost:8080" cmd/portainer/main.go --data=./tmp/data

View File

@@ -118,6 +118,7 @@ func snapshotNodes(snapshot *portainer.DockerSnapshot, cli *client.Client) error
}
snapshot.TotalCPU = int(nanoCpus / 1e9)
snapshot.TotalMemory = totalMem
snapshot.NodeCount = len(nodes)
return nil
}

120
api/exec/compose_stack.go Normal file
View File

@@ -0,0 +1,120 @@
package exec
import (
"fmt"
"os"
"path"
"strings"
wrapper "github.com/portainer/docker-compose-wrapper"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/proxy/factory"
)
// ComposeStackManager is a wrapper for docker-compose binary
type ComposeStackManager struct {
wrapper *wrapper.ComposeWrapper
dataPath string
proxyManager *proxy.Manager
}
// NewComposeStackManager returns a docker-compose wrapper if corresponding binary present, otherwise nil
func NewComposeStackManager(binaryPath string, dataPath string, proxyManager *proxy.Manager) (*ComposeStackManager, error) {
wrap, err := wrapper.NewComposeWrapper(binaryPath)
if err != nil {
return nil, err
}
return &ComposeStackManager{
wrapper: wrap,
proxyManager: proxyManager,
dataPath: dataPath,
}, nil
}
// ComposeSyntaxMaxVersion returns the maximum supported version of the docker compose syntax
func (w *ComposeStackManager) ComposeSyntaxMaxVersion() string {
return portainer.ComposeSyntaxMaxVersion
}
// Up builds, (re)creates and starts containers in the background. Wraps `docker-compose up -d` command
func (w *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
url, proxy, err := w.fetchEndpointProxy(endpoint)
if err != nil {
return err
}
if proxy != nil {
defer proxy.Close()
}
envFilePath, err := createEnvFile(stack)
if err != nil {
return err
}
filePath := stackFilePath(stack)
_, err = w.wrapper.Up(filePath, url, stack.Name, envFilePath, w.dataPath)
return err
}
// Down stops and removes containers, networks, images, and volumes. Wraps `docker-compose down --remove-orphans` command
func (w *ComposeStackManager) Down(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
url, proxy, err := w.fetchEndpointProxy(endpoint)
if err != nil {
return err
}
if proxy != nil {
defer proxy.Close()
}
filePath := stackFilePath(stack)
_, err = w.wrapper.Down(filePath, url, stack.Name)
return err
}
// NormalizeStackName returns the passed stack name, for interface implementation only
func (w *ComposeStackManager) NormalizeStackName(name string) string {
return name
}
func stackFilePath(stack *portainer.Stack) string {
return path.Join(stack.ProjectPath, stack.EntryPoint)
}
func (w *ComposeStackManager) fetchEndpointProxy(endpoint *portainer.Endpoint) (string, *factory.ProxyServer, error) {
if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") {
return "", nil, nil
}
proxy, err := w.proxyManager.CreateComposeProxyServer(endpoint)
if err != nil {
return "", nil, err
}
return fmt.Sprintf("http://127.0.0.1:%d", proxy.Port), proxy, nil
}
func createEnvFile(stack *portainer.Stack) (string, error) {
if stack.Env == nil || len(stack.Env) == 0 {
return "", 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 "", err
}
for _, v := range stack.Env {
envfile.WriteString(fmt.Sprintf("%s=%s\n", v.Name, v.Value))
}
envfile.Close()
return envFilePath, nil
}

View File

@@ -0,0 +1,79 @@
// +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{
URL: "unix://",
}
return stack, endpoint
}
func Test_UpAndDown(t *testing.T) {
stack, endpoint := setup(t)
w, err := NewComposeStackManager("", "", nil)
if err != nil {
t.Fatalf("Failed creating manager: %s", err)
}
err = w.Up(stack, endpoint)
if err != nil {
t.Fatalf("Error calling docker-compose up: %s", err)
}
if !containerExists(composedContainerName) {
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(containerName string) bool {
cmd := exec.Command("docker", "ps", "-a", "-f", fmt.Sprintf("name=%s", containerName))
out, err := cmd.Output()
if err != nil {
log.Fatalf("failed to list containers: %s", err)
}
return strings.Contains(string(out), containerName)
}

View File

@@ -0,0 +1,112 @@
package exec
import (
"io/ioutil"
"os"
"path"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert"
)
func Test_stackFilePath(t *testing.T) {
tests := []struct {
name string
stack *portainer.Stack
expected string
}{
// {
// name: "should return empty result if stack is missing",
// stack: nil,
// expected: "",
// },
// {
// name: "should return empty result if stack don't have entrypoint",
// stack: &portainer.Stack{},
// expected: "",
// },
{
name: "should allow file name and dir",
stack: &portainer.Stack{
ProjectPath: "dir",
EntryPoint: "file",
},
expected: path.Join("dir", "file"),
},
{
name: "should allow file name only",
stack: &portainer.Stack{
EntryPoint: "file",
},
expected: "file",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := stackFilePath(tt.stack)
assert.Equal(t, tt.expected, result)
})
}
}
func Test_createEnvFile(t *testing.T) {
dir := t.TempDir()
tests := []struct {
name string
stack *portainer.Stack
expected string
expectedFile bool
}{
// {
// name: "should not add env file option if stack is missing",
// stack: nil,
// expected: "",
// },
{
name: "should not add env file option if stack doesn't have env variables",
stack: &portainer.Stack{
ProjectPath: dir,
},
expected: "",
},
{
name: "should not add env file option if stack's env variables are empty",
stack: &portainer.Stack{
ProjectPath: dir,
Env: []portainer.Pair{},
},
expected: "",
},
{
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: "var1=value1\nvar2=value2\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, _ := createEnvFile(tt.stack)
if tt.expected != "" {
assert.Equal(t, path.Join(tt.stack.ProjectPath, "stack.env"), result)
f, _ := os.Open(path.Join(dir, "stack.env"))
content, _ := ioutil.ReadAll(f)
assert.Equal(t, tt.expected, string(content))
} else {
assert.Equal(t, "", result)
}
})
}
}

View File

@@ -2,71 +2,188 @@ package exec
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os/exec"
"path"
"runtime"
"strings"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
)
// KubernetesDeployer represents a service to deploy resources inside a Kubernetes environment.
type KubernetesDeployer struct {
binaryPath string
binaryPath string
dataStore portainer.DataStore
reverseTunnelService portainer.ReverseTunnelService
signatureService portainer.DigitalSignatureService
}
// NewKubernetesDeployer initializes a new KubernetesDeployer service.
func NewKubernetesDeployer(binaryPath string) *KubernetesDeployer {
func NewKubernetesDeployer(datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, binaryPath string) *KubernetesDeployer {
return &KubernetesDeployer{
binaryPath: binaryPath,
binaryPath: binaryPath,
dataStore: datastore,
reverseTunnelService: reverseTunnelService,
signatureService: signatureService,
}
}
// Deploy will deploy a Kubernetes manifest inside a specific namespace in a Kubernetes endpoint.
// If composeFormat is set to true, it will leverage the kompose binary to deploy a compose compliant manifest.
// Otherwise it will use kubectl to deploy the manifest.
func (deployer *KubernetesDeployer) Deploy(endpoint *portainer.Endpoint, data string, composeFormat bool, namespace string) ([]byte, error) {
if composeFormat {
convertedData, err := deployer.convertComposeData(data)
func (deployer *KubernetesDeployer) Deploy(endpoint *portainer.Endpoint, stackConfig string, namespace string) (string, error) {
if endpoint.Type == portainer.KubernetesLocalEnvironment {
token, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
if err != nil {
return nil, err
return "", err
}
data = string(convertedData)
command := path.Join(deployer.binaryPath, "kubectl")
if runtime.GOOS == "windows" {
command = path.Join(deployer.binaryPath, "kubectl.exe")
}
args := make([]string, 0)
args = append(args, "--server", endpoint.URL)
args = append(args, "--insecure-skip-tls-verify")
args = append(args, "--token", string(token))
args = append(args, "--namespace", namespace)
args = append(args, "apply", "-f", "-")
var stderr bytes.Buffer
cmd := exec.Command(command, args...)
cmd.Stderr = &stderr
cmd.Stdin = strings.NewReader(stackConfig)
output, err := cmd.Output()
if err != nil {
return "", errors.New(stderr.String())
}
return string(output), nil
}
token, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
// agent
endpointURL := endpoint.URL
if endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
tunnel := deployer.reverseTunnelService.GetTunnelDetails(endpoint.ID)
if tunnel.Status == portainer.EdgeAgentIdle {
err := deployer.reverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
if err != nil {
return "", err
}
settings, err := deployer.dataStore.Settings().Settings()
if err != nil {
return "", err
}
waitForAgentToConnect := time.Duration(settings.EdgeAgentCheckinInterval) * time.Second
time.Sleep(waitForAgentToConnect * 2)
}
endpointURL = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
}
transport := &http.Transport{}
if endpoint.TLSConfig.TLS {
tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
if err != nil {
return "", err
}
transport.TLSClientConfig = tlsConfig
}
httpCli := &http.Client{
Transport: transport,
}
if !strings.HasPrefix(endpointURL, "http") {
endpointURL = fmt.Sprintf("https://%s", endpointURL)
}
url, err := url.Parse(fmt.Sprintf("%s/v2/kubernetes/stack", endpointURL))
if err != nil {
return nil, err
return "", err
}
command := path.Join(deployer.binaryPath, "kubectl")
if runtime.GOOS == "windows" {
command = path.Join(deployer.binaryPath, "kubectl.exe")
}
args := make([]string, 0)
args = append(args, "--server", endpoint.URL)
args = append(args, "--insecure-skip-tls-verify")
args = append(args, "--token", string(token))
args = append(args, "--namespace", namespace)
args = append(args, "apply", "-f", "-")
var stderr bytes.Buffer
cmd := exec.Command(command, args...)
cmd.Stderr = &stderr
cmd.Stdin = strings.NewReader(data)
output, err := cmd.Output()
reqPayload, err := json.Marshal(
struct {
StackConfig string
Namespace string
}{
StackConfig: stackConfig,
Namespace: namespace,
})
if err != nil {
return nil, errors.New(stderr.String())
return "", err
}
return output, nil
req, err := http.NewRequest(http.MethodPost, url.String(), bytes.NewReader(reqPayload))
if err != nil {
return "", err
}
signature, err := deployer.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
if err != nil {
return "", err
}
req.Header.Set(portainer.PortainerAgentPublicKeyHeader, deployer.signatureService.EncodedPublicKey())
req.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
resp, err := httpCli.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var errorResponseData struct {
Message string
Details string
}
err = json.NewDecoder(resp.Body).Decode(&errorResponseData)
if err != nil {
output, parseStringErr := ioutil.ReadAll(resp.Body)
if parseStringErr != nil {
return "", parseStringErr
}
return "", fmt.Errorf("Failed parsing, body: %s, error: %w", output, err)
}
return "", fmt.Errorf("Deployment to agent failed: %s", errorResponseData.Details)
}
var responseData struct{ Output string }
err = json.NewDecoder(resp.Body).Decode(&responseData)
if err != nil {
parsedOutput, parseStringErr := ioutil.ReadAll(resp.Body)
if parseStringErr != nil {
return "", parseStringErr
}
return "", fmt.Errorf("Failed decoding, body: %s, err: %w", parsedOutput, err)
}
return responseData.Output, nil
}
func (deployer *KubernetesDeployer) convertComposeData(data string) ([]byte, error) {
// ConvertCompose leverages the kompose binary to deploy a compose compliant manifest.
func (deployer *KubernetesDeployer) ConvertCompose(data string) ([]byte, error) {
command := path.Join(deployer.binaryPath, "kompose")
if runtime.GOOS == "windows" {
command = path.Join(deployer.binaryPath, "kompose.exe")

View File

@@ -10,7 +10,7 @@ import (
"path"
"runtime"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
)
// SwarmStackManager represents a service for managing stacks.
@@ -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 != "" {

View File

@@ -9,7 +9,7 @@ import (
"io/ioutil"
"github.com/gofrs/uuid"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"io"
"os"
@@ -106,6 +106,66 @@ func (service *Service) GetStackProjectPath(stackIdentifier string) string {
return path.Join(service.fileStorePath, ComposeStorePath, stackIdentifier)
}
// Copy copies the file on fromFilePath to toFilePath
// if toFilePath exists func will fail unless deleteIfExists is true
func (service *Service) Copy(fromFilePath string, toFilePath string, deleteIfExists bool) error {
exists, err := service.FileExists(fromFilePath)
if err != nil {
return err
}
if !exists {
return errors.New("File doesn't exist")
}
finput, err := os.Open(fromFilePath)
if err != nil {
return err
}
defer finput.Close()
exists, err = service.FileExists(toFilePath)
if err != nil {
return err
}
if exists {
if !deleteIfExists {
return errors.New("Destination file exists")
}
err := os.Remove(toFilePath)
if err != nil {
return err
}
}
foutput, err := os.Create(toFilePath)
if err != nil {
return err
}
defer foutput.Close()
buf := make([]byte, 1024)
for {
n, err := finput.Read(buf)
if err != nil && err != io.EOF {
return err
}
if n == 0 {
break
}
if _, err := foutput.Write(buf[:n]); err != nil {
return err
}
}
return nil
}
// StoreStackFileFromBytes creates a subfolder in the ComposeStorePath and stores a new file from bytes.
// It returns the path to the folder where the file is stored.
func (service *Service) StoreStackFileFromBytes(stackIdentifier, fileName string, data []byte) (string, error) {
@@ -505,3 +565,8 @@ func (service *Service) GetTemporaryPath() (string, error) {
return path.Join(service.fileStorePath, TempPath, uid.String()), nil
}
// GetDataStorePath returns path to data folder
func (service *Service) GetDatastorePath() string {
return service.dataStorePath
}

219
api/git/azure.go Normal file
View File

@@ -0,0 +1,219 @@
package git
import (
"context"
"fmt"
"github.com/pkg/errors"
"github.com/portainer/portainer/api/archive"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
)
const (
azureDevOpsHost = "dev.azure.com"
visualStudioHostSuffix = ".visualstudio.com"
)
func isAzureUrl(s string) bool {
return strings.Contains(s, azureDevOpsHost) ||
strings.Contains(s, visualStudioHostSuffix)
}
type azureOptions struct {
organisation, project, repository string
// a user may pass credentials in a repository URL,
// for example https://<username>:<password>@<domain>/<path>
username, password string
}
type azureDownloader struct {
client *http.Client
baseUrl string
}
func NewAzureDownloader(client *http.Client) *azureDownloader {
return &azureDownloader{
client: client,
baseUrl: "https://dev.azure.com",
}
}
func (a *azureDownloader) download(ctx context.Context, destination string, options cloneOptions) error {
zipFilepath, err := a.downloadZipFromAzureDevOps(ctx, options)
if err != nil {
return errors.Wrap(err, "failed to download a zip file from Azure DevOps")
}
defer os.Remove(zipFilepath)
err = archive.UnzipFile(zipFilepath, destination)
if err != nil {
return errors.Wrap(err, "failed to unzip file")
}
return nil
}
func (a *azureDownloader) downloadZipFromAzureDevOps(ctx context.Context, options cloneOptions) (string, error) {
config, err := parseUrl(options.repositoryUrl)
if err != nil {
return "", errors.WithMessage(err, "failed to parse url")
}
downloadUrl, err := a.buildDownloadUrl(config, options.referenceName)
if err != nil {
return "", errors.WithMessage(err, "failed to build download url")
}
zipFile, err := ioutil.TempFile("", "azure-git-repo-*.zip")
if err != nil {
return "", errors.WithMessage(err, "failed to create temp file")
}
defer zipFile.Close()
req, err := http.NewRequestWithContext(ctx, "GET", downloadUrl, nil)
if options.username != "" || options.password != "" {
req.SetBasicAuth(options.username, options.password)
} else if config.username != "" || config.password != "" {
req.SetBasicAuth(config.username, config.password)
}
if err != nil {
return "", errors.WithMessage(err, "failed to create a new HTTP request")
}
res, err := a.client.Do(req)
if err != nil {
return "", errors.WithMessage(err, "failed to make an HTTP request")
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return "", fmt.Errorf("failed to download zip with a status \"%v\"", res.Status)
}
_, err = io.Copy(zipFile, res.Body)
if err != nil {
return "", errors.WithMessage(err, "failed to save HTTP response to a file")
}
return zipFile.Name(), nil
}
func parseUrl(rawUrl string) (*azureOptions, error) {
if strings.HasPrefix(rawUrl, "https://") || strings.HasPrefix(rawUrl, "http://") {
return parseHttpUrl(rawUrl)
}
if strings.HasPrefix(rawUrl, "git@ssh") {
return parseSshUrl(rawUrl)
}
if strings.HasPrefix(rawUrl, "ssh://") {
r := []rune(rawUrl)
return parseSshUrl(string(r[6:])) // remove the prefix
}
return nil, errors.Errorf("supported url schemes are https and ssh; recevied URL %s rawUrl", rawUrl)
}
var expectedSshUrl = "git@ssh.dev.azure.com:v3/Organisation/Project/Repository"
func parseSshUrl(rawUrl string) (*azureOptions, error) {
path := strings.Split(rawUrl, "/")
unexpectedUrlErr := errors.Errorf("want url %s, got %s", expectedSshUrl, rawUrl)
if len(path) != 4 {
return nil, unexpectedUrlErr
}
return &azureOptions{
organisation: path[1],
project: path[2],
repository: path[3],
}, nil
}
const expectedAzureDevOpsHttpUrl = "https://Organisation@dev.azure.com/Organisation/Project/_git/Repository"
const expectedVisualStudioHttpUrl = "https://organisation.visualstudio.com/project/_git/repository"
func parseHttpUrl(rawUrl string) (*azureOptions, error) {
u, err := url.Parse(rawUrl)
if err != nil {
return nil, errors.Wrap(err, "failed to parse HTTP url")
}
opt := azureOptions{}
switch {
case u.Host == azureDevOpsHost:
path := strings.Split(u.Path, "/")
if len(path) != 5 {
return nil, errors.Errorf("want url %s, got %s", expectedAzureDevOpsHttpUrl, u)
}
opt.organisation = path[1]
opt.project = path[2]
opt.repository = path[4]
case strings.HasSuffix(u.Host, visualStudioHostSuffix):
path := strings.Split(u.Path, "/")
if len(path) != 4 {
return nil, errors.Errorf("want url %s, got %s", expectedVisualStudioHttpUrl, u)
}
opt.organisation = strings.TrimSuffix(u.Host, visualStudioHostSuffix)
opt.project = path[1]
opt.repository = path[3]
default:
return nil, errors.Errorf("unknown azure host in url \"%s\"", rawUrl)
}
opt.username = u.User.Username()
opt.password, _ = u.User.Password()
return &opt, nil
}
func (a *azureDownloader) buildDownloadUrl(config *azureOptions, referenceName string) (string, error) {
rawUrl := fmt.Sprintf("%s/%s/%s/_apis/git/repositories/%s/items",
a.baseUrl,
url.PathEscape(config.organisation),
url.PathEscape(config.project),
url.PathEscape(config.repository))
u, err := url.Parse(rawUrl)
if err != nil {
return "", errors.Wrapf(err, "failed to parse download url path %s", rawUrl)
}
q := u.Query()
// scopePath=/&download=true&versionDescriptor.version=main&$format=zip&recursionLevel=full&api-version=6.0
q.Set("scopePath", "/")
q.Set("download", "true")
q.Set("versionDescriptor.versionType", getVersionType(referenceName))
q.Set("versionDescriptor.version", formatReferenceName(referenceName))
q.Set("$format", "zip")
q.Set("recursionLevel", "full")
q.Set("api-version", "6.0")
u.RawQuery = q.Encode()
return u.String(), nil
}
const (
branchPrefix = "refs/heads/"
tagPrefix = "refs/tags/"
)
func formatReferenceName(name string) string {
if strings.HasPrefix(name, branchPrefix) {
return strings.TrimPrefix(name, branchPrefix)
}
if strings.HasPrefix(name, tagPrefix) {
return strings.TrimPrefix(name, tagPrefix)
}
return name
}
func getVersionType(name string) string {
if strings.HasPrefix(name, branchPrefix) {
return "branch"
}
if strings.HasPrefix(name, tagPrefix) {
return "tag"
}
return "commit"
}

View File

@@ -0,0 +1,92 @@
package git
import (
"fmt"
"github.com/docker/docker/pkg/ioutils"
_ "github.com/joho/godotenv/autoload"
"github.com/stretchr/testify/assert"
"os"
"path/filepath"
"testing"
)
func TestService_ClonePublicRepository_Azure(t *testing.T) {
ensureIntegrationTest(t)
pat := getRequiredValue(t, "AZURE_DEVOPS_PAT")
service := NewService()
type args struct {
repositoryURLFormat string
referenceName string
username string
password string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "Clone Azure DevOps repo branch",
args: args{
repositoryURLFormat: "https://:%s@portainer.visualstudio.com/Playground/_git/dev_integration",
referenceName: "refs/heads/main",
username: "",
password: pat,
},
wantErr: false,
},
{
name: "Clone Azure DevOps repo tag",
args: args{
repositoryURLFormat: "https://:%s@portainer.visualstudio.com/Playground/_git/dev_integration",
referenceName: "refs/tags/v1.1",
username: "",
password: pat,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dst, err := ioutils.TempDir("", "clone")
assert.NoError(t, err)
defer os.RemoveAll(dst)
repositoryUrl := fmt.Sprintf(tt.args.repositoryURLFormat, tt.args.password)
err = service.ClonePublicRepository(repositoryUrl, tt.args.referenceName, dst)
assert.NoError(t, err)
assert.FileExists(t, filepath.Join(dst, "README.md"))
})
}
}
func TestService_ClonePrivateRepository_Azure(t *testing.T) {
ensureIntegrationTest(t)
pat := getRequiredValue(t, "AZURE_DEVOPS_PAT")
service := NewService()
dst, err := ioutils.TempDir("", "clone")
assert.NoError(t, err)
defer os.RemoveAll(dst)
repositoryUrl := "https://portainer.visualstudio.com/Playground/_git/dev_integration"
err = service.ClonePrivateRepositoryWithBasicAuth(repositoryUrl, "refs/heads/main", dst, "", pat)
assert.NoError(t, err)
assert.FileExists(t, filepath.Join(dst, "README.md"))
}
func getRequiredValue(t *testing.T, name string) string {
value, ok := os.LookupEnv(name)
if !ok {
t.Fatalf("can't find required env var \"%s\"", name)
}
return value
}
func ensureIntegrationTest(t *testing.T) {
if _, ok := os.LookupEnv("INTEGRATION_TEST"); !ok {
t.Skip("skip an integration test")
}
}

250
api/git/azure_test.go Normal file
View File

@@ -0,0 +1,250 @@
package git
import (
"context"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"net/url"
"testing"
)
func Test_buildDownloadUrl(t *testing.T) {
a := NewAzureDownloader(nil)
u, err := a.buildDownloadUrl(&azureOptions{
organisation: "organisation",
project: "project",
repository: "repository",
}, "refs/heads/main")
expectedUrl, _ := url.Parse("https://dev.azure.com/organisation/project/_apis/git/repositories/repository/items?scopePath=/&download=true&versionDescriptor.version=main&$format=zip&recursionLevel=full&api-version=6.0&versionDescriptor.versionType=branch")
actualUrl, _ := url.Parse(u)
if assert.NoError(t, err) {
assert.Equal(t, expectedUrl.Host, actualUrl.Host)
assert.Equal(t, expectedUrl.Scheme, actualUrl.Scheme)
assert.Equal(t, expectedUrl.Path, actualUrl.Path)
assert.Equal(t, expectedUrl.Query(), actualUrl.Query())
}
}
func Test_parseAzureUrl(t *testing.T) {
type args struct {
url string
}
tests := []struct {
name string
args args
want *azureOptions
wantErr bool
}{
{
name: "Expected SSH URL format starting with ssh://",
args: args{
url: "ssh://git@ssh.dev.azure.com:v3/Organisation/Project/Repository",
},
want: &azureOptions{
organisation: "Organisation",
project: "Project",
repository: "Repository",
},
wantErr: false,
},
{
name: "Expected SSH URL format starting with git@ssh",
args: args{
url: "git@ssh.dev.azure.com:v3/Organisation/Project/Repository",
},
want: &azureOptions{
organisation: "Organisation",
project: "Project",
repository: "Repository",
},
wantErr: false,
},
{
name: "Unexpected SSH URL format",
args: args{
url: "git@ssh.dev.azure.com:v3/Organisation/Repository",
},
wantErr: true,
},
{
name: "Expected HTTPS URL format",
args: args{
url: "https://Organisation@dev.azure.com/Organisation/Project/_git/Repository",
},
want: &azureOptions{
organisation: "Organisation",
project: "Project",
repository: "Repository",
username: "Organisation",
},
wantErr: false,
},
{
name: "HTTPS URL with credentials",
args: args{
url: "https://username:password@dev.azure.com/Organisation/Project/_git/Repository",
},
want: &azureOptions{
organisation: "Organisation",
project: "Project",
repository: "Repository",
username: "username",
password: "password",
},
wantErr: false,
},
{
name: "HTTPS URL with password",
args: args{
url: "https://:password@dev.azure.com/Organisation/Project/_git/Repository",
},
want: &azureOptions{
organisation: "Organisation",
project: "Project",
repository: "Repository",
password: "password",
},
wantErr: false,
},
{
name: "Visual Studio HTTPS URL with credentials",
args: args{
url: "https://username:password@organisation.visualstudio.com/project/_git/repository",
},
want: &azureOptions{
organisation: "organisation",
project: "project",
repository: "repository",
username: "username",
password: "password",
},
wantErr: false,
},
{
name: "Unexpected HTTPS URL format",
args: args{
url: "https://Organisation@dev.azure.com/Project/_git/Repository",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseUrl(tt.args.url)
if (err != nil) != tt.wantErr {
t.Errorf("parseUrl() error = %v, wantErr %v", err, tt.wantErr)
return
}
assert.Equal(t, tt.want, got)
})
}
}
func Test_isAzureUrl(t *testing.T) {
type args struct {
s string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "Is Azure url",
args: args{
s: "https://Organisation@dev.azure.com/Organisation/Project/_git/Repository",
},
want: true,
},
{
name: "Is Azure url",
args: args{
s: "https://portainer.visualstudio.com/project/_git/repository",
},
want: true,
},
{
name: "Is NOT Azure url",
args: args{
s: "https://github.com/Organisation/Repository",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, isAzureUrl(tt.args.s))
})
}
}
func Test_azureDownloader_downloadZipFromAzureDevOps(t *testing.T) {
type args struct {
options cloneOptions
}
type basicAuth struct {
username, password string
}
tests := []struct {
name string
args args
want *basicAuth
}{
{
name: "username, password embedded",
args: args{
options: cloneOptions{
repositoryUrl: "https://username:password@dev.azure.com/Organisation/Project/_git/Repository",
},
},
want: &basicAuth{
username: "username",
password: "password",
},
},
{
name: "username, password embedded, clone options take precedence",
args: args{
options: cloneOptions{
repositoryUrl: "https://username:password@dev.azure.com/Organisation/Project/_git/Repository",
username: "u",
password: "p",
},
},
want: &basicAuth{
username: "u",
password: "p",
},
},
{
name: "no credentials",
args: args{
options: cloneOptions{
repositoryUrl: "https://dev.azure.com/Organisation/Project/_git/Repository",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var zipRequestAuth *basicAuth
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if username, password, ok := r.BasicAuth(); ok {
zipRequestAuth = &basicAuth{username, password}
}
w.WriteHeader(http.StatusNotFound) // this makes function under test to return an error
}))
defer server.Close()
a := &azureDownloader{
client: server.Client(),
baseUrl: server.URL,
}
_, err := a.downloadZipFromAzureDevOps(context.Background(), tt.args.options)
assert.Error(t, err)
assert.Equal(t, tt.want, zipRequestAuth)
})
}
}

View File

@@ -1,21 +1,71 @@
package git
import (
"context"
"crypto/tls"
"github.com/pkg/errors"
"net/http"
"net/url"
"strings"
"os"
"path/filepath"
"time"
"gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/transport/client"
githttp "gopkg.in/src-d/go-git.v4/plumbing/transport/http"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/transport/client"
githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
)
type cloneOptions struct {
repositoryUrl string
username string
password string
referenceName string
depth int
}
type downloader interface {
download(ctx context.Context, dst string, opt cloneOptions) error
}
type gitClient struct{
preserveGitDirectory bool
}
func (c gitClient) download(ctx context.Context, dst string, opt cloneOptions) error {
gitOptions := git.CloneOptions{
URL: opt.repositoryUrl,
Depth: opt.depth,
}
if opt.password != "" || opt.username != "" {
gitOptions.Auth = &githttp.BasicAuth{
Username: opt.username,
Password: opt.password,
}
}
if opt.referenceName != "" {
gitOptions.ReferenceName = plumbing.ReferenceName(opt.referenceName)
}
_, err := git.PlainCloneContext(ctx, dst, false, &gitOptions)
if err != nil {
return errors.Wrap(err, "failed to clone git repository")
}
if !c.preserveGitDirectory {
os.RemoveAll(filepath.Join(dst, ".git"))
}
return nil
}
// Service represents a service for managing Git.
type Service struct {
httpsCli *http.Client
azure downloader
git downloader
}
// NewService initializes a new service.
@@ -31,32 +81,37 @@ func NewService() *Service {
return &Service{
httpsCli: httpsCli,
azure: NewAzureDownloader(httpsCli),
git: gitClient{},
}
}
// ClonePublicRepository clones a public git repository using the specified URL in the specified
// destination folder.
func (service *Service) ClonePublicRepository(repositoryURL, referenceName string, destination string) error {
return cloneRepository(repositoryURL, referenceName, destination)
func (service *Service) ClonePublicRepository(repositoryURL, referenceName, destination string) error {
return service.cloneRepository(destination, cloneOptions{
repositoryUrl: repositoryURL,
referenceName: referenceName,
depth: 1,
})
}
// ClonePrivateRepositoryWithBasicAuth clones a private git repository using the specified URL in the specified
// destination folder. It will use the specified username and password for basic HTTP authentication.
func (service *Service) ClonePrivateRepositoryWithBasicAuth(repositoryURL, referenceName string, destination, username, password string) error {
credentials := username + ":" + url.PathEscape(password)
repositoryURL = strings.Replace(repositoryURL, "://", "://"+credentials+"@", 1)
return cloneRepository(repositoryURL, referenceName, destination)
// destination folder. It will use the specified Username and Password for basic HTTP authentication.
func (service *Service) ClonePrivateRepositoryWithBasicAuth(repositoryURL, referenceName, destination, username, password string) error {
return service.cloneRepository(destination, cloneOptions{
repositoryUrl: repositoryURL,
username: username,
password: password,
referenceName: referenceName,
depth: 1,
})
}
func cloneRepository(repositoryURL, referenceName, destination string) error {
options := &git.CloneOptions{
URL: repositoryURL,
func (service *Service) cloneRepository(destination string, options cloneOptions) error {
if isAzureUrl(options.repositoryUrl) {
return service.azure.download(context.TODO(), destination, options)
}
if referenceName != "" {
options.ReferenceName = plumbing.ReferenceName(referenceName)
}
_, err := git.PlainClone(destination, false, options)
return err
return service.git.download(context.TODO(), destination, options)
}

View File

@@ -0,0 +1,26 @@
package git
import (
"github.com/docker/docker/pkg/ioutils"
"github.com/stretchr/testify/assert"
"os"
"path/filepath"
"testing"
)
func TestService_ClonePrivateRepository_GitHub(t *testing.T) {
ensureIntegrationTest(t)
pat := getRequiredValue(t, "GITHUB_PAT")
username := getRequiredValue(t, "GITHUB_USERNAME")
service := NewService()
dst, err := ioutils.TempDir("", "clone")
assert.NoError(t, err)
defer os.RemoveAll(dst)
repositoryUrl := "https://github.com/portainer/private-test-repository.git"
err = service.ClonePrivateRepositoryWithBasicAuth(repositoryUrl, "refs/heads/main", dst, username, pat)
assert.NoError(t, err)
assert.FileExists(t, filepath.Join(dst, "README.md"))
}

173
api/git/git_test.go Normal file
View File

@@ -0,0 +1,173 @@
package git
import (
"context"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/pkg/errors"
"github.com/portainer/portainer/api/archive"
"github.com/stretchr/testify/assert"
"io/ioutil"
"log"
"os"
"path/filepath"
"testing"
)
var bareRepoDir string
func TestMain(m *testing.M) {
if err := testMain(m); err != nil {
log.Fatal(err)
}
}
// testMain does extra setup/teardown before/after testing.
// The function is separated from TestMain due to necessity to call os.Exit/log.Fatal in the latter.
func testMain(m *testing.M) error {
dir, err := ioutil.TempDir("", "git-repo-")
if err != nil {
return errors.Wrap(err, "failed to create a temp dir")
}
defer os.RemoveAll(dir)
bareRepoDir = filepath.Join(dir, "test-clone.git")
file, err := os.OpenFile("./testdata/test-clone-git-repo.tar.gz", os.O_RDONLY, 0755)
if err != nil {
return errors.Wrap(err, "failed to open an archive")
}
err = archive.ExtractTarGz(file, dir)
if err != nil {
return errors.Wrapf(err, "failed to extract file from the archive to a folder %s\n", dir)
}
m.Run()
return nil
}
func Test_ClonePublicRepository_Shallow(t *testing.T) {
service := Service{git: gitClient{preserveGitDirectory: true}} // no need for http client since the test access the repo via file system.
repositoryURL := bareRepoDir
referenceName := "refs/heads/main"
destination := "shallow"
dir, err := ioutil.TempDir("", destination)
if err != nil {
t.Fatalf("failed to create a temp dir")
}
defer os.RemoveAll(dir)
t.Logf("Cloning into %s", dir)
err = service.ClonePublicRepository(repositoryURL, referenceName, dir)
assert.NoError(t, err)
assert.Equal(t, 1, getCommitHistoryLength(t, err, dir), "cloned repo has incorrect depth")
}
func Test_ClonePublicRepository_NoGitDirectory(t *testing.T) {
service := Service{git: gitClient{preserveGitDirectory: false}} // no need for http client since the test access the repo via file system.
repositoryURL := bareRepoDir
referenceName := "refs/heads/main"
destination := "shallow"
dir, err := ioutil.TempDir("", destination)
if err != nil {
t.Fatalf("failed to create a temp dir")
}
defer os.RemoveAll(dir)
t.Logf("Cloning into %s", dir)
err = service.ClonePublicRepository(repositoryURL, referenceName, dir)
assert.NoError(t, err)
assert.NoDirExists(t, filepath.Join(dir, ".git"))
}
func Test_cloneRepository(t *testing.T) {
service := Service{git: gitClient{preserveGitDirectory: true}} // no need for http client since the test access the repo via file system.
repositoryURL := bareRepoDir
referenceName := "refs/heads/main"
destination := "shallow"
dir, err := ioutil.TempDir("", destination)
if err != nil {
t.Fatalf("failed to create a temp dir")
}
defer os.RemoveAll(dir)
t.Logf("Cloning into %s", dir)
err = service.cloneRepository(dir, cloneOptions{
repositoryUrl: repositoryURL,
referenceName: referenceName,
depth: 10,
})
assert.NoError(t, err)
assert.Equal(t, 3, getCommitHistoryLength(t, err, dir), "cloned repo has incorrect depth")
}
func getCommitHistoryLength(t *testing.T, err error, dir string) int {
repo, err := git.PlainOpen(dir)
if err != nil {
t.Fatalf("can't open a git repo at %s with error %v", dir, err)
}
iter, err := repo.Log(&git.LogOptions{All: true})
if err != nil {
t.Fatalf("can't get a commit history iterator with error %v", err)
}
count := 0
err = iter.ForEach(func(_ *object.Commit) error {
count++
return nil
})
if err != nil {
t.Fatalf("can't iterate over the commit history with error %v", err)
}
return count
}
type testDownloader struct {
called bool
}
func (t *testDownloader) download(_ context.Context, _ string, _ cloneOptions) error {
t.called = true
return nil
}
func Test_cloneRepository_azure(t *testing.T) {
tests := []struct {
name string
url string
called bool
}{
{
name: "Azure HTTP URL",
url: "https://Organisation@dev.azure.com/Organisation/Project/_git/Repository",
called: true,
},
{
name: "Azure SSH URL",
url: "git@ssh.dev.azure.com:v3/Organisation/Project/Repository",
called: true,
},
{
name: "Something else",
url: "https://example.com",
called: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
azure := &testDownloader{}
git := &testDownloader{}
s := &Service{azure: azure, git: git}
s.cloneRepository("", cloneOptions{repositoryUrl: tt.url, depth: 1})
// if azure API is called, git isn't and vice versa
assert.Equal(t, tt.called, azure.called)
assert.Equal(t, tt.called, !git.called)
})
}
}

BIN
api/git/testdata/azure-repo.zip vendored Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -3,39 +3,48 @@ module github.com/portainer/portainer/api
go 1.13
require (
github.com/Microsoft/go-winio v0.4.14
github.com/Microsoft/go-winio v0.4.16
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a
github.com/asdine/storm/v3 v3.2.1
github.com/aws/aws-sdk-go v1.38.3
github.com/boltdb/bolt v1.3.1
github.com/containerd/containerd v1.3.1 // indirect
github.com/coreos/go-semver v0.3.0
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/docker/cli v0.0.0-20191126203649-54d085b857e9
github.com/docker/docker v0.0.0-00010101000000-000000000000
github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814
github.com/go-git/go-git/v5 v5.3.0
github.com/go-ldap/ldap/v3 v3.1.8
github.com/gofrs/uuid v3.2.0+incompatible
github.com/gorilla/mux v1.7.3
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/websocket v1.4.1
github.com/imdario/mergo v0.3.8 // indirect
github.com/joho/godotenv v1.3.0
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389
github.com/json-iterator/go v1.1.8
github.com/json-iterator/go v1.1.10
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
github.com/mattn/go-shellwords v1.0.6 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
github.com/pkg/errors v0.9.1
github.com/portainer/docker-compose-wrapper v0.0.0-20210415235931-d457f9aba1cc
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
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 // indirect
github.com/portainer/liblicense v0.0.0-20210409011001-c758dd044fbb
github.com/robfig/cron/v3 v3.0.1
github.com/stretchr/testify v1.7.0
go.etcd.io/bbolt v1.3.5 // indirect
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/src-d/go-git.v4 v4.13.1
k8s.io/api v0.17.2
k8s.io/apimachinery v0.17.2
k8s.io/client-go v0.17.2
)
replace github.com/docker/docker => github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203
replace golang.org/x/sys => golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456

View File

@@ -11,15 +11,19 @@ github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxB
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.3.8 h1:dvxbxtpTIjdAbx2OtL26p4eq0iEvys/U5yrsTJb3NZI=
github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/Microsoft/go-winio v0.3.8/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk=
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
github.com/Microsoft/hcsshim v0.8.6 h1:ZfF0+zZeYdzMIVMZHKtDKJvLHj76XCuVae/jNkjj0IA=
github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
@@ -34,6 +38,10 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/asdine/storm/v3 v3.2.1 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac=
github.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0=
github.com/aws/aws-sdk-go v1.38.3 h1:QCL/le04oAz2jELMRSuJVjGT7H+4hhoQc66eMPCfU/k=
github.com/aws/aws-sdk-go v1.38.3/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -47,7 +55,7 @@ github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc h1:TP+534wVl
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -75,10 +83,12 @@ github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e h1:p1yVGRW3nmb85p1Sh1ZJSDm4A4iKLS5QNbvUHMgGu/M=
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/evanphx/json-patch v4.2.0+incompatible h1:fUDGZCv/7iAN7u0puUVhvKCcsR6vRfwrJatElLBEf0I=
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
@@ -91,6 +101,15 @@ github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-asn1-ber/asn1-ber v1.3.1 h1:gvPdv/Hr++TRFCl0UbPFHC54P9N9jgsRPnmnr419Uck=
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-billy/v5 v5.1.0 h1:4pl5BV4o7ZG/lterP4S6WzJ6xr49Ba5ET9ygheTYahk=
github.com/go-git/go-billy/v5 v5.1.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12 h1:PbKy9zOy4aAKrJ5pibIRpVO2BXnK1Tlcg+caKI7Ox5M=
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
github.com/go-git/go-git/v5 v5.3.0 h1:8WKMtJR2j8RntEXR/uvTKagfEt4GYlwQ7mntE4+0GWc=
github.com/go-git/go-git/v5 v5.3.0/go.mod h1:xdX4bWJ48aOrdhnl2XqHYstHbbp6+LFS4r4X+lNVprw=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-ldap/ldap/v3 v3.1.8 h1:5vU/2jOh9HqprwXp8aF915s9p6Z8wmbSEVF7/gdTFhM=
github.com/go-ldap/ldap/v3 v3.1.8/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
@@ -104,7 +123,6 @@ github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dp
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I=
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
@@ -117,6 +135,8 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -141,17 +161,23 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jpillora/ansi v0.0.0-20170202005112-f496b27cd669 h1:l5rH/CnVVu+HPxjtxjM90nHrm4nov3j3RF9/62UjgLs=
github.com/jpillora/ansi v0.0.0-20170202005112-f496b27cd669/go.mod h1:kOeLNvjNBGSV3uYtFjvb72+fnZCMFJF1XDvRIjdom0g=
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
@@ -164,12 +190,13 @@ github.com/jpillora/sizestr v0.0.0-20160130011556-e2ea2fa42fb9/go.mod h1:1ffp+CR
github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck=
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c h1:N7A4JCA2G+j5fuFxCsJqjFU/sZe0mj8H0sSoSwbaikw=
@@ -177,13 +204,14 @@ github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c/go.mod h1:Nn
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v0.0.0-20150511174710-5cf931ef8f76/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-shellwords v1.0.6 h1:9Jok5pILi5S1MnDirGVTufYGtksUs/V2BWUP3ZkeUUI=
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
@@ -205,12 +233,13 @@ github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420 h1:Yu3681ykYHDfLoI6XVjL4JWmkE+3TX9yfIWwRCh1kFM=
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
@@ -220,20 +249,24 @@ github.com/opencontainers/runc v0.0.0-20161109192122-51371867a01c h1:iOMba/KmaXg
github.com/opencontainers/runc v0.0.0-20161109192122-51371867a01c/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 h1:lNCW6THrCKBiJBpz8kbVGjC7MgdCGKwuvBgc7LoD6sw=
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/portainer/docker-compose-wrapper v0.0.0-20210415235931-d457f9aba1cc h1:MvSEkOvhW3m2D3L0/Ymrjgg0t3CpHlHwpZfpgpIGNiw=
github.com/portainer/docker-compose-wrapper v0.0.0-20210415235931-d457f9aba1cc/go.mod h1:No8p8iZt9N2HOtDS9aWkh1ILxmQVoOTZZjHGlOij/ec=
github.com/portainer/libcompose v0.5.3 h1:tE4WcPuGvo+NKeDkDWpwNavNLZ5GHIJ4RvuZXsI9uI8=
github.com/portainer/libcompose v0.5.3/go.mod h1:7SKd/ho69rRKHDFSDUwkbMcol2TMKU5OslDsajr8Ro8=
github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2 h1:0PfgGLys9yHr4rtnirg0W0Cjvv6/DzxBIZk5sV59208=
github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2/go.mod h1:/wIeGwJOMYc1JplE/OvYMO5korce39HddIfI8VKGyAM=
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 h1:H8HR2dHdBf8HANSkUyVw4o8+4tegGcd+zyKZ3e599II=
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33/go.mod h1:Y2TfgviWI4rT2qaOTHr+hq6MdKIE5YjgQAu7qwptTV0=
github.com/portainer/liblicense v0.0.0-20210409011001-c758dd044fbb h1:H1UHoRATMJWQOzvvxYDBcPxVMctpsSzsKdHwPTCFyEU=
github.com/portainer/liblicense v0.0.0-20210409011001-c758dd044fbb/go.mod h1:6OSugUz07QF1WwrMmRNIMhgiq+drVMIZ98UTu5LVZP8=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8=
@@ -248,9 +281,10 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3 h1:CTwfnzjQ+8dS6MhHHu4YswVAD99sL2wjPqP+VkURmKE=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
@@ -258,38 +292,40 @@ github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTd
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
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/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.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/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=
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.1.0 h1:ngVtJC9TY/lg0AA/1k48FYhBrhRoFlEmWzsehpNAaZg=
github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 h1:anGSYQpPhQwXlwsu5wmfq0nWkCNaMEMUwAv13Y92hd8=
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -305,15 +341,14 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc h1:gkKoSkUmnU6bpS/VhkuO27bzQeSA51uaEfbOW5dNb68=
golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 h1:e6HwijUxhDe+hPNjZQQn9bA5PW3vNmnN64U2ZW759Lk=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210326060303-6b1517762897 h1:KrsHThm5nFk34YtATK1LsThyGhGbGe1olrte/HInHvs=
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
@@ -324,27 +359,16 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -356,13 +380,12 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7 h1:ZUjXAXmrAyrmmCPHgCA/vChHcpsX27MZ3yBonD/z1KE=
@@ -373,8 +396,12 @@ google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
@@ -384,42 +411,36 @@ gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOA
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE=
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/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=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.0.0-20191114100352-16d7abae0d2a h1:86XISgFlG7lPOWj6wYLxd+xqhhVt/WQjS4Tf39rP09s=
k8s.io/api v0.0.0-20191114100352-16d7abae0d2a/go.mod h1:qetVJgs5i8jwdFIdoOZ70ks0ecgU+dYwqZ2uD1srwOU=
k8s.io/api v0.17.2 h1:NF1UFXcKN7/OOv1uxdRz3qfra8AHsPav5M93hlV9+Dc=
k8s.io/api v0.17.2/go.mod h1:BS9fjjLc4CMuqfSO8vgbHPKMt5+SF0ET6u/RVDihTo4=
k8s.io/apimachinery v0.0.0-20191028221656-72ed19daf4bb h1:ZUNsbuPdXWrj0rZziRfCWcFg9ZP31OKkziqCbiphznI=
k8s.io/apimachinery v0.0.0-20191028221656-72ed19daf4bb/go.mod h1:llRdnznGEAqC3DcNm6yEj472xaFVfLM7hnYofMb12tQ=
k8s.io/apimachinery v0.17.2 h1:hwDQQFbdRlpnnsR64Asdi55GyCaIP/3WQpMmbNBeWr4=
k8s.io/apimachinery v0.17.2/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg=
k8s.io/client-go v0.0.0-20191114101535-6c5935290e33 h1:07mhG/2oEoo3N+sHVOo0L9PJ/qvbk3N5n2dj8IWefnQ=
k8s.io/client-go v0.0.0-20191114101535-6c5935290e33/go.mod h1:4L/zQOBkEf4pArQJ+CMk1/5xjA30B5oyWv+Bzb44DOw=
k8s.io/client-go v0.17.2 h1:ndIfkfXEGrNhLIgkr0+qhRguSD3u6DCmonepn1O6NYc=
k8s.io/client-go v0.17.2/go.mod h1:QAzRgsa0C2xl4/eVpeVAZMvikCn8Nm81yqVx3Kk9XYI=
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/klog v0.4.0 h1:lCJCxf/LIowc2IGS9TPjWDyXY4nOmdGdfcwwDQCOURQ=
k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU=
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
k8s.io/utils v0.0.0-20190801114015-581e00157fb1 h1:+ySTxfHnfzZb9ys375PXNlLhkJPLKgHajBU0N62BDvE=
k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo=
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=

View File

@@ -5,6 +5,8 @@ import "errors"
var (
// ErrEndpointAccessDenied Access denied to endpoint error
ErrEndpointAccessDenied = errors.New("Access denied to endpoint")
// ErrNoValidLicense Unauthorized error
ErrNoValidLicense = errors.New("No valid Portainer License found")
// ErrUnauthorized Unauthorized error
ErrUnauthorized = errors.New("Unauthorized")
// ErrResourceAccessDenied Access denied to resource error

View File

@@ -5,23 +5,28 @@ import (
"log"
"net/http"
"strings"
"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/internal/authorization"
)
type authenticatePayload struct {
Username string
Password string
// Username
Username string `example:"admin" validate:"required"`
// Password
Password string `example:"mypassword" validate:"required"`
}
type authenticateResponse struct {
JWT string `json:"jwt"`
// JWT token used to authenticate against the API
JWT string `json:"jwt" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsImV4cCI6MTQ5OTM3NjE1NH0.NJ6vE8FY1WG6jsRQzfMqeatJ4vh2TWAeeYfDhP71YEE"`
}
func (payload *authenticatePayload) Validate(r *http.Request) error {
@@ -34,40 +39,87 @@ func (payload *authenticatePayload) Validate(r *http.Request) error {
return nil
}
func (handler *Handler) authenticate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
// @id AuthenticateUser
// @summary Authenticate
// @description Use this endpoint to authenticate against Portainer using a username and password.
// @tags auth
// @accept json
// @produce json
// @param body body authenticatePayload true "Credentials used for authentication"
// @success 200 {object} authenticateResponse "Success"
// @failure 400 "Invalid request"
// @failure 422 "Invalid Credentials"
// @failure 500 "Server error"
// @router /auth [post]
func (handler *Handler) authenticate(rw http.ResponseWriter, r *http.Request) (*authMiddlewareResponse, *httperror.HandlerError) {
resp := &authMiddlewareResponse{
Method: portainer.AuthenticationInternal,
}
var payload authenticatePayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
return resp,
&httperror.HandlerError{
StatusCode: http.StatusBadRequest,
Message: "Invalid request payload",
Err: err,
}
}
resp.Username = payload.Username
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
return resp, &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Unable to retrieve settings from the database",
Err: err,
}
}
u, err := handler.DataStore.User().UserByUsername(payload.Username)
if err != nil && err != bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err}
return resp, &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Unable to retrieve a user with the specified username from the database",
Err: err,
}
}
if err == bolterrors.ErrObjectNotFound && (settings.AuthenticationMethod == portainer.AuthenticationInternal || settings.AuthenticationMethod == portainer.AuthenticationOAuth) {
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized}
return resp, &httperror.HandlerError{
StatusCode: http.StatusUnprocessableEntity,
Message: "Invalid credentials",
Err: httperrors.ErrUnauthorized,
}
}
if settings.AuthenticationMethod == portainer.AuthenticationLDAP {
if u == nil && settings.LDAPSettings.AutoCreateUsers {
return handler.authenticateLDAPAndCreateUser(w, payload.Username, payload.Password, &settings.LDAPSettings)
return handler.authenticateLDAPAndCreateUser(rw, payload.Username, payload.Password, &settings.LDAPSettings)
} else if u == nil && !settings.LDAPSettings.AutoCreateUsers {
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized}
return resp,
&httperror.HandlerError{
StatusCode: http.StatusUnprocessableEntity,
Message: "Invalid credentials",
Err: httperrors.ErrUnauthorized,
}
}
return handler.authenticateLDAP(w, u, payload.Password, &settings.LDAPSettings)
return handler.authenticateLDAP(rw, u, payload.Password, &settings.LDAPSettings)
}
return handler.authenticateInternal(w, u, payload.Password)
return handler.authenticateInternal(rw, u, payload.Password)
}
func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.User, password string, ldapSettings *portainer.LDAPSettings) *httperror.HandlerError {
func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.User, password string, ldapSettings *portainer.LDAPSettings) (*authMiddlewareResponse, *httperror.HandlerError) {
resp := &authMiddlewareResponse{
Method: portainer.AuthenticationLDAP,
Username: user.Username,
}
err := handler.LDAPService.AuthenticateUser(user.Username, password, ldapSettings)
if err != nil {
return handler.authenticateInternal(w, user, password)
@@ -78,32 +130,96 @@ func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
}
return handler.writeToken(w, user)
}
func (handler *Handler) authenticateInternal(w http.ResponseWriter, user *portainer.User, password string) *httperror.HandlerError {
err := handler.CryptoService.CompareHashAndData(user.Password, password)
err = handler.AuthorizationService.UpdateUsersAuthorizations()
if err != nil {
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized}
return resp,
&httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Unable to update user authorizations",
Err: err,
}
}
return handler.writeToken(w, user)
info := handler.LicenseService.Info()
if user.Role != portainer.AdministratorRole && !info.Valid {
return resp,
&httperror.HandlerError{
StatusCode: http.StatusForbidden,
Message: "License is not valid",
Err: httperrors.ErrNoValidLicense,
}
}
return handler.writeToken(w, user, resp.Method)
}
func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, username, password string, ldapSettings *portainer.LDAPSettings) *httperror.HandlerError {
func (handler *Handler) authenticateInternal(w http.ResponseWriter, user *portainer.User, password string) (*authMiddlewareResponse, *httperror.HandlerError) {
resp := &authMiddlewareResponse{
Method: portainer.AuthenticationInternal,
Username: user.Username,
}
err := handler.CryptoService.CompareHashAndData(user.Password, password)
if err != nil {
return resp,
&httperror.HandlerError{
StatusCode: http.StatusUnprocessableEntity,
Message: "Invalid credentials",
Err: httperrors.ErrUnauthorized,
}
}
info := handler.LicenseService.Info()
if user.Role != portainer.AdministratorRole && !info.Valid {
return resp,
&httperror.HandlerError{
StatusCode: http.StatusForbidden,
Message: "License is not valid",
Err: httperrors.ErrNoValidLicense,
}
}
return handler.writeToken(w, user, resp.Method)
}
func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, username, password string, ldapSettings *portainer.LDAPSettings) (*authMiddlewareResponse, *httperror.HandlerError) {
resp := &authMiddlewareResponse{
Method: portainer.AuthenticationLDAP,
Username: username,
}
err := handler.LDAPService.AuthenticateUser(username, password, ldapSettings)
if err != nil {
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", err}
return resp,
&httperror.HandlerError{
StatusCode: http.StatusUnprocessableEntity,
Message: "Invalid credentials",
Err: err,
}
}
user := &portainer.User{
Username: username,
Role: portainer.StandardUserRole,
Username: username,
Role: portainer.StandardUserRole,
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
}
err = handler.DataStore.User().CreateUser(user)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err}
return resp,
&httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Unable to persist user inside the database",
Err: err,
}
}
err = handler.addUserIntoTeams(user, ldapSettings)
@@ -111,26 +227,78 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
}
return handler.writeToken(w, user)
}
func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError {
tokenData := &portainer.TokenData{
ID: user.ID,
Username: user.Username,
Role: user.Role,
}
return handler.persistAndWriteToken(w, tokenData)
}
func (handler *Handler) persistAndWriteToken(w http.ResponseWriter, tokenData *portainer.TokenData) *httperror.HandlerError {
token, err := handler.JWTService.GenerateToken(tokenData)
err = handler.AuthorizationService.UpdateUsersAuthorizations()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to generate JWT token", err}
return resp,
&httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Unable to update user authorizations",
Err: err,
}
}
return response.JSON(w, &authenticateResponse{JWT: token})
info := handler.LicenseService.Info()
if !info.Valid {
return resp,
&httperror.HandlerError{
StatusCode: http.StatusForbidden,
Message: "License is not valid",
Err: httperrors.ErrNoValidLicense,
}
}
return handler.writeToken(w, user, resp.Method)
}
func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User, method portainer.AuthenticationMethod) (*authMiddlewareResponse, *httperror.HandlerError) {
tokenData := composeTokenData(user)
return handler.persistAndWriteToken(w, tokenData, nil, method)
}
func (handler *Handler) writeTokenForOAuth(w http.ResponseWriter, user *portainer.User, expiryTime *time.Time, method portainer.AuthenticationMethod) (*authMiddlewareResponse, *httperror.HandlerError) {
tokenData := composeTokenData(user)
return handler.persistAndWriteToken(w, tokenData, expiryTime, method)
}
func (handler *Handler) persistAndWriteToken(w http.ResponseWriter, tokenData *portainer.TokenData, expiryTime *time.Time, method portainer.AuthenticationMethod) (*authMiddlewareResponse, *httperror.HandlerError) {
resp := &authMiddlewareResponse{
Username: tokenData.Username,
Method: method,
}
var token string
var err error
if method == portainer.AuthenticationOAuth {
token, err = handler.JWTService.GenerateTokenForOAuth(tokenData, expiryTime)
if err != nil {
return resp,
&httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Unable to generate JWT token for OAuth",
Err: err,
}
}
} else {
token, err = handler.JWTService.GenerateToken(tokenData)
if err != nil {
return resp,
&httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Unable to generate JWT token",
Err: err,
}
}
}
return resp, response.JSON(w, &authenticateResponse{JWT: token})
}
func (handler *Handler) addUserIntoTeams(user *portainer.User, settings *portainer.LDAPSettings) error {
@@ -164,6 +332,7 @@ func (handler *Handler) addUserIntoTeams(user *portainer.User, settings *portain
err := handler.DataStore.TeamMembership().CreateTeamMembership(membership)
if err != nil {
return err
}
}
@@ -189,3 +358,11 @@ func teamMembershipExists(teamID portainer.TeamID, memberships []portainer.TeamM
}
return false
}
func composeTokenData(user *portainer.User) *portainer.TokenData {
return &portainer.TokenData{
ID: user.ID,
Username: user.Username,
Role: user.Role,
}
}

View File

@@ -8,12 +8,14 @@ import (
"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"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/internal/authorization"
)
type oauthPayload struct {
// OAuth code returned from OAuth Provided
Code string
}
@@ -24,63 +26,153 @@ func (payload *oauthPayload) Validate(r *http.Request) error {
return nil
}
func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuthSettings) (string, error) {
func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuthSettings) (*portainer.OAuthInfo, error) {
if code == "" {
return "", errors.New("Invalid OAuth authorization code")
return nil, errors.New("Invalid OAuth authorization code")
}
if settings == nil {
return "", errors.New("Invalid OAuth configuration")
return nil, errors.New("Invalid OAuth configuration")
}
username, err := handler.OAuthService.Authenticate(code, settings)
authInfo, err := handler.OAuthService.Authenticate(code, settings)
if err != nil {
return "", err
return nil, err
}
return username, nil
return authInfo, nil
}
func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
// @id ValidateOAuth
// @summary Authenticate with OAuth
// @tags auth
// @accept json
// @produce json
// @param body body oauthPayload true "OAuth Credentials used for authentication"
// @success 200 {object} authenticateResponse "Success"
// @failure 400 "Invalid request"
// @failure 422 "Invalid Credentials"
// @failure 500 "Server error"
// @router /auth/oauth/validate [post]
func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) (*authMiddlewareResponse, *httperror.HandlerError) {
resp := &authMiddlewareResponse{
Method: portainer.AuthenticationOAuth,
}
var payload oauthPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
return resp, &httperror.HandlerError{
StatusCode: http.StatusBadRequest,
Message: "Invalid request payload",
Err: err,
}
}
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
return resp, &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Unable to retrieve settings from the database",
Err: err,
}
}
if settings.AuthenticationMethod != 3 {
return &httperror.HandlerError{http.StatusForbidden, "OAuth authentication is not enabled", errors.New("OAuth authentication is not enabled")}
return resp, &httperror.HandlerError{
StatusCode: http.StatusForbidden,
Message: "OAuth authentication is not enabled",
Err: errors.New("OAuth authentication is not enabled"),
}
}
username, err := handler.authenticateOAuth(payload.Code, &settings.OAuthSettings)
authInfo, err := handler.authenticateOAuth(payload.Code, &settings.OAuthSettings)
if err != nil {
log.Printf("[DEBUG] - OAuth authentication error: %s", err)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate through OAuth", httperrors.ErrUnauthorized}
return resp, &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Unable to authenticate through OAuth",
Err: httperrors.ErrUnauthorized,
}
}
user, err := handler.DataStore.User().UserByUsername(username)
resp.Username = authInfo.Username
user, err := handler.DataStore.User().UserByUsername(authInfo.Username)
if err != nil && err != bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err}
return resp, &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Unable to retrieve a user with the specified username from the database",
Err: err,
}
}
if user == nil && !settings.OAuthSettings.OAuthAutoCreateUsers {
return &httperror.HandlerError{http.StatusForbidden, "Account not created beforehand in Portainer and automatic user provisioning not enabled", httperrors.ErrUnauthorized}
if user == nil && !settings.OAuthSettings.OAuthAutoMapTeamMemberships && !settings.OAuthSettings.OAuthAutoCreateUsers {
return resp, &httperror.HandlerError{
StatusCode: http.StatusForbidden,
Message: "Account not created beforehand in Portainer and automatic user provisioning not enabled",
Err: httperrors.ErrUnauthorized,
}
}
if user == nil && !settings.OAuthSettings.AdminAutoPopulate && !settings.OAuthSettings.OAuthAutoCreateUsers {
return resp, &httperror.HandlerError{
StatusCode: http.StatusForbidden,
Message: "Auto OAuth admin population failed: user not created beforehand in Portainer and automatic user provisioning not enabled",
Err: httperrors.ErrUnauthorized,
}
}
autoOAuthAdmin := false
if settings.OAuthSettings.AdminAutoPopulate {
isValid, err := validateClaimWithRegex(settings.OAuthSettings, authInfo.Teams)
if err != nil {
return resp, &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to validate OAuth teams with pre-set claim regexs",
Err: err,
}
}
if user != nil && isValid {
user.Role = portainer.AdministratorRole
if err := handler.DataStore.User().UpdateUser(user.ID, user); err != nil {
return resp, &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Unable to persist user changes inside the database",
Err: err,
}
}
if err := handler.AuthorizationService.UpdateUsersAuthorizations(); err != nil {
return resp, &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Unable to update user authorizations",
Err: err,
}
}
}
autoOAuthAdmin = isValid
}
if user == nil {
user = &portainer.User{
Username: username,
Role: portainer.StandardUserRole,
Username: authInfo.Username,
Role: portainer.StandardUserRole,
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
}
if autoOAuthAdmin {
user.Role = portainer.AdministratorRole
}
err = handler.DataStore.User().CreateUser(user)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err}
return resp, &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Unable to persist user inside the database",
Err: err,
}
}
if settings.OAuthSettings.DefaultTeamID != 0 {
@@ -92,11 +184,54 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
err = handler.DataStore.TeamMembership().CreateTeamMembership(membership)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist team membership inside the database", err}
return &authMiddlewareResponse{
Method: portainer.AuthenticationOAuth,
}, &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Unable to persist team membership inside the database",
Err: err,
}
}
}
err = handler.AuthorizationService.UpdateUsersAuthorizations()
if err != nil {
return resp, &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Unable to update user authorizations",
Err: err,
}
}
}
return handler.writeToken(w, user)
if settings.OAuthSettings.OAuthAutoMapTeamMemberships {
if settings.OAuthSettings.TeamMemberships.OAuthClaimName == "" {
return resp, &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Unable to process user oauth team memberships",
Err: errors.New("empty value set for oauth team membership Claim name"),
}
}
err = updateOAuthTeamMemberships(handler.DataStore, settings.OAuthSettings.TeamMemberships.OAuthClaimMappings, *user, authInfo.Teams)
if err != nil {
return resp, &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Unable to update user oauth team memberships",
Err: err,
}
}
}
info := handler.LicenseService.Info()
if user.Role != portainer.AdministratorRole && !info.Valid {
return resp, &httperror.HandlerError{
StatusCode: http.StatusForbidden,
Message: "License is not valid",
Err: httperrors.ErrNoValidLicense,
}
}
return handler.writeTokenForOAuth(w, user, authInfo.ExpiryTime, portainer.AuthenticationOAuth)
}

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