Compare commits

..

328 Commits

Author SHA1 Message Date
Felix Han
4aa0fe253b feat(settings): add admin mapping section EE-971 2021-06-23 16:44:56 +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
3835 changed files with 82430 additions and 187188 deletions

13
.babelrc Normal file
View File

@@ -0,0 +1,13 @@
{
"plugins": ["lodash", "angularjs-annotate"],
"presets": [
[
"@babel/preset-env",
{
"modules": false,
"useBuiltIns": "entry",
"corejs": "2"
}
]
]
}

44
.codeclimate.yml Normal file
View File

@@ -0,0 +1,44 @@
version: "2"
checks:
argument-count:
enabled: false
complex-logic:
enabled: false
file-lines:
enabled: false
method-complexity:
enabled: false
method-count:
enabled: false
method-lines:
enabled: false
nested-control-flow:
enabled: false
return-statements:
enabled: false
similar-code:
enabled: false
identical-code:
enabled: false
plugins:
gofmt:
enabled: true
eslint:
enabled: true
channel: "eslint-5"
config:
config: .eslintrc.yml
exclude_patterns:
- assets/
- build/
- dist/
- distribution/
- node_modules
- test/
- webpack/
- gruntfile.js
- webpack.config.js
- api/
- "!app/kubernetes/**"
- .github/
- .tmp/

View File

@@ -1,5 +1,3 @@
*
!dist
!build
!metadata.json
!docker-extension/build

View File

@@ -1 +0,0 @@
PORTAINER_EDITION=CE

View File

@@ -9,7 +9,6 @@ globals:
extends:
- 'eslint:recommended'
- 'plugin:storybook/recommended'
- prettier
plugins:
@@ -18,109 +17,12 @@ plugins:
parserOptions:
ecmaVersion: 2018
sourceType: module
project: './tsconfig.json'
ecmaFeatures:
modules: true
rules:
no-control-regex: 'off'
no-control-regex: off
no-empty: warn
no-empty-function: warn
no-useless-escape: 'off'
import/order:
[
'error',
{
pathGroups:
[
{ pattern: '@@/**', group: 'internal', position: 'after' },
{ pattern: '@/**', group: 'internal' },
{ pattern: '{Kubernetes,Portainer,Agent,Azure,Docker}/**', group: 'internal' },
],
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
pathGroupsExcludedImportTypes: ['internal'],
},
]
settings:
'import/resolver':
alias:
map:
- ['@@', './app/react/components']
- ['@', './app']
extensions: ['.js', '.ts', '.tsx']
overrides:
- files:
- app/**/*.ts{,x}
parserOptions:
project: './tsconfig.json'
parser: '@typescript-eslint/parser'
plugins:
- '@typescript-eslint'
- 'regex'
extends:
- airbnb
- airbnb-typescript
- 'plugin:eslint-comments/recommended'
- 'plugin:react-hooks/recommended'
- 'plugin:react/jsx-runtime'
- 'plugin:@typescript-eslint/recommended'
- 'plugin:@typescript-eslint/eslint-recommended'
- 'plugin:promise/recommended'
- 'plugin:storybook/recommended'
- prettier # should be last
settings:
react:
version: 'detect'
rules:
import/order:
[
'error',
{
pathGroups: [{ pattern: '@@/**', group: 'internal', position: 'after' }, { pattern: '@/**', group: 'internal' }],
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
'newlines-between': 'always',
},
]
no-plusplus: off
func-style: [error, 'declaration']
import/prefer-default-export: off
no-use-before-define: "off"
'@typescript-eslint/no-use-before-define': ['error', { functions: false, "allowNamedExports": true }]
no-shadow: 'off'
'@typescript-eslint/no-shadow': off
jsx-a11y/no-autofocus: warn
react/forbid-prop-types: off
react/require-default-props: off
react/no-array-index-key: off
no-underscore-dangle: off
react/jsx-filename-extension: [0]
import/no-extraneous-dependencies: ['error', { devDependencies: true }]
'@typescript-eslint/explicit-module-boundary-types': off
'@typescript-eslint/no-unused-vars': 'error'
'@typescript-eslint/no-explicit-any': 'error'
'jsx-a11y/label-has-associated-control': ['error', { 'assert': 'either', controlComponents: ['Input', 'Checkbox'] }]
'react/function-component-definition': ['error', { 'namedComponents': 'function-declaration' }]
'react/jsx-no-bind': off
'no-await-in-loop': 'off'
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }]
'regex/invalid': ['error', [{ 'regex': '<Icon icon="(.*)"', 'message': 'Please directly import the `lucide-react` icon instead of using the string' }]]
overrides: # allow props spreading for hoc files
- files:
- app/**/with*.ts{,x}
rules:
'react/jsx-props-no-spreading': off
- files:
- app/**/*.test.*
extends:
- 'plugin:jest/recommended'
- 'plugin:jest/style'
env:
'jest/globals': true
rules:
'react/jsx-no-constructed-context-values': off
- files:
- app/**/*.stories.*
rules:
'no-alert': off
no-useless-escape: off
import/order: error

View File

@@ -1,8 +0,0 @@
# prettier
cf5056d9c03b62d91a25c3b9127caac838695f98
# prettier v2
42e7db0ae7897d3cb72b0ea1ecf57ee2dd694169
# tailwind prettier
58d66d3142950bb90a7d85511c034ac9fabba9ba

View File

@@ -2,7 +2,7 @@
Thanks for opening an issue on Portainer !
Do you need help or have a question? Come chat with us on Slack https://portainer.io/slack/ or gitter https://gitter.im/portainer/Lobby.
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/ or gitter https://gitter.im/portainer/Lobby.
If you are reporting a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this
@@ -28,15 +28,17 @@ Briefly describe the problem you are having in a few paragraphs.
**Steps to reproduce the issue:**
1. 2. 3.
1.
2.
3.
Any other info e.g. Why do you consider this to be a bug? What did you expect to happen instead?
**Technical details:**
- Portainer version:
- Target Docker version (the host/cluster you manage):
- Platform (windows/linux):
- Command used to start Portainer (`docker run -p 9443:9443 portainer/portainer`):
- Target Swarm version (if applicable):
- Browser:
* Portainer version:
* Target Docker version (the host/cluster you manage):
* Platform (windows/linux):
* Command used to start Portainer (`docker run -p 9000:9000 portainer/portainer`):
* Target Swarm version (if applicable):
* Browser:

View File

@@ -1,9 +1,6 @@
---
name: Bug report
about: Create a bug report
title: ''
labels: bug/need-confirmation, kind/bug
assignees: ''
---
<!--
@@ -12,7 +9,7 @@ Thanks for reporting a bug for Portainer !
You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
Do you need help or have a question? Come chat with us on Slack https://portainer.io/slack/
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/.
Before opening a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this
@@ -30,7 +27,7 @@ A clear and concise description of what you expected to happen.
**Portainer Logs**
Provide the logs of your Portainer container or Service.
You can see how [here](https://documentation.portainer.io/r/portainer-logs)
You can see how [here](https://documentation.portainer.io/archive/1.23.2/faq/#how-do-i-get-the-logs-from-portainer)
**Steps to reproduce the issue:**
@@ -43,12 +40,9 @@ You can see how [here](https://documentation.portainer.io/r/portainer-logs)
- Portainer version:
- Docker version (managed by Portainer):
- Kubernetes version (managed by Portainer):
- Platform (windows/linux):
- Command used to start Portainer (`docker run -p 9443:9443 portainer/portainer`):
- Command used to start Portainer (`docker run -p 9000:9000 portainer/portainer`):
- Browser:
- Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commercial setup.
- Have you reviewed our technical documentation and knowledge base? Yes/No
**Additional context**
Add any other context about the problem here.

View File

@@ -1,25 +1,17 @@
---
name: Question
about: Ask us a question about Portainer usage or deployment
title: ''
labels: ''
assignees: ''
---
Before you start, we need a little bit more information from you:
Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commercial setup.
Have you reviewed our technical documentation and knowledge base? Yes/No
<!--
You can find more information about Portainer support framework policy here: https://old.portainer.io/2019/04/portainer-support-policy/
Do you need help or have a question? Come chat with us on Slack https://portainer.io/slack/
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
-->
**Question**:
How can I deploy Portainer on... ?
---
name: Question
about: Ask us a question about Portainer usage or deployment
---
<!--
You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/
Also, be sure to check our FAQ and documentation first: https://portainer.readthedocs.io
-->
**Question**:
How can I deploy Portainer on... ?

View File

@@ -1,33 +1,31 @@
---
name: Feature request
about: Suggest a feature/enhancement that should be added in Portainer
title: ''
labels: ''
assignees: ''
---
<!--
Thanks for opening a feature request for Portainer !
Do you need help or have a question? Come chat with us on Slack https://portainer.io/slack/
Before opening a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this
repository. If there is a duplicate, please close your issue and add a comment
to the existing issue instead.
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
-->
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
---
name: Feature request
about: Suggest a feature/enhancement that should be added in Portainer
---
<!--
Thanks for opening a feature request for Portainer !
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/
Before opening a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this
repository. If there is a duplicate, please close your issue and add a comment
to the existing issue instead.
Also, be sure to check our FAQ and documentation first: https://portainer.readthedocs.io
-->
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Portainer Business Edition - Get 3 nodes free
url: https://www.portainer.io/take-3
about: Portainer Business Edition has more features, more support and you can now get 3 nodes free for as long as you want.

View File

@@ -1,4 +0,0 @@
closes #0 <!-- Github issue number (remove if unknown) -->
closes [CE-0] <!-- Jira link number (remove if unknown). Please also add the same [CE-XXX] at the back of the PR title -->
### Changes:

53
.github/stale.yml vendored Normal file
View File

@@ -0,0 +1,53 @@
# Config for Stalebot, limited to only `issues`
only: issues
# Issues config
issues:
daysUntilStale: 60
daysUntilClose: 7
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 30
# Issues with these labels will never be considered stale
exemptLabels:
- kind/enhancement
- kind/question
- kind/style
- kind/workaround
- bug/need-confirmation
- bug/confirmed
- status/discuss
# Only issues with all of these labels are checked if stale. Defaults to `[]` (disabled)
onlyLabels: []
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: true
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
# Set to true to ignore issues with an assignee (defaults to false)
exemptAssignees: true
# Label to use when marking an issue as stale
staleLabel: status/stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been marked as stale as it has not had recent activity,
it will be closed if no further activity occurs in the next 7 days.
If you believe that it has been incorrectly labelled as stale,
leave a comment and the label will be removed.
# Comment to post when removing the stale label.
# unmarkComment: >
# Your comment here.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: >
Since no further activity has appeared on this issue it will be closed.
If you believe that it has been incorrectly closed, leave a comment
mentioning `ametdoohan`, `balasu` or `keverv` and one of our staff will then review the issue.
Note - If it is an old bug report, make sure that it is reproduceable in the
latest version of Portainer as it may have already been fixed.

View File

@@ -1,15 +0,0 @@
on:
push:
branches:
- develop
- 'release/**'
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: mschilde/auto-label-merge-conflicts@master
with:
CONFLICT_LABEL_NAME: 'has conflicts'
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MAX_RETRIES: 10
WAIT_MS: 60000

View File

@@ -1,46 +0,0 @@
name: Lint
on:
push:
branches:
- master
- develop
- release/*
pull_request:
branches:
- master
- develop
- release/*
jobs:
run-linters:
name: Run linters
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '18'
cache: 'yarn'
- uses: actions/setup-go@v4
with:
go-version: 1.19.5
- run: yarn --frozen-lockfile
- name: Run linters
uses: wearerequired/lint-action@v1
with:
eslint: true
eslint_extensions: ts,tsx,js,jsx
prettier: true
prettier_dir: app/
gofmt: true
gofmt_dir: api/
- name: Typecheck
uses: icrawl/action-tsc@v1
- name: GolangCI-Lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.54.1
working-directory: api
args: --timeout=10m -c .golangci.yaml

View File

@@ -1,205 +0,0 @@
name: Nightly Code Security Scan
on:
schedule:
- cron: '0 20 * * *'
workflow_dispatch:
jobs:
client-dependencies:
name: Client Dependency Check
runs-on: ubuntu-latest
if: >- # only run for develop branch
github.ref == 'refs/heads/develop'
outputs:
js: ${{ steps.set-matrix.outputs.js_result }}
steps:
- name: checkout repository
uses: actions/checkout@master
- name: scan vulnerabilities by Snyk
uses: snyk/actions/node@master
continue-on-error: true # To make sure that artifact upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
json: true
- name: upload scan result as develop artifact
uses: actions/upload-artifact@v3
with:
name: js-security-scan-develop-result
path: snyk.json
- name: develop scan report export to html
run: |
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=table --export --export-filename="/data/js-result")
- name: upload html file as artifact
uses: actions/upload-artifact@v3
with:
name: html-js-result-${{github.run_id}}
path: js-result.html
- name: analyse vulnerabilities
id: set-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=matrix)
echo "js_result=${result}" >> $GITHUB_OUTPUT
server-dependencies:
name: Server Dependency Check
runs-on: ubuntu-latest
if: >- # only run for develop branch
github.ref == 'refs/heads/develop'
outputs:
go: ${{ steps.set-matrix.outputs.go_result }}
steps:
- name: checkout repository
uses: actions/checkout@master
- name: install Go
uses: actions/setup-go@v3
with:
go-version: '1.19.5'
- name: download Go modules
run: cd ./api && go get -t -v -d ./...
- name: scan vulnerabilities by Snyk
continue-on-error: true # To make sure that artifact upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
run: |
yarn global add snyk
snyk test --file=./api/go.mod --json-file-output=snyk.json 2>/dev/null || :
- name: upload scan result as develop artifact
uses: actions/upload-artifact@v3
with:
name: go-security-scan-develop-result
path: snyk.json
- name: develop scan report export to html
run: |
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=table --export --export-filename="/data/go-result")
- name: upload html file as artifact
uses: actions/upload-artifact@v3
with:
name: html-go-result-${{github.run_id}}
path: go-result.html
- name: analyse vulnerabilities
id: set-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=matrix)
echo "go_result=${result}" >> $GITHUB_OUTPUT
image-vulnerability:
name: Image Vulnerability Check
runs-on: ubuntu-latest
if: >-
github.ref == 'refs/heads/develop'
outputs:
image: ${{ steps.set-matrix.outputs.image_result }}
steps:
- name: scan vulnerabilities by Trivy
uses: docker://docker.io/aquasec/trivy:latest
continue-on-error: true
with:
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress portainerci/portainer:develop
- name: upload image security scan result as artifact
uses: actions/upload-artifact@v3
with:
name: image-security-scan-develop-result
path: image-trivy.json
- name: develop scan report export to html
run: |
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=trivy --path="/data/image-trivy.json" --output-type=table --export --export-filename="/data/image-result")
- name: upload html file as artifact
uses: actions/upload-artifact@v3
with:
name: html-image-result-${{github.run_id}}
path: image-result.html
- name: analyse vulnerabilities
id: set-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=trivy --path="/data/image-trivy.json" --output-type=matrix)
echo "image_result=${result}" >> $GITHUB_OUTPUT
result-analysis:
name: Analyse Scan Results
needs: [client-dependencies, server-dependencies, image-vulnerability]
runs-on: ubuntu-latest
if: >-
github.ref == 'refs/heads/develop'
strategy:
matrix:
js: ${{fromJson(needs.client-dependencies.outputs.js)}}
go: ${{fromJson(needs.server-dependencies.outputs.go)}}
image: ${{fromJson(needs.image-vulnerability.outputs.image)}}
steps:
- name: display the results of js, Go, and image scan
run: |
echo "${{ matrix.js.status }}"
echo "${{ matrix.go.status }}"
echo "${{ matrix.image.status }}"
echo "${{ matrix.js.summary }}"
echo "${{ matrix.go.summary }}"
echo "${{ matrix.image.summary }}"
- name: send message to Slack
if: >-
matrix.js.status == 'failure' ||
matrix.go.status == 'failure' ||
matrix.image.status == 'failure'
uses: slackapi/slack-github-action@v1.23.0
with:
payload: |
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Code Scanning Result (*${{ github.repository }}*)\n*<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|GitHub Actions Workflow URL>*"
}
}
],
"attachments": [
{
"color": "#FF0000",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*JS dependency check*: *${{ matrix.js.status }}*\n${{ matrix.js.summary }}"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Go dependency check*: *${{ matrix.go.status }}*\n${{ matrix.go.summary }}"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Image vulnerability check*: *${{ matrix.image.status }}*\n${{ matrix.image.summary }}\n"
}
}
]
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SECURITY_SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK

View File

@@ -1,242 +0,0 @@
name: PR Code Security Scan
on:
pull_request_review:
types:
- submitted
- edited
paths:
- 'package.json'
- 'api/go.mod'
- 'gruntfile.js'
- 'build/linux/Dockerfile'
- 'build/linux/alpine.Dockerfile'
- 'build/windows/Dockerfile'
- '.github/workflows/pr-security.yml'
jobs:
client-dependencies:
name: Client Dependency Check
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
github.event.review.body == '/scan'
outputs:
jsdiff: ${{ steps.set-diff-matrix.outputs.js_diff_result }}
steps:
- name: checkout repository
uses: actions/checkout@master
- name: scan vulnerabilities by Snyk
uses: snyk/actions/node@master
continue-on-error: true # To make sure that artifact upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
json: true
- name: upload scan result as pull-request artifact
uses: actions/upload-artifact@v3
with:
name: js-security-scan-feat-result
path: snyk.json
- name: download artifacts from develop branch built by nightly scan
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mv ./snyk.json ./js-snyk-feature.json
(gh run download -n js-security-scan-develop-result -R ${{ github.repository }} 2>&1 >/dev/null) || :
if [[ -e ./snyk.json ]]; then
mv ./snyk.json ./js-snyk-develop.json
else
echo "null" > ./js-snyk-develop.json
fi
- name: pr vs develop scan report comparison export to html
run: |
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=snyk --path="/data/js-snyk-feature.json" --compare-to="/data/js-snyk-develop.json" --output-type=table --export --export-filename="/data/js-result")
- name: upload html file as artifact
uses: actions/upload-artifact@v3
with:
name: html-js-result-compare-to-develop-${{github.run_id}}
path: js-result.html
- name: analyse different vulnerabilities against develop branch
id: set-diff-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=snyk --path="/data/js-snyk-feature.json" --compare-to="/data/js-snyk-develop.json" --output-type=matrix)
echo "js_diff_result=${result}" >> $GITHUB_OUTPUT
server-dependencies:
name: Server Dependency Check
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
github.event.review.body == '/scan'
outputs:
godiff: ${{ steps.set-diff-matrix.outputs.go_diff_result }}
steps:
- name: checkout repository
uses: actions/checkout@master
- name: install Go
uses: actions/setup-go@v3
with:
go-version: '1.19.5'
- name: download Go modules
run: cd ./api && go get -t -v -d ./...
- name: scan vulnerabilities by Snyk
continue-on-error: true # To make sure that artifact upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
run: |
yarn global add snyk
snyk test --file=./api/go.mod --json-file-output=snyk.json 2>/dev/null || :
- name: upload scan result as pull-request artifact
uses: actions/upload-artifact@v3
with:
name: go-security-scan-feature-result
path: snyk.json
- name: download artifacts from develop branch built by nightly scan
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mv ./snyk.json ./go-snyk-feature.json
(gh run download -n go-security-scan-develop-result -R ${{ github.repository }} 2>&1 >/dev/null) || :
if [[ -e ./snyk.json ]]; then
mv ./snyk.json ./go-snyk-develop.json
else
echo "null" > ./go-snyk-develop.json
fi
- name: pr vs develop scan report comparison export to html
run: |
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=snyk --path="/data/go-snyk-feature.json" --compare-to="/data/go-snyk-develop.json" --output-type=table --export --export-filename="/data/go-result")
- name: upload html file as artifact
uses: actions/upload-artifact@v3
with:
name: html-go-result-compare-to-develop-${{github.run_id}}
path: go-result.html
- name: analyse different vulnerabilities against develop branch
id: set-diff-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=snyk --path="/data/go-snyk-feature.json" --compare-to="/data/go-snyk-develop.json" --output-type=matrix)
echo "go_diff_result=${result}" >> $GITHUB_OUTPUT
image-vulnerability:
name: Image Vulnerability Check
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
github.event.review.body == '/scan'
outputs:
imagediff: ${{ steps.set-diff-matrix.outputs.image_diff_result }}
steps:
- name: checkout code
uses: actions/checkout@master
- name: install Go 1.19.5
uses: actions/setup-go@v3
with:
go-version: '1.19.5'
- name: install Node.js 18.x
uses: actions/setup-node@v3
with:
node-version: 18.x
- name: Install packages
run: yarn --frozen-lockfile
- name: build
run: make build-all
- name: set up docker buildx
uses: docker/setup-buildx-action@v2
- name: build and compress image
uses: docker/build-push-action@v4
with:
context: .
file: build/linux/Dockerfile
tags: trivy-portainer:${{ github.sha }}
outputs: type=docker,dest=/tmp/trivy-portainer-image.tar
- name: load docker image
run: |
docker load --input /tmp/trivy-portainer-image.tar
- name: scan vulnerabilities by Trivy
uses: docker://docker.io/aquasec/trivy:latest
continue-on-error: true
with:
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress trivy-portainer:${{ github.sha }}
- name: upload image security scan result as artifact
uses: actions/upload-artifact@v3
with:
name: image-security-scan-feature-result
path: image-trivy.json
- name: download artifacts from develop branch built by nightly scan
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mv ./image-trivy.json ./image-trivy-feature.json
(gh run download -n image-security-scan-develop-result -R ${{ github.repository }} 2>&1 >/dev/null) || :
if [[ -e ./image-trivy.json ]]; then
mv ./image-trivy.json ./image-trivy-develop.json
else
echo "null" > ./image-trivy-develop.json
fi
- name: pr vs develop scan report comparison export to html
run: |
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=trivy --path="/data/image-trivy-feature.json" --compare-to="/data/image-trivy-develop.json" --output-type=table --export --export-filename="/data/image-result")
- name: upload html file as artifact
uses: actions/upload-artifact@v3
with:
name: html-image-result-compare-to-develop-${{github.run_id}}
path: image-result.html
- name: analyse different vulnerabilities against develop branch
id: set-diff-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=trivy --path="/data/image-trivy-feature.json" --compare-to="/data/image-trivy-develop.json" --output-type=matrix)
echo "image_diff_result=${result}" >> $GITHUB_OUTPUT
result-analysis:
name: Analyse Scan Result Against develop Branch
needs: [client-dependencies, server-dependencies, image-vulnerability]
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
github.event.review.body == '/scan'
strategy:
matrix:
jsdiff: ${{fromJson(needs.client-dependencies.outputs.jsdiff)}}
godiff: ${{fromJson(needs.server-dependencies.outputs.godiff)}}
imagediff: ${{fromJson(needs.image-vulnerability.outputs.imagediff)}}
steps:
- name: check job status of diff result
if: >-
matrix.jsdiff.status == 'failure' ||
matrix.godiff.status == 'failure' ||
matrix.imagediff.status == 'failure'
run: |
echo "${{ matrix.jsdiff.status }}"
echo "${{ matrix.godiff.status }}"
echo "${{ matrix.imagediff.status }}"
echo "${{ matrix.jsdiff.summary }}"
echo "${{ matrix.godiff.summary }}"
echo "${{ matrix.imagediff.summary }}"
exit 1

View File

@@ -1,19 +0,0 @@
name: Automatic Rebase
on:
issue_comment:
types: [created]
jobs:
rebase:
name: Rebase
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase')
runs-on: ubuntu-latest
steps:
- name: Checkout the latest code
uses: actions/checkout@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0 # otherwise, you will fail to push refs to dest repo
- name: Automatic Rebase
uses: cirrus-actions/rebase@1.4
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,27 +0,0 @@
name: Close Stale Issues
on:
schedule:
- cron: '0 12 * * *'
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/stale@v4.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
# Issue Config
days-before-issue-stale: 60
days-before-issue-close: 7
stale-issue-label: 'status/stale'
exempt-all-issue-milestones: true # Do not stale issues in a milestone
exempt-issue-labels: kind/enhancement, kind/style, kind/workaround, kind/refactor, bug/need-confirmation, bug/confirmed, status/discuss
stale-issue-message: 'This issue has been marked as stale as it has not had recent activity, it will be closed if no further activity occurs in the next 7 days. If you believe that it has been incorrectly labelled as stale, leave a comment and the label will be removed.'
close-issue-message: 'Since no further activity has appeared on this issue it will be closed. If you believe that it has been incorrectly closed, leave a comment mentioning `portainer/support` and one of our staff will then review the issue. Note - If it is an old bug report, make sure that it is reproduceable in the latest version of Portainer as it may have already been fixed.'
# Pull Request Config
days-before-pr-stale: -1 # Do not stale pull request
days-before-pr-close: -1 # Do not close pull request

View File

@@ -1,25 +0,0 @@
name: Test
on: push
jobs:
test-client:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '18'
cache: 'yarn'
- run: yarn --frozen-lockfile
- name: Run tests
run: yarn jest --maxWorkers=2
test-server:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: 1.19.5
- name: Run tests
run: make test-server

View File

@@ -1,29 +0,0 @@
name: Validate OpenAPI specs
on:
pull_request:
branches:
- master
- develop
- 'release/*'
jobs:
openapi-spec:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: '1.18'
- name: Download golang modules
run: cd ./api && go get -t -v -d ./...
- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- run: yarn --frozen-lockfile
- name: Validate OpenAPI Spec
run: make docs-validate

13
.gitignore vendored
View File

@@ -3,18 +3,15 @@ bower_components
dist
portainer-checksum.txt
api/cmd/portainer/portainer*
storybook-static
.tmp
**/.vscode/settings.json
**/.vscode/tasks.json
.vscode
*.DS_Store
.eslintcache
__debug_bin*
api/docs
.idea
test/e2e/cypress/screenshots
*.db
*.log
__debug_bin
api/docs
.env
go.work.sum

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
yarn lint-staged

View File

@@ -1,2 +0,0 @@
dist
api/datastore/test_data

View File

@@ -8,12 +8,6 @@
"options": {
"parser": "angular"
}
},
{
"files": ["*.{j,t}sx", "*.ts"],
"options": {
"printWidth": 80
}
}
]
}

View File

@@ -1,95 +0,0 @@
import { StorybookConfig } from '@storybook/react-webpack5';
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
import { Configuration } from 'webpack';
import postcss from 'postcss';
const config: StorybookConfig = {
stories: ['../app/**/*.stories.@(ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
{
name: '@storybook/addon-styling',
options: {
cssLoaderOptions: {
importLoaders: 1,
modules: {
localIdentName: '[path][name]__[local]',
auto: true,
exportLocalsConvention: 'camelCaseOnly',
},
},
postCss: {
implementation: postcss,
},
},
},
],
webpackFinal: (config) => {
const rules = config?.module?.rules || [];
const imageRule = rules.find((rule) => {
const test = (rule as { test: RegExp }).test;
if (!test) {
return false;
}
return test.test('.svg');
}) as { [key: string]: any };
imageRule.exclude = /\.svg$/;
rules.unshift({
test: /\.svg$/i,
type: 'asset',
resourceQuery: {
not: [/c/],
}, // exclude react component if *.svg?url
});
rules.unshift({
test: /\.svg$/i,
issuer: /\.(js|ts)(x)?$/,
resourceQuery: /c/,
// *.svg?c
use: [
{
loader: '@svgr/webpack',
options: {
icon: true,
},
},
],
});
return {
...config,
resolve: {
...config.resolve,
plugins: [
...(config.resolve?.plugins || []),
new TsconfigPathsPlugin({
extensions: config.resolve?.extensions,
}),
],
},
module: {
...config.module,
rules,
},
} satisfies Configuration;
},
staticDirs: ['./public'],
typescript: {
reactDocgen: 'react-docgen-typescript',
},
framework: {
name: '@storybook/react-webpack5',
options: {},
},
docs: {
autodocs: true,
},
};
export default config;

View File

@@ -1,48 +0,0 @@
import '../app/assets/css';
import { pushStateLocationPlugin, UIRouter } from '@uirouter/react';
import { initialize as initMSW, mswDecorator } from 'msw-storybook-addon';
import { handlers } from '@/setup-tests/server-handlers';
import { QueryClient, QueryClientProvider } from 'react-query';
// Initialize MSW
initMSW({
onUnhandledRequest: ({ method, url }) => {
if (url.pathname.startsWith('/api')) {
console.error(`Unhandled ${method} request to ${url}.
This exception has been only logged in the console, however, it's strongly recommended to resolve this error as you don't want unmocked data in Storybook stories.
If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses
`);
}
},
});
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
msw: {
handlers,
},
};
const testQueryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
export const decorators = [
(Story) => (
<QueryClientProvider client={testQueryClient}>
<UIRouter plugins={[pushStateLocationPlugin]}>
<Story />
</UIRouter>
</QueryClientProvider>
),
mswDecorator,
];

View File

@@ -1,328 +0,0 @@
/* eslint-disable */
/* tslint:disable */
/**
* Mock Service Worker (0.36.3).
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
*/
const INTEGRITY_CHECKSUM = '02f4ad4a2797f85668baf196e553d929';
const bypassHeaderName = 'x-msw-bypass';
const activeClientIds = new Set();
self.addEventListener('install', function () {
return self.skipWaiting();
});
self.addEventListener('activate', async function (event) {
return self.clients.claim();
});
self.addEventListener('message', async function (event) {
const clientId = event.source.id;
if (!clientId || !self.clients) {
return;
}
const client = await self.clients.get(clientId);
if (!client) {
return;
}
const allClients = await self.clients.matchAll();
switch (event.data) {
case 'KEEPALIVE_REQUEST': {
sendToClient(client, {
type: 'KEEPALIVE_RESPONSE',
});
break;
}
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: INTEGRITY_CHECKSUM,
});
break;
}
case 'MOCK_ACTIVATE': {
activeClientIds.add(clientId);
sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: true,
});
break;
}
case 'MOCK_DEACTIVATE': {
activeClientIds.delete(clientId);
break;
}
case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId);
const remainingClients = allClients.filter((client) => {
return client.id !== clientId;
});
// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister();
}
break;
}
}
});
// Resolve the "main" client for the given event.
// Client that issues a request doesn't necessarily equal the client
// that registered the worker. It's with the latter the worker should
// communicate with during the response resolving phase.
async function resolveMainClient(event) {
const client = await self.clients.get(event.clientId);
if (client.frameType === 'top-level') {
return client;
}
const allClients = await self.clients.matchAll();
return allClients
.filter((client) => {
// Get only those clients that are currently visible.
return client.visibilityState === 'visible';
})
.find((client) => {
// Find the client ID that's recorded in the
// set of clients that have registered the worker.
return activeClientIds.has(client.id);
});
}
async function handleRequest(event, requestId) {
const client = await resolveMainClient(event);
const response = await getResponse(event, client, requestId);
// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
(async function () {
const clonedResponse = response.clone();
sendToClient(client, {
type: 'RESPONSE',
payload: {
requestId,
type: clonedResponse.type,
ok: clonedResponse.ok,
status: clonedResponse.status,
statusText: clonedResponse.statusText,
body: clonedResponse.body === null ? null : await clonedResponse.text(),
headers: serializeHeaders(clonedResponse.headers),
redirected: clonedResponse.redirected,
},
});
})();
}
return response;
}
async function getResponse(event, client, requestId) {
const { request } = event;
const requestClone = request.clone();
const getOriginalResponse = () => fetch(requestClone);
// Bypass mocking when the request client is not active.
if (!client) {
return getOriginalResponse();
}
// Bypass initial page load requests (i.e. static assets).
// The absence of the immediate/parent client in the map of the active clients
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests.
if (!activeClientIds.has(client.id)) {
return await getOriginalResponse();
}
// Bypass requests with the explicit bypass header
if (requestClone.headers.get(bypassHeaderName) === 'true') {
const cleanRequestHeaders = serializeHeaders(requestClone.headers);
// Remove the bypass header to comply with the CORS preflight check.
delete cleanRequestHeaders[bypassHeaderName];
const originalRequest = new Request(requestClone, {
headers: new Headers(cleanRequestHeaders),
});
return fetch(originalRequest);
}
// Send the request to the client-side MSW.
const reqHeaders = serializeHeaders(request.headers);
const body = await request.text();
const clientMessage = await sendToClient(client, {
type: 'REQUEST',
payload: {
id: requestId,
url: request.url,
method: request.method,
headers: reqHeaders,
cache: request.cache,
mode: request.mode,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body,
bodyUsed: request.bodyUsed,
keepalive: request.keepalive,
},
});
switch (clientMessage.type) {
case 'MOCK_SUCCESS': {
return delayPromise(() => respondWithMock(clientMessage), clientMessage.payload.delay);
}
case 'MOCK_NOT_FOUND': {
return getOriginalResponse();
}
case 'NETWORK_ERROR': {
const { name, message } = clientMessage.payload;
const networkError = new Error(message);
networkError.name = name;
// Rejecting a request Promise emulates a network error.
throw networkError;
}
case 'INTERNAL_ERROR': {
const parsedBody = JSON.parse(clientMessage.payload.body);
console.error(
`\
[MSW] Uncaught exception in the request handler for "%s %s":
${parsedBody.location}
This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error, as it indicates a mistake in your code. If you wish to mock an error response, please see this guide: https://mswjs.io/docs/recipes/mocking-error-responses\
`,
request.method,
request.url
);
return respondWithMock(clientMessage);
}
}
return getOriginalResponse();
}
self.addEventListener('fetch', function (event) {
const { request } = event;
const accept = request.headers.get('accept') || '';
// Bypass server-sent events.
if (accept.includes('text/event-stream')) {
return;
}
// Bypass navigation requests.
if (request.mode === 'navigate') {
return;
}
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
return;
}
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been deleted (still remains active until the next reload).
if (activeClientIds.size === 0) {
return;
}
const requestId = uuidv4();
return event.respondWith(
handleRequest(event, requestId).catch((error) => {
if (error.name === 'NetworkError') {
console.warn('[MSW] Successfully emulated a network error for the "%s %s" request.', request.method, request.url);
return;
}
// At this point, any exception indicates an issue with the original request/response.
console.error(
`\
[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`,
request.method,
request.url,
`${error.name}: ${error.message}`
);
})
);
});
function serializeHeaders(headers) {
const reqHeaders = {};
headers.forEach((value, name) => {
reqHeaders[name] = reqHeaders[name] ? [].concat(reqHeaders[name]).concat(value) : value;
});
return reqHeaders;
}
function sendToClient(client, message) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel();
channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
return reject(event.data.error);
}
resolve(event.data);
};
client.postMessage(JSON.stringify(message), [channel.port2]);
});
}
function delayPromise(cb, duration) {
return new Promise((resolve) => {
setTimeout(() => resolve(cb()), duration);
});
}
function respondWithMock(clientMessage) {
return new Response(clientMessage.payload.body, {
...clientMessage.payload,
headers: clientMessage.payload.headers,
});
}
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0;
const v = c == 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}

View File

@@ -9,7 +9,7 @@
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceRoot}/api/cmd/portainer",
"program": "${workspaceRoot}/api/cmd/portainer/main.go",
"cwd": "${workspaceRoot}",
"env": {},
"showLog": true,

View File

@@ -15,15 +15,6 @@
// ],
// "description": "Log output to console"
// }
"React Named Export Component": {
"prefix": "rnec",
"body": [
"export function $TM_FILENAME_BASE() {",
" return <div>$TM_FILENAME_BASE</div>;",
"}"
],
"description": "React Named Export Component"
},
"Component": {
"scope": "javascript",
"prefix": "mycomponent",
@@ -159,7 +150,6 @@
"// @description ",
"// @description **Access policy**: ",
"// @tags ",
"// @security ApiKeyAuth",
"// @security jwt",
"// @accept json",
"// @produce json",
@@ -173,19 +163,5 @@
"// @failure 500 \"Server error\"",
"// @router /{id} [get]"
]
},
"analytics": {
"prefix": "nlt",
"body": ["analytics-on", "analytics-category=\"$1\"", "analytics-event=\"$2\""],
"description": "analytics"
},
"analytics-if": {
"prefix": "nltf",
"body": ["analytics-if=\"$1\""],
"description": "analytics"
},
"analytics-metadata": {
"prefix": "nltm",
"body": "analytics-properties=\"{ metadata: { $1 } }\""
}
}

View File

@@ -1,8 +0,0 @@
{
"go.lintTool": "golangci-lint",
"go.lintFlags": ["--fast", "-E", "exportloopref"],
"gopls": {
"build.expandWorkspaceToModule": false
},
"gitlens.advanced.blame.customArguments": ["--ignore-revs-file", ".git-blame-ignore-revs"]
}

View File

@@ -1,32 +0,0 @@
# Open Source License Attribution
This application uses Open Source components. You can find the source
code of their open source projects along with license information below.
We acknowledge and are grateful to these developers for their contributions
to open source.
### [angular-json-tree](https://github.com/awendland/angular-json-tree)
by [Alex Wendland](https://github.com/awendland) is licensed under [CC BY 4.0 License](https://creativecommons.org/licenses/by/4.0/)
### [caniuse-db](https://github.com/Fyrd/caniuse)
by [caniuse.com](caniuse.com) is licensed under [CC BY 4.0 License](https://creativecommons.org/licenses/by/4.0/)
### [caniuse-lite](https://github.com/ben-eb/caniuse-lite)
by [caniuse.com](caniuse.com) is licensed under [CC BY 4.0 License](https://creativecommons.org/licenses/by/4.0/)
### [spdx-exceptions](https://github.com/jslicense/spdx-exceptions.json)
by Kyle Mitchell using [SPDX](https://spdx.dev/) from Linux Foundation licensed under [CC BY 3.0 License](https://creativecommons.org/licenses/by/3.0/)
### [fontawesome-free](https://github.com/FortAwesome/Font-Awesome) Icons
by [Fort Awesome](https://fortawesome.com/) is licensed under [CC BY 4.0 License](https://creativecommons.org/licenses/by/4.0/)
Portainer also contains the following code, which is licensed under the [MIT license](https://opensource.org/licenses/MIT):
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (portainer.io)
rdash-angular: Copyright (c) [2014][elliot hesp]

View File

@@ -25,7 +25,7 @@ Each commit message should include a **type**, a **scope** and a **subject**:
<type>(<scope>): <subject>
```
Lines should not exceed 100 characters. This allows the message to be easier to read on GitHub as well as in various git tools and produces a nice, neat commit log ie:
Lines should not exceed 100 characters. This allows the message to be easier to read on github as well as in various git tools and produces a nice, neat commit log ie:
```
#271 feat(containers): add exposed ports in the containers view
@@ -63,7 +63,7 @@ The subject contains succinct description of the change:
## Contribution process
Our contribution process is described below. Some of the steps can be visualized inside GitHub via specific `status/` labels, such as `status/1-functional-review` or `status/2-technical-review`.
Our contribution process is described below. Some of the steps can be visualized inside Github via specific `status/` labels, such as `status/1-functional-review` or `status/2-technical-review`.
### Bug report
@@ -75,45 +75,25 @@ The feature request process is similar to the bug report process but has an extr
![portainer_featurerequest_workflow](https://user-images.githubusercontent.com/5485061/45727229-5ad39f00-bbf5-11e8-9550-16ba66c50615.png)
## Build and run Portainer locally
## Build Portainer locally
Ensure you have Docker, Node.js, yarn, and Golang installed in the correct versions.
Install dependencies:
Install dependencies with yarn:
```sh
$ make deps
$ yarn
```
Then build and run the project in a Docker container:
Then build and run the project:
```sh
$ make dev
$ yarn start
```
Portainer server can now be accessed at <https://localhost:9443>. and UI dev server runs on <http://localhost:8999>.
Portainer can now be accessed at <http://localhost:9000>.
if you want to build the project you can run:
```sh
make build-all
```
For additional make commands, run `make help`.
Find more detailed steps at <https://docs.portainer.io/contribute/build>.
### Build customization
You can customize the following settings:
- `PORTAINER_DATA`: The host dir or volume name used by portainer (default is `/tmp/portainer`, which won't persist over reboots).
- `PORTAINER_PROJECT`: The root dir of the repository - `${portainerRoot}/dist/` is imported into the container to get the build artifacts and external tools (defaults to `your current dir`).
- `PORTAINER_FLAGS`: a list of flags to be used on the portainer commandline, in the form `--admin-password=<pwd hash> --feat fdo=false --feat open-amt` (default: `""`).
## Testing your build
The `--log-level=DEBUG` flag can be passed to the Portainer container in order to provide additional debug output which may be useful when troubleshooting your builds. Please note that this flag was originally intended for internal use and as such the format, functionality and output may change between releases without warning.
Find more detailed steps at <https://documentation.portainer.io/contributing/instructions/>.
## Adding api docs
@@ -132,7 +112,6 @@ When adding a new route to an existing handler use the following as a template (
// @description
// @description **Access policy**:
// @tags
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
@@ -148,9 +127,3 @@ When adding a new route to an existing handler use the following as a template (
```
explanation about each line can be found (here)[https://github.com/swaggo/swag#api-operation]
## Licensing
See the [LICENSE](https://github.com/portainer/portainer/blob/develop/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.
We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes.

126
Makefile
View File

@@ -1,126 +0,0 @@
# See: https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63
# For a list of valid GOOS and GOARCH values
# Note: these can be overriden on the command line e.g. `make PLATFORM=<platform> ARCH=<arch>`
PLATFORM=$(shell go env GOOS)
ARCH=$(shell go env GOARCH)
# build target, can be one of "production", "testing", "development"
ENV=development
WEBPACK_CONFIG=webpack/webpack.$(ENV).js
TAG=latest
SWAG=go run github.com/swaggo/swag/cmd/swag@v1.8.11
GOTESTSUM=go run gotest.tools/gotestsum@latest
# Don't change anything below this line unless you know what you're doing
.DEFAULT_GOAL := help
##@ Building
.PHONY: init-dist build-storybook build build-client build-server build-image devops
init-dist:
@mkdir -p dist
build-all: deps build-server build-client ## Build the client, server and download external dependancies (doesn't build an image)
build-client: init-dist ## Build the client
export NODE_ENV=$(ENV) && yarn build --config $(WEBPACK_CONFIG)
build-server: init-dist ## Build the server binary
./build/build_binary.sh "$(PLATFORM)" "$(ARCH)"
build-image: build-all ## Build the Portainer image locally
docker buildx build --load -t portainerci/portainer:$(TAG) -f build/linux/Dockerfile .
build-storybook: ## Build and serve the storybook files
yarn storybook:build
devops: clean deps build-client ## Build the everything target specifically for CI
echo "Building the devops binary..."
@./build/build_binary_azuredevops.sh "$(PLATFORM)" "$(ARCH)"
##@ Build dependencies
.PHONY: deps server-deps client-deps tidy
deps: server-deps client-deps ## Download all client and server build dependancies
server-deps: init-dist ## Download dependant server binaries
@./build/download_binaries.sh $(PLATFORM) $(ARCH)
client-deps: ## Install client dependencies
yarn
tidy: ## Tidy up the go.mod file
cd api && go mod tidy
##@ Cleanup
.PHONY: clean
clean: ## Remove all build and download artifacts
@echo "Clearing the dist directory..."
@rm -rf dist/*
##@ Testing
.PHONY: test test-client test-server
test: test-server test-client ## Run all tests
test-client: ## Run client tests
yarn test
test-server: ## Run server tests
cd api && $(GOTESTSUM) --format pkgname-and-test-fails --format-hide-empty-pkg --hide-summary skipped -- -cover ./...
##@ Dev
.PHONY: dev dev-client dev-server
dev: ## Run both the client and server in development mode
make dev-server
make dev-client
dev-client: ## Run the client in development mode
yarn dev
dev-server: build-server ## Run the server in development mode
@./dev/run_container.sh
##@ Format
.PHONY: format format-client format-server
format: format-client format-server ## Format all code
format-client: ## Format client code
yarn format
format-server: ## Format server code
cd api && go fmt ./...
##@ Lint
.PHONY: lint lint-client lint-server
lint: lint-client lint-server ## Lint all code
lint-client: ## Lint client code
yarn lint
lint-server: ## Lint server code
cd api && go vet ./...
##@ Extension
.PHONY: dev-extension
dev-extension: build-server build-client ## Run the extension in development mode
make local -f build/docker-extension/Makefile
##@ Docs
.PHONY: docs-build docs-validate docs-clean docs-validate-clean
docs-build: init-dist ## Build docs
cd api && $(SWAG) init -o "../dist/docs" -ot "yaml" -g ./http/handler/handler.go --parseDependency --parseInternal --parseDepth 2 --markdownFiles ./
docs-validate: docs-build ## Validate docs
yarn swagger2openapi --warnOnly dist/docs/swagger.yaml -o dist/docs/openapi.yaml
yarn swagger-cli validate dist/docs/openapi.yaml
##@ Helpers
.PHONY: help
help: ## Display this help
@awk 'BEGIN {FS = ":.*##"; printf "Usage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

View File

@@ -1,65 +1,60 @@
<p align="center">
<img title="portainer" src='https://github.com/portainer/portainer/blob/develop/app/assets/images/portainer-github-banner.png?raw=true' />
<img title="portainer" src='https://github.com/portainer/portainer/blob/develop/app/assets/images/logo_alt.png?raw=true' />
</p>
**Portainer Community Edition** is a lightweight service delivery platform for containerized applications that can be used to manage Docker, Swarm, Kubernetes and ACI environments. It is designed to be as simple to deploy as it is to use. The application allows you to manage all your orchestrator resources (containers, images, volumes, networks and more) through a smart GUI and/or an extensive API.
[![Docker Pulls](https://img.shields.io/docker/pulls/portainer/portainer.svg)](https://hub.docker.com/r/portainer/portainer/)
[![Microbadger](https://images.microbadger.com/badges/image/portainer/portainer.svg)](http://microbadger.com/images/portainer/portainer 'Image size')
[![Build Status](https://portainer.visualstudio.com/Portainer%20CI/_apis/build/status/Portainer%20CI?branchName=develop)](https://portainer.visualstudio.com/Portainer%20CI/_build/latest?definitionId=3&branchName=develop)
[![Code Climate](https://codeclimate.com/github/portainer/portainer/badges/gpa.svg)](https://codeclimate.com/github/portainer/portainer)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6)
Portainer consists of a single container that can run on any cluster. It can be deployed as a Linux container or a Windows native container.
**_Portainer_** is a lightweight management UI which allows you to **easily** manage your different Docker environments (Docker hosts or Swarm clusters).
**_Portainer_** is meant to be as **simple** to deploy as it is to use. It consists of a single container that can run on any Docker engine (can be deployed as Linux container or a Windows native container, supports other platforms too).
**_Portainer_** allows you to manage all your Docker resources (containers, images, volumes, networks and more!) It is compatible with the _standalone Docker_ engine and with _Docker Swarm mode_.
**Portainer Business Edition** builds on the open-source base and includes a range of advanced features and functions (like RBAC and Support) that are specific to the needs of business users.
## Demo
- [Compare Portainer CE and Compare Portainer BE](https://portainer.io/products)
- [Take3 get 3 free nodes of Portainer Business for as long as you want them](https://www.portainer.io/take-3)
- [Portainer BE install guide](https://install.portainer.io)
You can try out the public demo instance: http://demo.portainer.io/ (login with the username **admin** and the password **tryportainer**).
## Latest Version
Please note that the public demo cluster is **reset every 15min**.
Portainer CE is updated regularly. We aim to do an update release every couple of months.
Alternatively, you can deploy a copy of the demo stack inside a [play-with-docker (PWD)](https://labs.play-with-docker.com) playground:
[![latest version](https://img.shields.io/github/v/release/portainer/portainer?color=%2344cc11&label=Latest%20release&style=for-the-badge)](https://github.com/portainer/portainer/releases/latest)
- Browse [PWD/?stack=portainer-demo/play-with-docker/docker-stack.yml](http://play-with-docker.com/?stack=https://raw.githubusercontent.com/portainer/portainer-demo/master/play-with-docker/docker-stack.yml)
- Sign in with your [Docker ID](https://docs.docker.com/docker-id)
- Follow [these](https://github.com/portainer/portainer-demo/blob/master/play-with-docker/docker-stack.yml#L5-L8) steps.
Unlike the public demo, the playground sessions are deleted after 4 hours. Apart from that, all the settings are the same, including default credentials.
## Getting started
- [Deploy Portainer](https://docs.portainer.io/start/install)
- [Documentation](https://docs.portainer.io)
- [Contribute to the project](https://docs.portainer.io/contribute/contribute)
## Features & Functions
View [this](https://www.portainer.io/products) table to see all of the Portainer CE functionality and compare to Portainer Business.
- [Portainer CE for Docker / Docker Swarm](https://www.portainer.io/solutions/docker)
- [Portainer CE for Kubernetes](https://www.portainer.io/solutions/kubernetes-ui)
- [Deploy Portainer](https://www.portainer.io/installation/)
- [Documentation](https://documentation.portainer.io)
## Getting help
Portainer CE is an open source project and is supported by the community. You can buy a supported version of Portainer at portainer.io
For FORMAL Support, please purchase a support subscription from here: https://www.portainer.io/products-services/portainer-business-support/
Learn more about Portainer's community support channels [here.](https://www.portainer.io/get-support-for-portainer)
For community support: You can find more information about Portainer's community support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
- Issues: https://github.com/portainer/portainer/issues
- Slack (chat): [https://portainer.io/slack](https://portainer.io/slack)
You can join the Portainer Community by visiting [https://www.portainer.io/join-our-community](https://www.portainer.io/join-our-community). This will give you advance notice of events, content and other related Portainer content.
- FAQ: https://documentation.portainer.io
- Slack (chat): https://portainer.io/slack/
## Reporting bugs and contributing
- Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new).
- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://docs.portainer.io/contribute/contribute) to build it locally and make a pull request.
- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://www.portainer.io/documentation/how-to-contribute/) to build it locally and make a pull request. We need all the help we can get!
## Security
- Here at Portainer, we believe in [responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure) of security issues. If you have found a security issue, please report it to <security@portainer.io>.
## Work for us
If you are a developer, and our code in this repo makes sense to you, we would love to hear from you. We are always on the hunt for awesome devs, either freelance or employed. Drop us a line to info@portainer.io with your details and/or visit our [careers page](https://portainer.io/careers).
## Privacy
**To make sure we focus our development effort in the right places we need to know which features get used most often. To give us this information we use [Matomo Analytics](https://matomo.org/), which is hosted in Germany and is fully GDPR compliant.**
When Portainer first starts, you are given the option to DISABLE analytics. If you **don't** choose to disable it, we collect anonymous usage as per [our privacy policy](https://www.portainer.io/privacy-policy). **Please note**, there is no personally identifiable information sent or stored at any time and we only use the data to help us improve Portainer.
When Portainer first starts, you are given the option to DISABLE analytics. If you **don't** choose to disable it, we collect anonymous usage as per [our privacy policy](https://www.portainer.io/documentation/in-app-analytics-and-privacy-policy/). **Please note**, there is no personally identifiable information sent or stored at any time and we only use the data to help us improve Portainer.
## Limitations
@@ -69,4 +64,8 @@ Portainer supports "Current - 2 docker versions only. Prior versions may operate
Portainer is licensed under the zlib license. See [LICENSE](./LICENSE) for reference.
Portainer also contains code from open source projects. See [ATTRIBUTIONS.md](./ATTRIBUTIONS.md) for a list.
Portainer also contains the following code, which is licensed under the [MIT license](https://opensource.org/licenses/MIT):
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (portainer.io)
rdash-angular: Copyright (c) [2014][elliot hesp]

View File

@@ -1,31 +0,0 @@
linters:
# Disable all linters, the defaults don't pass on our code yet
disable-all: true
# Enable these for now
enable:
- depguard
- govet
- errorlint
- exportloopref
linters-settings:
depguard:
rules:
main:
deny:
- pkg: 'github.com/sirupsen/logrus'
desc: 'logging is allowed only by github.com/rs/zerolog'
- pkg: 'golang.org/x/exp'
desc: 'exp is not allowed'
files:
- '!**/*_test.go'
- '!**/base.go'
- '!**/base_tx.go'
# errorlint is causing a typecheck error for some reason. The go compiler will report these
# anyway, so ignore them from the linter
issues:
exclude-rules:
- path: ./
linters:
- typecheck

View File

@@ -2,88 +2,59 @@ package adminmonitor
import (
"context"
"net/http"
"strings"
"sync"
"log"
"time"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/rs/zerolog/log"
)
const RedirectReasonAdminInitTimeout string = "AdminInitTimeout"
var logFatalf = log.Fatalf
type Monitor struct {
timeout time.Duration
datastore dataservices.DataStore
shutdownCtx context.Context
cancellationFunc context.CancelFunc
mu sync.RWMutex
adminInitDisabled bool
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 dataservices.DataStore, shutdownCtx context.Context) *Monitor {
func New(timeout time.Duration, datastore portainer.DataStore, shutdownCtx context.Context) *Monitor {
return &Monitor{
timeout: timeout,
datastore: datastore,
shutdownCtx: shutdownCtx,
adminInitDisabled: false,
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() {
m.mu.Lock()
defer m.mu.Unlock()
if m.cancellationFunc != nil {
return
}
cancellationCtx, cancellationFunc := context.WithCancel(context.Background())
m.cancellationFunc = cancellationFunc
go func() {
log.Debug().Msg("start initialization monitor")
log.Println("[DEBUG] [internal,init] [message: start initialization monitor ]")
select {
case <-time.After(m.timeout):
initialized, err := m.WasInitialized()
if err != nil {
log.Error().Err(err).Msg("AdminMonitor failed to determine if Portainer is Initialized")
return
logFatalf("failed getting admin user: %s", err)
}
if !initialized {
log.Info().Msg("the Portainer instance timed out for security purposes, to re-enable your Portainer instance, you will need to restart Portainer")
m.mu.Lock()
defer m.mu.Unlock()
m.adminInitDisabled = true
return
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.Debug().Msg("canceling initialization monitor")
log.Println("[DEBUG] [internal,init] [message: canceling initialization monitor]")
case <-m.shutdownCtx.Done():
log.Debug().Msg("shutting down initialization monitor")
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() {
m.mu.Lock()
defer m.mu.Unlock()
if m.cancellationFunc == nil {
return
}
m.cancellationFunc()
m.cancellationFunc = nil
}
@@ -94,27 +65,5 @@ func (m *Monitor) WasInitialized() (bool, error) {
if err != nil {
return false, err
}
return len(users) > 0, nil
}
func (m *Monitor) WasInstanceDisabled() bool {
m.mu.RLock()
defer m.mu.RUnlock()
return m.adminInitDisabled
}
// WithRedirect checks whether administrator initialisation timeout. If so, it will return the error with redirect reason.
// Otherwise, it will pass through the request to next
func (m *Monitor) WithRedirect(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if m.WasInstanceDisabled() && strings.HasPrefix(r.RequestURI, "/api") && r.RequestURI != "/api/status" && r.RequestURI != "/api/settings/public" {
w.Header().Set("redirect-reason", RedirectReasonAdminInitTimeout)
httperror.WriteError(w, http.StatusSeeOther, "Administrator initialization timeout", nil)
return
}
next.ServeHTTP(w, r)
})
}

View File

@@ -21,18 +21,6 @@ func Test_stopCouldBeCalledMultipleTimes(t *testing.T) {
monitor.Stop()
}
func Test_startOrStopCouldBeCalledMultipleTimesConcurrently(t *testing.T) {
monitor := New(1*time.Minute, nil, context.Background())
go monitor.Start()
monitor.Start()
go monitor.Stop()
monitor.Stop()
time.Sleep(2 * time.Second)
}
func Test_canStopStartedMonitor(t *testing.T) {
monitor := New(1*time.Minute, nil, context.Background())
monitor.Start()
@@ -42,13 +30,21 @@ func Test_canStopStartedMonitor(t *testing.T) {
assert.Nil(t, monitor.cancellationFunc, "cancellation function should absent in stopped monitor")
}
func Test_start_shouldDisableInstanceAfterTimeout_ifNotInitialized(t *testing.T) {
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)
<-time.After(20 * timeout)
assert.True(t, monitor.WasInstanceDisabled(), "monitor should have been timeout and instance is disabled")
assert.True(t, fataled, "monitor should been timeout and fatal")
}

View File

@@ -1,74 +0,0 @@
package agent
import (
"crypto/tls"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/url"
)
// GetAgentVersionAndPlatform returns the agent version and platform
//
// it sends a ping to the agent and parses the version and platform from the headers
func GetAgentVersionAndPlatform(endpointUrl string, tlsConfig *tls.Config) (portainer.AgentPlatform, string, error) {
httpCli := &http.Client{
Timeout: 3 * time.Second,
}
if tlsConfig != nil {
httpCli.Transport = &http.Transport{
TLSClientConfig: tlsConfig,
}
}
parsedURL, err := url.ParseURL(endpointUrl + "/ping")
if err != nil {
return 0, "", err
}
parsedURL.Scheme = "https"
req, err := http.NewRequest(http.MethodGet, parsedURL.String(), nil)
if err != nil {
return 0, "", err
}
resp, err := httpCli.Do(req)
if err != nil {
return 0, "", err
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
return 0, "", fmt.Errorf("Failed request with status %d", resp.StatusCode)
}
version := resp.Header.Get(portainer.PortainerAgentHeader)
if version == "" {
return 0, "", errors.New("Version Header is missing")
}
agentPlatformHeader := resp.Header.Get(portainer.HTTPResponseAgentPlatform)
if agentPlatformHeader == "" {
return 0, "", errors.New("Agent Platform Header is missing")
}
agentPlatformNumber, err := strconv.Atoi(agentPlatformHeader)
if err != nil {
return 0, "", err
}
if agentPlatformNumber == 0 {
return 0, "", errors.New("Agent platform is invalid")
}
return portainer.AgentPlatform(agentPlatformNumber), version, nil
}

View File

@@ -1,10 +1,10 @@
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://documentation.portainer.io/api/api-examples/
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 environments(endpoints) require to be authenticated as well as some level of authorization to be used.
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.
@@ -16,7 +16,7 @@ Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIs
# Security
Each API environment(endpoint) has an associated access policy, it is documented in the description of each environment(endpoint).
Each API endpoint has an associated access policy, it is documented in the description of each endpoint.
Different access policies are available:
@@ -27,38 +27,27 @@ Different access policies are available:
### Public access
No authentication is required to access the environments(endpoints) with this access policy.
No authentication is required to access the endpoints with this access policy.
### Authenticated access
Authentication is required to access the environments(endpoints) with this access policy.
Authentication is required to access the endpoints with this access policy.
### Restricted access
Authentication is required to access the environments(endpoints) with this access policy.
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 environments(endpoints) with this access policy.
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 environments(endpoints) to manage your Docker resources (create a container, remove a volume, etc...).
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 environment(endpoint) (which is not documented below due to Swagger limitations). This environment(endpoint) has a restricted access policy so you still need to be authenticated to be able to query this environment(endpoint). Any query on this environment(endpoint) will be proxied to the Docker API of the associated environment(endpoint) (requests and responses objects are the same as documented in the Docker 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).
# Private Registry
Using private registry, you will need to pass a based64 encoded JSON string {"registryId":\<registryID value\>} inside the Request Header. The parameter name is "X-Registry-Auth".
\<registryID value\> - The registry ID where the repository was created.
Example:
```
eyJyZWdpc3RyeUlkIjoxfQ==
```
**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://documentation.portainer.io/api/api-examples/).
**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).

View File

@@ -1,17 +0,0 @@
package apikey
import (
portainer "github.com/portainer/portainer/api"
)
// APIKeyService represents a service for managing API keys.
type APIKeyService interface {
HashRaw(rawKey string) []byte
GenerateApiKey(user portainer.User, description string) (string, *portainer.APIKey, error)
GetAPIKey(apiKeyID portainer.APIKeyID) (*portainer.APIKey, error)
GetAPIKeys(userID portainer.UserID) ([]portainer.APIKey, error)
GetDigestUserAndKey(digest []byte) (portainer.User, portainer.APIKey, error)
UpdateAPIKey(apiKey *portainer.APIKey) error
DeleteAPIKey(apiKeyID portainer.APIKeyID) error
InvalidateUserKeyCache(userId portainer.UserID) bool
}

View File

@@ -1,51 +0,0 @@
package apikey
import (
"testing"
"github.com/portainer/portainer/api/internal/securecookie"
"github.com/stretchr/testify/assert"
)
func Test_generateRandomKey(t *testing.T) {
is := assert.New(t)
tests := []struct {
name string
wantLenth int
}{
{
name: "Generate a random key of length 16",
wantLenth: 16,
},
{
name: "Generate a random key of length 32",
wantLenth: 32,
},
{
name: "Generate a random key of length 64",
wantLenth: 64,
},
{
name: "Generate a random key of length 128",
wantLenth: 128,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := securecookie.GenerateRandomKey(tt.wantLenth)
is.Equal(tt.wantLenth, len(got))
})
}
t.Run("Generated keys are unique", func(t *testing.T) {
keys := make(map[string]bool)
for i := 0; i < 100; i++ {
key := securecookie.GenerateRandomKey(8)
_, ok := keys[string(key)]
is.False(ok)
keys[string(key)] = true
}
})
}

View File

@@ -1,69 +0,0 @@
package apikey
import (
lru "github.com/hashicorp/golang-lru"
portainer "github.com/portainer/portainer/api"
)
const defaultAPIKeyCacheSize = 1024
// entry is a tuple containing the user and API key associated to an API key digest
type entry struct {
user portainer.User
apiKey portainer.APIKey
}
// apiKeyCache is a concurrency-safe, in-memory cache which primarily exists for to reduce database roundtrips.
// We store the api-key digest (keys) and the associated user and key-data (values) in the cache.
// This is required because HTTP requests will contain only the api-key digest in the x-api-key request header;
// digest value must be mapped to a portainer user (and respective key data) for validation.
// This cache is used to avoid multiple database queries to retrieve these user/key associated to the digest.
type apiKeyCache struct {
// cache type [string]entry cache (key: string(digest), value: user/key entry)
// note: []byte keys are not supported by golang-lru Cache
cache *lru.Cache
}
// NewAPIKeyCache creates a new cache for API keys
func NewAPIKeyCache(cacheSize int) *apiKeyCache {
cache, _ := lru.New(cacheSize)
return &apiKeyCache{cache: cache}
}
// Get returns the user/key associated to an api-key's digest
// This is required because HTTP requests will contain the digest of the API key in header,
// the digest value must be mapped to a portainer user.
func (c *apiKeyCache) Get(digest []byte) (portainer.User, portainer.APIKey, bool) {
val, ok := c.cache.Get(string(digest))
if !ok {
return portainer.User{}, portainer.APIKey{}, false
}
tuple := val.(entry)
return tuple.user, tuple.apiKey, true
}
// Set persists a user/key entry to the cache
func (c *apiKeyCache) Set(digest []byte, user portainer.User, apiKey portainer.APIKey) {
c.cache.Add(string(digest), entry{
user: user,
apiKey: apiKey,
})
}
// Delete evicts a digest's user/key entry key from the cache
func (c *apiKeyCache) Delete(digest []byte) {
c.cache.Remove(string(digest))
}
// InvalidateUserKeyCache loops through all the api-keys associated to a user and removes them from the cache
func (c *apiKeyCache) InvalidateUserKeyCache(userId portainer.UserID) bool {
present := false
for _, k := range c.cache.Keys() {
user, _, _ := c.Get([]byte(k.(string)))
if user.ID == userId {
present = c.cache.Remove(k)
}
}
return present
}

View File

@@ -1,181 +0,0 @@
package apikey
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert"
)
func Test_apiKeyCacheGet(t *testing.T) {
is := assert.New(t)
keyCache := NewAPIKeyCache(10)
// pre-populate cache
keyCache.cache.Add(string("foo"), entry{user: portainer.User{}, apiKey: portainer.APIKey{}})
keyCache.cache.Add(string(""), entry{user: portainer.User{}, apiKey: portainer.APIKey{}})
tests := []struct {
digest []byte
found bool
}{
{
digest: []byte("foo"),
found: true,
},
{
digest: []byte(""),
found: true,
},
{
digest: []byte("bar"),
found: false,
},
}
for _, test := range tests {
t.Run(string(test.digest), func(t *testing.T) {
_, _, found := keyCache.Get(test.digest)
is.Equal(test.found, found)
})
}
}
func Test_apiKeyCacheSet(t *testing.T) {
is := assert.New(t)
keyCache := NewAPIKeyCache(10)
// pre-populate cache
keyCache.Set([]byte("bar"), portainer.User{ID: 2}, portainer.APIKey{})
keyCache.Set([]byte("foo"), portainer.User{ID: 1}, portainer.APIKey{})
// overwrite existing entry
keyCache.Set([]byte("foo"), portainer.User{ID: 3}, portainer.APIKey{})
val, ok := keyCache.cache.Get(string("bar"))
is.True(ok)
tuple := val.(entry)
is.Equal(portainer.User{ID: 2}, tuple.user)
val, ok = keyCache.cache.Get(string("foo"))
is.True(ok)
tuple = val.(entry)
is.Equal(portainer.User{ID: 3}, tuple.user)
}
func Test_apiKeyCacheDelete(t *testing.T) {
is := assert.New(t)
keyCache := NewAPIKeyCache(10)
t.Run("Delete an existing entry", func(t *testing.T) {
keyCache.cache.Add(string("foo"), entry{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
keyCache.Delete([]byte("foo"))
_, ok := keyCache.cache.Get(string("foo"))
is.False(ok)
})
t.Run("Delete a non-existing entry", func(t *testing.T) {
nonPanicFunc := func() { keyCache.Delete([]byte("non-existent-key")) }
is.NotPanics(nonPanicFunc)
})
}
func Test_apiKeyCacheLRU(t *testing.T) {
is := assert.New(t)
tests := []struct {
name string
cacheLen int
key []string
foundKeys []string
evictedKeys []string
}{
{
name: "Cache length is 1, add 2 keys",
cacheLen: 1,
key: []string{"foo", "bar"},
foundKeys: []string{"bar"},
evictedKeys: []string{"foo"},
},
{
name: "Cache length is 1, add 3 keys",
cacheLen: 1,
key: []string{"foo", "bar", "baz"},
foundKeys: []string{"baz"},
evictedKeys: []string{"foo", "bar"},
},
{
name: "Cache length is 2, add 3 keys",
cacheLen: 2,
key: []string{"foo", "bar", "baz"},
foundKeys: []string{"bar", "baz"},
evictedKeys: []string{"foo"},
},
{
name: "Cache length is 2, add 4 keys",
cacheLen: 2,
key: []string{"foo", "bar", "baz", "qux"},
foundKeys: []string{"baz", "qux"},
evictedKeys: []string{"foo", "bar"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
keyCache := NewAPIKeyCache(test.cacheLen)
for _, key := range test.key {
keyCache.Set([]byte(key), portainer.User{ID: 1}, portainer.APIKey{})
}
for _, key := range test.foundKeys {
_, _, found := keyCache.Get([]byte(key))
is.True(found, "Key %s not found", key)
}
for _, key := range test.evictedKeys {
_, _, found := keyCache.Get([]byte(key))
is.False(found, "key %s should have been evicted", key)
}
})
}
}
func Test_apiKeyCacheInvalidateUserKeyCache(t *testing.T) {
is := assert.New(t)
keyCache := NewAPIKeyCache(10)
t.Run("Removes users keys from cache", func(t *testing.T) {
keyCache.cache.Add(string("foo"), entry{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
ok := keyCache.InvalidateUserKeyCache(1)
is.True(ok)
_, ok = keyCache.cache.Get(string("foo"))
is.False(ok)
})
t.Run("Does not affect other keys", func(t *testing.T) {
keyCache.cache.Add(string("foo"), entry{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
keyCache.cache.Add(string("bar"), entry{user: portainer.User{ID: 2}, apiKey: portainer.APIKey{}})
ok := keyCache.InvalidateUserKeyCache(1)
is.True(ok)
ok = keyCache.InvalidateUserKeyCache(1)
is.False(ok)
_, ok = keyCache.cache.Get(string("foo"))
is.False(ok)
_, ok = keyCache.cache.Get(string("bar"))
is.True(ok)
})
}

View File

@@ -1,128 +0,0 @@
package apikey
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/internal/securecookie"
"github.com/pkg/errors"
)
const portainerAPIKeyPrefix = "ptr_"
var ErrInvalidAPIKey = errors.New("Invalid API key")
type apiKeyService struct {
apiKeyRepository dataservices.APIKeyRepository
userRepository dataservices.UserService
cache *apiKeyCache
}
func NewAPIKeyService(apiKeyRepository dataservices.APIKeyRepository, userRepository dataservices.UserService) *apiKeyService {
return &apiKeyService{
apiKeyRepository: apiKeyRepository,
userRepository: userRepository,
cache: NewAPIKeyCache(defaultAPIKeyCacheSize),
}
}
// HashRaw computes a hash digest of provided raw API key.
func (a *apiKeyService) HashRaw(rawKey string) []byte {
hashDigest := sha256.Sum256([]byte(rawKey))
return hashDigest[:]
}
// GenerateApiKey generates a raw API key for a user (for one-time display).
// The generated API key is stored in the cache and database.
func (a *apiKeyService) GenerateApiKey(user portainer.User, description string) (string, *portainer.APIKey, error) {
randKey := securecookie.GenerateRandomKey(32)
encodedRawAPIKey := base64.StdEncoding.EncodeToString(randKey)
prefixedAPIKey := portainerAPIKeyPrefix + encodedRawAPIKey
hashDigest := a.HashRaw(prefixedAPIKey)
apiKey := &portainer.APIKey{
UserID: user.ID,
Description: description,
Prefix: prefixedAPIKey[:7],
DateCreated: time.Now().Unix(),
Digest: hashDigest,
}
err := a.apiKeyRepository.Create(apiKey)
if err != nil {
return "", nil, errors.Wrap(err, "Unable to create API key")
}
// persist api-key to cache
a.cache.Set(apiKey.Digest, user, *apiKey)
return prefixedAPIKey, apiKey, nil
}
// GetAPIKey returns an API key by its ID.
func (a *apiKeyService) GetAPIKey(apiKeyID portainer.APIKeyID) (*portainer.APIKey, error) {
return a.apiKeyRepository.Read(apiKeyID)
}
// GetAPIKeys returns all the API keys associated to a user.
func (a *apiKeyService) GetAPIKeys(userID portainer.UserID) ([]portainer.APIKey, error) {
return a.apiKeyRepository.GetAPIKeysByUserID(userID)
}
// GetDigestUserAndKey returns the user and api-key associated to a specified hash digest.
// A cache lookup is performed first; if the user/api-key is not found in the cache, respective database lookups are performed.
func (a *apiKeyService) GetDigestUserAndKey(digest []byte) (portainer.User, portainer.APIKey, error) {
// get api key from cache if possible
cachedUser, cachedKey, ok := a.cache.Get(digest)
if ok {
return cachedUser, cachedKey, nil
}
apiKey, err := a.apiKeyRepository.GetAPIKeyByDigest(digest)
if err != nil {
return portainer.User{}, portainer.APIKey{}, errors.Wrap(err, "Unable to retrieve API key")
}
user, err := a.userRepository.Read(apiKey.UserID)
if err != nil {
return portainer.User{}, portainer.APIKey{}, errors.Wrap(err, "Unable to retrieve digest user")
}
// persist api-key to cache - for quicker future lookups
a.cache.Set(apiKey.Digest, *user, *apiKey)
return *user, *apiKey, nil
}
// UpdateAPIKey updates an API key and in cache and database.
func (a *apiKeyService) UpdateAPIKey(apiKey *portainer.APIKey) error {
user, _, err := a.GetDigestUserAndKey(apiKey.Digest)
if err != nil {
return errors.Wrap(err, "Unable to retrieve API key")
}
a.cache.Set(apiKey.Digest, user, *apiKey)
return a.apiKeyRepository.Update(apiKey.ID, apiKey)
}
// DeleteAPIKey deletes an API key and removes the digest/api-key entry from the cache.
func (a *apiKeyService) DeleteAPIKey(apiKeyID portainer.APIKeyID) error {
// get api-key digest to remove from cache
apiKey, err := a.apiKeyRepository.Read(apiKeyID)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("Unable to retrieve API key: %d", apiKeyID))
}
// delete the user/api-key from cache
a.cache.Delete(apiKey.Digest)
return a.apiKeyRepository.Delete(apiKeyID)
}
func (a *apiKeyService) InvalidateUserKeyCache(userId portainer.UserID) bool {
return a.cache.InvalidateUserKeyCache(userId)
}

View File

@@ -1,302 +0,0 @@
package apikey
import (
"crypto/sha256"
"fmt"
"strings"
"testing"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/stretchr/testify/assert"
"github.com/rs/zerolog/log"
)
func Test_SatisfiesAPIKeyServiceInterface(t *testing.T) {
is := assert.New(t)
is.Implements((*APIKeyService)(nil), NewAPIKeyService(nil, nil))
}
func Test_GenerateApiKey(t *testing.T) {
is := assert.New(t)
_, store := datastore.MustNewTestStore(t, true, true)
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
t.Run("Successfully generates API key", func(t *testing.T) {
desc := "test-1"
rawKey, apiKey, err := service.GenerateApiKey(portainer.User{ID: 1}, desc)
is.NoError(err)
is.NotEmpty(rawKey)
is.NotEmpty(apiKey)
is.Equal(desc, apiKey.Description)
})
t.Run("Api key prefix is 7 chars", func(t *testing.T) {
rawKey, apiKey, err := service.GenerateApiKey(portainer.User{ID: 1}, "test-2")
is.NoError(err)
is.Equal(rawKey[:7], apiKey.Prefix)
is.Len(apiKey.Prefix, 7)
})
t.Run("Api key has 'ptr_' as prefix", func(t *testing.T) {
rawKey, _, err := service.GenerateApiKey(portainer.User{ID: 1}, "test-x")
is.NoError(err)
is.Equal(portainerAPIKeyPrefix, "ptr_")
is.True(strings.HasPrefix(rawKey, "ptr_"))
})
t.Run("Successfully caches API key", func(t *testing.T) {
user := portainer.User{ID: 1}
_, apiKey, err := service.GenerateApiKey(user, "test-3")
is.NoError(err)
userFromCache, apiKeyFromCache, ok := service.cache.Get(apiKey.Digest)
is.True(ok)
is.Equal(user, userFromCache)
is.Equal(apiKey, &apiKeyFromCache)
})
t.Run("Decoded raw api-key digest matches generated digest", func(t *testing.T) {
rawKey, apiKey, err := service.GenerateApiKey(portainer.User{ID: 1}, "test-4")
is.NoError(err)
generatedDigest := sha256.Sum256([]byte(rawKey))
is.Equal(apiKey.Digest, generatedDigest[:])
})
}
func Test_GetAPIKey(t *testing.T) {
is := assert.New(t)
_, store := datastore.MustNewTestStore(t, true, true)
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
t.Run("Successfully returns all API keys", func(t *testing.T) {
user := portainer.User{ID: 1}
_, apiKey, err := service.GenerateApiKey(user, "test-1")
is.NoError(err)
apiKeyGot, err := service.GetAPIKey(apiKey.ID)
is.NoError(err)
is.Equal(apiKey, apiKeyGot)
})
}
func Test_GetAPIKeys(t *testing.T) {
is := assert.New(t)
_, store := datastore.MustNewTestStore(t, true, true)
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
t.Run("Successfully returns all API keys", func(t *testing.T) {
user := portainer.User{ID: 1}
_, _, err := service.GenerateApiKey(user, "test-1")
is.NoError(err)
_, _, err = service.GenerateApiKey(user, "test-2")
is.NoError(err)
keys, err := service.GetAPIKeys(user.ID)
is.NoError(err)
is.Len(keys, 2)
})
}
func Test_GetDigestUserAndKey(t *testing.T) {
is := assert.New(t)
_, store := datastore.MustNewTestStore(t, true, true)
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
t.Run("Successfully returns user and api key associated to digest", func(t *testing.T) {
user := portainer.User{ID: 1}
_, apiKey, err := service.GenerateApiKey(user, "test-1")
is.NoError(err)
userGot, apiKeyGot, err := service.GetDigestUserAndKey(apiKey.Digest)
is.NoError(err)
is.Equal(user, userGot)
is.Equal(*apiKey, apiKeyGot)
})
t.Run("Successfully caches user and api key associated to digest", func(t *testing.T) {
user := portainer.User{ID: 1}
_, apiKey, err := service.GenerateApiKey(user, "test-1")
is.NoError(err)
userGot, apiKeyGot, err := service.GetDigestUserAndKey(apiKey.Digest)
is.NoError(err)
is.Equal(user, userGot)
is.Equal(*apiKey, apiKeyGot)
userFromCache, apiKeyFromCache, ok := service.cache.Get(apiKey.Digest)
is.True(ok)
is.Equal(userGot, userFromCache)
is.Equal(apiKeyGot, apiKeyFromCache)
})
}
func Test_UpdateAPIKey(t *testing.T) {
is := assert.New(t)
_, store := datastore.MustNewTestStore(t, true, true)
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
t.Run("Successfully updates the api-key LastUsed time", func(t *testing.T) {
user := portainer.User{ID: 1}
store.User().Create(&user)
_, apiKey, err := service.GenerateApiKey(user, "test-x")
is.NoError(err)
apiKey.LastUsed = time.Now().UTC().Unix()
err = service.UpdateAPIKey(apiKey)
is.NoError(err)
_, apiKeyGot, err := service.GetDigestUserAndKey(apiKey.Digest)
is.NoError(err)
log.Debug().Str("wanted", fmt.Sprintf("%+v", apiKey)).Str("got", fmt.Sprintf("%+v", apiKeyGot)).Msg("")
is.Equal(apiKey.LastUsed, apiKeyGot.LastUsed)
})
t.Run("Successfully updates api-key in cache upon api-key update", func(t *testing.T) {
_, apiKey, err := service.GenerateApiKey(portainer.User{ID: 1}, "test-x2")
is.NoError(err)
_, apiKeyFromCache, ok := service.cache.Get(apiKey.Digest)
is.True(ok)
is.Equal(*apiKey, apiKeyFromCache)
apiKey.LastUsed = time.Now().UTC().Unix()
is.NotEqual(*apiKey, apiKeyFromCache)
err = service.UpdateAPIKey(apiKey)
is.NoError(err)
_, updatedAPIKeyFromCache, ok := service.cache.Get(apiKey.Digest)
is.True(ok)
is.Equal(*apiKey, updatedAPIKeyFromCache)
})
}
func Test_DeleteAPIKey(t *testing.T) {
is := assert.New(t)
_, store := datastore.MustNewTestStore(t, true, true)
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
t.Run("Successfully updates the api-key", func(t *testing.T) {
user := portainer.User{ID: 1}
_, apiKey, err := service.GenerateApiKey(user, "test-1")
is.NoError(err)
_, apiKeyGot, err := service.GetDigestUserAndKey(apiKey.Digest)
is.NoError(err)
is.Equal(*apiKey, apiKeyGot)
err = service.DeleteAPIKey(apiKey.ID)
is.NoError(err)
_, _, err = service.GetDigestUserAndKey(apiKey.Digest)
is.Error(err)
})
t.Run("Successfully removes api-key from cache upon deletion", func(t *testing.T) {
user := portainer.User{ID: 1}
_, apiKey, err := service.GenerateApiKey(user, "test-1")
is.NoError(err)
_, apiKeyFromCache, ok := service.cache.Get(apiKey.Digest)
is.True(ok)
is.Equal(*apiKey, apiKeyFromCache)
err = service.DeleteAPIKey(apiKey.ID)
is.NoError(err)
_, _, ok = service.cache.Get(apiKey.Digest)
is.False(ok)
})
}
func Test_InvalidateUserKeyCache(t *testing.T) {
is := assert.New(t)
_, store := datastore.MustNewTestStore(t, true, true)
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
t.Run("Successfully updates evicts keys from cache", func(t *testing.T) {
// generate api keys
user := portainer.User{ID: 1}
_, apiKey1, err := service.GenerateApiKey(user, "test-1")
is.NoError(err)
_, apiKey2, err := service.GenerateApiKey(user, "test-2")
is.NoError(err)
// verify api keys are present in cache
_, apiKeyFromCache, ok := service.cache.Get(apiKey1.Digest)
is.True(ok)
is.Equal(*apiKey1, apiKeyFromCache)
_, apiKeyFromCache, ok = service.cache.Get(apiKey2.Digest)
is.True(ok)
is.Equal(*apiKey2, apiKeyFromCache)
// evict cache
ok = service.InvalidateUserKeyCache(user.ID)
is.True(ok)
// verify users keys have been flushed from cache
_, _, ok = service.cache.Get(apiKey1.Digest)
is.False(ok)
_, _, ok = service.cache.Get(apiKey2.Digest)
is.False(ok)
})
t.Run("User key eviction does not affect other users keys", func(t *testing.T) {
// generate keys for 2 users
user1 := portainer.User{ID: 1}
_, apiKey1, err := service.GenerateApiKey(user1, "test-1")
is.NoError(err)
user2 := portainer.User{ID: 2}
_, apiKey2, err := service.GenerateApiKey(user2, "test-2")
is.NoError(err)
// verify keys in cache
_, apiKeyFromCache, ok := service.cache.Get(apiKey1.Digest)
is.True(ok)
is.Equal(*apiKey1, apiKeyFromCache)
_, apiKeyFromCache, ok = service.cache.Get(apiKey2.Digest)
is.True(ok)
is.Equal(*apiKey2, apiKeyFromCache)
// evict key of single user from cache
ok = service.cache.InvalidateUserKeyCache(user1.ID)
is.True(ok)
// verify user1 key has been flushed from cache
_, _, ok = service.cache.Get(apiKey1.Digest)
is.False(ok)
// verify user2 key is still in cache
_, _, ok = service.cache.Get(apiKey2.Digest)
is.True(ok)
})
}

View File

@@ -34,45 +34,3 @@ func TarFileInBuffer(fileContent []byte, fileName string, mode int64) ([]byte, e
return buffer.Bytes(), nil
}
// tarFileInBuffer represents a tar archive buffer.
type tarFileInBuffer struct {
b *bytes.Buffer
w *tar.Writer
}
func NewTarFileInBuffer() *tarFileInBuffer {
var b bytes.Buffer
return &tarFileInBuffer{
b: &b,
w: tar.NewWriter(&b),
}
}
// Put puts a single file to tar archive buffer.
func (t *tarFileInBuffer) Put(fileContent []byte, fileName string, mode int64) error {
hdr := &tar.Header{
Name: fileName,
Mode: mode,
Size: int64(len(fileContent)),
}
if err := t.w.WriteHeader(hdr); err != nil {
return err
}
if _, err := t.w.Write(fileContent); err != nil {
return err
}
return nil
}
// Bytes returns the archive as a byte array.
func (t *tarFileInBuffer) Bytes() []byte {
return t.b.Bytes()
}
func (t *tarFileInBuffer) Close() error {
return t.w.Close()
}

View File

@@ -3,7 +3,6 @@ package archive
import (
"archive/tar"
"compress/gzip"
"errors"
"fmt"
"io"
"os"
@@ -85,7 +84,7 @@ func ExtractTarGz(r io.Reader, outputDirPath string) error {
for {
header, err := tarReader.Next()
if errors.Is(err, io.EOF) {
if err == io.EOF {
break
}
@@ -110,7 +109,7 @@ func ExtractTarGz(r io.Reader, outputDirPath string) error {
}
outFile.Close()
default:
return fmt.Errorf("tar: unknown type: %v in %s",
return fmt.Errorf("Tar: uknown type: %v in %s",
header.Typeflag,
header.Name)
}

View File

@@ -2,6 +2,7 @@ package archive
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
@@ -25,18 +26,22 @@ func listFiles(dir string) []string {
}
func Test_shouldCreateArhive(t *testing.T) {
tmpdir := t.TempDir()
tmpdir, _ := ioutil.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
content := []byte("content")
os.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
ioutil.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
os.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
os.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
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 := t.TempDir()
extractionDir, _ := ioutil.TempDir("", "extract")
defer os.RemoveAll(extractionDir)
cmd := exec.Command("tar", "-xzf", gzPath, "-C", extractionDir)
err = cmd.Run()
if err != nil {
@@ -47,7 +52,7 @@ func Test_shouldCreateArhive(t *testing.T) {
wasExtracted := func(p string) {
fullpath := path.Join(extractionDir, p)
assert.Contains(t, extractedFiles, fullpath)
copyContent, _ := os.ReadFile(fullpath)
copyContent, _ := ioutil.ReadFile(fullpath)
assert.Equal(t, content, copyContent)
}
@@ -57,18 +62,22 @@ func Test_shouldCreateArhive(t *testing.T) {
}
func Test_shouldCreateArhiveXXXXX(t *testing.T) {
tmpdir := t.TempDir()
tmpdir, _ := ioutil.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
content := []byte("content")
os.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
ioutil.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
os.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
os.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
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 := t.TempDir()
extractionDir, _ := ioutil.TempDir("", "extract")
defer os.RemoveAll(extractionDir)
r, _ := os.Open(gzPath)
ExtractTarGz(r, extractionDir)
if err != nil {
@@ -79,7 +88,7 @@ func Test_shouldCreateArhiveXXXXX(t *testing.T) {
wasExtracted := func(p string) {
fullpath := path.Join(extractionDir, p)
assert.Contains(t, extractedFiles, fullpath)
copyContent, _ := os.ReadFile(fullpath)
copyContent, _ := ioutil.ReadFile(fullpath)
assert.Equal(t, content, copyContent)
}

View File

@@ -4,12 +4,12 @@ import (
"archive/zip"
"bytes"
"fmt"
"github.com/pkg/errors"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
)
// UnzipArchive will unzip an archive from bytes into the dest destination folder on disk
@@ -36,7 +36,7 @@ func extractFileFromArchive(file *zip.File, dest string) error {
}
defer f.Close()
data, err := io.ReadAll(f)
data, err := ioutil.ReadAll(f)
if err != nil {
return err
}

View File

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

View File

@@ -1,61 +0,0 @@
package ecr
import (
"context"
"encoding/base64"
"fmt"
"strings"
"time"
)
func (s *Service) GetEncodedAuthorizationToken() (token *string, expiry *time.Time, err error) {
getAuthorizationTokenOutput, err := s.client.GetAuthorizationToken(context.TODO(), nil)
if err != nil {
return
}
if len(getAuthorizationTokenOutput.AuthorizationData) == 0 {
err = fmt.Errorf("AuthorizationData is empty")
return
}
authData := getAuthorizationTokenOutput.AuthorizationData[0]
token = authData.AuthorizationToken
expiry = authData.ExpiresAt
return
}
func (s *Service) GetAuthorizationToken() (token *string, expiry *time.Time, err error) {
tokenEncodedStr, expiry, err := s.GetEncodedAuthorizationToken()
if err != nil {
return
}
tokenByte, err := base64.StdEncoding.DecodeString(*tokenEncodedStr)
if err != nil {
return
}
tokenStr := string(tokenByte)
token = &tokenStr
return
}
func (s *Service) ParseAuthorizationToken(token string) (username string, password string, err error) {
if len(token) == 0 {
return
}
splitToken := strings.Split(token, ":")
if len(splitToken) < 2 {
err = fmt.Errorf("invalid ECR authorization token")
return
}
username = splitToken[0]
password = splitToken[1]
return
}

View File

@@ -1,32 +0,0 @@
package ecr
import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/ecr"
)
type (
Service struct {
accessKey string
secretKey string
region string
client *ecr.Client
}
)
func NewService(accessKey, secretKey, region string) *Service {
options := ecr.Options{
Region: region,
Credentials: aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(accessKey, secretKey, "")),
}
client := ecr.New(options)
return &Service{
accessKey: accessKey,
secretKey: secretKey,
region: region,
client: client,
}
}

View File

@@ -2,39 +2,52 @@ package backup
import (
"fmt"
"log"
"os"
"path"
"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/dataservices"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/offlinegate"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/portainer/portainer/api/s3"
)
const rwxr__r__ os.FileMode = 0744
var filesToBackup = []string{
"certs",
"compose",
"config.json",
"custom_templates",
"edge_jobs",
"edge_stacks",
"extensions",
"portainer.key",
"portainer.pub",
"tls",
"chisel",
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 dataservices.DataStore, filestorePath string) (string, error) {
func CreateBackupArchive(password string, gate *offlinegate.OfflineGate, datastore portainer.DataStore, filestorePath string) (string, error) {
unlock := gate.Lock()
defer unlock()
@@ -43,24 +56,12 @@ func CreateBackupArchive(password string, gate *offlinegate.OfflineGate, datasto
return "", errors.Wrap(err, "Failed to create backup dir")
}
{
// new export
exportFilename := path.Join(backupDirPath, fmt.Sprintf("export-%d.json", time.Now().Unix()))
err := datastore.Export(exportFilename)
if err != nil {
log.Error().Err(err).Str("filename", exportFilename).Msg("failed to export")
} else {
log.Debug().Str("filename", exportFilename).Msg("file exported")
}
}
if err := backupDb(backupDirPath, datastore); err != nil {
return "", errors.Wrap(err, "Failed to backup database")
}
for _, filename := range filesToBackup {
err := filesystem.CopyPath(filepath.Join(filestorePath, filename), backupDirPath)
err := copyPath(filepath.Join(filestorePath, filename), backupDirPath)
if err != nil {
return "", errors.Wrap(err, "Failed to create backup file")
}
@@ -81,7 +82,7 @@ func CreateBackupArchive(password string, gate *offlinegate.OfflineGate, datasto
return archivePath, nil
}
func backupDb(backupDirPath string, datastore dataservices.DataStore) error {
func backupDb(backupDirPath string, datastore portainer.DataStore) error {
backupWriter, err := os.Create(filepath.Join(backupDirPath, "portainer.db"))
if err != nil {
return 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"))
}

View File

@@ -3,25 +3,21 @@ package backup
import (
"context"
"io"
"io/fs"
"os"
"path/filepath"
"regexp"
"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/database/boltdb"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/filesystem"
"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 dataservices.DataStore, shutdownTrigger context.CancelFunc) error {
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)
@@ -45,12 +41,6 @@ func RestoreArchive(archive io.Reader, password string, filestorePath string, ga
return errors.Wrap(err, "Failed to stop db")
}
// At some point, backups were created containing a subdirectory, now we need to handle both
restorePath, err = getRestoreSourcePath(restorePath)
if err != nil {
return errors.Wrap(err, "failed to restore from backup. Portainer database missing from backup file")
}
if err = restoreFiles(restorePath, filestorePath); err != nil {
return errors.Wrap(err, "failed to restore the system state")
}
@@ -67,47 +57,12 @@ func extractArchive(r io.Reader, destinationDirPath string) error {
return archive.ExtractTarGz(r, destinationDirPath)
}
func getRestoreSourcePath(dir string) (string, error) {
// find portainer.db or portainer.edb file. Return the parent directory
var portainerdbRegex = regexp.MustCompile(`^portainer.e?db$`)
backupDirPath := dir
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if portainerdbRegex.MatchString(d.Name()) {
backupDirPath = filepath.Dir(path)
return filepath.SkipDir
}
return nil
})
return backupDirPath, err
}
func restoreFiles(srcDir string, destinationDir string) error {
for _, filename := range filesToRestore {
err := filesystem.CopyPath(filepath.Join(srcDir, filename), destinationDir)
err := copyPath(filepath.Join(srcDir, filename), destinationDir)
if err != nil {
return err
}
}
// TODO: This is very boltdb module specific once again due to the filename. Move to bolt module? Refactor for another day
// Prevent the possibility of having both databases. Remove any default new instance
os.Remove(filepath.Join(destinationDir, boltdb.DatabaseFileName))
os.Remove(filepath.Join(destinationDir, boltdb.EncryptedDatabaseFileName))
// Now copy the database. It'll be either portainer.db or portainer.edb
// Note: CopyPath does not return an error if the source file doesn't exist
err := filesystem.CopyPath(filepath.Join(srcDir, boltdb.EncryptedDatabaseFileName), destinationDir)
if err != nil {
return err
}
return filesystem.CopyPath(filepath.Join(srcDir, boltdb.DatabaseFileName), destinationDir)
return nil
}

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

@@ -0,0 +1,96 @@
package customtemplate
import (
"github.com/boltdb/bolt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "customtemplates"
)
// Service represents a service for managing custom template 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
}
// CustomTemplates return an array containing all the custom templates.
func (service *Service) CustomTemplates() ([]portainer.CustomTemplate, error) {
var customTemplates = make([]portainer.CustomTemplate, 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 customTemplate portainer.CustomTemplate
err := internal.UnmarshalObjectWithJsoniter(v, &customTemplate)
if err != nil {
return err
}
customTemplates = append(customTemplates, customTemplate)
}
return nil
})
return customTemplates, err
}
// CustomTemplate returns an custom template by ID.
func (service *Service) CustomTemplate(ID portainer.CustomTemplateID) (*portainer.CustomTemplate, error) {
var customTemplate portainer.CustomTemplate
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.connection, BucketName, identifier, &customTemplate)
if err != nil {
return nil, err
}
return &customTemplate, nil
}
// 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.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.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.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data, err := internal.MarshalObject(customTemplate)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(customTemplate.ID)), data)
})
}
// GetNextIdentifier returns the next identifier for a custom template.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.connection, BucketName)
}

147
api/bolt/datastore.go Normal file
View File

@@ -0,0 +1,147 @@
package bolt
import (
"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"
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/registry"
"github.com/portainer/portainer/api/bolt/resourcecontrol"
"github.com/portainer/portainer/api/bolt/role"
"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"
)
var (
databaseFileName = "portainer.db"
)
// Store defines the implementation of portainer.DataStore using
// BoltDB as the storage system.
type Store struct {
path string
connection *internal.DbConnection
isNew bool
fileService portainer.FileService
CustomTemplateService *customtemplate.Service
DockerHubService *dockerhub.Service
EdgeGroupService *edgegroup.Service
EdgeJobService *edgejob.Service
EdgeStackService *edgestack.Service
EndpointGroupService *endpointgroup.Service
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
TagService *tag.Service
TeamMembershipService *teammembership.Service
TeamService *team.Service
TunnelServerService *tunnelserver.Service
UserService *user.Service
VersionService *version.Service
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)
databaseFileExists, err := fileService.FileExists(databasePath)
if err != nil {
return nil, err
}
if databaseFileExists {
store.isNew = false
}
return store, nil
}
// Open opens and initializes the BoltDB database.
func (store *Store) Open() error {
databasePath := path.Join(store.path, databaseFileName)
db, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second})
if err != nil {
return err
}
store.connection.DB = db
return store.initServices()
}
// Close closes the BoltDB database.
func (store *Store) Close() error {
if store.connection.DB != nil {
return store.connection.Close()
}
return nil
}
// IsNew returns true if the database was just created and false if it is re-using
// existing data.
func (store *Store) IsNew() bool {
return store.isNew
}
// 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
})
}

View File

@@ -0,0 +1,46 @@
package dockerhub
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "dockerhub"
dockerHubKey = "DOCKERHUB"
)
// Service represents a service for managing Dockerhub 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
}
// DockerHub returns the DockerHub object.
func (service *Service) DockerHub() (*portainer.DockerHub, error) {
var dockerhub portainer.DockerHub
err := internal.GetObject(service.connection, BucketName, []byte(dockerHubKey), &dockerhub)
if err != nil {
return nil, err
}
return &dockerhub, nil
}
// UpdateDockerHub updates a DockerHub object.
func (service *Service) UpdateDockerHub(dockerhub *portainer.DockerHub) error {
return internal.UpdateObject(service.connection, BucketName, []byte(dockerHubKey), dockerhub)
}

View File

@@ -0,0 +1,94 @@
package edgegroup
import (
"github.com/boltdb/bolt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "edgegroups"
)
// Service represents a service for managing Edge group 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
}
// EdgeGroups return an array containing all the Edge groups.
func (service *Service) EdgeGroups() ([]portainer.EdgeGroup, error) {
var groups = make([]portainer.EdgeGroup, 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 group portainer.EdgeGroup
err := internal.UnmarshalObjectWithJsoniter(v, &group)
if err != nil {
return err
}
groups = append(groups, group)
}
return nil
})
return groups, err
}
// EdgeGroup returns an Edge group by ID.
func (service *Service) EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGroup, error) {
var group portainer.EdgeGroup
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.connection, BucketName, identifier, &group)
if err != nil {
return nil, err
}
return &group, nil
}
// 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.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.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.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
group.ID = portainer.EdgeGroupID(id)
data, err := internal.MarshalObject(group)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(group.ID)), data)
})
}

101
api/bolt/edgejob/edgejob.go Normal file
View File

@@ -0,0 +1,101 @@
package edgejob
import (
"github.com/boltdb/bolt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "edgejobs"
)
// Service represents a service for managing edge jobs 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
}
// EdgeJobs returns a list of Edge jobs
func (service *Service) EdgeJobs() ([]portainer.EdgeJob, error) {
var edgeJobs = make([]portainer.EdgeJob, 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 edgeJob portainer.EdgeJob
err := internal.UnmarshalObject(v, &edgeJob)
if err != nil {
return err
}
edgeJobs = append(edgeJobs, edgeJob)
}
return nil
})
return edgeJobs, err
}
// EdgeJob returns an Edge job by ID
func (service *Service) EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, error) {
var edgeJob portainer.EdgeJob
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.connection, BucketName, identifier, &edgeJob)
if err != nil {
return nil, err
}
return &edgeJob, nil
}
// CreateEdgeJob creates a new Edge job
func (service *Service) CreateEdgeJob(edgeJob *portainer.EdgeJob) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
if edgeJob.ID == 0 {
id, _ := bucket.NextSequence()
edgeJob.ID = portainer.EdgeJobID(id)
}
data, err := internal.MarshalObject(edgeJob)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(edgeJob.ID)), data)
})
}
// 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.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.connection, BucketName, identifier)
}
// GetNextIdentifier returns the next identifier for an endpoint.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.connection, BucketName)
}

View File

@@ -0,0 +1,101 @@
package edgestack
import (
"github.com/boltdb/bolt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "edge_stack"
)
// Service represents a service for managing Edge stack 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
}
// EdgeStacks returns an array containing all edge stacks
func (service *Service) EdgeStacks() ([]portainer.EdgeStack, error) {
var stacks = make([]portainer.EdgeStack, 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 stack portainer.EdgeStack
err := internal.UnmarshalObject(v, &stack)
if err != nil {
return err
}
stacks = append(stacks, stack)
}
return nil
})
return stacks, err
}
// EdgeStack returns an Edge stack by ID.
func (service *Service) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStack, error) {
var stack portainer.EdgeStack
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.connection, BucketName, identifier, &stack)
if err != nil {
return nil, err
}
return &stack, nil
}
// CreateEdgeStack assign an ID to a new Edge stack and saves it.
func (service *Service) CreateEdgeStack(edgeStack *portainer.EdgeStack) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
if edgeStack.ID == 0 {
id, _ := bucket.NextSequence()
edgeStack.ID = portainer.EdgeStackID(id)
}
data, err := internal.MarshalObject(edgeStack)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(edgeStack.ID)), data)
})
}
// 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.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.connection, BucketName, identifier)
}
// GetNextIdentifier returns the next identifier for an endpoint.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.connection, BucketName)
}

View File

@@ -0,0 +1,145 @@
package endpoint
import (
"github.com/boltdb/bolt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "endpoints"
)
// 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
}
// Endpoint returns an endpoint by ID.
func (service *Service) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
var endpoint portainer.Endpoint
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.connection, BucketName, identifier, &endpoint)
if err != nil {
return nil, err
}
return &endpoint, nil
}
// UpdateEndpoint updates an endpoint.
func (service *Service) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
identifier := internal.Itob(int(ID))
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.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.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 endpoint portainer.Endpoint
err := internal.UnmarshalObjectWithJsoniter(v, &endpoint)
if err != nil {
return err
}
endpoints = append(endpoints, endpoint)
}
return nil
})
return endpoints, err
}
// CreateEndpoint assign an ID to a new endpoint and saves it.
func (service *Service) CreateEndpoint(endpoint *portainer.Endpoint) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
// We manually manage sequences for endpoints
err := bucket.SetSequence(uint64(endpoint.ID))
if err != nil {
return err
}
data, err := internal.MarshalObject(endpoint)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(endpoint.ID)), data)
})
}
// GetNextIdentifier returns the next identifier for an endpoint.
func (service *Service) GetNextIdentifier() int {
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.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
for _, endpoint := range toCreate {
id, _ := bucket.NextSequence()
endpoint.ID = portainer.EndpointID(id)
data, err := internal.MarshalObject(endpoint)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(endpoint.ID)), data)
if err != nil {
return err
}
}
for _, endpoint := range toUpdate {
data, err := internal.MarshalObject(endpoint)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(endpoint.ID)), data)
if err != nil {
return err
}
}
for _, endpoint := range toDelete {
err := bucket.Delete(internal.Itob(int(endpoint.ID)))
if err != nil {
return err
}
}
return nil
})
}

View File

@@ -0,0 +1,95 @@
package endpointgroup
import (
portainer "github.com/portainer/portainer/api"
"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 = "endpoint_groups"
)
// 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
}
// EndpointGroup returns an endpoint group by ID.
func (service *Service) EndpointGroup(ID portainer.EndpointGroupID) (*portainer.EndpointGroup, error) {
var endpointGroup portainer.EndpointGroup
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.connection, BucketName, identifier, &endpointGroup)
if err != nil {
return nil, err
}
return &endpointGroup, nil
}
// 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.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.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.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 endpointGroup portainer.EndpointGroup
err := internal.UnmarshalObject(v, &endpointGroup)
if err != nil {
return err
}
endpointGroups = append(endpointGroups, endpointGroup)
}
return nil
})
return endpointGroups, err
}
// CreateEndpointGroup assign an ID to a new endpoint group and saves it.
func (service *Service) CreateEndpointGroup(endpointGroup *portainer.EndpointGroup) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
endpointGroup.ID = portainer.EndpointGroupID(id)
data, err := internal.MarshalObject(endpointGroup)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(endpointGroup.ID)), data)
})
}

View File

@@ -0,0 +1,68 @@
package endpointrelation
import (
"github.com/boltdb/bolt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "endpoint_relations"
)
// Service represents a service for managing endpoint relation 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
}
// EndpointRelation returns a Endpoint relation object by EndpointID
func (service *Service) EndpointRelation(endpointID portainer.EndpointID) (*portainer.EndpointRelation, error) {
var endpointRelation portainer.EndpointRelation
identifier := internal.Itob(int(endpointID))
err := internal.GetObject(service.connection, BucketName, identifier, &endpointRelation)
if err != nil {
return nil, err
}
return &endpointRelation, nil
}
// CreateEndpointRelation saves endpointRelation
func (service *Service) CreateEndpointRelation(endpointRelation *portainer.EndpointRelation) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data, err := internal.MarshalObject(endpointRelation)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(endpointRelation.EndpointID)), data)
})
}
// 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.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.connection, BucketName, identifier)
}

View File

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

View File

@@ -0,0 +1,86 @@
package extension
import (
portainer "github.com/portainer/portainer/api"
"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 = "extension"
)
// 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
}
// Extension returns a extension by ID
func (service *Service) Extension(ID portainer.ExtensionID) (*portainer.Extension, error) {
var extension portainer.Extension
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.connection, BucketName, identifier, &extension)
if err != nil {
return nil, err
}
return &extension, nil
}
// Extensions return an array containing all the extensions.
func (service *Service) Extensions() ([]portainer.Extension, error) {
var extensions = make([]portainer.Extension, 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 extension portainer.Extension
err := internal.UnmarshalObject(v, &extension)
if err != nil {
return err
}
extensions = append(extensions, extension)
}
return nil
})
return extensions, err
}
// Persist persists a extension inside the database.
func (service *Service) Persist(extension *portainer.Extension) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data, err := internal.MarshalObject(extension)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(extension.ID)), data)
})
}
// DeleteExtension deletes a Extension.
func (service *Service) DeleteExtension(ID portainer.ExtensionID) error {
identifier := internal.Itob(int(ID))
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())
}
}

112
api/bolt/init.go Normal file
View File

@@ -0,0 +1,112 @@
package bolt
import (
"github.com/gofrs/uuid"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
)
// Init creates the default data set.
func (store *Store) Init() error {
instanceID, err := store.VersionService.InstanceID()
if err == errors.ErrObjectNotFound {
uid, err := uuid.NewV4()
if err != nil {
return err
}
instanceID = uid.String()
err = store.VersionService.StoreInstanceID(instanceID)
if err != nil {
return err
}
} else if err != nil {
return err
}
_, err = store.SettingsService.Settings()
if err == errors.ErrObjectNotFound {
defaultSettings := &portainer.Settings{
AuthenticationMethod: portainer.AuthenticationInternal,
BlackListedLabels: make([]portainer.Pair, 0),
LDAPSettings: portainer.LDAPSettings{
AnonymousMode: true,
AutoCreateUsers: true,
TLSConfig: portainer.TLSConfiguration{},
URLs: []string{},
SearchSettings: []portainer.LDAPSearchSettings{
portainer.LDAPSearchSettings{},
},
GroupSearchSettings: []portainer.LDAPGroupSearchSettings{
portainer.LDAPGroupSearchSettings{},
},
},
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)
if err != nil {
return err
}
} else if err != nil {
return err
}
_, err = store.DockerHubService.DockerHub()
if err == errors.ErrObjectNotFound {
defaultDockerHub := &portainer.DockerHub{
Authentication: false,
Username: "",
Password: "",
}
err := store.DockerHubService.UpdateDockerHub(defaultDockerHub)
if err != nil {
return err
}
} else if err != nil {
return err
}
groups, err := store.EndpointGroupService.EndpointGroups()
if err != nil {
return err
}
if len(groups) == 0 {
unassignedGroup := &portainer.EndpointGroup{
Name: "Unassigned",
Description: "Unassigned endpoints",
Labels: []portainer.Pair{},
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
TagIDs: []portainer.TagID{},
}
err = store.EndpointGroupService.CreateEndpointGroup(unassignedGroup)
if err != nil {
return err
}
}
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
}

100
api/bolt/internal/db.go Normal file
View File

@@ -0,0 +1,100 @@
package internal
import (
"encoding/binary"
"github.com/boltdb/bolt"
"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.
func Itob(v int) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, uint64(v))
return b
}
// CreateBucket is a generic function used to create a bucket inside a bolt database.
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
}
return nil
})
}
// GetObject is a generic function used to retrieve an unmarshalled object from a bolt database.
func GetObject(connection *DbConnection, bucketName string, key []byte, object interface{}) error {
var data []byte
err := connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
value := bucket.Get(key)
if value == nil {
return errors.ErrObjectNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return err
}
return UnmarshalObject(data, object)
}
// UpdateObject is a generic function used to update an object inside a bolt database.
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)
if err != nil {
return err
}
err = bucket.Put(key, data)
if err != nil {
return err
}
return nil
})
}
// DeleteObject is a generic function used to delete an object inside a bolt database.
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(connection *DbConnection, bucketName string) int {
var identifier int
connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
id, err := bucket.NextSequence()
if err != nil {
return err
}
identifier = int(id)
return nil
})
return identifier
}

25
api/bolt/internal/json.go Normal file
View File

@@ -0,0 +1,25 @@
package internal
import (
"encoding/json"
jsoniter "github.com/json-iterator/go"
)
// MarshalObject encodes an object to binary format
func MarshalObject(object interface{}) ([]byte, error) {
return json.Marshal(object)
}
// UnmarshalObject decodes an object from binary data
func UnmarshalObject(data []byte, object interface{}) error {
return json.Unmarshal(data, object)
}
// UnmarshalObjectWithJsoniter decodes an object from binary data
// using the jsoniter library. It is mainly used to accelerate endpoint
// decoding at the moment.
func UnmarshalObjectWithJsoniter(data []byte, object interface{}) error {
var jsoni = jsoniter.ConfigCompatibleWithStandardLibrary
return jsoni.Unmarshal(data, &object)
}

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

@@ -0,0 +1,37 @@
package migrator
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/user"
)
func (m *Migrator) updateAdminUserToDBVersion1() error {
u, err := m.userService.UserByUsername("admin")
if err == nil {
admin := &portainer.User{
Username: "admin",
Password: u.Password,
Role: portainer.AdministratorRole,
}
err = m.userService.CreateUser(admin)
if err != nil {
return err
}
err = m.removeLegacyAdminUser()
if err != nil {
return err
}
} else if err != nil && err != errors.ErrObjectNotFound {
return err
}
return nil
}
func (m *Migrator) removeLegacyAdminUser() error {
return m.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(user.BucketName))
return bucket.Delete([]byte("admin"))
})
}

View File

@@ -0,0 +1,103 @@
package migrator
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
func (m *Migrator) updateResourceControlsToDBVersion2() error {
legacyResourceControls, err := m.retrieveLegacyResourceControls()
if err != nil {
return err
}
for _, resourceControl := range legacyResourceControls {
resourceControl.SubResourceIDs = []string{}
resourceControl.TeamAccesses = []portainer.TeamResourceAccess{}
owner, err := m.userService.User(resourceControl.OwnerID)
if err != nil {
return err
}
if owner.Role == portainer.AdministratorRole {
resourceControl.AdministratorsOnly = true
resourceControl.UserAccesses = []portainer.UserResourceAccess{}
} else {
resourceControl.AdministratorsOnly = false
userAccess := portainer.UserResourceAccess{
UserID: resourceControl.OwnerID,
AccessLevel: portainer.ReadWriteAccessLevel,
}
resourceControl.UserAccesses = []portainer.UserResourceAccess{userAccess}
}
err = m.resourceControlService.CreateResourceControl(&resourceControl)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) updateEndpointsToDBVersion2() error {
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
endpoint.AuthorizedTeams = []portainer.TeamID{}
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) retrieveLegacyResourceControls() ([]portainer.ResourceControl, error) {
legacyResourceControls := make([]portainer.ResourceControl, 0)
err := m.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte("containerResourceControl"))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var resourceControl portainer.ResourceControl
err := internal.UnmarshalObject(v, &resourceControl)
if err != nil {
return err
}
resourceControl.Type = portainer.ContainerResourceControl
legacyResourceControls = append(legacyResourceControls, resourceControl)
}
bucket = tx.Bucket([]byte("serviceResourceControl"))
cursor = bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var resourceControl portainer.ResourceControl
err := internal.UnmarshalObject(v, &resourceControl)
if err != nil {
return err
}
resourceControl.Type = portainer.ServiceResourceControl
legacyResourceControls = append(legacyResourceControls, resourceControl)
}
bucket = tx.Bucket([]byte("volumeResourceControl"))
cursor = bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var resourceControl portainer.ResourceControl
err := internal.UnmarshalObject(v, &resourceControl)
if err != nil {
return err
}
resourceControl.Type = portainer.VolumeResourceControl
legacyResourceControls = append(legacyResourceControls, resourceControl)
}
return nil
})
return legacyResourceControls, err
}

View File

@@ -0,0 +1,28 @@
package migrator
import "github.com/portainer/portainer/api"
func (m *Migrator) updateEndpointsToVersion11() error {
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
if endpoint.Type == portainer.AgentOnDockerEnvironment {
endpoint.TLSConfig.TLS = true
endpoint.TLSConfig.TLSSkipVerify = true
} else {
if endpoint.TLSConfig.TLSSkipVerify && !endpoint.TLSConfig.TLS {
endpoint.TLSConfig.TLSSkipVerify = false
}
}
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,127 @@
package migrator
import (
"strconv"
"strings"
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/portainer/portainer/api/bolt/stack"
)
func (m *Migrator) updateEndpointsToVersion12() error {
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
endpoint.Tags = []string{}
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) updateEndpointGroupsToVersion12() error {
legacyEndpointGroups, err := m.endpointGroupService.EndpointGroups()
if err != nil {
return err
}
for _, group := range legacyEndpointGroups {
group.Tags = []string{}
err = m.endpointGroupService.UpdateEndpointGroup(group.ID, &group)
if err != nil {
return err
}
}
return nil
}
type legacyStack struct {
ID string `json:"Id"`
Name string `json:"Name"`
EndpointID portainer.EndpointID `json:"EndpointId"`
SwarmID string `json:"SwarmId"`
EntryPoint string `json:"EntryPoint"`
Env []portainer.Pair `json:"Env"`
ProjectPath string
}
func (m *Migrator) updateStacksToVersion12() error {
legacyStacks, err := m.retrieveLegacyStacks()
if err != nil {
return err
}
for _, legacyStack := range legacyStacks {
err := m.convertLegacyStack(&legacyStack)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) convertLegacyStack(s *legacyStack) error {
stackID := m.stackService.GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Name: s.Name,
Type: portainer.DockerSwarmStack,
SwarmID: s.SwarmID,
EndpointID: 0,
EntryPoint: s.EntryPoint,
Env: s.Env,
}
stack.ProjectPath = strings.Replace(s.ProjectPath, s.ID, strconv.Itoa(stackID), 1)
err := m.fileService.Rename(s.ProjectPath, stack.ProjectPath)
if err != nil {
return err
}
err = m.deleteLegacyStack(s.ID)
if err != nil {
return err
}
return m.stackService.CreateStack(stack)
}
func (m *Migrator) deleteLegacyStack(legacyID string) error {
return m.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(stack.BucketName))
return bucket.Delete([]byte(legacyID))
})
}
func (m *Migrator) retrieveLegacyStacks() ([]legacyStack, error) {
var legacyStacks = make([]legacyStack, 0)
err := m.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(stack.BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var stack legacyStack
err := internal.UnmarshalObject(v, &stack)
if err != nil {
return err
}
legacyStacks = append(legacyStacks, stack)
}
return nil
})
return legacyStacks, err
}

View File

@@ -0,0 +1,17 @@
package migrator
import "github.com/portainer/portainer/api"
func (m *Migrator) updateSettingsToVersion13() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.LDAPSettings.AutoCreateUsers = false
legacySettings.LDAPSettings.GroupSearchSettings = []portainer.LDAPGroupSearchSettings{
portainer.LDAPGroupSearchSettings{},
}
return m.settingsService.UpdateSettings(legacySettings)
}

View File

@@ -0,0 +1,19 @@
package migrator
func (m *Migrator) updateResourceControlsToDBVersion14() error {
resourceControls, err := m.resourceControlService.ResourceControls()
if err != nil {
return err
}
for _, resourceControl := range resourceControls {
if resourceControl.AdministratorsOnly == true {
err = m.resourceControlService.DeleteResourceControl(resourceControl.ID)
if err != nil {
return err
}
}
}
return nil
}

View File

@@ -0,0 +1,16 @@
package migrator
func (m *Migrator) updateSettingsToDBVersion15() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.EnableHostManagementFeatures = false
return m.settingsService.UpdateSettings(legacySettings)
}
func (m *Migrator) updateTemplatesToVersion15() error {
// Removed with the entire template management layer, part of https://github.com/portainer/portainer/issues/3707
return nil
}

View File

@@ -0,0 +1,14 @@
package migrator
func (m *Migrator) updateSettingsToDBVersion16() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
if legacySettings.SnapshotInterval == "" {
legacySettings.SnapshotInterval = "5m"
}
return m.settingsService.UpdateSettings(legacySettings)
}

View File

@@ -0,0 +1,19 @@
package migrator
func (m *Migrator) updateExtensionsToDBVersion17() error {
legacyExtensions, err := m.extensionService.Extensions()
if err != nil {
return err
}
for _, extension := range legacyExtensions {
extension.License.Valid = true
err = m.extensionService.Persist(&extension)
if err != nil {
return err
}
}
return nil
}

View File

@@ -2,14 +2,10 @@ package migrator
import (
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
func (m *Migrator) updateUsersToDBVersion18() error {
log.Info().Msg("updating users")
legacyUsers, err := m.userService.ReadAll()
legacyUsers, err := m.userService.Users()
if err != nil {
return err
}
@@ -33,7 +29,7 @@ func (m *Migrator) updateUsersToDBVersion18() error {
portainer.OperationPortainerUserMemberships: true,
}
err = m.userService.Update(user.ID, &user)
err = m.userService.UpdateUser(user.ID, &user)
if err != nil {
return err
}
@@ -43,8 +39,6 @@ func (m *Migrator) updateUsersToDBVersion18() error {
}
func (m *Migrator) updateEndpointsToDBVersion18() error {
log.Info().Msg("updating endpoints")
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
@@ -75,9 +69,7 @@ func (m *Migrator) updateEndpointsToDBVersion18() error {
}
func (m *Migrator) updateEndpointGroupsToDBVersion18() error {
log.Info().Msg("updating endpoint groups")
legacyEndpointGroups, err := m.endpointGroupService.ReadAll()
legacyEndpointGroups, err := m.endpointGroupService.EndpointGroups()
if err != nil {
return err
}
@@ -97,7 +89,7 @@ func (m *Migrator) updateEndpointGroupsToDBVersion18() error {
}
}
err = m.endpointGroupService.Update(endpointGroup.ID, &endpointGroup)
err = m.endpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
if err != nil {
return err
}
@@ -107,9 +99,7 @@ func (m *Migrator) updateEndpointGroupsToDBVersion18() error {
}
func (m *Migrator) updateRegistriesToDBVersion18() error {
log.Info().Msg("updating registries")
legacyRegistries, err := m.registryService.ReadAll()
legacyRegistries, err := m.registryService.Registries()
if err != nil {
return err
}
@@ -125,7 +115,7 @@ func (m *Migrator) updateRegistriesToDBVersion18() error {
registry.TeamAccessPolicies[teamID] = portainer.AccessPolicy{}
}
err = m.registryService.Update(registry.ID, &registry)
err = m.registryService.UpdateRegistry(registry.ID, &registry)
if err != nil {
return err
}

View File

@@ -0,0 +1,16 @@
package migrator
import portainer "github.com/portainer/portainer/api"
func (m *Migrator) updateSettingsToDBVersion19() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
if legacySettings.EdgeAgentCheckinInterval == 0 {
legacySettings.EdgeAgentCheckinInterval = portainer.DefaultEdgeAgentCheckinIntervalInSeconds
}
return m.settingsService.UpdateSettings(legacySettings)
}

View File

@@ -2,20 +2,15 @@ package migrator
import (
"strings"
"github.com/rs/zerolog/log"
)
const scheduleScriptExecutionJobType = 1
func (m *Migrator) updateUsersToDBVersion20() error {
log.Info().Msg("updating user authentication")
return m.authorizationService.UpdateUsersAuthorizations()
}
func (m *Migrator) updateSettingsToDBVersion20() error {
log.Info().Msg("updating settings")
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
@@ -27,8 +22,6 @@ func (m *Migrator) updateSettingsToDBVersion20() error {
}
func (m *Migrator) updateSchedulesToDBVersion20() error {
log.Info().Msg("updating schedules")
legacySchedules, err := m.scheduleService.Schedules()
if err != nil {
return err

View File

@@ -0,0 +1,20 @@
package migrator
import "github.com/portainer/portainer/api"
func (m *Migrator) updateSettingsToDBVersion3() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.AuthenticationMethod = portainer.AuthenticationInternal
legacySettings.LDAPSettings = portainer.LDAPSettings{
TLSConfig: portainer.TLSConfiguration{},
SearchSettings: []portainer.LDAPSearchSettings{
portainer.LDAPSearchSettings{},
},
}
return m.settingsService.UpdateSettings(legacySettings)
}

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