Compare commits

..

328 Commits

Author SHA1 Message Date
Matt Hook
96a626324c fix(buildscripts): make build process more closely resemble EE (#8881) 2023-05-04 13:04:16 +12:00
Chaim Lev-Ari
5fd36ee986 chore(build): remove grunt and add makefile [EE-4824] (#8803) 2023-05-02 12:49:51 +07:00
cmeng
7c2fcb67eb fix(stack) add skip TLS toggle for edit stack EE-5391 (#8850) 2023-04-28 13:35:38 +12:00
matias-portainer
2eb4453487 fix(images): avoid returning null on registryId default value EE-5394 (#8842) 2023-04-26 10:24:49 -03:00
cmeng
535e499cc5 fix(webhook) remove NaN fom webhook url EE-5373 (#8815) 2023-04-21 10:56:59 +12:00
Matt Hook
fee315b07e bump version to 2.18.2 (#8808) 2023-04-20 09:05:18 +12:00
Ali
d1166b5294 fix(editor): fix styles [EE-5369] (#8810)
* fix(editor): fix styles [EE-5369]

* rm hash

---------

Co-authored-by: testa113 <testa113>
2023-04-20 08:27:31 +12:00
Matt Hook
e3b727a636 bump version to 2.18.1 (#8802) 2023-04-18 14:50:18 +12:00
Chaim Lev-Ari
d56ea05218 fix(edge/updates): add padding for edge groups [EE-5349] (#8771) 2023-04-18 13:40:06 +12:00
Dakota Walsh
8e724e3fbe feat(libhelm): allow passing optional env and http client [EE-5252] (#8798)
Co-authored-by: Matt Hook <hookenz@gmail.com>
2023-04-14 15:51:11 +12:00
cmeng
33b141bcd3 fix(backup) add description text to backup EE-5283 (#8776) 2023-04-13 16:04:59 +12:00
Matt Hook
ded8ce48a8 feat(cert): ce teasers for ca cert [EE-5252] (#8769) 2023-04-13 15:32:58 +12:00
Oscar Zhou
e60635bf32 fix(swagger): correct endpoint api annotations [EE-5333] (#8762) 2023-04-13 15:31:18 +12:00
cmeng
6fb4951949 fix(stack): upgrade docker-compose EE-5334 (#8756) 2023-04-11 17:55:53 +12:00
Oscar Zhou
c429b29216 fix(k8s/gitops): missing git auth toggle in k8s app edit page [EE-5320] (#8740) 2023-04-10 20:14:04 +12:00
Ali
8ab490f224 fix(ns): add selection caching back [EE-5273] (#8739)
Co-authored-by: testa113 <testa113>
2023-04-06 14:28:05 +12:00
Matt Hook
78b83420bf search for correct source directory when doing a restore (#8677) 2023-04-06 10:39:16 +12:00
cmeng
b4dbc341cc fix(homepage) move heartbeat logic to backend EE-5317 (#8736) 2023-04-06 09:09:13 +12:00
Matt Hook
3118c639f6 fix(docs): add missing swagger docs for upload file [EE-4886] (#8707)
* add docs for uploading files via host management features

* fix other doc issues
2023-04-04 16:59:26 +12:00
cmeng
5d7ab85473 fix(security): potential vulnerability of path traversal attacks EE-5303 (#8727) 2023-04-04 09:00:11 +12:00
Chaim Lev-Ari
99331a81d4 feat(gitops): allow to skip tls verification [EE-5023] (#8679) 2023-04-03 09:19:09 +03:00
Prabhat Khera
ab1a8c1d6a fix(ui): namespace caching issue EE-5273 (#8710)
* fix namespace caching issue

* fix(apps): add loading state [EE-5273]

* rm endpoint provider

* fix(namespace): remove caching [EE-5273]

* variable typo

---------

Co-authored-by: testa113 <testa113>
2023-03-31 13:25:00 +13:00
Chaim Lev-Ari
e063cba81b fix(ui/code-editor): stretch code editor content full height [EE-5202] (#8672) 2023-03-30 12:26:35 +03:00
Ali
23e6a982b9 fix(ns): save filter to local storage [EE-5287] (#8724)
* fix(ns): save filter to local storage [EE-5287]

* allow system ns and save per user

---------

Co-authored-by: testa113 <testa113>
2023-03-30 11:21:08 +13:00
andres-portainer
0bf75ae113 fix(snapshots): change the snapshot object to maintain backwards compatibility EE-5240 (#8704) 2023-03-23 13:30:50 -03:00
Ali
72b41dde01 fix(apps) UI release fixes [EE-5197] (#8703)
* fix(apps) searchbar flex resizing and insights

* UI fixes

* update stacks datatable

---------

Co-authored-by: testa113 <testa113>
2023-03-23 08:20:34 +13:00
Ali
36b122ca21 fix(dashboard): use faster proxy request [EE-5160] (#8694)
Co-authored-by: testa113 <testa113>
2023-03-22 15:34:48 +13:00
Prabhat Khera
649799069b fix Gpus null issue (#8691) 2023-03-21 16:05:55 +13:00
Oscar Zhou
0ca56ddbb1 fix(stack/git): fix cursor movement issue in git text fields (#8656) 2023-03-20 10:00:35 +13:00
Chaim Lev-Ari
3a30c8ed1e fix(ui/box-selector): BE link and use icons standard size [EE-5133] (#8659) 2023-03-19 13:37:44 +01:00
Ali
151db6bfe7 fix(kubeconfig): fix download checkbox [EE-5199] (#8675)
Co-authored-by: testa113 <testa113>
2023-03-17 10:34:00 +13:00
Ali
106c719a34 fix(wizard): Capitalise Kubernetes [EE-5178] (#8663)
Co-authored-by: testa113 <testa113>
2023-03-16 18:50:58 +13:00
Dakota Walsh
1cfd031db1 fix(kubernetes): Prevent rerunning initial cluster detection [EE-5170] (#8667) 2023-03-16 15:39:43 +13:00
Prabhat Khera
fbc1a2d44d fix(ui): namespace cache refresh on reload EE-5155 (#8657) 2023-03-16 10:10:26 +13:00
Oscar Zhou
47478efd1e fix(stack/git): remove duplicate code used to backup compose dir (#8620) 2023-03-15 12:27:23 +13:00
Ali
50940b7fba fix(annotations) ingress tip to match ee [EE-5158] (#8654)
Co-authored-by: testa113 <testa113>
2023-03-14 10:41:41 +13:00
matias-portainer
7468d5637b fix(upgrade): remove yellow upgrade banner EE-5141 (#8641) 2023-03-13 09:01:39 -03:00
Ali
6edc210ae7 fix(kube): check for ns on enter [EE-5160] (#8648)
Co-authored-by: testa113 <testa113>
2023-03-13 13:57:07 +13:00
Prabhat Khera
f859876cb6 fix typo in delete image modal dialog (#8622) 2023-03-13 11:05:55 +13:00
Matt Hook
5e434a82ed reduce throttling in the kube client (#8631) 2023-03-13 09:47:23 +13:00
Ali
d9f6471a00 fix(annotation): update wording/styling [EE-5158] (#8643)
Co-authored-by: testa113 <testa113>
2023-03-10 16:52:15 +13:00
cmeng
a7d1a20dfb fix(edge-stack) always show edge group selector [EE-5157] (#8638) 2023-03-10 10:48:53 +13:00
Ali
17517d7521 fix(app): restrict ns fix create app [EE-5123] (#8633)
Co-authored-by: testa113 <testa113>
2023-03-10 10:24:20 +13:00
andres-portainer
c609f6912f fix(home): disable live connect for async [EE-5000] (#8628) 2023-03-09 15:50:36 -03:00
Ali
346fe9e3f1 refactor(GPU): colocate and update UI [EE-5127] (#8634)
Co-authored-by: testa113 <testa113>
2023-03-09 22:06:49 +13:00
matias-portainer
69f14e569b fix(stacks): pass WorkingDir to deployer command EE-5142 (#8624) 2023-03-08 19:34:50 -03:00
Ali
89194405ee fix(kube): improve dashboard load speed [EE-4941] (#8572)
* apply changes from EE

* clear query cache when logging out

* Text transitions in smoother
2023-03-08 11:22:08 +13:00
Chaim Lev-Ari
5f0af62521 fix(git): fix more files form error [EE-5125] (#8601) 2023-03-07 18:44:26 -03:00
Dakota Walsh
e3299eddd5 fix(docker): container health table [EE-5008] (#8591) 2023-03-08 10:33:15 +13:00
Prabhat Khera
bdde278139 feat(applications): application page performance improvements EE-4956 (#8569) 2023-03-08 10:27:42 +13:00
Chaim Lev-Ari
01ea9afe33 feat(edge): hide FDO features behind a flag [EE-5128] (#8600) 2023-03-07 13:45:39 -03:00
matias-portainer
8345d1471e fix(stacks): remove old dependency from stacks redeploy controller EE-5138 (#8609) 2023-03-07 13:35:43 -03:00
Prabhat Khera
2a55d20eff fix(annotations): minor issues in Annotations EE-4089 (#8612) 2023-03-07 21:09:41 +13:00
Ali
7dca784ec6 fix(env): get edited environment info [EE-5129] (#8603)
Co-authored-by: testa113 <testa113>
2023-03-07 14:44:19 +13:00
Oscar Zhou
37484566eb fix(ui/wizard): edge agent ui issue [EE-5126] (#8598) 2023-03-07 10:27:40 +13:00
Chaim Lev-Ari
70710cfeb7 feat(edge): associate edge env to meta fields [EE-3209] (#8551)
* refactor(edge/groups): load edge groups in selector

fix(edge/stacks): remove double groups title

* feat(edge): supply meta fields to edge script [EE-5043]

* feat(edge): auto assign aeec envs to groups and tags [EE-5043]

fix [EE-5043]

fix(envs): fix global key test

* fix(edge/groups): save group type

* refactor(edge/devices): move loading of devices to table

* refactor(tags): select paramter for query

* feat(edge/devices): show meta fields

* refactor(home): simplify filter

* feat(edge/devices): filter by meta fields

* refactor(edge/devices): break filter and loading hook
2023-03-07 09:25:04 +13:00
Chaim Lev-Ari
03712966e4 fix(ui/code-editor): apply theme colors [EE-5104] (#8558)
* fix(ui/code-editor): apply theme colors [EE-5104]

fix [EE-5104]

* fix(kube/yaml): expand yaml inspector

* fix(ui/code-editor): have default height

* fix(ui/code-editor): boolean color

* fix(ui/code-editor): style search bar
2023-03-06 09:13:42 +02:00
Chaim Lev-Ari
07100258cd fix(git): file path background [EE-5114] (#8573)
* fix(git): file path background [EE-5114]

also disabled url check on CE because
the http handler wasn't available and raised errors

* fix(git): highlight hovered path option

* feat(git): hide path options when choosing
2023-03-06 09:13:22 +02:00
cmeng
4c6f5f961e fix(wizard): fix tunnel add validation [EE-5124] (#8595) 2023-03-06 10:43:08 +13:00
Chaim Lev-Ari
77e1f5aa34 fix(oauth): danger confirm for hiding prompt [EE-4576] (#8574) 2023-03-05 15:22:49 +02:00
Chaim Lev-Ari
3baab6d695 fix(ui): import components for envs and registries [EE-5120] (#8583) 2023-03-05 15:22:00 +02:00
Chaim Lev-Ari
d546ff269b fix(ui/input-group): remove z-index [EE-5121] (#8582) 2023-03-05 12:14:08 +02:00
cmeng
60275dd31c feat(edge): EE-4621 support high latency for tunnel (#8302) 2023-03-04 09:13:37 +13:00
Ali
07df4b1591 fix(notifications): limit only in header [EE-4815] (#8579)
Co-authored-by: testa113 <testa113>
2023-03-03 15:08:15 +13:00
Ali
fd916bc8a2 feat(gpu): rework docker GPU for UI performance [EE-4918] (#8518) 2023-03-03 14:47:10 +13:00
Ali
769c8372fb feat(microk8s): BE teasers in wizard [EE-4772] (#8449)
* feat(microk8s): separate existing/new envs EE-4772

update icons

fix be teaser box selector style

* match environment wizard box selectors

* revert title back

* updated kaas description in wizard

---------

Co-authored-by: testa113 <testa113>
Co-authored-by: Prabhat Khera <prabhat.khera@portainer.io>
2023-03-03 12:27:24 +13:00
James Carppe
d032119ebc Update docs URL and add --log-level docs (#8511) 2023-03-03 11:38:19 +13:00
Matt Hook
ac47649631 feat(kubernetes): list all kube services screen [EE-1571] (#8524)
* port services from ee

* fix external link

* post review improvements

* remove applications-ports-datatable

* minor post review updates

* add services help url

* post review update

* more post review updates

* post review updates

* rename index to component

* fix external ip display and sorting

* fix external apps tag

* fix ingress screen time format

* use uid for row id. Prevent blank link

* fix some missing bits ported from EE

* match ee

* fix display of show system resources

* remove icon next to service type
2023-03-03 08:45:19 +13:00
Chaim Lev-Ari
8d6797dc9f fix(edge): show edge settings [EE-4959] (#8581)
* fix(edge): show edge settings [EE-4959]

fix [EE-4959]

* fix(edge/settings): validate ce
2023-03-02 19:32:43 +02:00
andres-portainer
197b0bcbde fix(code): add missing returns after sending HTTP errors EE-4442 (#7868) 2023-03-02 12:52:10 -03:00
Chaim Lev-Ari
6918da2414 refactor(stacks): extract auto update logic [EE-4945] (#8545) 2023-03-02 17:07:50 +02:00
cmeng
085381e6fc fix(logs-viewer): fail to search json logs [EE-4857] (#8482) 2023-03-02 23:55:13 +13:00
Chaim Lev-Ari
6074d1fcb5 fix(docker): clone config [EE-5115] (#8575) 2023-03-02 11:38:21 +02:00
Ali
96e5d44cc2 fix(teaser): add up-to-date teaser [EE-5014] (#8548)
Co-authored-by: testa113 <testa113>
2023-03-02 10:14:50 +13:00
matias-portainer
a45ef3d72e fix(azure): ensure azure client HTTPS verification EE-4444 (#8137) 2023-03-01 17:52:29 -03:00
Chaim Lev-Ari
c819d4e7f7 feat(environments): create async edge [EE-4480] (#8527) 2023-03-01 20:33:05 +02:00
LP B
bc6a667a6b feat(api/snapshot): extend docker container snapshot type (#8537) 2023-03-01 17:33:40 +01:00
matias-portainer
7dcd6f9b9e fix(ui): fix search in associated endpoints selector EE-4532 (#8454) 2023-03-01 10:34:07 -03:00
Chaim Lev-Ari
c8d334e603 fix(server): skip file deletion errors [EE-4916] (#8443) 2023-03-01 13:44:34 +02:00
Chaim Lev-Ari
ab9b0c2147 feat(docker/containers): show name in log view [EE-5108] (#8557) 2023-03-01 09:37:23 +02:00
cmeng
6d659b4a2c fix(edge-stack): add validation for edge stack name [EE-4283] (#8504) 2023-03-01 19:51:08 +13:00
Prabhat Khera
defce0cf6d feat(kuberenetes): add annotations to kube objects EE-4089 (#8499)
* add annotations BE teaser
* fix settings icon click on home screen for kube env
* add debouce to namespace validation
* ingress button tooltip fixed
* fix tooltip text
2023-03-01 13:11:12 +13:00
Dakota Walsh
5f66020e42 fix(docker): container health alignment EE-5008 (#8553) 2023-03-01 12:48:41 +13:00
Chaim Lev-Ari
b3e72ecaa0 fix(gitops): load correct auth creds [EE-4849] (#8550)
close [EE-4849]
2023-02-28 17:55:54 +02:00
Chaim Lev-Ari
b98c71f1ab refactor(ui): move react components to react codebase [EE-3354] (#8258)
* refactor(ui): move react components to react codebase [EE-3354]

* refactor(app): move bocx selector options

* refactor(react): spearate portainer components

* fix(app): fix imports
2023-02-28 17:32:29 +02:00
matias-portainer
f9a09301a8 fix(edgejobs): fix data race in edge jobs tasks collect EE-4766 (#8542) 2023-02-28 12:14:09 -03:00
Chaim Lev-Ari
2c247efd0f fix(settings/oauth): show internal auth prompt by default [EE-4576] (#8481)
* fix(settings/oauth): show internal auth prompt by default [EE-4576]

fix [EE-4576]

* fix(oauth): use new confirm modal
2023-02-28 16:49:46 +02:00
Chaim Lev-Ari
86d0e30eb7 fix(build): ignore source maps for 3rd party [EE-5106] (#8549) 2023-02-28 14:54:52 +02:00
Chaim Lev-Ari
69a91ff90a fix(ui/box-selectors): make multi use square [EE-3856] (#8470) 2023-02-27 12:47:29 +02:00
Chaim Lev-Ari
e0481f69b1 chore(deps): replace semver compare with local solutions [EE-5018] (#8491) 2023-02-26 15:25:24 +02:00
matias-portainer
088262b6dc fix(edgejobs): fix data race on task logs clear EE-4767 (#8236) 2023-02-23 16:21:17 -03:00
Oscar Zhou
1b12ee9f01 fix(security): update dependency and binary version [EE-4863] (#8394) 2023-02-23 22:29:48 +13:00
Chaim Lev-Ari
5507b1e8c9 refactor(ui/editor): migrate code-editor to react [EE-4848] (#8257) 2023-02-23 09:10:31 +04:00
Chaim Lev-Ari
273a3f9a10 refactor(gitops): migrate git form to react [EE-4849] (#8268) 2023-02-23 01:43:33 +05:30
Oscar Zhou
afe6cd6df0 fix(code/security): vcs status error [EE-5062] (#8510) 2023-02-22 10:40:09 +13:00
andres-portainer
95ac2cc4c3 chore(edge): add transaction support for common objects EE-5079 (#8522) 2023-02-20 16:11:18 -03:00
Chaim Lev-Ari
9a8e95d017 feat(upgrade): show subtle banner [EE-5017] (#8489) 2023-02-19 09:47:50 +05:30
Oscar Zhou
631503fc1b fix(upgrade): add label to upgrade mustache template [EE-5029] (#8505) 2023-02-17 13:56:01 +13:00
Ali
23f3008500 chore(notifications):improve performance [EE-4815] (#8475)
* chore(notifications):improve performance [EE-4815]

Co-authored-by: testa113 <testa113>
2023-02-17 12:51:00 +13:00
matias-portainer
89dd72b4ac fix(registry): correct words in registry page and other places EE-4894 (#8356) 2023-02-16 10:31:08 -03:00
Chaim Lev-Ari
5a375ff055 refactor(environment/edge): show copy token button [EE-3691] (#8497) 2023-02-16 09:13:30 +05:30
andres-portainer
f081631808 fix(edgegroups): fix data-race in edgeGroupCreate EE-4435 (#8477) 2023-02-14 15:18:07 -03:00
Chaim Lev-Ari
e66dea44e3 refactor(ui/modals): replace bootbox with react solution [EE-4541] (#8010) 2023-02-14 13:49:41 +05:30
cmeng
392c7f74b8 fix(backup): reword region placeholder [EE-5012 (#8496) 2023-02-14 16:13:53 +13:00
Matt Hook
1dba5e464b minor fix (#8485) 2023-02-14 10:35:21 +13:00
Matt Hook
56dc2d1000 fix typo (#8486) 2023-02-13 23:23:44 +13:00
Ali
5c05ec489e fix(nameField): imperatively call setDebouncedValue when the value is changed by setFieldValue [EE-5002] (#8468)
Co-authored-by: testa113 <testa113>
2023-02-13 15:06:14 +13:00
Oscar Zhou
cef9255161 fix(snapshot): prevent snapshot containers from fast failing in Swarm mode (#8308) 2023-02-13 14:42:10 +13:00
Ali
0befdacc0e chore(prettier): ignore tailwind changes [EE-4809] (#8483)
Co-authored-by: testa113 <testa113>
2023-02-13 11:38:06 +13:00
Matt Hook
b2105f3614 feat(dockerfile): layered images [EE-4879] (#8301)
* multistage build to target production and storybook

* missing slash

* add storybook for windows too and build targets

* feature flag the storybook api

* remove kompose and prep for new FF lib

* todo comment for FF

* update to new feature flags library

* simplify logic

* fix compilation error

* simplified it
2023-02-13 11:28:32 +13:00
Ali
58d66d3142 chore(prettier): add tailwind prettier plugin [EE-4809] (#8221)
* add prettier plugin

* apply tailwind prettier formatting
2023-02-13 10:04:24 +13:00
Ali
9f6702d0b8 feat(resourcequotas): reduce resource quota requests [EE-4757] (#8420) 2023-02-10 18:28:53 +13:00
Matt Hook
44d69f3a3f hide the password in the response (#8437) 2023-02-10 18:26:18 +13:00
Matt Hook
e255bd710f chore(golang) remove api pkg and go mod tidy (#8474)
* remove this.  It exists and should be in the top level directory

* go mod tidy
2023-02-10 16:48:53 +13:00
Prabhat Khera
d73622ed9c fix cursor on autofill (#8378) 2023-02-10 09:19:43 +13:00
andres-portainer
4753d52532 fix(tls): specify the TLS MinVersion always EE-4427 (#7869) 2023-02-09 16:13:35 -03:00
Matt Hook
f9bbe000fb fix(docker): remove prepended slash by default on container names [EE-3592] (#8195)
* remove prepended slash by default if present

* trimcontainername still needed
2023-02-09 17:18:19 +13:00
Matt Hook
bfc610c192 feat(featureflags): improved feature flag handling [EE-4609] (#8222)
* updated and improved feature flags using new module

* merge init into parse

* update the package documentation

* better docs

* minor tidy
2023-02-09 17:17:46 +13:00
cmeng
51b9804fab fix(updater): specify docker client version [EE-5010] (#8459) 2023-02-08 17:00:22 +13:00
Matt Hook
e2168d21c7 guard around kube actions for endpoint inspect (#8430) 2023-02-07 23:13:52 +13:00
Chaim Lev-Ari
2dddc1c6b9 refactor(ui/box-selector): replace all selectors [EE-3856] (#7902) 2023-02-07 09:03:57 +05:30
Ali
c9253319d9 fix(texttip): fix texttip in edge groups [EE-4990] (#8441)
Co-authored-by: testa113 <testa113>
2023-02-03 13:39:45 +13:00
Chaim Lev-Ari
968fc98401 fix(home): show tooltip for disabled envs [EE-4859] (#8283) 2023-02-02 18:24:00 -03:00
LP B
921e9cfc6e fix(app/edge): updater/rollback calendar visual issues (#8386) 2023-02-02 12:57:53 -03:00
LP B
9b53960906 fix(api/edgestack): update deployments count when env relations are updated (#8433) 2023-02-02 12:04:58 -03:00
Oscar Zhou
402a62a5e2 fix(schedule): update date picker after removing edge group [EE-4963] (#8418) 2023-02-02 11:14:48 +13:00
cmeng
3470ea049a fix(update): prevent formik reinitialize [EE-4962] (#8426) 2023-02-02 09:59:17 +13:00
Ali
7fd263e8cc fix(texttip): fix texttip placement [EE-4990] (#8427)
Co-authored-by: testa113 <testa113>
2023-02-01 20:34:04 +13:00
Chaim Lev-Ari
36c6d3f21b fix(home): reduce update text for small screens [EE-4936] (#8422) 2023-02-01 12:14:53 +05:30
andres-portainer
5f3dd0a64f fix(edgestacks): fix edge stacks cache invalidation EE-4909 (#8399) 2023-02-01 02:16:04 -03:00
cmeng
42ca1287df fix(edge/stack): not clear stack status if version not updated [EE-4970] (#8408) 2023-02-01 09:18:04 +13:00
matias-portainer
2874a79279 fix(doc): update endpoint creation swagger documentation EE-4925 (#8415) 2023-01-31 11:06:27 -03:00
Ali
8574dd2371 fix(edge stacks): allow viewing existing kompose stacks [EE-4967] (#8405)
Co-authored-by: testa113 <testa113>
Co-authored-by: Matt Hook <hookenz@gmail.com>
2023-01-31 10:03:21 +13:00
Dakota Walsh
53eb5aa1ee fix(kube): 30 second delay to storage detection EE-4822 (#8360) 2023-01-31 09:58:57 +13:00
cmeng
eb8644330e fix(edge/job): init endpoints if it is null [EE-4972] (#8411) 2023-01-27 22:08:13 +13:00
cmeng
8663de580a fix(settings): EE-4959 Cannot turn on Edge Compute Features on CE (#8396) 2023-01-27 17:04:40 +13:00
Oscar Zhou
34298d96c5 fix: pass endpoint entity instead of endpoint.id (#8407) 2023-01-27 12:41:54 +13:00
cmeng
9d103ffbeb fix(UI): EE-4937 low resolution hides add container button (#8401) 2023-01-27 09:18:48 +13:00
Chaim Lev-Ari
5847c2b8ef fix(system/update): submit license form [EE-4743] (#8381) 2023-01-26 20:35:04 +05:30
matias-portainer
a09fe7e10c chore(edgejobs): AddEdgeJob disregards async mode EE-4855 (#8287) 2023-01-26 11:32:11 -03:00
Ali
5640cce4d6 chore(kompose): remove from settings [EE-4741] (#8375) 2023-01-26 16:03:44 +13:00
Chaim Lev-Ari
00bbf4ac63 refactor(auth): cache user data [EE-4935] (#8380) 2023-01-26 07:40:05 +05:30
matias-portainer
a748e15c16 feat(ui): bump codemirror version EE-4892 (#8393) 2023-01-25 10:31:12 -03:00
matias-portainer
cfdb9c126f fix(endpoints): check environment type before start metrics detection EE-4944 (#8391) 2023-01-25 10:29:11 -03:00
Chaim Lev-Ari
851a3346a9 fix(edge/update): remove schedule date for old envs [EE-3023] (#8315) 2023-01-24 12:20:55 +05:30
Oscar Zhou
c9aae27b29 fix(ci/security): update the node and golang version (#8387) 2023-01-24 14:41:00 +13:00
Prabhat Khera
087848539f fix(kubernetes): detect metrics API for kubernetes endspoints EE-4865 (#8351) 2023-01-24 09:05:15 +13:00
LP B
a74e389521 chore(dependencies): upgrade msw to fix xmldom CVE (#8362)
* chore(dependencies): upgrade msw to fix xmldom CVE

* refactor(msw): rename msw DefaultRequestBody to DefaultBodyType
2023-01-23 14:03:11 +01:00
Chaim Lev-Ari
eff6ec9df9 fix(edge/group): show tag selector when no tags [EE-4924] (#8368) 2023-01-23 11:05:23 +05:30
Prabhat Khera
8dec95c2cd chore(lint): add golangci linter to GitHub workflow EE-4872 (#8366) 2023-01-23 18:31:15 +13:00
Ali
5b02f636d7 fix(namespace): move warning [EE-4885] (#8370)
Co-authored-by: testa113 <testa113>
2023-01-23 10:41:39 +13:00
Prabhat Khera
ac458d0daa fix(ui): tooltip link color EE-4914 (#8365) 2023-01-23 10:15:47 +13:00
Chaim Lev-Ari
5b5dc320d5 fix(settings): save only the existing values [EE-4903] (#8326) 2023-01-23 02:34:39 +05:30
cmeng
d04747b309 fix(home): EE-4874 homepage ui issues (#8319) 2023-01-23 09:14:06 +13:00
LP B
07dd6bbe84 fix(home): dont display disconnected status similar to disabled (#8311) 2023-01-20 17:34:16 +01:00
Chaim Lev-Ari
406ff8812c feat(system/upgrade): add get license dialog [EE-4743] (#8249) 2023-01-19 15:31:49 +02:00
Chaim Lev-Ari
5942f4ff58 fix(home): dark mode for edit buttons [EE-4828] (#8241) 2023-01-19 10:16:34 +02:00
Prabhat Khera
adf92ce5e0 fix golang ci linter prehook (#8359) 2023-01-19 12:11:09 +13:00
cmeng
fed3d14adf fix(home): EE-4906 home page tiles for edge devices have the wrong url (#8353) 2023-01-19 09:00:43 +13:00
Prabhat Khera
73db588080 chore(linting): configure go linter EE-4871 (#8288) 2023-01-18 15:20:42 +13:00
Ali
6769326c8b fix(ingress): update label [EE-4902] (#8330)
Co-authored-by: testa113 <testa113>
2023-01-18 13:29:59 +13:00
Ali
e6d0e297dd fix(app): update redeploy wording [EE-4889] (#8317)
Co-authored-by: testa113 <testa113>
2023-01-18 08:30:06 +13:00
matias-portainer
0cd272211a fix(edgejobs): fix edge jobs log collection EE-4893 (#8328) 2023-01-17 14:21:13 -03:00
matias-portainer
6570f1f8eb fix(edgejobs): remove endpoint from edge job mapping on endpoint deletion EE-4764 (#8212) 2023-01-17 09:47:23 -03:00
Chaim Lev-Ari
1c180346e4 fix(ldap): sync user teams when needed [EE-4802] (#8235) 2023-01-16 10:41:32 +02:00
Chaim Lev-Ari
1d5d1bb12d fix(home): enable kubeconfig button [EE-4833] (#8233) 2023-01-15 11:32:38 +02:00
cmeng
0c27316034 fix(UI) EE-4883 stack repository method console error (#8304)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2023-01-13 16:07:52 +13:00
Chaim Lev-Ari
d3bed3072b fix(home): link to correct env route [EE-4877] (#8295) 2023-01-12 15:02:37 +02:00
Chaim Lev-Ari
329e8bcad5 fix(home): allow non-admins [EE-4873] (#8297) 2023-01-12 15:01:52 +02:00
Chaim Lev-Ari
4bdf30c038 feat(home): remove margins from env list [EE-4868] (#8285) 2023-01-11 10:59:56 +02:00
Matt Hook
7793b98813 straight not back ticks (#8292) 2023-01-11 14:54:50 +13:00
Matt Hook
02de7b2715 feat(version): add version setting script bump version EE-2529 (#6820)
* version bumper

* tell the user to check update
2023-01-11 14:07:18 +13:00
Chaim Lev-Ari
9c0e0607a4 fix(sidebar): put behind modal [EE-4866] (#8282)
* fix(sidebar): put behind modal

* fix(system/upgrade): validate on open dialog
2023-01-11 08:32:03 +13:00
Chaim Lev-Ari
baf9c3db0a feat(environments): add edge device [EE-4840] (#8246)
* feat(environments): add edge device [EE-4840]

fix [EE-4840]

* fix(home): fix tests
2023-01-11 08:30:49 +13:00
Prabhat Khera
6c193a8a45 refactor(log): log.fatal usage review EE-4607 (#8280) 2023-01-10 15:12:24 +13:00
Dakota Walsh
48a0f40621 fix(padding): fix margin-right on DatatableFooter pagination EE-4831 (#8237) 2023-01-10 10:14:46 +13:00
Matt Hook
4dc643acd9 fix(rolling-restart): wording and icon changes [EE-4834] (#8239)
* icon and wording changes

* fix inconsistencies and grammar

* fix(ui/buttons): show tooltip

* Change icon and confirmation dialog

* edit icon

* rename be-only-button to be-teaser-button for consistency

Co-authored-by: Chaim Lev-Ari <chaim.levi-ari@portainer.io>
2023-01-10 10:02:07 +13:00
Ali
1d42db93f1 fix(rbac): fix false negative rbac result in github microk8s environments [EE-4829] 2023-01-09 17:55:28 +13:00
andres-portainer
33c3f8460c Bump to v2.18.0. (#8266) 2023-01-07 12:08:23 -03:00
andres-portainer
dd0d1737b0 fix(performance): optimize performance for edge EE-3311 (#8040) 2023-01-06 16:25:41 -03:00
andres-portainer
3d28a6f877 chore(edgejobs): clean up EE-4850 (#8259) 2023-01-03 13:12:12 -03:00
andres-portainer
2fc518f221 chore(edgestacks): clean up EE-4851 (#8260) 2023-01-03 10:49:29 -03:00
andres-portainer
137ce37096 chore(nil): remove unnecessary nil checks EE-4847 (#8254) 2022-12-30 15:26:46 -03:00
andres-portainer
e529327851 chore(utils): remove dead code EE-4846 (#8253) 2022-12-30 14:52:58 -03:00
andres-portainer
3625ab6faa fix(rand): seed the RNG EE-4845 (#8252) 2022-12-30 14:52:18 -03:00
Chaim Lev-Ari
afb024d2a4 chore(docs): fix api reference [EE-4835] (#8242) 2022-12-25 10:47:21 +02:00
andres-portainer
b2bc4b92d6 fix(ui): remove extra margins from Dockerfile details section EE-4790 (#8213) 2022-12-21 19:09:40 -03:00
Chaim Lev-Ari
e5fd0c9595 fix(system): ignore failure to connect to docker [EE-4825] (#8231) 2022-12-21 18:08:18 +02:00
Chaim Lev-Ari
649c1c9cee feat(system): check BE image existence before upgrade [EE-4071] (#8230) 2022-12-21 17:17:51 +02:00
cmeng
919a854d93 feat(edge): EE-4570 allow pre-pull images with edge stack deployment (#8210)
Co-authored-by: Matt Hook <hookenz@gmail.com>
2022-12-21 13:18:51 +13:00
Chaim Lev-Ari
7fe0712b61 feat(home): move edge device to view [EE-4559] (#8189)
Co-authored-by: matias.spinarolli <matias.spinarolli@portainer.io>
2022-12-21 10:07:34 +13:00
Chaim Lev-Ari
b4a6f6911c fix(home): wrap items on small screens [EE-4798] (#8220) 2022-12-20 10:57:59 +02:00
Chaim Lev-Ari
59d35d26d8 feat(home): open env on click [EE-4792] (#8219) 2022-12-20 10:57:25 +02:00
Dakota Walsh
95558ed4ad fix(EE-4782): add portainer internal label to created ingress rules (#8196) 2022-12-20 16:46:51 +13:00
matias-portainer
e1b474d04f feat(edgejobs): support edge groups when using edge jobs EE-3873 (#8099) 2022-12-19 18:54:51 -03:00
Matt Hook
9732d1b5d8 remove kubeshell pod constraint (#8185) 2022-12-20 10:32:49 +13:00
Chaim Lev-Ari
701410d259 feat(app): clear env when log out [EE-4791] (#8218) 2022-12-19 08:56:39 +02:00
Chaim Lev-Ari
123754cee7 feat(system): make ui fixes to upgrade dialog [EE-4071] (#8199)
* feat(system): make ui fixes to upgrade dialog

* fix(system): hide on non-enabled platforms
2022-12-18 19:57:41 +02:00
Chaim Lev-Ari
d75d2ba9ce fix(docker): fix links to docs [EE-4486] (#8166) 2022-12-18 10:13:18 +02:00
Dakota Walsh
046738c967 feat(kubernetes): cluster setup reasonable defaults EE-4518 (#8082) 2022-12-16 16:03:40 +13:00
Matt Hook
0436be7bc4 update pull latest image wording (#8191) 2022-12-16 14:57:07 +13:00
cmeng
94d64997cc fix(compose): EE-4777 Toast Error displays insufficient details when creating stack in Standalone (#8208) 2022-12-16 14:43:24 +13:00
Chaim Lev-Ari
294d1668d4 feat(edge/updates): sync code changes from EE [EE-3023] (#8186)
* feat(edge/updates): add info about each field

* feat(edge/update): add beta feature

* fix(edge/update): add info
2022-12-16 14:20:22 +13:00
Chaim Lev-Ari
4bd6618fb9 chore(deps): upgrade docker binary [EE-4545] (#8146)
closes [EE-4545]
upgrades docker to v20.10.21

Co-authored-by: Yi Chen <69284638+yi-portainer@users.noreply.github.com>
2022-12-16 14:07:21 +13:00
Chaim Lev-Ari
62197a67f7 chore(deps): upgrade docker compose [EE-4642] (#8147)
closes [EE-4642]
upgrades docker compose to v2.13.0
2022-12-16 14:05:14 +13:00
Matt Hook
c1dc1b49d1 fix(pagination): vert center pagination control on all screens and add consistent spacing [EE-4006] (#8192) 2022-12-16 10:55:54 +13:00
andres-portainer
b917e12b62 fix(fileservice): add missing interface EE-3458 (#8207) 2022-12-15 17:21:58 -03:00
andres-portainer
a8ccd2b153 feat(filestore): add function to save mTLS certificates (#8206) 2022-12-15 16:16:29 -03:00
LP B
68975620c5 fix(app): cursor jumps to the end of inputs [EE-4359] (#8163)
* feat(app/react): introduce controlled inputs HOC to avoid creating uncontrolled React components

* fix(app/env): jumping inputs when adding gpus to existing environment
2022-12-15 17:49:36 +01:00
LP B
67d3abcc9d fix(app/analytics): exclude login page in analytics (#8172) 2022-12-15 17:48:59 +01:00
matias-portainer
90b0cb84f4 fix(wording): change wording on CE teaser EE-4590 (#8136) 2022-12-15 11:25:13 -03:00
Oscar Zhou
b22cdb3559 fix(access/team): team member cannot change ownership to its own team [EE-4423] (#8052) 2022-12-15 21:27:18 +13:00
andres-portainer
37896661d6 fix(edgestacks): avoid a data race in edge stack status update endpoint EE-4737 (#8168) 2022-12-14 10:41:45 -03:00
Chaim Lev-Ari
f38b8234d9 fix(edge/groups): show tags for environments [EE-4422] (#7940) 2022-12-14 09:17:49 +02:00
Chaim Lev-Ari
52e150fa29 fix(notifications): break lines for long message [EE-4521] (#8110) 2022-12-14 09:17:12 +02:00
Matt Hook
929749c0da incorrect variable name used (#8198) 2022-12-14 17:49:20 +13:00
Ali
09bf5d03f4 give docker specific tooltip link (#8197)
Co-authored-by: testa113 <testa113>
2022-12-14 17:26:19 +13:00
Ali
ac6f52ab76 fix(gitapp): set manifest in git settings [EE-4734] (#8190) 2022-12-14 14:30:15 +13:00
Dakota Walsh
0ddcad66f3 fix(auth): invalidate session when permissions change EE-3320 (#8103) 2022-12-14 10:12:00 +13:00
Chaim Lev-Ari
930d9e5628 feat(edge/stacks): use namespace in manifest [EE-4507] (#8145) 2022-12-13 22:56:47 +02:00
Chaim Lev-Ari
8936ae9b7a feat(home): add connect and browse buttons [EE-4182] (#8175) 2022-12-13 22:56:09 +02:00
Chaim Lev-Ari
db9d87c918 feat(system): path to upgrade swarm to BE [EE-4624] (#8124) 2022-12-13 22:52:06 +02:00
matias-portainer
b59a0ba823 fix(settings): fix settings save/update EE-3604 (#8180) 2022-12-12 15:11:12 -03:00
Matt Hook
2188005b48 feat(kubernetes): add rolling restart button teaser [EE-4510] (#8126)
* rolling restart teaser button

* add be only rolling restart

* move position of button
2022-12-12 14:30:05 +13:00
Prabhat Khera
a1528475ba feat(UI): tooltip html message support and width fix [EE-3445] (#8165)
Co-authored-by: testa113 <testa113>
2022-12-12 14:03:50 +13:00
Chaim Lev-Ari
5cbf52377d feat(system): path to upgrade standalone to BE [EE-4071] (#8095) 2022-12-11 08:58:22 +02:00
andres-portainer
756ac034ec fix(go): add Go workspace EE-4763 (#8182) 2022-12-09 13:23:01 -03:00
Oscar Zhou
1008afd1fe fix(session): reset the environment session storage after deleting it (#8119) 2022-12-09 11:11:24 +13:00
Ali
563ead85cc fix(gitoptions): git app edit ui tweaks [EE-4584] (#8159) 2022-12-09 10:41:11 +13:00
Chaim Lev-Ari
eba5879ec8 feat(home): change layout of env tile [EE-4479] (#8061) 2022-12-07 16:51:20 +02:00
matias-portainer
b48aa1274d fix(host): fix host info request EE-4641 (#8169) 2022-12-07 10:48:42 -03:00
Matt Hook
3e485c3152 feat(api): remove golang experimental packages [EE-3648] (#8081)
* remove golang experimental packages

* rebase and fix references
2022-12-07 17:15:52 +13:00
Oscar Zhou
dffd45c5f9 refactor(ui): extract TagButton from TagSelector component [EE-4194] (#8127) 2022-12-07 16:15:09 +13:00
Ali
c1cc8bad77 feat(rbac): detect if rbac is enabled [EE-4308] (#8139) 2022-12-07 15:53:06 +13:00
matias-portainer
8dcc5e4adb fix(edgestacks): fix repository edge stack creation EE-4732 (#8160) 2022-12-06 10:26:18 -03:00
Matt Hook
4558ce84cf fix(header): removed red dot from help menu [EE-4586] (#8133)
* remove red dot from help menu

* fix cursor, add menu-icon class back
2022-12-06 10:24:38 +13:00
Ali
adc87b8f8e feat(deployment): PO feedback [EE-4416] (#8143) 2022-12-06 08:49:26 +13:00
Chaim Lev-Ari
ce8455953e chore(deps): upgrade k8s client [EE-4543] (#8019) 2022-12-05 09:58:55 +02:00
Prabhat Khera
a61b18dd93 feat(kubernetes): fix wordings for ingresses toggle EE-2647 (#8151) 2022-12-05 10:47:56 +13:00
Matt Hook
d6a3fe23e9 feat(libhelm) update missed package paths [EE-4650] (#8134)
* add missing pkg paths

* fix go tests

* fixed pkg paths
2022-12-05 10:38:16 +13:00
Prabhat Khera
cbaba43842 fix(ui): tooltip stays open on hover [EE-3445] (#8051) 2022-12-05 09:47:43 +13:00
Matt Hook
c173888b64 remove old digest library and update path (#8141) 2022-12-02 10:46:59 +13:00
Chaim Lev-Ari
82e9e2a895 refactor(edge/updates): sync changes from EE [EE-4288] (#7726) 2022-12-01 08:40:52 +02:00
Matt Hook
4fee359247 feat(libhelm) import libhelm into CE pkg directory [EE-4650] (#8138)
* import libhelm and update some paths

* remove file

* update readme
2022-12-01 14:27:49 +13:00
Ali
9cdc0da615 feat(kompose): hide kompose [EE-4562] (#8084) 2022-12-01 13:46:23 +13:00
Matt Hook
8fd0efa34f add third_party digest lib to top level (#8140) 2022-12-01 13:26:39 +13:00
Matt Hook
79bfd8f6fe fix package import for docker/distribution (#8132) 2022-12-01 09:43:51 +13:00
Ali
2114c15f55 refactor(data-cy): duplicate data cy [EE-4656] (#8131) 2022-11-30 16:54:28 +13:00
Matt Hook
d2f6d1e415 import libhelm into portainer (#8128) 2022-11-30 14:25:47 +13:00
Matt Hook
241440a474 fix(fdo): import deleted digest library [EE-4654] (#8129)
* import digest lib

* update references

* fix lint errors
2022-11-30 09:11:49 +13:00
LP B
2e19f4ea6d fix(docker/container): auto select private access when enabling UAC on public container edit (#8032) [EE-44-64] 2022-11-29 11:24:13 +01:00
Matt Hook
95bc508462 fix(migrator): fix setting version struct fields after migration [EE-4613] (#8090)
* fix setting version struct fields

* fix go tests

* remove versionUpdateRequired

* remove old code that was originally for debugging purposes
2022-11-28 19:28:10 +13:00
Ali
d78b762f7b refactor(icons): replace fa icons [EE-4459] (#7907)
refactor(icons): remove fontawesome EE-4459

refactor(icon) replace feather with lucide EE-4472
2022-11-28 15:00:28 +13:00
andres-portainer
9dfac98a26 fix(tags): add missing error handling EE-4622 (#8102) 2022-11-24 14:16:04 -03:00
andres-portainer
e26a607d28 fix(edgegroups): avoid a last-write-wins situation when updating edge groups concurrently EE-3732 (#8101) 2022-11-23 21:36:17 -03:00
Dakota Walsh
6dc1841c14 fix(kube): disable namespace resource quota by default EE-4421 (#8080) 2022-11-23 15:01:41 +13:00
andres-portainer
c28be7aced fix(token-cache-manager): refactor to avoid data races EE-4438 (#8094) 2022-11-22 18:31:14 -03:00
Prabhat Khera
dd01165224 show secrets in kube applications datatable (#8065) 2022-11-23 08:48:36 +13:00
Chaim Lev-Ari
d484a0eb64 fix(docker): remove word break in details [EE-4481] (#7996) 2022-11-22 15:00:55 +02:00
Chaim Lev-Ari
fe8e834dbf refactor(ui/datatables): migrate views to use datatable component [EE-4064] (#7609) 2022-11-22 14:16:34 +02:00
Prabhat Khera
0f0513c684 feat(kubernetes): edit yaml support EE-2855 (#8016) 2022-11-22 09:40:44 +13:00
Chaim Lev-Ari
7006c17ce4 fix(wizard): debounce name state [EE-4177] (#8042)
move debouncing to the component (from the validation).
debounce returns undefined when it's not calling the debounced function,
and undefined is considered a validation error.
2022-11-21 19:33:08 +02:00
Chaim Lev-Ari
253a3a2b40 fix(ui): remove loading of missing interceptors [EE-4604] (#8086) 2022-11-21 17:25:09 +02:00
Chaim Lev-Ari
1e4c4e2616 refactor(edge): move edge codebase to react (#7781) 2022-11-21 09:51:55 +02:00
Chaim Lev-Ari
75f40fe485 refactor(portainer): remove offline mode [EE-4348] (#7761)
Co-authored-by: deviantony <anthony.lapenna@portainer.io>
2022-11-21 09:50:47 +02:00
Ali
61e8e68c31 fix(svg): fix footer height [EE-4547] (#8071) 2022-11-21 09:22:41 +13:00
Matt Hook
583346321e feat(version): migrate version to semver [EE-3756] (#7693)
redisigned version bucket and migration code
2022-11-18 13:18:09 +13:00
cmeng
4cfa584c7c fix(git): EE-4577 Git Repository Fields are Missing in Edge Stacks (#8057) 2022-11-18 08:59:58 +13:00
Ali
d012a4efc4 feat(deployment): enforce deployment options EE-4416 (#7974) 2022-11-17 22:00:34 +13:00
Oscar Zhou
e0f3a8c0a2 fix(access/viwer): update the viwer filter key to user.id (#8055) 2022-11-17 10:38:34 +13:00
Yi Chen
bb48ab00cb * remove empty examples (#7952)
* fix missing type
2022-11-17 06:55:08 +13:00
Chaim Lev-Ari
eccc8131dd feat(system/upgrade): add upgrade banner [EE-4564] (#8046) 2022-11-16 18:38:39 +02:00
matias-portainer
c21921a08d fix(edgestacks): return 400 instead of 500 on edge stack create when an invalid payload is provided EE-4429 (#7880) 2022-11-15 17:40:56 -03:00
Prabhat Khera
573e05d1c7 feat(kubernetes): BE teasure added EE-2647 (#7927) 2022-11-15 20:57:01 +13:00
Prabhat Khera
246e351817 remove wrong checked-in files from parent dir (#8048) 2022-11-15 09:01:28 +13:00
Prabhat Khera
6775c7b6ec clean database models directory (#8026) 2022-11-14 08:33:57 +13:00
Chaim Lev-Ari
881e99df53 refactor(nomad): sync frontend with EE [EE-3353] (#7758) 2022-11-13 12:29:25 +02:00
Chaim Lev-Ari
78dcba614d refactor(portainer): move to react [EE-3350] (#7915) 2022-11-13 10:10:18 +02:00
matias-portainer
30e23ea5b4 fix(ui): tidy up Edge Devices configuration EE-3604 (#7797) 2022-11-10 16:13:20 -03:00
Oscar Zhou
e1e81731b9 fix(setting/auth): allow to save all auth method if custom ldap server is empty string (#7990) 2022-11-10 08:28:09 +13:00
Oscar Zhou
16377221f9 fix(stack): check if endpoint exists before checking the user permission on the endpoint (#7967) 2022-11-09 12:20:19 +13:00
Chaim Lev-Ari
a0237852ef fix: fix occurred typo [EE-4536] (#7994) 2022-11-08 07:30:37 +02:00
andres-portainer
193e0c7d6f fix(snapshots): remove snapshots when removing endpoints EE-4527 (#7973)
* fix(snapshots): remove snapshots when removing endpoints EE-4527

* Fix nil pointer dereference.

Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
2022-11-07 20:28:18 -03:00
Chaim Lev-Ari
77c29ff87e refactor(kubernetes): move react codebase [EE-3349] (#7953) 2022-11-07 08:03:11 +02:00
Chaim Lev-Ari
2868da296a fix(stack): validate original containers names [EE-4520] (#7978) 2022-11-06 10:40:33 +02:00
Chaim Lev-Ari
ff10588383 fix(edge): show select all checkbox [EE-4129] (#7948) 2022-11-06 09:33:22 +02:00
LP B
6b02d9a1e3 fix(app/logs): change pattern to detect double serialized JSON logs [EE-4525] (#7962)
* fix(app/logs): change pattern to detect double serialized JSON logs

* fix(app/logs): fallback to raw display when parsing fails + include timestamp for Zerolog logs
2022-11-04 13:58:18 +01:00
Ali
9f3d5185b0 fix(slider): use and update react slider EE-4522 (#7987) 2022-11-04 14:12:53 +13:00
congs
f94147b07b fix(registry): EE-4526 Registry Manage access link broken (#7975) 2022-11-04 12:10:42 +13:00
andres-portainer
49d02e0386 fix(db): remove a bucket overwrite EE-4424 (#7867) 2022-11-03 18:51:15 -03:00
Ali
e82d0cfbdb fix(apps): fix edit error EE-4529 (#7963) 2022-11-03 13:04:56 +13:00
Oscar Zhou
c8051b68d4 fix(auth/ldap): allow to save internal authentication if custom ldap server is not set [EE-3155] (#7959) 2022-11-03 10:06:56 +13:00
Chaim Lev-Ari
37d4a80769 refactor(environments): remove endpoints cache [DTD-100] (#6408) 2022-11-02 13:29:26 +02:00
Chaim Lev-Ari
9ef2e27aae fix(environments): debounce name validation [EE-4177] (#7889) 2022-11-02 12:44:31 +02:00
Oscar Zhou
9e1f80cf37 chore(ui/ldap): add texttip for group search configuration (#7944) 2022-11-02 11:31:44 +13:00
Dakota Walsh
459c95169a fix(ingresses): migrate to new allow/disallow format EE-4465 (#7893) 2022-11-02 11:17:32 +13:00
fhanportainer
5048f08b5f fix(box-selector): fixed incorrect wording for Private Box selector under UAC. (#7949) 2022-11-01 11:06:11 +13:00
fhanportainer
e785d1572e fix(web-editor): fixed web editor scroll bar. (#7941) 2022-10-31 11:03:50 +13:00
Rex Wang
95a4f83466 fix(docker): docker template UI bug fix [EE-4034] (#7912)
* EE-4034 fix(docker): docker template UI bug fix

* EE-4034 fix(docker): fix ui
2022-10-30 14:56:23 +08:00
Dmitry Salakhov
4edf232e41 fix: document edge endoint url requirement (#7735) [EE-3425] 2022-10-28 13:00:12 +13:00
Dmitry Salakhov
903cf284e7 fix(image): build image from file (#7929) [EE-4501] 2022-10-27 23:31:31 +13:00
Prabhat Khera
a550bfaedb fix showing namespaces for standard user (#7917) 2022-10-27 16:14:54 +13:00
Hao
446febb0f6 fix(image): hide button issues [EE-4166] (#7845)
* fix(image): hide button issues [EE-4166]
2022-10-25 15:02:59 +08:00
Oscar Zhou
cb9fe2606c fix(team): disable team leader setting when external auth sync is enabled [EE-3579] (#7852) 2022-10-25 14:39:24 +13:00
Dakota Walsh
55211ef00e fix(ingress): allow none controller type EE-4420 (#7883)
Co-authored-by: testA113 <alex.harris@portainer.io>
2022-10-25 09:41:30 +13:00
Chaim Lev-Ari
e48ceb15e9 refactor(environments): move environments ts code to react [EE-3443] (#7747) 2022-10-23 09:53:25 +03:00
Rex Wang
1b12cc9f31 EE-4376 fix(docker): fix malformed struct of template (#7803) 2022-10-21 16:29:18 +08:00
Hao
0365ed8e70 fix(docker): comfirm modal for removing secrets/networks/configs [EE-4211] (#7882)
* fix(ui): comfirm modal for removing secrets/networks/configs [EE-4211]

* fix(ui): comfirm modal for removing secrets/networks/configs [EE-4211]

* fix(ui): comfirm modal for removing secrets/networks/configs [EE-4211]
2022-10-21 16:03:41 +08:00
Chaim Lev-Ari
7624ff10ee chore(edge): add aria-label for edge-group selector [EE-4466] (#7896)
* chore(edge): add aria-label for edge-group selector

* style(edge): remove comment
2022-10-21 08:22:49 +03:00
andres-portainer
535a26412f fix(logging): default to pretty logging [EE-4371] (#7847)
* fix(logging): default to pretty logging EE-4371

* feat(app/logs): prettify stack traces in JSON logs

* feat(nomad/logs): prettify JSON logs in log viewer

* feat(kubernetes/logs): prettigy JSON logs in log viewers

* feat(app/logs): format and color zerolog prettified logs

* fix(app/logs): pre-parse logs when they are double serialized

Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
2022-10-20 16:33:54 +02:00
Chaim Lev-Ari
ee5600b6af chore(build): incremental ts build [EE-4204] (#7888) 2022-10-20 17:23:56 +03:00
Rex Wang
3f51d077ac fix(docker): create custom template [EE-4114] (#7812)
* EE-4114 fix(docker): create custom template

* Update customtemplate_create.go

remove white space
2022-10-20 16:42:49 +08:00
Rex Wang
0219d41ba7 fix(docker): Show stopped container on dashboard [EE-4327] (#7833)
* EE-4327 fix(docker): Show stopped container on dashboard

* Update ContainerStatus.tsx

remove comment

* EE-4327 fix(docker): show stopped container on dashboard
2022-10-20 15:10:39 +08:00
Prabhat Khera
f3e2ccd487 fix volume claims with k8s app (#7899) 2022-10-20 15:43:19 +13:00
Ali
368e6b2a44 fix(helm): helm charts view bad icon aspect ratio EE-4451 (#7875) 2022-10-20 14:14:55 +13:00
Dmitry Salakhov
1100a2bd28 feat: move jwt lib to v4 (#7773) 2022-10-20 10:26:11 +13:00
itsconquest
16dc66f173 fix(UAC): put team into resource control when editing as team lead [EE-4457] (#7886)
* fix(UAC): put team into resource control when editing as team lead [EE-4457]

* populate form values & payload correctly
2022-10-20 10:18:56 +13:00
itsconquest
c1f94be9b2 fix(notifications): sort by newest first by default [EE-4467] (#7891) 2022-10-19 15:25:20 +13:00
Matt Hook
58947fee69 fix(libhelm): new libhelm with relaxed validation when adding chart repo [EE-4440] (#7874)
update helm to fix adding some chart repos
2022-10-19 12:43:33 +13:00
Dakota Walsh
0c995ae1c8 fix(kubernetes): create proxied kubeclient EE-4326 (#7850) 2022-10-18 10:46:27 +13:00
itsconquest
f6d6be90e4 fix(UAC): provide required UI context [EE-4415] (#7854) 2022-10-18 09:45:47 +13:00
andres-portainer
5488389278 fix(code): replace calls to ioutil EE-4425 (#7878) 2022-10-17 15:29:12 -03:00
andres-portainer
69f498c431 fix(tests): add missing context cancel EE-4433 (#7879) 2022-10-17 13:57:41 -03:00
Prabhat Khera
669327da7c fix reloading page when ing class disallowed (#7830) 2022-10-17 10:44:17 +13:00
andres-portainer
191f8e17ee fix(code): remove unused code EE-4431 (#7866) 2022-10-14 19:42:31 -03:00
andres-portainer
ae2bec4bd9 fix(code): clean up EE-4432 (#7865) 2022-10-14 18:09:07 -03:00
andres-portainer
367f3dd6d4 fix(tags): remove a data race EE-4310 (#7862) 2022-10-13 11:12:12 -03:00
matias-portainer
8f1ac38963 fix(tags): get tags when loading associated endpoints selector EE-4140 (#7857) 2022-10-13 11:03:19 -03:00
1926 changed files with 49982 additions and 28508 deletions

View File

@@ -1,44 +0,0 @@
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

@@ -83,6 +83,7 @@ overrides:
'newlines-between': 'always',
},
]
no-plusplus: off
func-style: [error, 'declaration']
import/prefer-default-export: off
no-use-before-define: ['error', { functions: false }]
@@ -99,12 +100,17 @@ overrides:
'@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' }]
'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': 'data-feather="(.*)"', 'message': 'Please use `react-feather` package instead' }]]
'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:

View File

@@ -2,4 +2,7 @@
cf5056d9c03b62d91a25c3b9127caac838695f98
# prettier v2
42e7db0ae7897d3cb72b0ea1ecf57ee2dd694169
42e7db0ae7897d3cb72b0ea1ecf57ee2dd694169
# tailwind prettier
58d66d3142950bb90a7d85511c034ac9fabba9ba

View File

@@ -23,6 +23,9 @@ jobs:
with:
node-version: '14'
cache: 'yarn'
- uses: actions/setup-go@v3
with:
go-version: 1.19.4
- run: yarn --frozen-lockfile
- name: Run linters
@@ -36,3 +39,9 @@ jobs:
gofmt_dir: api/
- name: Typecheck
uses: icrawl/action-tsc@v1
- name: GolangCI-Lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
working-directory: api
args: -c .golangci.yaml

View File

@@ -1,16 +1,16 @@
name: Nightly Code Security Scan
on:
on:
schedule:
- cron: '0 8 * * *'
workflow_dispatch:
jobs:
client-dependencies:
name: Client dependency check
runs-on: ubuntu-latest
if: >- # only run for develop branch
github.ref == 'refs/heads/develop'
github.ref == 'refs/heads/develop'
outputs:
js: ${{ steps.set-matrix.outputs.js_result }}
steps:
@@ -24,14 +24,14 @@ jobs:
with:
json: true
- name: Upload js security scan result as artifact
- name: Upload js security scan result as artifact
uses: actions/upload-artifact@v3
with:
name: js-security-scan-develop-result
path: snyk.json
- name: Export scan result to html file
run: |
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=table -export -export-filename="/data/js-result")
- name: Upload js result html file
@@ -42,7 +42,7 @@ jobs:
- name: Analyse the js result
id: set-matrix
run: |
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=matrix)
echo "::set-output name=js_result::${result}"
@@ -50,23 +50,26 @@ jobs:
name: Server dependency check
runs-on: ubuntu-latest
if: >- # only run for develop branch
github.ref == 'refs/heads/develop'
github.ref == 'refs/heads/develop'
outputs:
go: ${{ steps.set-matrix.outputs.go_result }}
steps:
- uses: actions/checkout@master
- uses: actions/setup-go@v3
with:
go-version: '1.19.4'
- name: Download go modules
run: cd ./api && go get -t -v -d ./...
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/golang@master
continue-on-error: true # To make sure that artifact upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --file=./api/go.mod
json: true
run: |
yarn global add snyk
snyk test --file=./api/go.mod --json-file-output=snyk.json 2>/dev/null || :
- name: Upload go security scan result as artifact
uses: actions/upload-artifact@v3
@@ -74,8 +77,8 @@ jobs:
name: go-security-scan-develop-result
path: snyk.json
- name: Export scan result to html file
run: |
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=table -export -export-filename="/data/go-result")
- name: Upload go result html file
@@ -86,7 +89,7 @@ jobs:
- name: Analyse the go result
id: set-matrix
run: |
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=matrix)
echo "::set-output name=go_result::${result}"
@@ -101,18 +104,21 @@ jobs:
- name: Checkout code
uses: actions/checkout@master
- name: Use golang 1.18
- name: Use golang 1.19.4
uses: actions/setup-go@v3
with:
go-version: '1.18'
go-version: '1.19.4'
- name: Use Node.js 12.x
- name: Use Node.js 18.x
uses: actions/setup-node@v1
with:
node-version: 12.x
node-version: 18.x
- name: Install packages and build
run: yarn install && yarn build
- name: Install packages
run: yarn --frozen-lockfile
- name: build
run: make build
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
@@ -131,9 +137,9 @@ jobs:
- name: Run Trivy vulnerability scanner
uses: docker://docker.io/aquasec/trivy:latest
continue-on-error: true
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 }}
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
@@ -141,8 +147,8 @@ jobs:
name: image-security-scan-develop-result
path: image-trivy.json
- name: Export scan result to html file
run: |
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=trivy -path="/data/image-trivy.json" -output-type=table -export -export-filename="/data/image-result")
- name: Upload go result html file
@@ -153,7 +159,7 @@ jobs:
- name: Analyse the trivy result
id: set-matrix
run: |
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=trivy -path="/data/image-trivy.json" -output-type=matrix)
echo "::set-output name=image_result::${result}"
@@ -164,7 +170,7 @@ jobs:
if: >-
github.ref == 'refs/heads/develop'
strategy:
matrix:
matrix:
js: ${{fromJson(needs.client-dependencies.outputs.js)}}
go: ${{fromJson(needs.server-dependencies.outputs.go)}}
image: ${{fromJson(needs.image-vulnerability.outputs.image)}}
@@ -179,7 +185,7 @@ jobs:
echo ${{ matrix.image.summary }}
- name: Send Slack message
if: >-
if: >-
matrix.js.status == 'failure' ||
matrix.go.status == 'failure' ||
matrix.image.status == 'failure'

View File

@@ -12,7 +12,7 @@ on:
- 'build/linux/Dockerfile'
- 'build/linux/alpine.Dockerfile'
- 'build/windows/Dockerfile'
jobs:
client-dependencies:
name: Client dependency check
@@ -51,8 +51,8 @@ jobs:
echo "null" > ./js-snyk-develop.json
fi
- name: Export scan result to html file
run: |
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 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 js result html file
@@ -63,7 +63,7 @@ jobs:
- name: Analyse the js diff result
id: set-diff-matrix
run: |
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/js-snyk-feature.json" -compare-to="./data/js-snyk-develop.json" -output-type=matrix)
echo "::set-output name=js_diff_result::${result}"
@@ -78,17 +78,20 @@ jobs:
steps:
- uses: actions/checkout@master
- uses: actions/setup-go@v3
with:
go-version: '1.19.4'
- name: Download go modules
run: cd ./api && go get -t -v -d ./...
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/golang@master
continue-on-error: true # To make sure that artifact upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --file=./api/go.mod
json: true
run: |
yarn global add snyk
snyk test --file=./api/go.mod --json-file-output=snyk.json 2>/dev/null || :
- name: Upload go security scan result as artifact
uses: actions/upload-artifact@v3
@@ -108,8 +111,8 @@ jobs:
echo "null" > ./go-snyk-develop.json
fi
- name: Export scan result to html file
run: |
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 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 go result html file
@@ -120,7 +123,7 @@ jobs:
- name: Analyse the go diff result
id: set-diff-matrix
run: |
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/go-snyk-feature.json" -compare-to="/data/go-snyk-develop.json" -output-type=matrix)
echo "::set-output name=go_diff_result::${result}"
@@ -136,18 +139,21 @@ jobs:
- name: Checkout code
uses: actions/checkout@master
- name: Use golang 1.18
- name: Use golang 1.19.4
uses: actions/setup-go@v3
with:
go-version: '1.18'
go-version: '1.19.4'
- name: Use Node.js 12.x
- name: Use Node.js 18.x
uses: actions/setup-node@v1
with:
node-version: 12.x
node-version: 18.x
- name: Install packages and build
run: yarn install && yarn build
- name: Install packages
run: yarn --frozen-lockfile
- name: build
run: make build
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
@@ -166,9 +172,9 @@ jobs:
- name: Run Trivy vulnerability scanner
uses: docker://docker.io/aquasec/trivy:latest
continue-on-error: true
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 }}
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
@@ -188,8 +194,8 @@ jobs:
echo "null" > ./image-trivy-develop.json
fi
- name: Export scan result to html file
run: |
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 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 image result html file
@@ -200,7 +206,7 @@ jobs:
- name: Analyse the image diff result
id: set-diff-matrix
run: |
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=trivy -path="/data/image-trivy-feature.json" -compare-to="./data/image-trivy-develop.json" -output-type=matrix)
echo "::set-output name=image_diff_result::${result}"
@@ -212,17 +218,16 @@ jobs:
github.event.pull_request &&
github.event.review.body == '/scan'
strategy:
matrix:
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'
matrix.imagediff.status == 'failure'
run: |
echo ${{ matrix.jsdiff.status }}
echo ${{ matrix.godiff.status }}

View File

@@ -8,12 +8,12 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '14'
node-version: '18'
cache: 'yarn'
- run: yarn --frozen-lockfile
- name: Run tests
run: yarn test:client
run: yarn jest --maxWorkers=2
# test-server:
# runs-on: ubuntu-latest
# env:

View File

@@ -0,0 +1,29 @@
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: '14'
cache: 'yarn'
- run: yarn --frozen-lockfile
- name: Validate OpenAPI Spec
run: make docs-validate

View File

@@ -1,53 +0,0 @@
name: Validate
on:
pull_request:
branches:
- master
- develop
- 'release/*'
jobs:
openapi-spec:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Setup Node v14
uses: actions/setup-node@v2
with:
node-version: 14
# https://github.com/actions/cache/blob/main/examples.md#node---yarn
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Setup Go v1.17.3
uses: actions/setup-go@v2
with:
go-version: '^1.17.3'
- name: Prebuild docs
run: yarn prebuild:docs
- name: Build OpenAPI 2.0 Spec
run: yarn build:docs
# Install dependencies globally to bypass installing all frontend deps
- name: Install swagger2openapi and swagger-cli
run: yarn global add swagger2openapi @apidevtools/swagger-cli
# OpenAPI2.0 does not support multiple body params (which we utilise in some of our handlers).
# OAS3.0 however does support multiple body params - hence its best to convert the generated OAS 2.0
# to OAS 3.0 and validate the output of generated OAS 3.0 instead.
- name: Convert OpenAPI 2.0 to OpenAPI 3.0 and validate spec
run: yarn validate:docs

View File

@@ -29,6 +29,23 @@ module.exports = {
extensions: config.resolve.extensions,
}),
];
const svgRule = config.module.rules.find((rule) => rule.test && typeof rule.test.test === 'function' && rule.test.test('.svg'));
svgRule.test = new RegExp(svgRule.test.source.replace('svg|', ''));
config.module.rules.unshift({
test: /\.svg$/i,
type: 'asset',
resourceQuery: { not: [/c/] }, // exclude react component if *.svg?url
});
config.module.rules.unshift({
test: /\.svg$/i,
issuer: /\.(js|ts)(x)?$/,
resourceQuery: /c/, // *.svg?c
use: [{ loader: '@svgr/webpack', options: { icon: true } }],
});
return config;
},
core: {

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
@@ -93,7 +93,7 @@ $ yarn start
Portainer can now be accessed at <https://localhost:9443>.
Find more detailed steps at <https://documentation.portainer.io/contributing/instructions/>.
Find more detailed steps at <https://docs.portainer.io/contribute/build>.
### Build customisation
@@ -103,6 +103,10 @@ You can customise the following settings:
- `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.
## Adding api docs
When adding a new resource (or a route handler), we should add a new tag to api/http/handler/handler.go#L136 like this:

122
Makefile Normal file
View File

@@ -0,0 +1,122 @@
# 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)
TAG=latest
SWAG_VERSION=v1.8.11
# build target, can be one of "production", "testing", "development"
ENV=development
WEBPACK_CONFIG=webpack/webpack.$(ENV).js
.DEFAULT_GOAL := help
.PHONY: help build-storybook build-client devops download-binaries tidy clean client-deps
##@ Building
init-dist:
@mkdir -p dist
build-storybook:
yarn storybook:build
build: build-server build-client ## Build the server and client
build-client: init-dist client-deps ## 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 ## Build the Portainer image
docker buildx build --load -t portainerci/portainer:$(TAG) -f build/linux/Dockerfile .
devops: clean init-dist download-binaries build-client ## Build the server binary for CI
echo "Building the devops binary..."
@./build/build_binary_azuredevops.sh "$(PLATFORM)" "$(ARCH)"
##@ Dependencies
download-binaries: ## Download dependant binaries
@./build/download_binaries.sh $(PLATFORM) $(ARCH)
tidy: ## Tidy up the go.mod file
cd api && go mod tidy
client-deps: ## Install client dependencies
yarn
##@ Cleanup
clean: ## Remove all build and download artifacts
@echo "Clearing the dist directory..."
@rm -rf dist/*
##@ Testing
test-client: ## Run client tests
yarn test
test-server: ## Run server tests
cd api && go test -v ./...
test: test-client test-server ## Run all tests
##@ Dev
dev-client: ## Run the client in development mode
yarn dev
dev-server: build-image ## Run the server in development mode
@./dev/run_container.sh
##@ Format
format-client: ## Format client code
yarn format
format-server: ## Format server code
cd api && go fmt ./...
format: format-client format-server ## Format all code
##@ Lint
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
dev-extension: build-server build-client ## Run the extension in development mode
make local -f build/docker-extension/Makefile
##@ Docs
docs-deps: ## Install docs dependencies
go install github.com/swaggo/swag/cmd/swag@$(SWAG_VERSION)
docs-build: docs-deps ## Build docs
cd api && swag init -g ./http/handler/handler.go --parseDependency --parseInternal --parseDepth 2 --markdownFiles ./
docs-validate: docs-build ## Validate docs
yarn swagger2openapi --warnOnly api/docs/swagger.yaml -o api/docs/openapi.yaml
yarn swagger-cli validate api/docs/openapi.yaml
docs-clean: ## Clean docs
rm -rf api/docs
docs-validate-clean: docs-validate docs-clean ## Validate and clean docs
##@ Helpers
help: ## Display this help
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\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)

26
api/.golangci.yaml Normal file
View File

@@ -0,0 +1,26 @@
linters:
# Disable all linters.
disable-all: true
enable:
- depguard
linters-settings:
depguard:
list-type: denylist
include-go-root: true
packages:
- github.com/sirupsen/logrus
- golang.org/x/exp
packages-with-error-message:
- github.com/sirupsen/logrus: 'logging is allowed only by github.com/rs/zerolog'
ignore-file-rules:
- "**/*_test.go"
# Create additional guards that follow the same configuration pattern.
# Results from all guards are aggregated together.
# additional-guards:
# - list-type: allowlist
# include-go-root: false
# packages:
# - github.com/sirupsen/logrus
# # Specify rules by which the linter ignores certain files for consideration.
# ignore-file-rules:
# - "!**/*_test.go"

View File

@@ -21,7 +21,7 @@ type Monitor struct {
datastore dataservices.DataStore
shutdownCtx context.Context
cancellationFunc context.CancelFunc
mu sync.Mutex
mu sync.RWMutex
adminInitDisabled bool
}
@@ -54,7 +54,8 @@ func (m *Monitor) Start() {
case <-time.After(m.timeout):
initialized, err := m.WasInitialized()
if err != nil {
log.Fatal().Err(err).Msg("")
log.Error().Err(err).Msg("AdminMonitor failed to determine if Portainer is Initialized")
return
}
if !initialized {
@@ -82,6 +83,7 @@ func (m *Monitor) Stop() {
if m.cancellationFunc == nil {
return
}
m.cancellationFunc()
m.cancellationFunc = nil
}
@@ -92,12 +94,14 @@ func (m *Monitor) WasInitialized() (bool, error) {
if err != nil {
return false, err
}
return len(users) > 0, nil
}
func (m *Monitor) WasInstanceDisabled() bool {
m.mu.Lock()
defer m.mu.Unlock()
m.mu.RLock()
defer m.mu.RUnlock()
return m.adminInitDisabled
}

View File

@@ -2,7 +2,6 @@ package archive
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
@@ -28,10 +27,10 @@ func listFiles(dir string) []string {
func Test_shouldCreateArhive(t *testing.T) {
tmpdir := t.TempDir()
content := []byte("content")
ioutil.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
os.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
ioutil.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
ioutil.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
os.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
os.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
gzPath, err := TarGzDir(tmpdir)
assert.Nil(t, err)
@@ -48,7 +47,7 @@ func Test_shouldCreateArhive(t *testing.T) {
wasExtracted := func(p string) {
fullpath := path.Join(extractionDir, p)
assert.Contains(t, extractedFiles, fullpath)
copyContent, _ := ioutil.ReadFile(fullpath)
copyContent, _ := os.ReadFile(fullpath)
assert.Equal(t, content, copyContent)
}
@@ -60,10 +59,10 @@ func Test_shouldCreateArhive(t *testing.T) {
func Test_shouldCreateArhiveXXXXX(t *testing.T) {
tmpdir := t.TempDir()
content := []byte("content")
ioutil.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
os.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
ioutil.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
ioutil.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
os.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
os.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
gzPath, err := TarGzDir(tmpdir)
assert.Nil(t, err)
@@ -80,7 +79,7 @@ func Test_shouldCreateArhiveXXXXX(t *testing.T) {
wasExtracted := func(p string) {
fullpath := path.Join(extractionDir, p)
assert.Contains(t, extractedFiles, fullpath)
copyContent, _ := ioutil.ReadFile(fullpath)
copyContent, _ := os.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 := ioutil.ReadAll(f)
data, err := io.ReadAll(f)
if err != nil {
return err
}

View File

@@ -1,9 +1,10 @@
package archive
import (
"github.com/stretchr/testify/assert"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestUnzipFile(t *testing.T) {

View File

@@ -1,110 +1,110 @@
package backup
// import (
// "fmt"
// "os"
// "path"
// "path/filepath"
// "time"
import (
"fmt"
"os"
"path"
"path/filepath"
"time"
// "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/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/pkg/errors"
"github.com/rs/zerolog/log"
)
// const rwxr__r__ os.FileMode = 0744
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",
// }
var filesToBackup = []string{
"certs",
"compose",
"config.json",
"custom_templates",
"edge_jobs",
"edge_stacks",
"extensions",
"portainer.key",
"portainer.pub",
"tls",
}
// // 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) {
// unlock := gate.Lock()
// defer unlock()
// 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) {
unlock := gate.Lock()
defer unlock()
// backupDirPath := filepath.Join(filestorePath, "backup", time.Now().Format("2006-01-02_15-04-05"))
// if err := os.MkdirAll(backupDirPath, rwxr__r__); err != nil {
// return "", errors.Wrap(err, "Failed to create backup dir")
// }
backupDirPath := filepath.Join(filestorePath, "backup", time.Now().Format("2006-01-02_15-04-05"))
if err := os.MkdirAll(backupDirPath, rwxr__r__); err != nil {
return "", errors.Wrap(err, "Failed to create backup dir")
}
// {
// // new export
// exportFilename := path.Join(backupDirPath, fmt.Sprintf("export-%d.json", time.Now().Unix()))
{
// 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")
// }
// }
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")
// }
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)
// if err != nil {
// return "", errors.Wrap(err, "Failed to create backup file")
// }
// }
for _, filename := range filesToBackup {
err := filesystem.CopyPath(filepath.Join(filestorePath, filename), backupDirPath)
if err != nil {
return "", errors.Wrap(err, "Failed to create backup file")
}
}
// archivePath, err := archive.TarGzDir(backupDirPath)
// if err != nil {
// return "", errors.Wrap(err, "Failed to make an archive")
// }
archivePath, err := archive.TarGzDir(backupDirPath)
if err != nil {
return "", errors.Wrap(err, "Failed to make an archive")
}
// if password != "" {
// archivePath, err = encrypt(archivePath, password)
// if err != nil {
// return "", errors.Wrap(err, "Failed to encrypt backup with the password")
// }
// }
if password != "" {
archivePath, err = encrypt(archivePath, password)
if err != nil {
return "", errors.Wrap(err, "Failed to encrypt backup with the password")
}
}
// return archivePath, nil
// }
return archivePath, nil
}
// func backupDb(backupDirPath string, datastore dataservices.DataStore) error {
// backupWriter, err := os.Create(filepath.Join(backupDirPath, "portainer.db"))
// if err != nil {
// return err
// }
// if err = datastore.BackupTo(backupWriter); err != nil {
// return err
// }
// return backupWriter.Close()
// }
func backupDb(backupDirPath string, datastore dataservices.DataStore) error {
backupWriter, err := os.Create(filepath.Join(backupDirPath, "portainer.db"))
if err != nil {
return err
}
if err = datastore.BackupTo(backupWriter); err != nil {
return err
}
return backupWriter.Close()
}
// func encrypt(path string, passphrase string) (string, error) {
// in, err := os.Open(path)
// if err != nil {
// return "", err
// }
// defer in.Close()
func encrypt(path string, passphrase string) (string, error) {
in, err := os.Open(path)
if err != nil {
return "", err
}
defer in.Close()
// outFileName := fmt.Sprintf("%s.encrypted", path)
// out, err := os.Create(outFileName)
// if err != nil {
// return "", err
// }
outFileName := fmt.Sprintf("%s.encrypted", path)
out, err := os.Create(outFileName)
if err != nil {
return "", err
}
// err = crypto.AesEncrypt(in, out, []byte(passphrase))
err = crypto.AesEncrypt(in, out, []byte(passphrase))
// return outFileName, err
// }
return outFileName, err
}

View File

@@ -1,85 +1,113 @@
package backup
// import (
// "context"
// "io"
// "os"
// "path/filepath"
// "time"
import (
"context"
"io"
"io/fs"
"os"
"path/filepath"
"regexp"
"time"
// "github.com/pkg/errors"
// "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/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")
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 {
// var err error
// if password != "" {
// archive, err = decrypt(archive, password)
// if err != nil {
// return errors.Wrap(err, "failed to decrypt the archive")
// }
// }
// 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 {
var err error
if password != "" {
archive, err = decrypt(archive, password)
if err != nil {
return errors.Wrap(err, "failed to decrypt the archive")
}
}
// restorePath := filepath.Join(filestorePath, "restore", time.Now().Format("20060102150405"))
// defer os.RemoveAll(filepath.Dir(restorePath))
restorePath := filepath.Join(filestorePath, "restore", time.Now().Format("20060102150405"))
defer os.RemoveAll(filepath.Dir(restorePath))
// err = extractArchive(archive, restorePath)
// if err != nil {
// return errors.Wrap(err, "cannot extract files from the archive. Please ensure the password is correct and try again")
// }
err = extractArchive(archive, restorePath)
if err != nil {
return errors.Wrap(err, "cannot extract files from the archive. Please ensure the password is correct and try again")
}
// unlock := gate.Lock()
// defer unlock()
unlock := gate.Lock()
defer unlock()
// if err = datastore.Close(); err != nil {
// return errors.Wrap(err, "Failed to stop db")
// }
if err = datastore.Close(); err != nil {
return errors.Wrap(err, "Failed to stop db")
}
// if err = restoreFiles(restorePath, filestorePath); err != nil {
// return errors.Wrap(err, "failed to restore the system state")
// }
// 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")
}
// shutdownTrigger()
// return nil
// }
if err = restoreFiles(restorePath, filestorePath); err != nil {
return errors.Wrap(err, "failed to restore the system state")
}
// func decrypt(r io.Reader, password string) (io.Reader, error) {
// return crypto.AesDecrypt(r, []byte(password))
// }
shutdownTrigger()
return nil
}
// func extractArchive(r io.Reader, destinationDirPath string) error {
// return archive.ExtractTarGz(r, destinationDirPath)
// }
func decrypt(r io.Reader, password string) (io.Reader, error) {
return crypto.AesDecrypt(r, []byte(password))
}
// func restoreFiles(srcDir string, destinationDir string) error {
// for _, filename := range filesToRestore {
// err := filesystem.CopyPath(filepath.Join(srcDir, filename), destinationDir)
// if err != nil {
// return err
// }
// }
func extractArchive(r io.Reader, destinationDirPath string) error {
return archive.ExtractTarGz(r, destinationDirPath)
}
// // TODO: This is very boltdb module specific once again due to the filename. Move to bolt module? Refactor for another day
func getRestoreSourcePath(dir string) (string, error) {
// find portainer.db or portainer.edb file. Return the parent directory
var portainerdbRegex = regexp.MustCompile(`^portainer.e?db$`)
// // 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))
backupDirPath := dir
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
// // Now copy the database. It'll be either portainer.db or portainer.edb
if portainerdbRegex.MatchString(d.Name()) {
backupDirPath = filepath.Dir(path)
return filepath.SkipDir
}
return nil
})
// // 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 backupDirPath, err
}
// // return filesystem.CopyPath(filepath.Join(srcDir, boltdb.DatabaseFileName), destinationDir)
// return nil
// }
func restoreFiles(srcDir string, destinationDir string) error {
for _, filename := range filesToRestore {
err := filesystem.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)
}

View File

@@ -2,12 +2,17 @@ package chisel
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/edge/cache"
)
// AddEdgeJob register an EdgeJob inside the tunnel details associated to an environment(endpoint).
func (service *Service) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *portainer.EdgeJob) {
func (service *Service) AddEdgeJob(endpoint *portainer.Endpoint, edgeJob *portainer.EdgeJob) {
if endpoint.Edge.AsyncMode {
return
}
service.mu.Lock()
tunnel := service.getTunnelDetails(endpointID)
tunnel := service.getTunnelDetails(endpoint.ID)
existingJobIndex := -1
for idx, existingJob := range tunnel.Jobs {
@@ -23,6 +28,8 @@ func (service *Service) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *por
tunnel.Jobs[existingJobIndex] = *edgeJob
}
cache.Del(endpoint.ID)
service.mu.Unlock()
}
@@ -30,8 +37,7 @@ func (service *Service) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *por
func (service *Service) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {
service.mu.Lock()
for _, tunnel := range service.tunnelDetailsMap {
// Filter in-place
for endpointID, tunnel := range service.tunnelDetailsMap {
n := 0
for _, edgeJob := range tunnel.Jobs {
if edgeJob.ID != edgeJobID {
@@ -41,7 +47,28 @@ func (service *Service) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {
}
tunnel.Jobs = tunnel.Jobs[:n]
cache.Del(endpointID)
}
service.mu.Unlock()
}
func (service *Service) RemoveEdgeJobFromEndpoint(endpointID portainer.EndpointID, edgeJobID portainer.EdgeJobID) {
service.mu.Lock()
tunnel := service.getTunnelDetails(endpointID)
n := 0
for _, edgeJob := range tunnel.Jobs {
if edgeJob.ID != edgeJobID {
tunnel.Jobs[n] = edgeJob
n++
}
}
tunnel.Jobs = tunnel.Jobs[:n]
cache.Del(endpointID)
service.mu.Unlock()
}

View File

@@ -206,15 +206,23 @@ func (service *Service) checkTunnels() {
service.mu.Lock()
for key, tunnel := range service.tunnelDetailsMap {
if tunnel.LastActivity.IsZero() || tunnel.Status == portainer.EdgeAgentIdle {
continue
}
if tunnel.Status == portainer.EdgeAgentManagementRequired && time.Since(tunnel.LastActivity) < requiredTimeout {
continue
}
if tunnel.Status == portainer.EdgeAgentActive && time.Since(tunnel.LastActivity) < activeTimeout {
continue
}
tunnels[key] = *tunnel
}
service.mu.Unlock()
for endpointID, tunnel := range tunnels {
if tunnel.LastActivity.IsZero() || tunnel.Status == portainer.EdgeAgentIdle {
continue
}
elapsed := time.Since(tunnel.LastActivity)
log.Debug().
Int("endpoint_id", int(endpointID)).
@@ -222,9 +230,7 @@ func (service *Service) checkTunnels() {
Float64("status_time_seconds", elapsed.Seconds()).
Msg("environment tunnel monitoring")
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() < requiredTimeout.Seconds() {
continue
} else if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() > requiredTimeout.Seconds() {
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed > requiredTimeout {
log.Debug().
Int("endpoint_id", int(endpointID)).
Str("status", tunnel.Status).
@@ -233,9 +239,7 @@ func (service *Service) checkTunnels() {
Msg("REQUIRED state timeout exceeded")
}
if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() < activeTimeout.Seconds() {
continue
} else if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() > activeTimeout.Seconds() {
if tunnel.Status == portainer.EdgeAgentActive && elapsed > activeTimeout {
log.Debug().
Int("endpoint_id", int(endpointID)).
Str("status", tunnel.Status).

View File

@@ -2,14 +2,17 @@ package chisel
import (
"encoding/base64"
"errors"
"fmt"
"math/rand"
"strings"
"time"
"github.com/dchest/uniuri"
"github.com/portainer/libcrypto"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/edge/cache"
"github.com/dchest/uniuri"
)
const (
@@ -49,6 +52,8 @@ func (service *Service) getTunnelDetails(endpointID portainer.EndpointID) *porta
service.tunnelDetailsMap[endpointID] = tunnel
cache.Del(endpointID)
return tunnel
}
@@ -62,6 +67,10 @@ func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) portai
// GetActiveTunnel retrieves an active tunnel which allows communicating with edge agent
func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (portainer.TunnelDetails, error) {
if endpoint.Edge.AsyncMode {
return portainer.TunnelDetails{}, errors.New("cannot open tunnel on async endpoint")
}
tunnel := service.GetTunnelDetails(endpoint.ID)
if tunnel.Status == portainer.EdgeAgentActive {
@@ -99,6 +108,8 @@ func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID)
tunnel.Credentials = ""
tunnel.LastActivity = time.Now()
service.mu.Unlock()
cache.Del(endpointID)
}
// SetTunnelStatusToIdle update the status of the tunnel associated to the specified environment(endpoint).
@@ -121,6 +132,8 @@ func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
service.ProxyManager.DeleteEndpointProxy(endpointID)
service.mu.Unlock()
cache.Del(endpointID)
}
// SetTunnelStatusToRequired update the status of the tunnel associated to the specified environment(endpoint).
@@ -129,6 +142,8 @@ func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
// and generate temporary credentials that can be used to establish a reverse tunnel on that port.
// Credentials are encrypted using the Edge ID associated to the environment(endpoint).
func (service *Service) SetTunnelStatusToRequired(endpointID portainer.EndpointID) error {
defer cache.Del(endpointID)
tunnel := service.getTunnelDetails(endpointID)
service.mu.Lock()

View File

@@ -36,7 +36,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
DemoEnvironment: kingpin.Flag("demo", "Demo environment").Bool(),
EndpointURL: kingpin.Flag("host", "Environment URL").Short('H').String(),
FeatureFlags: BoolPairs(kingpin.Flag("feat", "List of feature flags").Hidden()),
FeatureFlags: kingpin.Flag("feat", "List of feature flags").Strings(),
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app (deprecated)").Bool(),
TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(),
@@ -62,6 +62,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
MaxBatchDelay: kingpin.Flag("max-batch-delay", "Maximum delay before a batch starts").Duration(),
SecretKeyName: kingpin.Flag("secret-key-name", "Secret key name for encryption and will be used as /run/secrets/<secret-key-name>.").Default(defaultSecretKeyName).String(),
LogLevel: kingpin.Flag("log-level", "Set the minimum logging level to show").Default("INFO").Enum("DEBUG", "INFO", "WARN", "ERROR"),
LogMode: kingpin.Flag("log-mode", "Set the logging output mode").Default("PRETTY").Enum("PRETTY", "JSON"),
}
kingpin.Parse()

View File

@@ -2,14 +2,14 @@ package cli
import (
"bufio"
"log"
"fmt"
"os"
"strings"
)
// Confirm starts a rollback db cli application
func Confirm(message string) (bool, error) {
log.Printf("%s [y/N]", message)
fmt.Printf("%s [y/N]", message)
reader := bufio.NewReader(os.Stdin)
answer, err := reader.ReadString('\n')

View File

@@ -1,7 +1,9 @@
package main
import (
"fmt"
stdlog "log"
"os"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
@@ -31,3 +33,23 @@ func setLoggingLevel(level string) {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
}
func setLoggingMode(mode string) {
switch mode {
case "PRETTY":
log.Logger = log.Output(zerolog.ConsoleWriter{
Out: os.Stderr,
NoColor: true,
TimeFormat: "2006/01/02 03:04PM",
FormatMessage: formatMessage})
case "JSON":
log.Logger = log.Output(os.Stderr)
}
}
func formatMessage(i interface{}) string {
if i == nil {
return ""
}
return fmt.Sprintf("%s |", i)
}

View File

@@ -3,13 +3,14 @@ package main
import (
"context"
"crypto/sha256"
"fmt"
"math/rand"
"os"
"path"
"strconv"
"strings"
"time"
"github.com/portainer/libhelm"
libstack "github.com/portainer/docker-compose-wrapper"
"github.com/portainer/docker-compose-wrapper/compose"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/apikey"
"github.com/portainer/portainer/api/build"
@@ -17,6 +18,8 @@ import (
"github.com/portainer/portainer/api/cli"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/database"
"github.com/portainer/portainer/api/database/boltdb"
"github.com/portainer/portainer/api/database/models"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/demo"
@@ -31,8 +34,10 @@ import (
kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/edge"
"github.com/portainer/portainer/api/internal/edge/edgestacks"
"github.com/portainer/portainer/api/internal/snapshot"
"github.com/portainer/portainer/api/internal/ssl"
"github.com/portainer/portainer/api/internal/upgrade"
"github.com/portainer/portainer/api/jwt"
"github.com/portainer/portainer/api/kubernetes"
kubecli "github.com/portainer/portainer/api/kubernetes/cli"
@@ -40,7 +45,10 @@ import (
"github.com/portainer/portainer/api/oauth"
"github.com/portainer/portainer/api/scheduler"
"github.com/portainer/portainer/api/stacks/deployments"
"github.com/portainer/portainer/pkg/featureflags"
"github.com/portainer/portainer/pkg/libhelm"
"github.com/gofrs/uuid"
"github.com/rs/zerolog/log"
)
@@ -69,28 +77,34 @@ func initFileService(dataStorePath string) portainer.FileService {
}
func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService portainer.FileService, shutdownCtx context.Context) dataservices.DataStore {
connection, err := database.NewDatabase("sqlite", *flags.Data, secretKey)
connection, err := database.NewDatabase("boltdb", *flags.Data, secretKey)
if err != nil {
log.Fatal().Err(err).Msg("failed creating database connection")
}
if bconn, ok := connection.(*boltdb.DbConnection); ok {
bconn.MaxBatchSize = *flags.MaxBatchSize
bconn.MaxBatchDelay = *flags.MaxBatchDelay
bconn.InitialMmapSize = *flags.InitialMmapSize
} else {
log.Fatal().Msg("failed creating database connection: expecting a boltdb database type but a different one was received")
}
store := datastore.NewStore(*flags.Data, fileService, connection)
isNew, err := store.Open()
if err != nil {
log.Fatal().Err(err).Msg("failed opening store")
}
// if *flags.Rollback {
// err := store.Rollback(false)
// if err != nil {
// log.Fatal().Err(err).Msg("failed rolling back")
// }
if *flags.Rollback {
err := store.Rollback(false)
if err != nil {
log.Fatal().Err(err).Msg("failed rolling back")
}
// log.Info().Msg("exiting rollback")
// os.Exit(0)
// return nil
// }
log.Info().Msg("exiting rollback")
os.Exit(0)
}
// Init sets some defaults - it's basically a migration
err = store.Init()
@@ -99,25 +113,28 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
}
if isNew {
// from MigrateData
store.VersionService.StoreDBVersion(portainer.DBVersion)
instanceId, err := uuid.NewV4()
if err != nil {
log.Fatal().Err(err).Msg("failed generating instance id")
}
err := updateSettingsFromFlags(store, flags)
// from MigrateData
v := models.Version{
SchemaVersion: portainer.APIVersion,
Edition: int(portainer.PortainerCE),
InstanceID: instanceId.String(),
}
store.VersionService.UpdateVersion(&v)
err = updateSettingsFromFlags(store, flags)
if err != nil {
log.Fatal().Err(err).Msg("failed updating settings from flags")
}
} else {
// storedVersion, err := store.VersionService.DBVersion()
// if err != nil {
// log.Fatal().Err(err).Msg("failure during creation of new database")
// }
// if storedVersion != portainer.DBVersion {
// err = store.MigrateData()
// if err != nil {
// log.Fatal().Err(err).Msg("failed migration")
// }
// }
err = store.MigrateData()
if err != nil {
log.Fatal().Err(err).Msg("failed migration")
}
}
err = updateSettingsFromFlags(store, flags)
@@ -129,22 +146,13 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
go func() {
<-shutdownCtx.Done()
defer connection.Close()
// exportFilename := path.Join(*flags.Data, fmt.Sprintf("export-%d.json", time.Now().Unix()))
// err := store.Export(exportFilename)
// if err != nil {
// log.Error().Str("filename", exportFilename).Err(err).Msg("failed to export")
// } else {
// log.Debug().Str("filename", exportFilename).Msg("exported")
// }
}()
return store
}
func initComposeStackManager(assetsPath string, configPath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
composeWrapper, err := exec.NewComposeStackManager(assetsPath, configPath, proxyManager)
func initComposeStackManager(composeDeployer libstack.Deployer, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
composeWrapper, err := exec.NewComposeStackManager(composeDeployer, proxyManager)
if err != nil {
log.Fatal().Err(err).Msg("failed creating compose manager")
}
@@ -229,8 +237,8 @@ func initDockerClientFactory(signatureService portainer.DigitalSignatureService,
return docker.NewClientFactory(signatureService, reverseTunnelService)
}
func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, instanceID string, dataStore dataservices.DataStore) *kubecli.ClientFactory {
return kubecli.NewClientFactory(signatureService, reverseTunnelService, instanceID, dataStore)
func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, dataStore dataservices.DataStore, instanceID, addrHTTPS, userSessionTimeout string) (*kubecli.ClientFactory, error) {
return kubecli.NewClientFactory(signatureService, reverseTunnelService, dataStore, instanceID, addrHTTPS, userSessionTimeout)
}
func initSnapshotService(
@@ -306,51 +314,7 @@ func updateSettingsFromFlags(dataStore dataservices.DataStore, flags *portainer.
sslSettings.HTTPEnabled = true
}
err = dataStore.SSLSettings().UpdateSettings(sslSettings)
if err != nil {
return err
}
return nil
}
// enableFeaturesFromFlags turns on or off feature flags
// e.g. portainer --feat open-amt --feat fdo=true ... (defaults to true)
// note, settings are persisted to the DB. To turn off `--feat open-amt=false`
func enableFeaturesFromFlags(dataStore dataservices.DataStore, flags *portainer.CLIFlags) error {
settings, err := dataStore.Settings().Settings()
if err != nil {
return err
}
if settings.FeatureFlagSettings == nil {
settings.FeatureFlagSettings = make(map[portainer.Feature]bool)
}
// loop through feature flags to check if they are supported
for _, feat := range *flags.FeatureFlags {
var correspondingFeature *portainer.Feature
for i, supportedFeat := range portainer.SupportedFeatureFlags {
if strings.EqualFold(feat.Name, string(supportedFeat)) {
correspondingFeature = &portainer.SupportedFeatureFlags[i]
}
}
if correspondingFeature == nil {
return fmt.Errorf("unknown feature flag '%s'", feat.Name)
}
featureState, err := strconv.ParseBool(feat.Value)
if err != nil {
return fmt.Errorf("feature flag's '%s' value should be true or false", feat.Name)
}
log.Info().Str("feature", string(*correspondingFeature)).Bool("state", featureState).Msg("")
settings.FeatureFlagSettings[*correspondingFeature] = featureState
}
return dataStore.Settings().UpdateSettings(settings)
return dataStore.SSLSettings().UpdateSettings(sslSettings)
}
func loadAndParseKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
@@ -543,6 +507,10 @@ func loadEncryptionSecretKey(keyfilename string) []byte {
func buildServer(flags *portainer.CLIFlags) portainer.Server {
shutdownCtx, shutdownTrigger := context.WithCancel(context.Background())
if flags.FeatureFlags != nil {
featureflags.Parse(*flags.FeatureFlags, portainer.SupportedFeatureFlags)
}
fileService := initFileService(*flags.Data)
encryptionKey := loadEncryptionSecretKey(*flags.SecretKeyName)
if encryptionKey == nil {
@@ -572,11 +540,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
log.Fatal().Err(err).Msg("failed initializing JWT service")
}
err = enableFeaturesFromFlags(dataStore, flags)
if err != nil {
log.Fatal().Err(err).Msg("failed enabling feature flag")
}
ldapService := initLDAPService()
oauthService := initOAuthService()
@@ -589,6 +552,8 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
digitalSignatureService := initDigitalSignatureService()
edgeStacksService := edgestacks.NewService(dataStore)
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.SSLCert, *flags.SSLKey, fileService, dataStore, shutdownTrigger)
if err != nil {
log.Fatal().Err(err).Msg("")
@@ -607,7 +572,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
reverseTunnelService := chisel.NewService(dataStore, shutdownCtx)
dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
kubernetesClientFactory := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, instanceID, dataStore)
kubernetesClientFactory, err := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, dataStore, instanceID, *flags.AddrHTTPS, settings.UserSessionTimeout)
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx)
if err != nil {
@@ -628,7 +593,12 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
dockerConfigPath := fileService.GetDockerConfigPath()
composeStackManager := initComposeStackManager(*flags.Assets, dockerConfigPath, reverseTunnelService, proxyManager)
composeDeployer, err := compose.NewComposeDeployer(*flags.Assets, dockerConfigPath)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing compose deployer")
}
composeStackManager := initComposeStackManager(composeDeployer, reverseTunnelService, proxyManager)
swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, digitalSignatureService, fileService, reverseTunnelService, dataStore)
if err != nil {
@@ -714,6 +684,24 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
log.Fatal().Msg("failed to fetch SSL settings from DB")
}
upgradeService, err := upgrade.NewService(*flags.Assets, composeDeployer)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing upgrade service")
}
// Our normal migrations run as part of the database initialization
// but some more complex migrations require access to a kubernetes or docker
// client. Therefore we run a separate migration process just before
// starting the server.
postInitMigrator := datastore.NewPostInitMigrator(
kubernetesClientFactory,
dockerClientFactory,
dataStore,
)
if err := postInitMigrator.PostInitMigrate(); err != nil {
log.Fatal().Err(err).Msg("failure during post init migrations")
}
return &http.Server{
AuthorizationService: authorizationService,
ReverseTunnelService: reverseTunnelService,
@@ -723,6 +711,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
HTTPEnabled: sslDBSettings.HTTPEnabled,
AssetsPath: *flags.Assets,
DataStore: dataStore,
EdgeStacksService: edgeStacksService,
SwarmStackManager: swarmStackManager,
ComposeStackManager: composeStackManager,
KubernetesDeployer: kubernetesDeployer,
@@ -748,15 +737,20 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
ShutdownTrigger: shutdownTrigger,
StackDeployer: stackDeployer,
DemoService: demoService,
UpgradeService: upgradeService,
}
}
func main() {
rand.Seed(time.Now().UnixNano())
configureLogger()
setLoggingMode("PRETTY")
flags := initCLI()
setLoggingLevel(*flags.LogLevel)
setLoggingMode(*flags.LogMode)
for {
server := buildServer(flags)

View File

@@ -1,111 +0,0 @@
package main
// import (
// "fmt"
// "testing"
// portainer "github.com/portainer/portainer/api"
// "github.com/portainer/portainer/api/cli"
// "github.com/portainer/portainer/api/dataservices"
// "github.com/portainer/portainer/api/datastore"
// "github.com/stretchr/testify/assert"
// "gopkg.in/alecthomas/kingpin.v2"
// )
// type mockKingpinSetting string
// func (m mockKingpinSetting) SetValue(value kingpin.Value) {
// value.Set(string(m))
// }
// func Test_enableFeaturesFromFlags(t *testing.T) {
// is := assert.New(t)
// _, store, teardown := datastore.MustNewTestStore(t, true, true)
// defer teardown()
// tests := []struct {
// featureFlag string
// isSupported bool
// }{
// {"test", false},
// }
// for _, test := range tests {
// t.Run(fmt.Sprintf("%s succeeds:%v", test.featureFlag, test.isSupported), func(t *testing.T) {
// mockKingpinSetting := mockKingpinSetting(test.featureFlag)
// flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
// err := enableFeaturesFromFlags(store, flags)
// if test.isSupported {
// is.NoError(err)
// } else {
// is.Error(err)
// }
// })
// }
// t.Run("passes for all supported feature flags", func(t *testing.T) {
// for _, flag := range portainer.SupportedFeatureFlags {
// mockKingpinSetting := mockKingpinSetting(flag)
// flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
// err := enableFeaturesFromFlags(store, flags)
// is.NoError(err)
// }
// })
// }
// const FeatTest portainer.Feature = "optional-test"
// func optionalFunc(dataStore dataservices.DataStore) string {
// // TODO: this is a code smell - finding out if a feature flag is enabled should not require having access to the store, and asking for a settings obj.
// // ideally, the `if` should look more like:
// // if featureflags.FlagEnabled(FeatTest) {}
// settings, err := dataStore.Settings().Settings()
// if err != nil {
// return err.Error()
// }
// if settings.FeatureFlagSettings[FeatTest] {
// return "enabled"
// }
// return "disabled"
// }
// func Test_optionalFeature(t *testing.T) {
// portainer.SupportedFeatureFlags = append(portainer.SupportedFeatureFlags, FeatTest)
// is := assert.New(t)
// _, store, teardown := datastore.MustNewTestStore(t, true, true)
// defer teardown()
// // Enable the test feature
// t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
// mockKingpinSetting := mockKingpinSetting(FeatTest)
// flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
// err := enableFeaturesFromFlags(store, flags)
// is.NoError(err)
// is.Equal("enabled", optionalFunc(store))
// })
// // Same store, so the feature flag should still be enabled
// t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
// is.Equal("enabled", optionalFunc(store))
// })
// // disable the test feature
// t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
// mockKingpinSetting := mockKingpinSetting(FeatTest + "=false")
// flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
// err := enableFeaturesFromFlags(store, flags)
// is.NoError(err)
// is.Equal("disabled", optionalFunc(store))
// })
// // Same store, so feature flag should still be disabled
// t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
// is.Equal("disabled", optionalFunc(store))
// })
// }

View File

@@ -1,20 +1,54 @@
package portainer
import "gorm.io/gorm"
import (
"io"
)
type ReadTransaction interface {
GetObject(bucketName string, key []byte, object interface{}) error
GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
GetAllWithJsoniter(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj interface{}, append func(o interface{}) (interface{}, error)) error
}
type Transaction interface {
ReadTransaction
SetServiceName(bucketName string) error
UpdateObject(bucketName string, key []byte, object interface{}) error
DeleteObject(bucketName string, key []byte) error
CreateObject(bucketName string, fn func(uint64) (int, interface{})) error
CreateObjectWithId(bucketName string, id int, obj interface{}) error
CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error
DeleteAllObjects(bucketName string, obj interface{}, matching func(o interface{}) (id int, ok bool)) error
GetNextIdentifier(bucketName string) int
}
type Connection interface {
Transaction
Open() error
Close() error
Init() error
UpdateTx(fn func(Transaction) error) error
ViewTx(fn func(Transaction) error) error
GetDB() *gorm.DB
// write the db contents to filename as json (the schema needs defining)
ExportRaw(filename string) error
// TODO: this one is very database specific atm
BackupTo(w io.Writer) error
GetDatabaseFileName() string
GetDatabaseFilePath() string
GetStorePath() string
SetEncrypted(encrypted bool)
IsEncryptedStore() bool
NeedsEncryptionMigration() (bool, error)
SetEncrypted(encrypted bool)
BackupMetadata() (map[string]interface{}, error)
RestoreMetadata(s map[string]interface{}) error
UpdateObjectFunc(bucketName string, key []byte, object any, updateFn func()) error
ConvertToKey(v int) []byte
}

View File

@@ -13,7 +13,7 @@ import (
// Person with better knowledge is welcomed to improve it.
// sourced from https://golang.org/src/crypto/cipher/example_test.go
var emptySalt []byte = make([]byte, 0, 0)
var emptySalt []byte = make([]byte, 0)
// AesEncrypt reads from input, encrypts with AES-256 and writes to the output.
// passphrase is used to generate an encryption key.

View File

@@ -2,7 +2,6 @@ package crypto
import (
"io"
"io/ioutil"
"os"
"path/filepath"
"testing"
@@ -20,7 +19,7 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
)
content := []byte("content")
ioutil.WriteFile(originFilePath, content, 0600)
os.WriteFile(originFilePath, content, 0600)
originFile, _ := os.Open(originFilePath)
defer originFile.Close()
@@ -30,7 +29,7 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
err := AesEncrypt(originFile, encryptedFileWriter, []byte("passphrase"))
assert.Nil(t, err, "Failed to encrypt a file")
encryptedContent, err := ioutil.ReadFile(encryptedFilePath)
encryptedContent, err := os.ReadFile(encryptedFilePath)
assert.Nil(t, err, "Couldn't read encrypted file")
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
@@ -45,7 +44,7 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
io.Copy(decryptedFileWriter, decryptedReader)
decryptedContent, _ := ioutil.ReadFile(decryptedFilePath)
decryptedContent, _ := os.ReadFile(decryptedFilePath)
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
}
@@ -59,7 +58,7 @@ func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
)
content := []byte("content")
ioutil.WriteFile(originFilePath, content, 0600)
os.WriteFile(originFilePath, content, 0600)
originFile, _ := os.Open(originFilePath)
defer originFile.Close()
@@ -69,7 +68,7 @@ func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
err := AesEncrypt(originFile, encryptedFileWriter, []byte(""))
assert.Nil(t, err, "Failed to encrypt a file")
encryptedContent, err := ioutil.ReadFile(encryptedFilePath)
encryptedContent, err := os.ReadFile(encryptedFilePath)
assert.Nil(t, err, "Couldn't read encrypted file")
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
@@ -84,7 +83,7 @@ func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
io.Copy(decryptedFileWriter, decryptedReader)
decryptedContent, _ := ioutil.ReadFile(decryptedFilePath)
decryptedContent, _ := os.ReadFile(decryptedFilePath)
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
}
@@ -98,7 +97,7 @@ func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T)
)
content := []byte("content")
ioutil.WriteFile(originFilePath, content, 0600)
os.WriteFile(originFilePath, content, 0600)
originFile, _ := os.Open(originFilePath)
defer originFile.Close()
@@ -108,7 +107,7 @@ func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T)
err := AesEncrypt(originFile, encryptedFileWriter, []byte("passphrase"))
assert.Nil(t, err, "Failed to encrypt a file")
encryptedContent, err := ioutil.ReadFile(encryptedFilePath)
encryptedContent, err := os.ReadFile(encryptedFilePath)
assert.Nil(t, err, "Couldn't read encrypted file")
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
@@ -123,6 +122,6 @@ func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T)
io.Copy(decryptedFileWriter, decryptedReader)
decryptedContent, _ := ioutil.ReadFile(decryptedFilePath)
decryptedContent, _ := os.ReadFile(decryptedFilePath)
assert.NotEqual(t, content, decryptedContent, "Original and decrypted content should NOT match")
}

View File

@@ -3,11 +3,11 @@ package crypto
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"os"
)
// CreateServerTLSConfiguration creates a basic tls.Config to be used by servers with recommended TLS settings
func CreateServerTLSConfiguration() *tls.Config {
// CreateTLSConfiguration creates a basic tls.Config with recommended TLS settings
func CreateTLSConfiguration() *tls.Config {
return &tls.Config{
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
@@ -27,7 +27,7 @@ func CreateServerTLSConfiguration() *tls.Config {
// CreateTLSConfigurationFromBytes initializes a tls.Config using a CA certificate, a certificate and a key
// loaded from memory.
func CreateTLSConfigurationFromBytes(caCert, cert, key []byte, skipClientVerification, skipServerVerification bool) (*tls.Config, error) {
config := &tls.Config{}
config := CreateTLSConfiguration()
config.InsecureSkipVerify = skipServerVerification
if !skipClientVerification {
@@ -50,7 +50,7 @@ func CreateTLSConfigurationFromBytes(caCert, cert, key []byte, skipClientVerific
// CreateTLSConfigurationFromDisk initializes a tls.Config using a CA certificate, a certificate and a key
// loaded from disk.
func CreateTLSConfigurationFromDisk(caCertPath, certPath, keyPath string, skipServerVerification bool) (*tls.Config, error) {
config := &tls.Config{}
config := CreateTLSConfiguration()
config.InsecureSkipVerify = skipServerVerification
if certPath != "" && keyPath != "" {
@@ -63,7 +63,7 @@ func CreateTLSConfigurationFromDisk(caCertPath, certPath, keyPath string, skipSe
}
if !skipServerVerification && caCertPath != "" {
caCert, err := ioutil.ReadFile(caCertPath)
caCert, err := os.ReadFile(caCertPath)
if err != nil {
return nil, err
}

366
api/database/boltdb/db.go Normal file
View File

@@ -0,0 +1,366 @@
package boltdb
import (
"encoding/binary"
"errors"
"fmt"
"io"
"os"
"path"
"time"
portainer "github.com/portainer/portainer/api"
dserrors "github.com/portainer/portainer/api/dataservices/errors"
"github.com/rs/zerolog/log"
bolt "go.etcd.io/bbolt"
)
const (
DatabaseFileName = "portainer.db"
EncryptedDatabaseFileName = "portainer.edb"
)
var (
ErrHaveEncryptedAndUnencrypted = errors.New("Portainer has detected both an encrypted and un-encrypted database and cannot start. Only one database should exist")
ErrHaveEncryptedWithNoKey = errors.New("The portainer database is encrypted, but no secret was loaded")
)
type DbConnection struct {
Path string
MaxBatchSize int
MaxBatchDelay time.Duration
InitialMmapSize int
EncryptionKey []byte
isEncrypted bool
*bolt.DB
}
// GetDatabaseFileName get the database filename
func (connection *DbConnection) GetDatabaseFileName() string {
if connection.IsEncryptedStore() {
return EncryptedDatabaseFileName
}
return DatabaseFileName
}
// GetDataseFilePath get the path + filename for the database file
func (connection *DbConnection) GetDatabaseFilePath() string {
if connection.IsEncryptedStore() {
return path.Join(connection.Path, EncryptedDatabaseFileName)
}
return path.Join(connection.Path, DatabaseFileName)
}
// GetStorePath get the filename and path for the database file
func (connection *DbConnection) GetStorePath() string {
return connection.Path
}
func (connection *DbConnection) SetEncrypted(flag bool) {
connection.isEncrypted = flag
}
// Return true if the database is encrypted
func (connection *DbConnection) IsEncryptedStore() bool {
return connection.getEncryptionKey() != nil
}
// NeedsEncryptionMigration returns true if database encryption is enabled and
// we have an un-encrypted DB that requires migration to an encrypted DB
func (connection *DbConnection) NeedsEncryptionMigration() (bool, error) {
// Cases: Note, we need to check both portainer.db and portainer.edb
// to determine if it's a new store. We only need to differentiate between cases 2,3 and 5
// 1) portainer.edb + key => False
// 2) portainer.edb + no key => ERROR Fatal!
// 3) portainer.db + key => True (needs migration)
// 4) portainer.db + no key => False
// 5) NoDB (new) + key => False
// 6) NoDB (new) + no key => False
// 7) portainer.db & portainer.edb => ERROR Fatal!
// If we have a loaded encryption key, always set encrypted
if connection.EncryptionKey != nil {
connection.SetEncrypted(true)
}
// Check for portainer.db
dbFile := path.Join(connection.Path, DatabaseFileName)
_, err := os.Stat(dbFile)
haveDbFile := err == nil
// Check for portainer.edb
edbFile := path.Join(connection.Path, EncryptedDatabaseFileName)
_, err = os.Stat(edbFile)
haveEdbFile := err == nil
if haveDbFile && haveEdbFile {
// 7 - encrypted and unencrypted db?
return false, ErrHaveEncryptedAndUnencrypted
}
if haveDbFile && connection.EncryptionKey != nil {
// 3 - needs migration
return true, nil
}
if haveEdbFile && connection.EncryptionKey == nil {
// 2 - encrypted db, but no key?
return false, ErrHaveEncryptedWithNoKey
}
// 1, 4, 5, 6
return false, nil
}
// Open opens and initializes the BoltDB database.
func (connection *DbConnection) Open() error {
log.Info().Str("filename", connection.GetDatabaseFileName()).Msg("loading PortainerDB")
// Now we open the db
databasePath := connection.GetDatabaseFilePath()
db, err := bolt.Open(databasePath, 0600, &bolt.Options{
Timeout: 1 * time.Second,
InitialMmapSize: connection.InitialMmapSize,
})
if err != nil {
return err
}
db.MaxBatchSize = connection.MaxBatchSize
db.MaxBatchDelay = connection.MaxBatchDelay
connection.DB = db
return nil
}
// Close closes the BoltDB database.
// Safe to being called multiple times.
func (connection *DbConnection) Close() error {
if connection.DB != nil {
return connection.DB.Close()
}
return nil
}
func (connection *DbConnection) txFn(fn func(portainer.Transaction) error) func(*bolt.Tx) error {
return func(tx *bolt.Tx) error {
return fn(&DbTransaction{conn: connection, tx: tx})
}
}
// UpdateTx executes the given function inside a read-write transaction
func (connection *DbConnection) UpdateTx(fn func(portainer.Transaction) error) error {
if connection.MaxBatchDelay > 0 && connection.MaxBatchSize > 1 {
return connection.Batch(connection.txFn(fn))
}
return connection.Update(connection.txFn(fn))
}
// ViewTx executes the given function inside a read-only transaction
func (connection *DbConnection) ViewTx(fn func(portainer.Transaction) error) error {
return connection.View(connection.txFn(fn))
}
// BackupTo backs up db to a provided writer.
// It does hot backup and doesn't block other database reads and writes
func (connection *DbConnection) BackupTo(w io.Writer) error {
return connection.View(func(tx *bolt.Tx) error {
_, err := tx.WriteTo(w)
return err
})
}
func (connection *DbConnection) ExportRaw(filename string) error {
databasePath := connection.GetDatabaseFilePath()
if _, err := os.Stat(databasePath); err != nil {
return fmt.Errorf("stat on %s failed: %s", databasePath, err)
}
b, err := connection.ExportJSON(databasePath, true)
if err != nil {
return err
}
return os.WriteFile(filename, b, 0600)
}
// ConvertToKey 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 (connection *DbConnection) ConvertToKey(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 database.
func (connection *DbConnection) SetServiceName(bucketName string) error {
return connection.UpdateTx(func(tx portainer.Transaction) error {
return tx.SetServiceName(bucketName)
})
}
// GetObject is a generic function used to retrieve an unmarshalled object from a database.
func (connection *DbConnection) GetObject(bucketName string, key []byte, object interface{}) error {
return connection.ViewTx(func(tx portainer.Transaction) error {
return tx.GetObject(bucketName, key, object)
})
}
func (connection *DbConnection) getEncryptionKey() []byte {
if !connection.isEncrypted {
return nil
}
return connection.EncryptionKey
}
// UpdateObject is a generic function used to update an object inside a database.
func (connection *DbConnection) UpdateObject(bucketName string, key []byte, object interface{}) error {
return connection.UpdateTx(func(tx portainer.Transaction) error {
return tx.UpdateObject(bucketName, key, object)
})
}
// UpdateObjectFunc is a generic function used to update an object safely without race conditions.
func (connection *DbConnection) UpdateObjectFunc(bucketName string, key []byte, object any, updateFn func()) error {
return connection.Batch(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
data := bucket.Get(key)
if data == nil {
return dserrors.ErrObjectNotFound
}
err := connection.UnmarshalObjectWithJsoniter(data, object)
if err != nil {
return err
}
updateFn()
data, err = connection.MarshalObject(object)
if err != nil {
return err
}
return bucket.Put(key, data)
})
}
// DeleteObject is a generic function used to delete an object inside a database.
func (connection *DbConnection) DeleteObject(bucketName string, key []byte) error {
return connection.UpdateTx(func(tx portainer.Transaction) error {
return tx.DeleteObject(bucketName, key)
})
}
// DeleteAllObjects delete all objects where matching() returns (id, ok).
// TODO: think about how to return the error inside (maybe change ok to type err, and use "notfound"?
func (connection *DbConnection) DeleteAllObjects(bucketName string, obj interface{}, matching func(o interface{}) (id int, ok bool)) error {
return connection.UpdateTx(func(tx portainer.Transaction) error {
return tx.DeleteAllObjects(bucketName, obj, matching)
})
}
// GetNextIdentifier is a generic function that returns the specified bucket identifier incremented by 1.
func (connection *DbConnection) GetNextIdentifier(bucketName string) int {
var identifier int
_ = connection.UpdateTx(func(tx portainer.Transaction) error {
identifier = tx.GetNextIdentifier(bucketName)
return nil
})
return identifier
}
// CreateObject creates a new object in the bucket, using the next bucket sequence id
func (connection *DbConnection) CreateObject(bucketName string, fn func(uint64) (int, interface{})) error {
return connection.UpdateTx(func(tx portainer.Transaction) error {
return tx.CreateObject(bucketName, fn)
})
}
// CreateObjectWithId creates a new object in the bucket, using the specified id
func (connection *DbConnection) CreateObjectWithId(bucketName string, id int, obj interface{}) error {
return connection.UpdateTx(func(tx portainer.Transaction) error {
return tx.CreateObjectWithId(bucketName, id, obj)
})
}
// CreateObjectWithStringId creates a new object in the bucket, using the specified id
func (connection *DbConnection) CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error {
return connection.UpdateTx(func(tx portainer.Transaction) error {
return tx.CreateObjectWithStringId(bucketName, id, obj)
})
}
func (connection *DbConnection) GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error {
return connection.ViewTx(func(tx portainer.Transaction) error {
return tx.GetAll(bucketName, obj, append)
})
}
// TODO: decide which Unmarshal to use, and use one...
func (connection *DbConnection) GetAllWithJsoniter(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error {
return connection.ViewTx(func(tx portainer.Transaction) error {
return tx.GetAllWithJsoniter(bucketName, obj, append)
})
}
func (connection *DbConnection) GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj interface{}, append func(o interface{}) (interface{}, error)) error {
return connection.ViewTx(func(tx portainer.Transaction) error {
return tx.GetAllWithKeyPrefix(bucketName, keyPrefix, obj, append)
})
}
// BackupMetadata will return a copy of the boltdb sequence numbers for all buckets.
func (connection *DbConnection) BackupMetadata() (map[string]interface{}, error) {
buckets := map[string]interface{}{}
err := connection.View(func(tx *bolt.Tx) error {
err := tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
bucketName := string(name)
seqId := bucket.Sequence()
buckets[bucketName] = int(seqId)
return nil
})
return err
})
return buckets, err
}
// RestoreMetadata will restore the boltdb sequence numbers for all buckets.
func (connection *DbConnection) RestoreMetadata(s map[string]interface{}) error {
var err error
for bucketName, v := range s {
id, ok := v.(float64) // JSON ints are unmarshalled to interface as float64. See: https://pkg.go.dev/encoding/json#Decoder.Decode
if !ok {
log.Error().Str("bucket", bucketName).Msg("failed to restore metadata to bucket, skipped")
continue
}
err = connection.Batch(func(tx *bolt.Tx) error {
bucket, err := tx.CreateBucketIfNotExists([]byte(bucketName))
if err != nil {
return err
}
return bucket.SetSequence(uint64(id))
})
}
return err
}

View File

@@ -0,0 +1,124 @@
package boltdb
import (
"os"
"path"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_NeedsEncryptionMigration(t *testing.T) {
// Test the specific scenarios mentioned in NeedsEncryptionMigration
// i.e.
// Cases: Note, we need to check both portainer.db and portainer.edb
// to determine if it's a new store. We only need to differentiate between cases 2,3 and 5
// 1) portainer.edb + key => False
// 2) portainer.edb + no key => ERROR Fatal!
// 3) portainer.db + key => True (needs migration)
// 4) portainer.db + no key => False
// 5) NoDB (new) + key => False
// 6) NoDB (new) + no key => False
// 7) portainer.db & portainer.edb (key not important) => ERROR Fatal!
is := assert.New(t)
dir := t.TempDir()
cases := []struct {
name string
dbname string
key bool
expectError error
expectResult bool
}{
{
name: "portainer.edb + key",
dbname: EncryptedDatabaseFileName,
key: true,
expectError: nil,
expectResult: false,
},
{
name: "portainer.db + key (migration needed)",
dbname: DatabaseFileName,
key: true,
expectError: nil,
expectResult: true,
},
{
name: "portainer.db + no key",
dbname: DatabaseFileName,
key: false,
expectError: nil,
expectResult: false,
},
{
name: "NoDB (new) + key",
dbname: "",
key: false,
expectError: nil,
expectResult: false,
},
{
name: "NoDB (new) + no key",
dbname: "",
key: false,
expectError: nil,
expectResult: false,
},
// error tests
{
name: "portainer.edb + no key",
dbname: EncryptedDatabaseFileName,
key: false,
expectError: ErrHaveEncryptedWithNoKey,
expectResult: false,
},
{
name: "portainer.db & portainer.edb",
dbname: "both",
key: true,
expectError: ErrHaveEncryptedAndUnencrypted,
expectResult: false,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
connection := DbConnection{Path: dir}
if tc.dbname == "both" {
// Special case. If portainer.db and portainer.edb exist.
dbFile1 := path.Join(connection.Path, DatabaseFileName)
f, _ := os.Create(dbFile1)
f.Close()
defer os.Remove(dbFile1)
dbFile2 := path.Join(connection.Path, EncryptedDatabaseFileName)
f, _ = os.Create(dbFile2)
f.Close()
defer os.Remove(dbFile2)
} else if tc.dbname != "" {
dbFile := path.Join(connection.Path, tc.dbname)
f, _ := os.Create(dbFile)
f.Close()
defer os.Remove(dbFile)
}
if tc.key {
connection.EncryptionKey = []byte("secret")
}
result, err := connection.NeedsEncryptionMigration()
is.Equal(tc.expectError, err, "Fatal Error failure. Test: %s", tc.name)
is.Equal(result, tc.expectResult, "Failed test: %s", tc.name)
})
}
}

View File

@@ -0,0 +1,112 @@
package boltdb
import (
"encoding/json"
"time"
"github.com/rs/zerolog/log"
bolt "go.etcd.io/bbolt"
)
func backupMetadata(connection *bolt.DB) (map[string]interface{}, error) {
buckets := map[string]interface{}{}
err := connection.View(func(tx *bolt.Tx) error {
err := tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
bucketName := string(name)
seqId := bucket.Sequence()
buckets[bucketName] = int(seqId)
return nil
})
return err
})
return buckets, err
}
// ExportJSON creates a JSON representation from a DbConnection. You can include
// the database's metadata or ignore it. Ensure the database is closed before
// using this function.
// inspired by github.com/konoui/boltdb-exporter (which has no license)
// but very much simplified, based on how we use boltdb
func (c *DbConnection) ExportJSON(databasePath string, metadata bool) ([]byte, error) {
log.Debug().Str("databasePath", databasePath).Msg("exportJson")
connection, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second, ReadOnly: true})
if err != nil {
return []byte("{}"), err
}
defer connection.Close()
backup := make(map[string]interface{})
if metadata {
meta, err := backupMetadata(connection)
if err != nil {
log.Error().Err(err).Msg("failed exporting metadata")
}
backup["__metadata"] = meta
}
err = connection.View(func(tx *bolt.Tx) error {
err = tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
bucketName := string(name)
var list []interface{}
version := make(map[string]string)
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
if v == nil {
continue
}
var obj interface{}
err := c.UnmarshalObject(v, &obj)
if err != nil {
log.Error().
Str("bucket", bucketName).
Str("object", string(v)).
Err(err).
Msg("failed to unmarshal")
obj = v
}
if bucketName == "version" {
version[string(k)] = string(v)
} else {
list = append(list, obj)
}
}
if bucketName == "version" {
backup[bucketName] = version
return nil
}
if len(list) > 0 {
if bucketName == "ssl" ||
bucketName == "settings" ||
bucketName == "tunnel_server" {
backup[bucketName] = nil
if len(list) > 0 {
backup[bucketName] = list[0]
}
return nil
}
backup[bucketName] = list
return nil
}
return nil
})
return err
})
if err != nil {
return []byte("{}"), err
}
return json.MarshalIndent(backup, "", " ")
}

133
api/database/boltdb/json.go Normal file
View File

@@ -0,0 +1,133 @@
package boltdb
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/json"
"fmt"
"io"
jsoniter "github.com/json-iterator/go"
"github.com/pkg/errors"
)
var errEncryptedStringTooShort = fmt.Errorf("encrypted string too short")
// MarshalObject encodes an object to binary format
func (connection *DbConnection) MarshalObject(object interface{}) (data []byte, err error) {
// Special case for the VERSION bucket. Here we're not using json
if v, ok := object.(string); ok {
data = []byte(v)
} else {
data, err = json.Marshal(object)
if err != nil {
return data, err
}
}
if connection.getEncryptionKey() == nil {
return data, nil
}
return encrypt(data, connection.getEncryptionKey())
}
// UnmarshalObject decodes an object from binary data
func (connection *DbConnection) UnmarshalObject(data []byte, object interface{}) error {
var err error
if connection.getEncryptionKey() != nil {
data, err = decrypt(data, connection.getEncryptionKey())
if err != nil {
return errors.Wrap(err, "Failed decrypting object")
}
}
e := json.Unmarshal(data, object)
if e != nil {
// Special case for the VERSION bucket. Here we're not using json
// So we need to return it as a string
s, ok := object.(*string)
if !ok {
return errors.Wrap(err, e.Error())
}
*s = string(data)
}
return err
}
// UnmarshalObjectWithJsoniter decodes an object from binary data
// using the jsoniter library. It is mainly used to accelerate environment(endpoint)
// decoding at the moment.
func (connection *DbConnection) UnmarshalObjectWithJsoniter(data []byte, object interface{}) error {
if connection.getEncryptionKey() != nil {
var err error
data, err = decrypt(data, connection.getEncryptionKey())
if err != nil {
return err
}
}
var jsoni = jsoniter.ConfigCompatibleWithStandardLibrary
err := jsoni.Unmarshal(data, &object)
if err != nil {
if s, ok := object.(*string); ok {
*s = string(data)
return nil
}
return err
}
return nil
}
// mmm, don't have a KMS .... aes GCM seems the most likely from
// https://gist.github.com/atoponce/07d8d4c833873be2f68c34f9afc5a78a#symmetric-encryption
func encrypt(plaintext []byte, passphrase []byte) (encrypted []byte, err error) {
block, _ := aes.NewCipher(passphrase)
gcm, err := cipher.NewGCM(block)
if err != nil {
return encrypted, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return encrypted, err
}
ciphertextByte := gcm.Seal(
nonce,
nonce,
plaintext,
nil)
return ciphertextByte, nil
}
func decrypt(encrypted []byte, passphrase []byte) (plaintextByte []byte, err error) {
if string(encrypted) == "false" {
return []byte("false"), nil
}
block, err := aes.NewCipher(passphrase)
if err != nil {
return encrypted, errors.Wrap(err, "Error creating cypher block")
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return encrypted, errors.Wrap(err, "Error creating GCM")
}
nonceSize := gcm.NonceSize()
if len(encrypted) < nonceSize {
return encrypted, errEncryptedStringTooShort
}
nonce, ciphertextByteClean := encrypted[:nonceSize], encrypted[nonceSize:]
plaintextByte, err = gcm.Open(
nil,
nonce,
ciphertextByteClean,
nil)
if err != nil {
return encrypted, errors.Wrap(err, "Error decrypting text")
}
return plaintextByte, err
}

View File

@@ -0,0 +1,177 @@
package boltdb
import (
"crypto/sha256"
"fmt"
"testing"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
)
const (
jsonobject = `{"LogoURL":"","BlackListedLabels":[],"AuthenticationMethod":1,"InternalAuthSettings": {"RequiredPasswordLength": 12}"LDAPSettings":{"AnonymousMode":true,"ReaderDN":"","URL":"","TLSConfig":{"TLS":false,"TLSSkipVerify":false},"StartTLS":false,"SearchSettings":[{"BaseDN":"","Filter":"","UserNameAttribute":""}],"GroupSearchSettings":[{"GroupBaseDN":"","GroupFilter":"","GroupAttribute":""}],"AutoCreateUsers":true},"OAuthSettings":{"ClientID":"","AccessTokenURI":"","AuthorizationURI":"","ResourceURI":"","RedirectURI":"","UserIdentifier":"","Scopes":"","OAuthAutoCreateUsers":false,"DefaultTeamID":0,"SSO":true,"LogoutURI":"","KubeSecretKey":"j0zLVtY/lAWBk62ByyF0uP80SOXaitsABP0TTJX8MhI="},"OpenAMTConfiguration":{"Enabled":false,"MPSServer":"","MPSUser":"","MPSPassword":"","MPSToken":"","CertFileContent":"","CertFileName":"","CertFilePassword":"","DomainName":""},"FeatureFlagSettings":{},"SnapshotInterval":"5m","TemplatesURL":"https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json","EdgeAgentCheckinInterval":5,"EnableEdgeComputeFeatures":false,"UserSessionTimeout":"8h","KubeconfigExpiry":"0","EnableTelemetry":true,"HelmRepositoryURL":"https://charts.bitnami.com/bitnami","KubectlShellImage":"portainer/kubectl-shell","DisplayDonationHeader":false,"DisplayExternalContributors":false,"EnableHostManagementFeatures":false,"AllowVolumeBrowserForRegularUsers":false,"AllowBindMountsForRegularUsers":false,"AllowPrivilegedModeForRegularUsers":false,"AllowHostNamespaceForRegularUsers":false,"AllowStackManagementForRegularUsers":false,"AllowDeviceMappingForRegularUsers":false,"AllowContainerCapabilitiesForRegularUsers":false}`
passphrase = "my secret key"
)
func secretToEncryptionKey(passphrase string) []byte {
hash := sha256.Sum256([]byte(passphrase))
return hash[:]
}
func Test_MarshalObjectUnencrypted(t *testing.T) {
is := assert.New(t)
uuid := uuid.Must(uuid.NewV4())
tests := []struct {
object interface{}
expected string
}{
{
object: nil,
expected: `null`,
},
{
object: true,
expected: `true`,
},
{
object: false,
expected: `false`,
},
{
object: 123,
expected: `123`,
},
{
object: "456",
expected: "456",
},
{
object: uuid,
expected: "\"" + uuid.String() + "\"",
},
{
object: uuid.String(),
expected: uuid.String(),
},
{
object: map[string]interface{}{"key": "value"},
expected: `{"key":"value"}`,
},
{
object: []bool{true, false},
expected: `[true,false]`,
},
{
object: []int{1, 2, 3},
expected: `[1,2,3]`,
},
{
object: []string{"1", "2", "3"},
expected: `["1","2","3"]`,
},
{
object: []map[string]interface{}{{"key1": "value1"}, {"key2": "value2"}},
expected: `[{"key1":"value1"},{"key2":"value2"}]`,
},
{
object: []interface{}{1, "2", false, map[string]interface{}{"key1": "value1"}},
expected: `[1,"2",false,{"key1":"value1"}]`,
},
}
conn := DbConnection{}
for _, test := range tests {
t.Run(fmt.Sprintf("%s -> %s", test.object, test.expected), func(t *testing.T) {
data, err := conn.MarshalObject(test.object)
is.NoError(err)
is.Equal(test.expected, string(data))
})
}
}
func Test_UnMarshalObjectUnencrypted(t *testing.T) {
is := assert.New(t)
// Based on actual data entering and what we expect out of the function
tests := []struct {
object []byte
expected string
}{
{
object: []byte(""),
expected: "",
},
{
object: []byte("35"),
expected: "35",
},
{
// An unmarshalled byte string should return the same without error
object: []byte("9ca4a1dd-a439-4593-b386-a7dfdc2e9fc6"),
expected: "9ca4a1dd-a439-4593-b386-a7dfdc2e9fc6",
},
{
// An un-marshalled json object string should return the same as a string without error also
object: []byte(jsonobject),
expected: jsonobject,
},
}
conn := DbConnection{}
for _, test := range tests {
t.Run(fmt.Sprintf("%s -> %s", test.object, test.expected), func(t *testing.T) {
var object string
err := conn.UnmarshalObject(test.object, &object)
is.NoError(err)
is.Equal(test.expected, string(object))
})
}
}
func Test_ObjectMarshallingEncrypted(t *testing.T) {
is := assert.New(t)
// Based on actual data entering and what we expect out of the function
tests := []struct {
object []byte
expected string
}{
{
object: []byte(""),
},
{
object: []byte("35"),
},
{
// An unmarshalled byte string should return the same without error
object: []byte("9ca4a1dd-a439-4593-b386-a7dfdc2e9fc6"),
},
{
// An un-marshalled json object string should return the same as a string without error also
object: []byte(jsonobject),
},
}
key := secretToEncryptionKey(passphrase)
conn := DbConnection{EncryptionKey: key}
for _, test := range tests {
t.Run(fmt.Sprintf("%s -> %s", test.object, test.expected), func(t *testing.T) {
data, err := conn.MarshalObject(test.object)
is.NoError(err)
var object []byte
err = conn.UnmarshalObject(data, &object)
is.NoError(err)
is.Equal(test.object, object)
})
}
}

172
api/database/boltdb/tx.go Normal file
View File

@@ -0,0 +1,172 @@
package boltdb
import (
"bytes"
dserrors "github.com/portainer/portainer/api/dataservices/errors"
"github.com/rs/zerolog/log"
bolt "go.etcd.io/bbolt"
)
type DbTransaction struct {
conn *DbConnection
tx *bolt.Tx
}
func (tx *DbTransaction) SetServiceName(bucketName string) error {
_, err := tx.tx.CreateBucketIfNotExists([]byte(bucketName))
return err
}
func (tx *DbTransaction) GetObject(bucketName string, key []byte, object interface{}) error {
bucket := tx.tx.Bucket([]byte(bucketName))
value := bucket.Get(key)
if value == nil {
return dserrors.ErrObjectNotFound
}
data := make([]byte, len(value))
copy(data, value)
return tx.conn.UnmarshalObjectWithJsoniter(data, object)
}
func (tx *DbTransaction) UpdateObject(bucketName string, key []byte, object interface{}) error {
data, err := tx.conn.MarshalObject(object)
if err != nil {
return err
}
bucket := tx.tx.Bucket([]byte(bucketName))
return bucket.Put(key, data)
}
func (tx *DbTransaction) DeleteObject(bucketName string, key []byte) error {
bucket := tx.tx.Bucket([]byte(bucketName))
return bucket.Delete(key)
}
func (tx *DbTransaction) DeleteAllObjects(bucketName string, obj interface{}, matching func(o interface{}) (id int, ok bool)) error {
bucket := tx.tx.Bucket([]byte(bucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
err := tx.conn.UnmarshalObject(v, &obj)
if err != nil {
return err
}
if id, ok := matching(obj); ok {
err := bucket.Delete(tx.conn.ConvertToKey(id))
if err != nil {
return err
}
}
}
return nil
}
func (tx *DbTransaction) GetNextIdentifier(bucketName string) int {
bucket := tx.tx.Bucket([]byte(bucketName))
id, err := bucket.NextSequence()
if err != nil {
log.Error().Err(err).Str("bucket", bucketName).Msg("failed to get the next identifer")
return 0
}
return int(id)
}
func (tx *DbTransaction) CreateObject(bucketName string, fn func(uint64) (int, interface{})) error {
bucket := tx.tx.Bucket([]byte(bucketName))
seqId, _ := bucket.NextSequence()
id, obj := fn(seqId)
data, err := tx.conn.MarshalObject(obj)
if err != nil {
return err
}
return bucket.Put(tx.conn.ConvertToKey(int(id)), data)
}
func (tx *DbTransaction) CreateObjectWithId(bucketName string, id int, obj interface{}) error {
bucket := tx.tx.Bucket([]byte(bucketName))
data, err := tx.conn.MarshalObject(obj)
if err != nil {
return err
}
return bucket.Put(tx.conn.ConvertToKey(id), data)
}
func (tx *DbTransaction) CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error {
bucket := tx.tx.Bucket([]byte(bucketName))
data, err := tx.conn.MarshalObject(obj)
if err != nil {
return err
}
return bucket.Put(id, data)
}
func (tx *DbTransaction) GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error {
bucket := tx.tx.Bucket([]byte(bucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
err := tx.conn.UnmarshalObject(v, obj)
if err != nil {
return err
}
obj, err = append(obj)
if err != nil {
return err
}
}
return nil
}
func (tx *DbTransaction) GetAllWithJsoniter(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error {
bucket := tx.tx.Bucket([]byte(bucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
err := tx.conn.UnmarshalObjectWithJsoniter(v, obj)
if err != nil {
return err
}
obj, err = append(obj)
if err != nil {
return err
}
}
return nil
}
func (tx *DbTransaction) GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj interface{}, append func(o interface{}) (interface{}, error)) error {
cursor := tx.tx.Bucket([]byte(bucketName)).Cursor()
for k, v := cursor.Seek(keyPrefix); k != nil && bytes.HasPrefix(k, keyPrefix); k, v = cursor.Next() {
err := tx.conn.UnmarshalObjectWithJsoniter(v, obj)
if err != nil {
return err
}
obj, err = append(obj)
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,126 @@
package boltdb
import (
"errors"
"testing"
portainer "github.com/portainer/portainer/api"
dserrors "github.com/portainer/portainer/api/dataservices/errors"
)
const testBucketName = "test-bucket"
const testId = 1234
type testStruct struct {
Key string
Value string
}
func TestTxs(t *testing.T) {
conn := DbConnection{
Path: t.TempDir(),
}
err := conn.Open()
if err != nil {
t.Fatal(err)
}
defer conn.Close()
// Error propagation
err = conn.UpdateTx(func(tx portainer.Transaction) error {
return errors.New("this is an error")
})
if err == nil {
t.Fatal("an error was expected, got nil instead")
}
// Create an object
newObj := testStruct{
Key: "key",
Value: "value",
}
err = conn.UpdateTx(func(tx portainer.Transaction) error {
err = tx.SetServiceName(testBucketName)
if err != nil {
return err
}
return tx.CreateObjectWithId(testBucketName, testId, newObj)
})
if err != nil {
t.Fatal(err)
}
obj := testStruct{}
err = conn.ViewTx(func(tx portainer.Transaction) error {
return tx.GetObject(testBucketName, conn.ConvertToKey(testId), &obj)
})
if err != nil {
t.Fatal(err)
}
if obj.Key != newObj.Key || obj.Value != newObj.Value {
t.Fatalf("expected %s:%s, got %s:%s instead", newObj.Key, newObj.Value, obj.Key, obj.Value)
}
// Update an object
updatedObj := testStruct{
Key: "updated-key",
Value: "updated-value",
}
err = conn.UpdateTx(func(tx portainer.Transaction) error {
return tx.UpdateObject(testBucketName, conn.ConvertToKey(testId), &updatedObj)
})
err = conn.ViewTx(func(tx portainer.Transaction) error {
return tx.GetObject(testBucketName, conn.ConvertToKey(testId), &obj)
})
if err != nil {
t.Fatal(err)
}
if obj.Key != updatedObj.Key || obj.Value != updatedObj.Value {
t.Fatalf("expected %s:%s, got %s:%s instead", updatedObj.Key, updatedObj.Value, obj.Key, obj.Value)
}
// Delete an object
err = conn.UpdateTx(func(tx portainer.Transaction) error {
return tx.DeleteObject(testBucketName, conn.ConvertToKey(testId))
})
if err != nil {
t.Fatal(err)
}
err = conn.ViewTx(func(tx portainer.Transaction) error {
return tx.GetObject(testBucketName, conn.ConvertToKey(testId), &obj)
})
if err != dserrors.ErrObjectNotFound {
t.Fatal(err)
}
// Get next identifier
err = conn.UpdateTx(func(tx portainer.Transaction) error {
id1 := tx.GetNextIdentifier(testBucketName)
id2 := tx.GetNextIdentifier(testBucketName)
if id1+1 != id2 {
return errors.New("unexpected identifier sequence")
}
return nil
})
if err != nil {
t.Fatal(err)
}
// Try to write in a read transaction
err = conn.ViewTx(func(tx portainer.Transaction) error {
return tx.CreateObjectWithId(testBucketName, testId, newObj)
})
if err == nil {
t.Fatal("an error was expected, got nil instead")
}
}

View File

@@ -4,14 +4,14 @@ import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/sqlite"
"github.com/portainer/portainer/api/database/boltdb"
)
// NewDatabase should use config options to return a connection to the requested database
func NewDatabase(storeType, storePath string, encryptionKey []byte) (connection portainer.Connection, err error) {
switch storeType {
case "sqlite":
return &sqlite.DbConnection{
case "boltdb":
return &boltdb.DbConnection{
Path: storePath,
EncryptionKey: encryptionKey,
}, nil

View File

@@ -1,9 +0,0 @@
package models
type (
// AccessPolicy represent a policy that can be associated to a user or team
AccessPolicy struct {
// Role identifier. Reference the role that will be associated to this access policy
RoleID RoleID `json:"RoleId" example:"1"`
}
)

View File

@@ -1,9 +0,0 @@
package models
type (
// Authorization represents an authorization associated to an operation
Authorization string
// Authorizations represents a set of authorizations associated to a role
Authorizations map[Authorization]bool
)

View File

@@ -1,21 +0,0 @@
package models
type (
FDOConfiguration struct {
Enabled bool `json:"enabled"`
OwnerURL string `json:"ownerURL"`
OwnerUsername string `json:"ownerUsername"`
OwnerPassword string `json:"ownerPassword"`
}
// FDOProfileID represents a fdo profile id
FDOProfileID int
FDOProfile struct {
ID FDOProfileID `json:"id"`
Name string `json:"name"`
FilePath string `json:"filePath"`
NumberDevices int `json:"numberDevices"`
DateCreated int64 `json:"dateCreated"`
}
)

View File

@@ -1,48 +0,0 @@
package models
type (
// LDAPSettings represents the settings used to connect to a LDAP server
LDAPSettings struct {
// Enable this option if the server is configured for Anonymous access. When enabled, ReaderDN and Password will not be used
AnonymousMode bool `json:"AnonymousMode" example:"true" validate:"validate_bool"`
// Account that will be used to search for users
ReaderDN string `json:"ReaderDN" example:"cn=readonly-account,dc=ldap,dc=domain,dc=tld" validate:"required_if=AnonymousMode false"`
// Password of the account that will be used to search users
Password string `json:"Password,omitempty" example:"readonly-password" validate:"required_if=AnonymousMode false"`
// URL or IP address of the LDAP server
URL string `json:"URL" example:"myldap.domain.tld:389" validate:"hostname_port"`
TLSConfig TLSConfiguration `json:"TLSConfig"`
// Whether LDAP connection should use StartTLS
StartTLS bool `json:"StartTLS" example:"true"`
SearchSettings []LDAPSearchSettings `json:"SearchSettings"`
GroupSearchSettings []LDAPGroupSearchSettings `json:"GroupSearchSettings"`
// Automatically provision users and assign them to matching LDAP group names
AutoCreateUsers bool `json:"AutoCreateUsers" example:"true"`
}
// LDAPUser represents a LDAP user
LDAPUser struct {
Name string
Groups []string
}
// LDAPGroupSearchSettings represents settings used to search for groups in a LDAP server
LDAPGroupSearchSettings struct {
// The distinguished name of the element from which the LDAP server will search for groups
GroupBaseDN string `json:"GroupBaseDN" example:"dc=ldap,dc=domain,dc=tld"`
// The LDAP search filter used to select group elements, optional
GroupFilter string `json:"GroupFilter" example:"(objectClass=account"`
// LDAP attribute which denotes the group membership
GroupAttribute string `json:"GroupAttribute" example:"member"`
}
// LDAPSearchSettings represents settings used to search for users in a LDAP server
LDAPSearchSettings struct {
// The distinguished name of the element from which the LDAP server will search for users
BaseDN string `json:"BaseDN" example:"dc=ldap,dc=domain,dc=tld"`
// Optional LDAP search filter used to select user elements
Filter string `json:"Filter" example:"(objectClass=account)"`
// LDAP attribute which denotes the username
UserNameAttribute string `json:"UserNameAttribute" example:"uid"`
}
)

View File

@@ -1,6 +0,0 @@
package models
type (
// MembershipRole represents the role of a user within a team
MembershipRole int
)

View File

@@ -1,37 +0,0 @@
package models
type (
// PowerState represents an AMT managed device power state
PowerState int
// OpenAMTConfiguration represents the credentials and configurations used to connect to an OpenAMT MPS server
OpenAMTConfiguration struct {
Enabled bool `json:"enabled"`
MPSServer string `json:"mpsServer"`
MPSUser string `json:"mpsUser"`
MPSPassword string `json:"mpsPassword"`
MPSToken string `json:"mpsToken"` // retrieved from API
CertFileName string `json:"certFileName"`
CertFileContent string `json:"certFileContent"`
CertFilePassword string `json:"certFilePassword"`
DomainName string `json:"domainName"`
}
// OpenAMTDeviceInformation represents an AMT managed device information
OpenAMTDeviceInformation struct {
GUID string `json:"guid"`
HostName string `json:"hostname"`
ConnectionStatus bool `json:"connectionStatus"`
PowerState PowerState `json:"powerState"`
EnabledFeatures *OpenAMTDeviceEnabledFeatures `json:"features"`
}
// OpenAMTDeviceEnabledFeatures represents an AMT managed device features information
OpenAMTDeviceEnabledFeatures struct {
Redirection bool `json:"redirection"`
KVM bool `json:"KVM"`
SOL bool `json:"SOL"`
IDER bool `json:"IDER"`
UserConsent string `json:"userConsent"`
}
)

View File

@@ -1,6 +0,0 @@
package models
type (
// ResourceAccessLevel represents the level of control associated to a resource
ResourceAccessLevel int
)

View File

@@ -1,20 +0,0 @@
package models
type (
// Role represents a set of authorizations that can be associated to a user or
// to a team.
Role struct {
// Role Identifier
ID RoleID `json:"Id" example:"1"`
// Role name
Name string `json:"Name" example:"HelpDesk"`
// Role description
Description string `json:"Description" example:"Read-only access of all resources in an environment(endpoint)"`
// Authorizations associated to a role
Authorizations Authorizations `json:"Authorizations"`
Priority int `json:"Priority"`
}
// RoleID represents a role identifier
RoleID int
)

View File

@@ -1,118 +0,0 @@
package models
const (
_ AuthenticationMethod = iota
// AuthenticationInternal represents the internal authentication method (authentication against Portainer API)
AuthenticationInternal
// AuthenticationLDAP represents the LDAP authentication method (authentication against a LDAP server)
AuthenticationLDAP
//AuthenticationOAuth represents the OAuth authentication method (authentication against a authorization server)
AuthenticationOAuth
)
type (
// AuthenticationMethod represents the authentication method used to authenticate a user
AuthenticationMethod int
// InternalAuthSettings represents settings used for the default 'internal' authentication
InternalAuthSettings struct {
RequiredPasswordLength int
}
// OAuthSettings represents the settings used to authorize with an authorization server
OAuthSettings struct {
ClientID string `json:"ClientID"`
ClientSecret string `json:"ClientSecret,omitempty"`
AccessTokenURI string `json:"AccessTokenURI"`
AuthorizationURI string `json:"AuthorizationURI"`
ResourceURI string `json:"ResourceURI"`
RedirectURI string `json:"RedirectURI"`
UserIdentifier string `json:"UserIdentifier"`
Scopes string `json:"Scopes"`
OAuthAutoCreateUsers bool `json:"OAuthAutoCreateUsers"`
DefaultTeamID TeamID `json:"DefaultTeamID"`
SSO bool `json:"SSO"`
LogoutURI string `json:"LogoutURI"`
KubeSecretKey []byte `json:"KubeSecretKey"`
}
// Settings represents the application settings
Settings struct {
// URL to a logo that will be displayed on the login page as well as on top of the sidebar. Will use default Portainer logo when value is empty string
LogoURL string `json:"LogoURL" example:"https://mycompany.mydomain.tld/logo.png"`
// A list of label name & value that will be used to hide containers when querying containers
BlackListedLabels []Pair `json:"BlackListedLabels"`
// Active authentication method for the Portainer instance. Valid values are: 1 for internal, 2 for LDAP, or 3 for oauth
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod" example:"1"`
InternalAuthSettings InternalAuthSettings `json:"InternalAuthSettings" example:""`
LDAPSettings LDAPSettings `json:"LDAPSettings" example:""`
OAuthSettings OAuthSettings `json:"OAuthSettings" example:""`
OpenAMTConfiguration OpenAMTConfiguration `json:"openAMTConfiguration" example:""`
FDOConfiguration FDOConfiguration `json:"fdoConfiguration" example:""`
FeatureFlagSettings map[Feature]bool `json:"FeatureFlagSettings" example:""`
// The interval in which environment(endpoint) snapshots are created
SnapshotInterval string `json:"SnapshotInterval" example:"5m"`
// URL to the templates that will be displayed in the UI when navigating to App Templates
TemplatesURL string `json:"TemplatesURL" example:"https://raw.githubusercontent.com/portainer/templates/master/templates.json"`
// The default check in interval for edge agent (in seconds)
EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval" example:"5"`
// Whether edge compute features are enabled
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures" example:""`
// The duration of a user session
UserSessionTimeout string `json:"UserSessionTimeout" example:"5m"`
// The expiry of a Kubeconfig
KubeconfigExpiry string `json:"KubeconfigExpiry" example:"24h"`
// Whether telemetry is enabled
EnableTelemetry bool `json:"EnableTelemetry" example:"false"`
// Helm repository URL, defaults to "https://charts.bitnami.com/bitnami"
HelmRepositoryURL string `json:"HelmRepositoryURL" example:"https://charts.bitnami.com/bitnami"`
// KubectlImage, defaults to portainer/kubectl-shell
KubectlShellImage string `json:"KubectlShellImage" example:"portainer/kubectl-shell"`
// TrustOnFirstConnect makes Portainer accepting edge agent connection by default
TrustOnFirstConnect bool `json:"TrustOnFirstConnect" example:"false"`
// EnforceEdgeID makes Portainer store the Edge ID instead of accepting anyone
EnforceEdgeID bool `json:"EnforceEdgeID" example:"false"`
// Container environment parameter AGENT_SECRET
AgentSecret string `json:"AgentSecret"`
// EdgePortainerURL is the URL that is exposed to edge agents
EdgePortainerURL string `json:"EdgePortainerUrl"`
Edge struct {
// The command list interval for edge agent - used in edge async mode (in seconds)
CommandInterval int `json:"CommandInterval" example:"5"`
// The ping interval for edge agent - used in edge async mode (in seconds)
PingInterval int `json:"PingInterval" example:"5"`
// The snapshot interval for edge agent - used in edge async mode (in seconds)
SnapshotInterval int `json:"SnapshotInterval" example:"5"`
// EdgeAsyncMode enables edge async mode by default
AsyncMode bool
}
// Deprecated fields
DisplayDonationHeader bool
DisplayExternalContributors bool
// Deprecated fields v26
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"`
AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"`
AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"`
AllowContainerCapabilitiesForRegularUsers bool `json:"AllowContainerCapabilitiesForRegularUsers"`
}
/**
extras
*/
// Pair defines a key/value string pair
Pair struct {
Name string `json:"name" example:"name"`
Value string `json:"value" example:"value"`
}
// Feature represents a feature that can be enabled or disabled via feature flags
Feature string
)

View File

@@ -1,38 +0,0 @@
package models
type (
// Team represents a list of user accounts
Team struct {
// Team Identifier
ID TeamID `json:"Id" example:"1"`
// Team name
Name string `json:"Name" example:"developers"`
}
// TeamAccessPolicies represent the association of an access policy and a team
TeamAccessPolicies map[TeamID]AccessPolicy
// TeamID represents a team identifier
TeamID int
// TeamMembership represents a membership association between a user and a team
TeamMembership struct {
// Membership Identifier
ID TeamMembershipID `json:"Id" example:"1"`
// User identifier
UserID UserID `json:"UserID" example:"1"`
// Team identifier
TeamID TeamID `json:"TeamID" example:"1"`
// Team role (1 for team leader and 2 for team member)
Role MembershipRole `json:"Role" example:"1"`
}
// TeamMembershipID represents a team membership identifier
TeamMembershipID int
// TeamResourceAccess represents the level of control on a resource for a specific team
TeamResourceAccess struct {
TeamID TeamID `json:"TeamId"`
AccessLevel ResourceAccessLevel `json:"AccessLevel"`
}
)

View File

@@ -1,17 +0,0 @@
package models
type (
// TLSConfiguration represents a TLS configuration
TLSConfiguration struct {
// Use TLS
TLS bool `json:"TLS" example:"true"`
// Skip the verification of the server TLS certificate
TLSSkipVerify bool `json:"TLSSkipVerify" example:"false"`
// Path to the TLS CA certificate file
TLSCACertPath string `json:"TLSCACert,omitempty" example:"/data/tls/ca.pem"`
// Path to the TLS client certificate file
TLSCertPath string `json:"TLSCert,omitempty" example:"/data/tls/cert.pem"`
// Path to the TLS client key file
TLSKeyPath string `json:"TLSKey,omitempty" example:"/data/tls/key.pem"`
}
)

View File

@@ -1,6 +0,0 @@
package models
type (
// UserID represents a user identifier
UserID int
)

View File

@@ -1,19 +1,8 @@
package models
import "net/http"
const (
VersionKey string = "DB_VERSION"
InstanceKey string = "INSTANCE_ID"
EditionKey string = "EDITION"
UpdatingKey string = "DB_UPDATING"
)
type Version struct {
Key string `json:"Key"`
Value string `json:"Value"`
}
func (r *Version) Validate(request *http.Request) error {
return nil
SchemaVersion string
MigratorCount int
Edition int
InstanceID string
}

View File

@@ -1,167 +0,0 @@
package sqlite
import (
"errors"
"os"
"path"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"github.com/portainer/portainer/api/database/models"
"github.com/rs/zerolog/log"
)
const (
DatabaseFileName = "portainer.db"
EncryptedDatabaseFileName = "portainer.edb"
)
var (
ErrHaveEncryptedAndUnencrypted = errors.New("Portainer has detected both an encrypted and un-encrypted database and cannot start. Only one database should exist")
ErrHaveEncryptedWithNoKey = errors.New("The portainer database is encrypted, but no secret was loaded")
)
type DbConnection struct {
Path string
EncryptionKey []byte
isEncrypted bool
*gorm.DB
}
func (connection *DbConnection) GetDB() *gorm.DB {
if connection.DB == nil {
err := connection.Open()
if err != nil {
panic(err)
}
}
return connection.DB
}
// GetDatabaseFileName get the database filename
func (connection *DbConnection) GetDatabaseFileName() string {
if connection.IsEncryptedStore() {
return EncryptedDatabaseFileName
}
return DatabaseFileName
}
// GetDataseFilePath get the path + filename for the database file
func (connection *DbConnection) GetDatabaseFilePath() string {
if connection.IsEncryptedStore() {
return path.Join(connection.Path, EncryptedDatabaseFileName)
}
return path.Join(connection.Path, DatabaseFileName)
}
// GetStorePath get the filename and path for the database file
func (connection *DbConnection) GetStorePath() string {
return connection.Path
}
func (connection *DbConnection) SetEncrypted(flag bool) {
connection.isEncrypted = flag
}
// Return true if the database is encrypted
func (connection *DbConnection) IsEncryptedStore() bool {
return connection.getEncryptionKey() != nil
}
// NeedsEncryptionMigration returns true if database encryption is enabled and
// we have an un-encrypted DB that requires migration to an encrypted DB
func (connection *DbConnection) NeedsEncryptionMigration() (bool, error) {
// Cases: Note, we need to check both portainer.db and portainer.edb
// to determine if it's a new store. We only need to differentiate between cases 2,3 and 5
// 1) portainer.edb + key => False
// 2) portainer.edb + no key => ERROR Fatal!
// 3) portainer.db + key => True (needs migration)
// 4) portainer.db + no key => False
// 5) NoDB (new) + key => False
// 6) NoDB (new) + no key => False
// 7) portainer.db & portainer.edb => ERROR Fatal!
// If we have a loaded encryption key, always set encrypted
if connection.EncryptionKey != nil {
connection.SetEncrypted(true)
}
// Check for portainer.db
dbFile := path.Join(connection.Path, DatabaseFileName)
_, err := os.Stat(dbFile)
haveDbFile := err == nil
// Check for portainer.edb
edbFile := path.Join(connection.Path, EncryptedDatabaseFileName)
_, err = os.Stat(edbFile)
haveEdbFile := err == nil
if haveDbFile && haveEdbFile {
// 7 - encrypted and unencrypted db?
return false, ErrHaveEncryptedAndUnencrypted
}
if haveDbFile && connection.EncryptionKey != nil {
// 3 - needs migration
return true, nil
}
if haveEdbFile && connection.EncryptionKey == nil {
// 2 - encrypted db, but no key?
return false, ErrHaveEncryptedWithNoKey
}
// 1, 4, 5, 6
return false, nil
}
// Open opens and initializes the BoltDB database.
func (connection *DbConnection) Open() error {
log.Info().Str("filename", connection.GetDatabaseFileName()).Msg("loading PortainerDB")
// Now we open the db
databasePath := connection.GetDatabaseFilePath()
db, err := gorm.Open(sqlite.Open(databasePath), &gorm.Config{})
if err != nil {
return err
}
sqlDB, err := db.DB()
if err != nil {
return err
}
sqlDB.SetMaxOpenConns(5)
sqlDB.SetMaxOpenConns(10)
connection.DB = db
return nil
}
func (connection *DbConnection) Close() error {
sqlDB, err := connection.DB.DB()
if err != nil {
return err
}
connection.DB = nil
return sqlDB.Close()
}
func (connection *DbConnection) getEncryptionKey() []byte {
if !connection.isEncrypted {
return nil
}
return connection.EncryptionKey
}
func (connection *DbConnection) Init() error {
connection.DB.AutoMigrate(&models.Version{})
return nil
}

View File

@@ -1,7 +1,13 @@
package apikeyrepository
import (
"bytes"
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/rs/zerolog/log"
)
const (
@@ -16,10 +22,10 @@ type Service struct {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
// err := connection.SetServiceName(BucketName)
// if err != nil {
// return nil, err
// }
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
@@ -30,93 +36,90 @@ func NewService(connection portainer.Connection) (*Service, error) {
func (service *Service) GetAPIKeysByUserID(userID portainer.UserID) ([]portainer.APIKey, error) {
var result = make([]portainer.APIKey, 0)
// err := service.connection.GetAll(
// BucketName,
// &portainer.APIKey{},
// func(obj interface{}) (interface{}, error) {
// record, ok := obj.(*portainer.APIKey)
// if !ok {
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to APIKey object")
// return nil, fmt.Errorf("Failed to convert to APIKey object: %s", obj)
// }
err := service.connection.GetAll(
BucketName,
&portainer.APIKey{},
func(obj interface{}) (interface{}, error) {
record, ok := obj.(*portainer.APIKey)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to APIKey object")
return nil, fmt.Errorf("Failed to convert to APIKey object: %s", obj)
}
// if record.UserID == userID {
// result = append(result, *record)
// }
if record.UserID == userID {
result = append(result, *record)
}
// return &portainer.APIKey{}, nil
// })
return &portainer.APIKey{}, nil
})
return result, nil
return result, err
}
// GetAPIKeyByDigest returns the API key for the associated digest.
// Note: there is a 1-to-1 mapping of api-key and digest
func (service *Service) GetAPIKeyByDigest(digest []byte) (*portainer.APIKey, error) {
// var k *portainer.APIKey
// stop := fmt.Errorf("ok")
// err := service.connection.GetAll(
// BucketName,
// &portainer.APIKey{},
// func(obj interface{}) (interface{}, error) {
// key, ok := obj.(*portainer.APIKey)
// if !ok {
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to APIKey object")
// return nil, fmt.Errorf("Failed to convert to APIKey object: %s", obj)
// }
// if bytes.Equal(key.Digest, digest) {
// k = key
// return nil, stop
// }
var k *portainer.APIKey
stop := fmt.Errorf("ok")
err := service.connection.GetAll(
BucketName,
&portainer.APIKey{},
func(obj interface{}) (interface{}, error) {
key, ok := obj.(*portainer.APIKey)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to APIKey object")
return nil, fmt.Errorf("Failed to convert to APIKey object: %s", obj)
}
if bytes.Equal(key.Digest, digest) {
k = key
return nil, stop
}
// return &portainer.APIKey{}, nil
// })
return &portainer.APIKey{}, nil
})
// if err == stop {
// return k, nil
// }
if err == stop {
return k, nil
}
// if err == nil {
// return nil, errors.ErrObjectNotFound
// }
if err == nil {
return nil, errors.ErrObjectNotFound
}
return nil, nil
return nil, err
}
// CreateAPIKey creates a new APIKey object.
func (service *Service) CreateAPIKey(record *portainer.APIKey) error {
// return service.connection.CreateObject(
// BucketName,
// func(id uint64) (int, interface{}) {
// record.ID = portainer.APIKeyID(id)
return service.connection.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
record.ID = portainer.APIKeyID(id)
// return int(record.ID), record
// },
// )
return nil
return int(record.ID), record
},
)
}
// GetAPIKey retrieves an existing APIKey object by api key ID.
func (service *Service) GetAPIKey(keyID portainer.APIKeyID) (*portainer.APIKey, error) {
var key portainer.APIKey
// identifier := service.connection.ConvertToKey(int(keyID))
identifier := service.connection.ConvertToKey(int(keyID))
// err := service.connection.GetObject(BucketName, identifier, &key)
// if err != nil {
// return nil, err
// }
err := service.connection.GetObject(BucketName, identifier, &key)
if err != nil {
return nil, err
}
return &key, nil
}
func (service *Service) UpdateAPIKey(key *portainer.APIKey) error {
// identifier := service.connection.ConvertToKey(int(key.ID))
// return service.connection.UpdateObject(BucketName, identifier, key)
return nil
identifier := service.connection.ConvertToKey(int(key.ID))
return service.connection.UpdateObject(BucketName, identifier, key)
}
func (service *Service) DeleteAPIKey(ID portainer.APIKeyID) error {
// identifier := service.connection.ConvertToKey(int(ID))
// return service.connection.DeleteObject(BucketName, identifier)
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.DeleteObject(BucketName, identifier)
}

View File

@@ -1,7 +1,11 @@
package customtemplate
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
const (
@@ -20,10 +24,10 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
// err := connection.SetServiceName(BucketName)
// if err != nil {
// return nil, err
// }
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
@@ -34,60 +38,56 @@ func NewService(connection portainer.Connection) (*Service, error) {
func (service *Service) CustomTemplates() ([]portainer.CustomTemplate, error) {
var customTemplates = make([]portainer.CustomTemplate, 0)
// err := service.connection.GetAll(
// BucketName,
// &portainer.CustomTemplate{},
// func(obj interface{}) (interface{}, error) {
// //var tag portainer.Tag
// customTemplate, ok := obj.(*portainer.CustomTemplate)
// if !ok {
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to CustomTemplate object")
// return nil, fmt.Errorf("Failed to convert to CustomTemplate object: %s", obj)
// }
// customTemplates = append(customTemplates, *customTemplate)
err := service.connection.GetAll(
BucketName,
&portainer.CustomTemplate{},
func(obj interface{}) (interface{}, error) {
//var tag portainer.Tag
customTemplate, ok := obj.(*portainer.CustomTemplate)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to CustomTemplate object")
return nil, fmt.Errorf("Failed to convert to CustomTemplate object: %s", obj)
}
customTemplates = append(customTemplates, *customTemplate)
// return &portainer.CustomTemplate{}, nil
// })
return &portainer.CustomTemplate{}, nil
})
return customTemplates, 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 := service.connection.ConvertToKey(int(ID))
identifier := service.connection.ConvertToKey(int(ID))
// err := service.connection.GetObject(BucketName, identifier, &customTemplate)
// if err != nil {
// return nil, err
// }
err := service.connection.GetObject(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 := service.connection.ConvertToKey(int(ID))
// return service.connection.UpdateObject(BucketName, identifier, customTemplate)
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.UpdateObject(BucketName, identifier, customTemplate)
}
// DeleteCustomTemplate deletes an custom template.
func (service *Service) DeleteCustomTemplate(ID portainer.CustomTemplateID) error {
// identifier := service.connection.ConvertToKey(int(ID))
// return service.connection.DeleteObject(BucketName, identifier)
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.DeleteObject(BucketName, identifier)
}
// CreateCustomTemplate uses the existing id and saves it.
// TODO: where does the ID come from, and is it safe?
func (service *Service) Create(customTemplate *portainer.CustomTemplate) error {
// return service.connection.CreateObjectWithId(BucketName, int(customTemplate.ID), customTemplate)
return nil
return service.connection.CreateObjectWithId(BucketName, int(customTemplate.ID), customTemplate)
}
// GetNextIdentifier returns the next identifier for a custom template.
func (service *Service) GetNextIdentifier() int {
// return service.connection.GetNextIdentifier(BucketName)
return 0
return service.connection.GetNextIdentifier(BucketName)
}

View File

@@ -21,10 +21,10 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
// err := connection.SetServiceName(BucketName)
// if err != nil {
// return nil, err
// }
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
@@ -35,16 +35,15 @@ func NewService(connection portainer.Connection) (*Service, error) {
func (service *Service) DockerHub() (*portainer.DockerHub, error) {
var dockerhub portainer.DockerHub
// err := service.connection.GetObject(BucketName, []byte(dockerHubKey), &dockerhub)
// if err != nil {
// return nil, err
// }
err := service.connection.GetObject(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 service.connection.UpdateObject(BucketName, []byte(dockerHubKey), dockerhub)
return nil
return service.connection.UpdateObject(BucketName, []byte(dockerHubKey), dockerhub)
}

View File

@@ -4,10 +4,8 @@ import (
portainer "github.com/portainer/portainer/api"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "edgegroups"
)
// BucketName represents the name of the bucket where this service stores data.
const BucketName = "edgegroups"
// Service represents a service for managing Edge group data.
type Service struct {
@@ -20,72 +18,75 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
// err := connection.SetServiceName(BucketName)
// if err != nil {
// return nil, err
// }
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
}
// EdgeGroups return an array containing all the Edge groups.
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
return ServiceTx{
service: service,
tx: tx,
}
}
// EdgeGroups return a slice containing all the Edge groups.
func (service *Service) EdgeGroups() ([]portainer.EdgeGroup, error) {
var groups = make([]portainer.EdgeGroup, 0)
var groups []portainer.EdgeGroup
var err error
// err := service.connection.GetAllWithJsoniter(
// BucketName,
// &portainer.EdgeGroup{},
// func(obj interface{}) (interface{}, error) {
// group, ok := obj.(*portainer.EdgeGroup)
// if !ok {
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeGroup object")
// return nil, fmt.Errorf("Failed to convert to EdgeGroup object: %s", obj)
// }
// groups = append(groups, *group)
err = service.connection.ViewTx(func(tx portainer.Transaction) error {
groups, err = service.Tx(tx).EdgeGroups()
return err
})
// return &portainer.EdgeGroup{}, nil
// })
return groups, 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 := service.connection.ConvertToKey(int(ID))
var group *portainer.EdgeGroup
var err error
// err := service.connection.GetObject(BucketName, identifier, &group)
// if err != nil {
// return nil, err
// }
err = service.connection.ViewTx(func(tx portainer.Transaction) error {
group, err = service.Tx(tx).EdgeGroup(ID)
return err
})
return &group, nil
return group, err
}
// UpdateEdgeGroup updates an Edge group.
// UpdateEdgeGroup updates an edge group.
func (service *Service) UpdateEdgeGroup(ID portainer.EdgeGroupID, group *portainer.EdgeGroup) error {
// identifier := service.connection.ConvertToKey(int(ID))
// return service.connection.UpdateObject(BucketName, identifier, group)
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.UpdateObject(BucketName, identifier, group)
}
// Deprecated: UpdateEdgeGroupFunc updates an edge group inside a transaction avoiding data races.
func (service *Service) UpdateEdgeGroupFunc(ID portainer.EdgeGroupID, updateFunc func(edgeGroup *portainer.EdgeGroup)) error {
id := service.connection.ConvertToKey(int(ID))
edgeGroup := &portainer.EdgeGroup{}
return service.connection.UpdateObjectFunc(BucketName, id, edgeGroup, func() {
updateFunc(edgeGroup)
})
}
// DeleteEdgeGroup deletes an Edge group.
func (service *Service) DeleteEdgeGroup(ID portainer.EdgeGroupID) error {
// identifier := service.connection.ConvertToKey(int(ID))
// return service.connection.DeleteObject(BucketName, identifier)
return nil
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
return service.Tx(tx).DeleteEdgeGroup(ID)
})
}
// CreateEdgeGroup assign an ID to a new Edge group and saves it.
func (service *Service) Create(group *portainer.EdgeGroup) error {
// return service.connection.CreateObject(
// BucketName,
// func(id uint64) (int, interface{}) {
// group.ID = portainer.EdgeGroupID(id)
// return int(group.ID), group
// },
// )
return nil
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
return service.Tx(tx).Create(group)
})
}

View File

@@ -0,0 +1,80 @@
package edgegroup
import (
"errors"
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
type ServiceTx struct {
service *Service
tx portainer.Transaction
}
func (service ServiceTx) BucketName() string {
return BucketName
}
// EdgeGroups return a slice containing all the Edge groups.
func (service ServiceTx) EdgeGroups() ([]portainer.EdgeGroup, error) {
var groups = make([]portainer.EdgeGroup, 0)
err := service.tx.GetAllWithJsoniter(
BucketName,
&portainer.EdgeGroup{},
func(obj interface{}) (interface{}, error) {
group, ok := obj.(*portainer.EdgeGroup)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeGroup object")
return nil, fmt.Errorf("Failed to convert to EdgeGroup object: %s", obj)
}
groups = append(groups, *group)
return &portainer.EdgeGroup{}, nil
})
return groups, err
}
// EdgeGroup returns an Edge group by ID.
func (service ServiceTx) EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGroup, error) {
var group portainer.EdgeGroup
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.GetObject(BucketName, identifier, &group)
if err != nil {
return nil, err
}
return &group, nil
}
// UpdateEdgeGroup updates an edge group.
func (service ServiceTx) UpdateEdgeGroup(ID portainer.EdgeGroupID, group *portainer.EdgeGroup) error {
identifier := service.service.connection.ConvertToKey(int(ID))
return service.tx.UpdateObject(BucketName, identifier, group)
}
// UpdateEdgeGroupFunc is a no-op inside a transaction.
func (service ServiceTx) UpdateEdgeGroupFunc(ID portainer.EdgeGroupID, updateFunc func(edgeGroup *portainer.EdgeGroup)) error {
return errors.New("cannot be called inside a transaction")
}
// DeleteEdgeGroup deletes an Edge group.
func (service ServiceTx) DeleteEdgeGroup(ID portainer.EdgeGroupID) error {
identifier := service.service.connection.ConvertToKey(int(ID))
return service.tx.DeleteObject(BucketName, identifier)
}
func (service ServiceTx) Create(group *portainer.EdgeGroup) error {
return service.tx.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
group.ID = portainer.EdgeGroupID(id)
return int(group.ID), group
},
)
}

View File

@@ -1,13 +1,15 @@
package edgejob
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "edgejobs"
)
// BucketName represents the name of the bucket where this service stores data.
const BucketName = "edgejobs"
// Service represents a service for managing edge jobs data.
type Service struct {
@@ -20,79 +22,92 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
// err := connection.SetServiceName(BucketName)
// if err != nil {
// return nil, err
// }
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
}
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
return ServiceTx{
service: service,
tx: tx,
}
}
// EdgeJobs returns a list of Edge jobs
func (service *Service) EdgeJobs() ([]portainer.EdgeJob, error) {
var edgeJobs = make([]portainer.EdgeJob, 0)
// err := service.connection.GetAll(
// BucketName,
// &portainer.EdgeJob{},
// func(obj interface{}) (interface{}, error) {
// job, ok := obj.(*portainer.EdgeJob)
// if !ok {
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeJob object")
// return nil, fmt.Errorf("Failed to convert to EdgeJob object: %s", obj)
// }
err := service.connection.GetAll(
BucketName,
&portainer.EdgeJob{},
func(obj interface{}) (interface{}, error) {
job, ok := obj.(*portainer.EdgeJob)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeJob object")
return nil, fmt.Errorf("Failed to convert to EdgeJob object: %s", obj)
}
// edgeJobs = append(edgeJobs, *job)
edgeJobs = append(edgeJobs, *job)
// return &portainer.EdgeJob{}, nil
// })
return &portainer.EdgeJob{}, nil
})
return edgeJobs, 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 := service.connection.ConvertToKey(int(ID))
identifier := service.connection.ConvertToKey(int(ID))
// err := service.connection.GetObject(BucketName, identifier, &edgeJob)
// if err != nil {
// return nil, err
// }
err := service.connection.GetObject(BucketName, identifier, &edgeJob)
if err != nil {
return nil, err
}
return &edgeJob, nil
}
// Create creates a new EdgeJob
func (service *Service) Create(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
// edgeJob.ID = ID
edgeJob.ID = ID
// return service.connection.CreateObjectWithId(
// BucketName,
// int(edgeJob.ID),
// edgeJob,
// )
return nil
return service.connection.CreateObjectWithId(
BucketName,
int(edgeJob.ID),
edgeJob,
)
}
// UpdateEdgeJob updates an Edge job by ID
// Deprecated: use UpdateEdgeJobFunc instead
func (service *Service) UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
// identifier := service.connection.ConvertToKey(int(ID))
// return service.connection.UpdateObject(BucketName, identifier, edgeJob)
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.UpdateObject(BucketName, identifier, edgeJob)
}
// UpdateEdgeJobFunc updates an edge job inside a transaction avoiding data races.
func (service *Service) UpdateEdgeJobFunc(ID portainer.EdgeJobID, updateFunc func(edgeJob *portainer.EdgeJob)) error {
id := service.connection.ConvertToKey(int(ID))
edgeJob := &portainer.EdgeJob{}
return service.connection.UpdateObjectFunc(BucketName, id, edgeJob, func() {
updateFunc(edgeJob)
})
}
// DeleteEdgeJob deletes an Edge job
func (service *Service) DeleteEdgeJob(ID portainer.EdgeJobID) error {
// identifier := service.connection.ConvertToKey(int(ID))
// return service.connection.DeleteObject(BucketName, identifier)
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.DeleteObject(BucketName, identifier)
}
// GetNextIdentifier returns the next identifier for an environment(endpoint).
func (service *Service) GetNextIdentifier() int {
// return service.connection.GetNextIdentifier(BucketName)
return 0
return service.connection.GetNextIdentifier(BucketName)
}

View File

@@ -0,0 +1,84 @@
package edgejob
import (
"errors"
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
type ServiceTx struct {
service *Service
tx portainer.Transaction
}
func (service ServiceTx) BucketName() string {
return BucketName
}
// EdgeJobs returns a list of Edge jobs
func (service ServiceTx) EdgeJobs() ([]portainer.EdgeJob, error) {
var edgeJobs = make([]portainer.EdgeJob, 0)
err := service.tx.GetAll(
BucketName,
&portainer.EdgeJob{},
func(obj interface{}) (interface{}, error) {
job, ok := obj.(*portainer.EdgeJob)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeJob object")
return nil, fmt.Errorf("failed to convert to EdgeJob object: %s", obj)
}
edgeJobs = append(edgeJobs, *job)
return &portainer.EdgeJob{}, nil
})
return edgeJobs, err
}
// EdgeJob returns an Edge job by ID
func (service ServiceTx) EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, error) {
var edgeJob portainer.EdgeJob
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.GetObject(BucketName, identifier, &edgeJob)
if err != nil {
return nil, err
}
return &edgeJob, nil
}
// Create creates a new EdgeJob
func (service ServiceTx) Create(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
edgeJob.ID = ID
return service.tx.CreateObjectWithId(BucketName, int(edgeJob.ID), edgeJob)
}
// UpdateEdgeJob updates an edge job
func (service ServiceTx) UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
identifier := service.service.connection.ConvertToKey(int(ID))
return service.tx.UpdateObject(BucketName, identifier, edgeJob)
}
// UpdateEdgeJobFunc is a no-op inside a transaction.
func (service ServiceTx) UpdateEdgeJobFunc(ID portainer.EdgeJobID, updateFunc func(edgeJob *portainer.EdgeJob)) error {
return errors.New("cannot be called inside a transaction")
}
// DeleteEdgeJob deletes an Edge job
func (service ServiceTx) DeleteEdgeJob(ID portainer.EdgeJobID) error {
identifier := service.service.connection.ConvertToKey(int(ID))
return service.tx.DeleteObject(BucketName, identifier)
}
// GetNextIdentifier returns the next identifier for an environment(endpoint).
func (service ServiceTx) GetNextIdentifier() int {
return service.tx.GetNextIdentifier(BucketName)
}

View File

@@ -1,17 +1,23 @@
package edgestack
import (
"fmt"
"sync"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "edge_stack"
)
// BucketName represents the name of the bucket where this service stores data.
const BucketName = "edge_stack"
// Service represents a service for managing Edge stack data.
type Service struct {
connection portainer.Connection
connection portainer.Connection
idxVersion map[portainer.EdgeStackID]int
mu sync.RWMutex
cacheInvalidationFn func(portainer.EdgeStackID)
}
func (service *Service) BucketName() string {
@@ -19,82 +25,160 @@ func (service *Service) BucketName() string {
}
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
// err := connection.SetServiceName(BucketName)
// if err != nil {
// return nil, err
// }
func NewService(connection portainer.Connection, cacheInvalidationFn func(portainer.EdgeStackID)) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
s := &Service{
connection: connection,
idxVersion: make(map[portainer.EdgeStackID]int),
cacheInvalidationFn: cacheInvalidationFn,
}
if s.cacheInvalidationFn == nil {
s.cacheInvalidationFn = func(portainer.EdgeStackID) {}
}
es, err := s.EdgeStacks()
if err != nil {
return nil, err
}
for _, e := range es {
s.idxVersion[e.ID] = e.Version
}
return s, nil
}
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
return ServiceTx{
service: service,
tx: tx,
}
}
// EdgeStacks returns an array containing all edge stacks
func (service *Service) EdgeStacks() ([]portainer.EdgeStack, error) {
var stacks = make([]portainer.EdgeStack, 0)
// err := service.connection.GetAll(
// BucketName,
// &portainer.EdgeStack{},
// func(obj interface{}) (interface{}, error) {
// //var tag portainer.Tag
// stack, ok := obj.(*portainer.EdgeStack)
// if !ok {
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeStack object")
// return nil, fmt.Errorf("Failed to convert to EdgeStack object: %s", obj)
// }
err := service.connection.GetAll(
BucketName,
&portainer.EdgeStack{},
func(obj interface{}) (interface{}, error) {
stack, ok := obj.(*portainer.EdgeStack)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeStack object")
return nil, fmt.Errorf("Failed to convert to EdgeStack object: %s", obj)
}
// stacks = append(stacks, *stack)
stacks = append(stacks, *stack)
// return &portainer.EdgeStack{}, nil
// })
return &portainer.EdgeStack{}, nil
})
return stacks, 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 := service.connection.ConvertToKey(int(ID))
identifier := service.connection.ConvertToKey(int(ID))
// err := service.connection.GetObject(BucketName, identifier, &stack)
// if err != nil {
// return nil, err
// }
err := service.connection.GetObject(BucketName, identifier, &stack)
if err != nil {
return nil, err
}
return &stack, nil
}
// EdgeStackVersion returns the version of the given edge stack ID directly from an in-memory index
func (service *Service) EdgeStackVersion(ID portainer.EdgeStackID) (int, bool) {
service.mu.RLock()
v, ok := service.idxVersion[ID]
service.mu.RUnlock()
return v, ok
}
// CreateEdgeStack saves an Edge stack object to db.
func (service *Service) Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
edgeStack.ID = id
// edgeStack.ID = id
err := service.connection.CreateObjectWithId(
BucketName,
int(edgeStack.ID),
edgeStack,
)
if err != nil {
return err
}
service.mu.Lock()
service.idxVersion[id] = edgeStack.Version
service.cacheInvalidationFn(id)
service.mu.Unlock()
// return service.connection.CreateObjectWithId(
// BucketName,
// int(edgeStack.ID),
// edgeStack,
// )
return nil
}
// UpdateEdgeStack updates an Edge stack.
// Deprecated: Use UpdateEdgeStackFunc instead.
func (service *Service) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
// identifier := service.connection.ConvertToKey(int(ID))
// return service.connection.UpdateObject(BucketName, identifier, edgeStack)
service.mu.Lock()
defer service.mu.Unlock()
identifier := service.connection.ConvertToKey(int(ID))
err := service.connection.UpdateObject(BucketName, identifier, edgeStack)
if err != nil {
return err
}
service.idxVersion[ID] = edgeStack.Version
service.cacheInvalidationFn(ID)
return nil
}
// UpdateEdgeStackFunc updates an Edge stack inside a transaction avoiding data races.
func (service *Service) UpdateEdgeStackFunc(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error {
id := service.connection.ConvertToKey(int(ID))
edgeStack := &portainer.EdgeStack{}
service.mu.Lock()
defer service.mu.Unlock()
return service.connection.UpdateObjectFunc(BucketName, id, edgeStack, func() {
updateFunc(edgeStack)
service.idxVersion[ID] = edgeStack.Version
service.cacheInvalidationFn(ID)
})
}
// DeleteEdgeStack deletes an Edge stack.
func (service *Service) DeleteEdgeStack(ID portainer.EdgeStackID) error {
// identifier := service.connection.ConvertToKey(int(ID))
// return service.connection.DeleteObject(BucketName, identifier)
service.mu.Lock()
defer service.mu.Unlock()
identifier := service.connection.ConvertToKey(int(ID))
err := service.connection.DeleteObject(BucketName, identifier)
if err != nil {
return err
}
delete(service.idxVersion, ID)
service.cacheInvalidationFn(ID)
return nil
}
// GetNextIdentifier returns the next identifier for an environment(endpoint).
func (service *Service) GetNextIdentifier() int {
// return service.connection.GetNextIdentifier(BucketName)
return 0
return service.connection.GetNextIdentifier(BucketName)
}

View File

@@ -0,0 +1,131 @@
package edgestack
import (
"errors"
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
type ServiceTx struct {
service *Service
tx portainer.Transaction
}
func (service ServiceTx) BucketName() string {
return BucketName
}
// EdgeStacks returns an array containing all edge stacks
func (service ServiceTx) EdgeStacks() ([]portainer.EdgeStack, error) {
var stacks = make([]portainer.EdgeStack, 0)
err := service.tx.GetAll(
BucketName,
&portainer.EdgeStack{},
func(obj interface{}) (interface{}, error) {
stack, ok := obj.(*portainer.EdgeStack)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeStack object")
return nil, fmt.Errorf("Failed to convert to EdgeStack object: %s", obj)
}
stacks = append(stacks, *stack)
return &portainer.EdgeStack{}, nil
})
return stacks, err
}
// EdgeStack returns an Edge stack by ID.
func (service ServiceTx) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStack, error) {
var stack portainer.EdgeStack
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.GetObject(BucketName, identifier, &stack)
if err != nil {
return nil, err
}
return &stack, nil
}
// EdgeStackVersion returns the version of the given edge stack ID directly from an in-memory index
func (service ServiceTx) EdgeStackVersion(ID portainer.EdgeStackID) (int, bool) {
service.service.mu.RLock()
v, ok := service.service.idxVersion[ID]
service.service.mu.RUnlock()
return v, ok
}
// CreateEdgeStack saves an Edge stack object to db.
func (service ServiceTx) Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
edgeStack.ID = id
err := service.tx.CreateObjectWithId(
BucketName,
int(edgeStack.ID),
edgeStack,
)
if err != nil {
return err
}
service.service.mu.Lock()
service.service.idxVersion[id] = edgeStack.Version
service.service.cacheInvalidationFn(id)
service.service.mu.Unlock()
return nil
}
// UpdateEdgeStack updates an Edge stack.
func (service ServiceTx) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
service.service.mu.Lock()
defer service.service.mu.Unlock()
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.UpdateObject(BucketName, identifier, edgeStack)
if err != nil {
return err
}
service.service.idxVersion[ID] = edgeStack.Version
service.service.cacheInvalidationFn(ID)
return nil
}
// UpdateEdgeStackFunc is a no-op inside a transaction.
func (service ServiceTx) UpdateEdgeStackFunc(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error {
return errors.New("cannot be called inside a transaction")
}
// DeleteEdgeStack deletes an Edge stack.
func (service ServiceTx) DeleteEdgeStack(ID portainer.EdgeStackID) error {
service.service.mu.Lock()
defer service.service.mu.Unlock()
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.DeleteObject(BucketName, identifier)
if err != nil {
return err
}
delete(service.service.idxVersion, ID)
service.service.cacheInvalidationFn(ID)
return nil
}
// GetNextIdentifier returns the next identifier for an environment(endpoint).
func (service ServiceTx) GetNextIdentifier() int {
return service.tx.GetNextIdentifier(BucketName)
}

View File

@@ -1,185 +0,0 @@
package edgeupdateschedule
import (
"sync"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/edgetypes"
"github.com/pkg/errors"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "edge_update_schedule"
)
// Service represents a service for managing Edge Update Schedule data.
type Service struct {
connection portainer.Connection
mu sync.Mutex
idxActiveSchedules map[portainer.EndpointID]*edgetypes.EndpointUpdateScheduleRelation
}
func (service *Service) BucketName() string {
return BucketName
}
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
// err := connection.SetServiceName(BucketName)
// if err != nil {
// return nil, err
// }
service := &Service{
connection: connection,
}
service.idxActiveSchedules = map[portainer.EndpointID]*edgetypes.EndpointUpdateScheduleRelation{}
schedules, err := service.List()
if err != nil {
return nil, errors.WithMessage(err, "Unable to list schedules")
}
for _, schedule := range schedules {
service.setRelation(&schedule)
}
return service, nil
}
func (service *Service) ActiveSchedule(environmentID portainer.EndpointID) *edgetypes.EndpointUpdateScheduleRelation {
service.mu.Lock()
defer service.mu.Unlock()
return service.idxActiveSchedules[environmentID]
}
func (service *Service) ActiveSchedules(environmentsIDs []portainer.EndpointID) []edgetypes.EndpointUpdateScheduleRelation {
service.mu.Lock()
defer service.mu.Unlock()
schedules := []edgetypes.EndpointUpdateScheduleRelation{}
for _, environmentID := range environmentsIDs {
if s, ok := service.idxActiveSchedules[environmentID]; ok {
schedules = append(schedules, *s)
}
}
return schedules
}
// List return an array containing all the items in the bucket.
func (service *Service) List() ([]edgetypes.UpdateSchedule, error) {
var list = make([]edgetypes.UpdateSchedule, 0)
// err := service.connection.GetAll(
// BucketName,
// &edgetypes.UpdateSchedule{},
// func(obj interface{}) (interface{}, error) {
// item, ok := obj.(*edgetypes.UpdateSchedule)
// if !ok {
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeUpdateSchedule object")
// return nil, fmt.Errorf("Failed to convert to EdgeUpdateSchedule object: %s", obj)
// }
// list = append(list, *item)
// return &edgetypes.UpdateSchedule{}, nil
// })
return list, nil
}
// Item returns a item by ID.
func (service *Service) Item(ID edgetypes.UpdateScheduleID) (*edgetypes.UpdateSchedule, error) {
var item edgetypes.UpdateSchedule
// identifier := service.connection.ConvertToKey(int(ID))
// err := service.connection.GetObject(BucketName, identifier, &item)
// if err != nil {
// return nil, err
// }
return &item, nil
}
// Create assign an ID to a new object and saves it.
func (service *Service) Create(item *edgetypes.UpdateSchedule) error {
// err := service.connection.CreateObject(
// BucketName,
// func(id uint64) (int, interface{}) {
// item.ID = edgetypes.UpdateScheduleID(id)
// return int(item.ID), item
// },
// )
// if err != nil {
// return err
// }
return service.setRelation(item)
}
// Update updates an item.
func (service *Service) Update(id edgetypes.UpdateScheduleID, item *edgetypes.UpdateSchedule) error {
// identifier := service.connection.ConvertToKey(int(id))
// err := service.connection.UpdateObject(BucketName, identifier, item)
// if err != nil {
// return err
// }
service.cleanRelation(id)
return service.setRelation(item)
}
// Delete deletes an item.
func (service *Service) Delete(id edgetypes.UpdateScheduleID) error {
service.cleanRelation(id)
// identifier := service.connection.ConvertToKey(int(id))
// return service.connection.DeleteObject(BucketName, identifier)
return nil
}
func (service *Service) cleanRelation(id edgetypes.UpdateScheduleID) {
service.mu.Lock()
defer service.mu.Unlock()
for _, schedule := range service.idxActiveSchedules {
if schedule != nil && schedule.ScheduleID == id {
delete(service.idxActiveSchedules, schedule.EnvironmentID)
}
}
}
func (service *Service) setRelation(schedule *edgetypes.UpdateSchedule) error {
service.mu.Lock()
defer service.mu.Unlock()
for environmentID, environmentStatus := range schedule.Status {
if environmentStatus.Status != edgetypes.UpdateScheduleStatusPending {
continue
}
// this should never happen
if service.idxActiveSchedules[environmentID] != nil && service.idxActiveSchedules[environmentID].ScheduleID != schedule.ID {
return errors.New("Multiple schedules are pending for the same environment")
}
service.idxActiveSchedules[environmentID] = &edgetypes.EndpointUpdateScheduleRelation{
EnvironmentID: environmentID,
ScheduleID: schedule.ID,
TargetVersion: environmentStatus.TargetVersion,
Status: environmentStatus.Status,
Error: environmentStatus.Error,
Type: schedule.Type,
}
}
return nil
}

View File

@@ -1,17 +1,21 @@
package endpoint
import (
"sync"
"time"
portainer "github.com/portainer/portainer/api"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "endpoints"
)
// BucketName represents the name of the bucket where this service stores data.
const BucketName = "endpoints"
// Service represents a service for managing environment(endpoint) data.
type Service struct {
connection portainer.Connection
mu sync.RWMutex
idxEdgeID map[string]portainer.EndpointID
heartbeats sync.Map
}
func (service *Service) BucketName() string {
@@ -20,73 +24,130 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
// err := connection.SetServiceName(BucketName)
// if err != nil {
// return nil, err
// }
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
s := &Service{
connection: connection,
}, nil
idxEdgeID: make(map[string]portainer.EndpointID),
}
es, err := s.Endpoints()
if err != nil {
return nil, err
}
for _, e := range es {
if len(e.EdgeID) > 0 {
s.idxEdgeID[e.EdgeID] = e.ID
}
s.heartbeats.Store(e.ID, e.LastCheckInDate)
}
return s, nil
}
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
return ServiceTx{
service: service,
tx: tx,
}
}
// Endpoint returns an environment(endpoint) by ID.
func (service *Service) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
var endpoint portainer.Endpoint
// identifier := service.connection.ConvertToKey(int(ID))
var endpoint *portainer.Endpoint
var err error
// err := service.connection.GetObject(BucketName, identifier, &endpoint)
// if err != nil {
// return nil, err
// }
err = service.connection.ViewTx(func(tx portainer.Transaction) error {
endpoint, err = service.Tx(tx).Endpoint(ID)
return err
})
if err != nil {
return nil, err
}
return &endpoint, nil
endpoint.LastCheckInDate, _ = service.Heartbeat(ID)
return endpoint, nil
}
// UpdateEndpoint updates an environment(endpoint).
func (service *Service) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
// identifier := service.connection.ConvertToKey(int(ID))
// return service.connection.UpdateObject(BucketName, identifier, endpoint)
return nil
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
return service.Tx(tx).UpdateEndpoint(ID, endpoint)
})
}
// DeleteEndpoint deletes an environment(endpoint).
func (service *Service) DeleteEndpoint(ID portainer.EndpointID) error {
// identifier := service.connection.ConvertToKey(int(ID))
// return service.connection.DeleteObject(BucketName, identifier)
return nil
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
return service.Tx(tx).DeleteEndpoint(ID)
})
}
// Endpoints return an array containing all the environments(endpoints).
func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
var endpoints = make([]portainer.Endpoint, 0)
var endpoints []portainer.Endpoint
var err error
// err := service.connection.GetAllWithJsoniter(
// BucketName,
// &portainer.Endpoint{},
// func(obj interface{}) (interface{}, error) {
// endpoint, ok := obj.(*portainer.Endpoint)
// if !ok {
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Endpoint object")
// return nil, fmt.Errorf("failed to convert to Endpoint object: %s", obj)
// }
err = service.connection.ViewTx(func(tx portainer.Transaction) error {
endpoints, err = service.Tx(tx).Endpoints()
return err
})
// endpoints = append(endpoints, *endpoint)
if err != nil {
return endpoints, err
}
// return &portainer.Endpoint{}, nil
// })
for i, e := range endpoints {
t, _ := service.Heartbeat(e.ID)
endpoints[i].LastCheckInDate = t
}
return endpoints, nil
}
// EndpointIDByEdgeID returns the EndpointID from the given EdgeID using an in-memory index
func (service *Service) EndpointIDByEdgeID(edgeID string) (portainer.EndpointID, bool) {
service.mu.RLock()
endpointID, ok := service.idxEdgeID[edgeID]
service.mu.RUnlock()
return endpointID, ok
}
func (service *Service) Heartbeat(endpointID portainer.EndpointID) (int64, bool) {
if t, ok := service.heartbeats.Load(endpointID); ok {
return t.(int64), true
}
return 0, false
}
func (service *Service) UpdateHeartbeat(endpointID portainer.EndpointID) {
service.heartbeats.Store(endpointID, time.Now().Unix())
}
// CreateEndpoint assign an ID to a new environment(endpoint) and saves it.
func (service *Service) Create(endpoint *portainer.Endpoint) error {
// return service.connection.CreateObjectWithId(BucketName, int(endpoint.ID), endpoint)
return nil
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
return service.Tx(tx).Create(endpoint)
})
}
// GetNextIdentifier returns the next identifier for an environment(endpoint).
func (service *Service) GetNextIdentifier() int {
// return service.connection.GetNextIdentifier(BucketName)
return 0
var identifier int
service.connection.UpdateTx(func(tx portainer.Transaction) error {
identifier = service.Tx(tx).GetNextIdentifier()
return nil
})
return identifier
}

View File

@@ -0,0 +1,137 @@
package endpoint
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/edge/cache"
"github.com/rs/zerolog/log"
)
type ServiceTx struct {
service *Service
tx portainer.Transaction
}
func (service ServiceTx) BucketName() string {
return BucketName
}
// Endpoint returns an environment(endpoint) by ID.
func (service ServiceTx) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
var endpoint portainer.Endpoint
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.GetObject(BucketName, identifier, &endpoint)
if err != nil {
return nil, err
}
return &endpoint, nil
}
// UpdateEndpoint updates an environment(endpoint).
func (service ServiceTx) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.UpdateObject(BucketName, identifier, endpoint)
if err != nil {
return err
}
service.service.mu.Lock()
if len(endpoint.EdgeID) > 0 {
service.service.idxEdgeID[endpoint.EdgeID] = ID
}
service.service.heartbeats.Store(ID, endpoint.LastCheckInDate)
service.service.mu.Unlock()
cache.Del(endpoint.ID)
return nil
}
// DeleteEndpoint deletes an environment(endpoint).
func (service ServiceTx) DeleteEndpoint(ID portainer.EndpointID) error {
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.DeleteObject(BucketName, identifier)
if err != nil {
return err
}
service.service.mu.Lock()
for edgeID, endpointID := range service.service.idxEdgeID {
if endpointID == ID {
delete(service.service.idxEdgeID, edgeID)
break
}
}
service.service.heartbeats.Delete(ID)
service.service.mu.Unlock()
cache.Del(ID)
return nil
}
// Endpoints return an array containing all the environments(endpoints).
func (service ServiceTx) Endpoints() ([]portainer.Endpoint, error) {
var endpoints = make([]portainer.Endpoint, 0)
err := service.tx.GetAllWithJsoniter(
BucketName,
&portainer.Endpoint{},
func(obj interface{}) (interface{}, error) {
endpoint, ok := obj.(*portainer.Endpoint)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Endpoint object")
return nil, fmt.Errorf("failed to convert to Endpoint object: %s", obj)
}
endpoints = append(endpoints, *endpoint)
return &portainer.Endpoint{}, nil
})
return endpoints, err
}
func (service ServiceTx) EndpointIDByEdgeID(edgeID string) (portainer.EndpointID, bool) {
log.Error().Str("func", "EndpointIDByEdgeID").Msg("cannot be called inside a transaction")
return 0, false
}
func (service ServiceTx) Heartbeat(endpointID portainer.EndpointID) (int64, bool) {
log.Error().Str("func", "Heartbeat").Msg("cannot be called inside a transaction")
return 0, false
}
func (service ServiceTx) UpdateHeartbeat(endpointID portainer.EndpointID) {
log.Error().Str("func", "UpdateHeartbeat").Msg("cannot be called inside a transaction")
}
// CreateEndpoint assign an ID to a new environment(endpoint) and saves it.
func (service ServiceTx) Create(endpoint *portainer.Endpoint) error {
err := service.tx.CreateObjectWithId(BucketName, int(endpoint.ID), endpoint)
if err != nil {
return err
}
service.service.mu.Lock()
if len(endpoint.EdgeID) > 0 {
service.service.idxEdgeID[endpoint.EdgeID] = endpoint.ID
}
service.service.heartbeats.Store(endpoint.ID, endpoint.LastCheckInDate)
service.service.mu.Unlock()
return nil
}
// GetNextIdentifier returns the next identifier for an environment(endpoint).
func (service ServiceTx) GetNextIdentifier() int {
return service.tx.GetNextIdentifier(BucketName)
}

View File

@@ -1,7 +1,11 @@
package endpointgroup
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
const (
@@ -20,73 +24,77 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
// err := connection.SetServiceName(BucketName)
// if err != nil {
// return nil, err
// }
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
}
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
return ServiceTx{
service: service,
tx: tx,
}
}
// EndpointGroup returns an environment(endpoint) group by ID.
func (service *Service) EndpointGroup(ID portainer.EndpointGroupID) (*portainer.EndpointGroup, error) {
var endpointGroup portainer.EndpointGroup
// identifier := service.connection.ConvertToKey(int(ID))
identifier := service.connection.ConvertToKey(int(ID))
// err := service.connection.GetObject(BucketName, identifier, &endpointGroup)
// if err != nil {
// return nil, err
// }
err := service.connection.GetObject(BucketName, identifier, &endpointGroup)
if err != nil {
return nil, err
}
return &endpointGroup, nil
}
// UpdateEndpointGroup updates an environment(endpoint) group.
func (service *Service) UpdateEndpointGroup(ID portainer.EndpointGroupID, endpointGroup *portainer.EndpointGroup) error {
// identifier := service.connection.ConvertToKey(int(ID))
// return service.connection.UpdateObject(BucketName, identifier, endpointGroup)
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.UpdateObject(BucketName, identifier, endpointGroup)
}
// DeleteEndpointGroup deletes an environment(endpoint) group.
func (service *Service) DeleteEndpointGroup(ID portainer.EndpointGroupID) error {
// identifier := service.connection.ConvertToKey(int(ID))
// return service.connection.DeleteObject(BucketName, identifier)
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.DeleteObject(BucketName, identifier)
}
// EndpointGroups return an array containing all the environment(endpoint) groups.
func (service *Service) EndpointGroups() ([]portainer.EndpointGroup, error) {
var endpointGroups = make([]portainer.EndpointGroup, 0)
// err := service.connection.GetAll(
// BucketName,
// &portainer.EndpointGroup{},
// func(obj interface{}) (interface{}, error) {
// endpointGroup, ok := obj.(*portainer.EndpointGroup)
// if !ok {
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EndpointGroup object")
// return nil, fmt.Errorf("Failed to convert to EndpointGroup object: %s", obj)
// }
err := service.connection.GetAll(
BucketName,
&portainer.EndpointGroup{},
func(obj interface{}) (interface{}, error) {
endpointGroup, ok := obj.(*portainer.EndpointGroup)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EndpointGroup object")
return nil, fmt.Errorf("Failed to convert to EndpointGroup object: %s", obj)
}
// endpointGroups = append(endpointGroups, *endpointGroup)
endpointGroups = append(endpointGroups, *endpointGroup)
// return &portainer.EndpointGroup{}, nil
// })
return &portainer.EndpointGroup{}, nil
})
return endpointGroups, nil
return endpointGroups, err
}
// CreateEndpointGroup assign an ID to a new environment(endpoint) group and saves it.
func (service *Service) Create(endpointGroup *portainer.EndpointGroup) error {
// return service.connection.CreateObject(
// BucketName,
// func(id uint64) (int, interface{}) {
// endpointGroup.ID = portainer.EndpointGroupID(id)
// return int(endpointGroup.ID), endpointGroup
// },
// )
return nil
return service.connection.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
endpointGroup.ID = portainer.EndpointGroupID(id)
return int(endpointGroup.ID), endpointGroup
},
)
}

View File

@@ -0,0 +1,76 @@
package endpointgroup
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
type ServiceTx struct {
service *Service
tx portainer.Transaction
}
func (service ServiceTx) BucketName() string {
return BucketName
}
// EndpointGroup returns an environment(endpoint) group by ID.
func (service ServiceTx) EndpointGroup(ID portainer.EndpointGroupID) (*portainer.EndpointGroup, error) {
var endpointGroup portainer.EndpointGroup
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.GetObject(BucketName, identifier, &endpointGroup)
if err != nil {
return nil, err
}
return &endpointGroup, nil
}
// UpdateEndpointGroup updates an environment(endpoint) group.
func (service ServiceTx) UpdateEndpointGroup(ID portainer.EndpointGroupID, endpointGroup *portainer.EndpointGroup) error {
identifier := service.service.connection.ConvertToKey(int(ID))
return service.tx.UpdateObject(BucketName, identifier, endpointGroup)
}
// DeleteEndpointGroup deletes an environment(endpoint) group.
func (service ServiceTx) DeleteEndpointGroup(ID portainer.EndpointGroupID) error {
identifier := service.service.connection.ConvertToKey(int(ID))
return service.tx.DeleteObject(BucketName, identifier)
}
// EndpointGroups return an array containing all the environment(endpoint) groups.
func (service ServiceTx) EndpointGroups() ([]portainer.EndpointGroup, error) {
var endpointGroups = make([]portainer.EndpointGroup, 0)
err := service.tx.GetAll(
BucketName,
&portainer.EndpointGroup{},
func(obj interface{}) (interface{}, error) {
endpointGroup, ok := obj.(*portainer.EndpointGroup)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EndpointGroup object")
return nil, fmt.Errorf("failed to convert to EndpointGroup object: %s", obj)
}
endpointGroups = append(endpointGroups, *endpointGroup)
return &portainer.EndpointGroup{}, nil
})
return endpointGroups, err
}
// CreateEndpointGroup assign an ID to a new environment(endpoint) group and saves it.
func (service ServiceTx) Create(endpointGroup *portainer.EndpointGroup) error {
return service.tx.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
endpointGroup.ID = portainer.EndpointGroupID(id)
return int(endpointGroup.ID), endpointGroup
},
)
}

View File

@@ -1,86 +1,186 @@
package endpointrelation
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/edge/cache"
"github.com/rs/zerolog/log"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "endpoint_relations"
)
// BucketName represents the name of the bucket where this service stores data.
const BucketName = "endpoint_relations"
// Service represents a service for managing environment(endpoint) relation data.
type Service struct {
connection portainer.Connection
connection portainer.Connection
updateStackFn func(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error
}
func (service *Service) BucketName() string {
return BucketName
}
func (service *Service) RegisterUpdateStackFunction(updateFunc func(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error) {
service.updateStackFn = updateFunc
}
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
// err := connection.SetServiceName(BucketName)
// if err != nil {
// return nil, err
// }
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
}
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
return ServiceTx{
service: service,
tx: tx,
}
}
// EndpointRelations returns an array of all EndpointRelations
func (service *Service) EndpointRelations() ([]portainer.EndpointRelation, error) {
var all = make([]portainer.EndpointRelation, 0)
// err := service.connection.GetAll(
// BucketName,
// &portainer.EndpointRelation{},
// func(obj interface{}) (interface{}, error) {
// r, ok := obj.(*portainer.EndpointRelation)
// if !ok {
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EndpointRelation object")
// return nil, fmt.Errorf("Failed to convert to EndpointRelation object: %s", obj)
// }
err := service.connection.GetAll(
BucketName,
&portainer.EndpointRelation{},
func(obj interface{}) (interface{}, error) {
r, ok := obj.(*portainer.EndpointRelation)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EndpointRelation object")
return nil, fmt.Errorf("Failed to convert to EndpointRelation object: %s", obj)
}
// all = append(all, *r)
all = append(all, *r)
// return &portainer.EndpointRelation{}, nil
// })
return &portainer.EndpointRelation{}, nil
})
return all, nil
return all, err
}
// EndpointRelation returns a Environment(Endpoint) relation object by EndpointID
func (service *Service) EndpointRelation(endpointID portainer.EndpointID) (*portainer.EndpointRelation, error) {
var endpointRelation portainer.EndpointRelation
// identifier := service.connection.ConvertToKey(int(endpointID))
identifier := service.connection.ConvertToKey(int(endpointID))
// err := service.connection.GetObject(BucketName, identifier, &endpointRelation)
// if err != nil {
// return nil, err
// }
err := service.connection.GetObject(BucketName, identifier, &endpointRelation)
if err != nil {
return nil, err
}
return &endpointRelation, nil
}
// CreateEndpointRelation saves endpointRelation
func (service *Service) Create(endpointRelation *portainer.EndpointRelation) error {
// return service.connection.CreateObjectWithId(BucketName, int(endpointRelation.EndpointID), endpointRelation)
return nil
err := service.connection.CreateObjectWithId(BucketName, int(endpointRelation.EndpointID), endpointRelation)
cache.Del(endpointRelation.EndpointID)
return err
}
// UpdateEndpointRelation updates an Environment(Endpoint) relation object
func (service *Service) UpdateEndpointRelation(EndpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error {
// identifier := service.connection.ConvertToKey(int(EndpointID))
// return service.connection.UpdateObject(BucketName, identifier, endpointRelation)
func (service *Service) UpdateEndpointRelation(endpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error {
previousRelationState, _ := service.EndpointRelation(endpointID)
identifier := service.connection.ConvertToKey(int(endpointID))
err := service.connection.UpdateObject(BucketName, identifier, endpointRelation)
cache.Del(endpointID)
if err != nil {
return err
}
updatedRelationState, _ := service.EndpointRelation(endpointID)
service.updateEdgeStacksAfterRelationChange(previousRelationState, updatedRelationState)
return nil
}
// DeleteEndpointRelation deletes an Environment(Endpoint) relation object
func (service *Service) DeleteEndpointRelation(EndpointID portainer.EndpointID) error {
// identifier := service.connection.ConvertToKey(int(EndpointID))
// return service.connection.DeleteObject(BucketName, identifier)
func (service *Service) DeleteEndpointRelation(endpointID portainer.EndpointID) error {
deletedRelation, _ := service.EndpointRelation(endpointID)
identifier := service.connection.ConvertToKey(int(endpointID))
err := service.connection.DeleteObject(BucketName, identifier)
cache.Del(endpointID)
if err != nil {
return err
}
service.updateEdgeStacksAfterRelationChange(deletedRelation, nil)
return nil
}
func (service *Service) InvalidateEdgeCacheForEdgeStack(edgeStackID portainer.EdgeStackID) {
rels, err := service.EndpointRelations()
if err != nil {
log.Error().Err(err).Msg("cannot retrieve endpoint relations")
return
}
for _, rel := range rels {
for id := range rel.EdgeStacks {
if edgeStackID == id {
cache.Del(rel.EndpointID)
}
}
}
}
func (service *Service) updateEdgeStacksAfterRelationChange(previousRelationState *portainer.EndpointRelation, updatedRelationState *portainer.EndpointRelation) {
relations, _ := service.EndpointRelations()
stacksToUpdate := map[portainer.EdgeStackID]bool{}
if previousRelationState != nil {
for stackId, enabled := range previousRelationState.EdgeStacks {
// flag stack for update if stack is not in the updated relation state
// = stack has been removed for this relation
// or this relation has been deleted
if enabled && (updatedRelationState == nil || !updatedRelationState.EdgeStacks[stackId]) {
stacksToUpdate[stackId] = true
}
}
}
if updatedRelationState != nil {
for stackId, enabled := range updatedRelationState.EdgeStacks {
// flag stack for update if stack is not in the previous relation state
// = stack has been added for this relation
if enabled && (previousRelationState == nil || !previousRelationState.EdgeStacks[stackId]) {
stacksToUpdate[stackId] = true
}
}
}
// for each stack referenced by the updated relation
// list how many time this stack is referenced in all relations
// in order to update the stack deployments count
for refStackId, refStackEnabled := range stacksToUpdate {
if refStackEnabled {
numDeployments := 0
for _, r := range relations {
for sId, enabled := range r.EdgeStacks {
if enabled && sId == refStackId {
numDeployments += 1
}
}
}
service.updateStackFn(refStackId, func(edgeStack *portainer.EdgeStack) {
edgeStack.NumDeployments = numDeployments
})
}
}
}

View File

@@ -0,0 +1,159 @@
package endpointrelation
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/edge/cache"
"github.com/rs/zerolog/log"
)
type ServiceTx struct {
service *Service
tx portainer.Transaction
}
func (service ServiceTx) BucketName() string {
return BucketName
}
// EndpointRelations returns an array of all EndpointRelations
func (service ServiceTx) EndpointRelations() ([]portainer.EndpointRelation, error) {
var all = make([]portainer.EndpointRelation, 0)
err := service.tx.GetAll(
BucketName,
&portainer.EndpointRelation{},
func(obj interface{}) (interface{}, error) {
r, ok := obj.(*portainer.EndpointRelation)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EndpointRelation object")
return nil, fmt.Errorf("failed to convert to EndpointRelation object: %s", obj)
}
all = append(all, *r)
return &portainer.EndpointRelation{}, nil
})
return all, err
}
// EndpointRelation returns an Environment(Endpoint) relation object by EndpointID
func (service ServiceTx) EndpointRelation(endpointID portainer.EndpointID) (*portainer.EndpointRelation, error) {
var endpointRelation portainer.EndpointRelation
identifier := service.service.connection.ConvertToKey(int(endpointID))
err := service.tx.GetObject(BucketName, identifier, &endpointRelation)
if err != nil {
return nil, err
}
return &endpointRelation, nil
}
// CreateEndpointRelation saves endpointRelation
func (service ServiceTx) Create(endpointRelation *portainer.EndpointRelation) error {
err := service.tx.CreateObjectWithId(BucketName, int(endpointRelation.EndpointID), endpointRelation)
cache.Del(endpointRelation.EndpointID)
return err
}
// UpdateEndpointRelation updates an Environment(Endpoint) relation object
func (service ServiceTx) UpdateEndpointRelation(endpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error {
previousRelationState, _ := service.EndpointRelation(endpointID)
identifier := service.service.connection.ConvertToKey(int(endpointID))
err := service.tx.UpdateObject(BucketName, identifier, endpointRelation)
cache.Del(endpointID)
if err != nil {
return err
}
updatedRelationState, _ := service.EndpointRelation(endpointID)
service.updateEdgeStacksAfterRelationChange(previousRelationState, updatedRelationState)
return nil
}
// DeleteEndpointRelation deletes an Environment(Endpoint) relation object
func (service ServiceTx) DeleteEndpointRelation(endpointID portainer.EndpointID) error {
deletedRelation, _ := service.EndpointRelation(endpointID)
identifier := service.service.connection.ConvertToKey(int(endpointID))
err := service.tx.DeleteObject(BucketName, identifier)
cache.Del(endpointID)
if err != nil {
return err
}
service.updateEdgeStacksAfterRelationChange(deletedRelation, nil)
return nil
}
func (service ServiceTx) InvalidateEdgeCacheForEdgeStack(edgeStackID portainer.EdgeStackID) {
rels, err := service.EndpointRelations()
if err != nil {
log.Error().Err(err).Msg("cannot retrieve endpoint relations")
return
}
for _, rel := range rels {
for id := range rel.EdgeStacks {
if edgeStackID == id {
cache.Del(rel.EndpointID)
}
}
}
}
func (service ServiceTx) updateEdgeStacksAfterRelationChange(previousRelationState *portainer.EndpointRelation, updatedRelationState *portainer.EndpointRelation) {
relations, _ := service.EndpointRelations()
stacksToUpdate := map[portainer.EdgeStackID]bool{}
if previousRelationState != nil {
for stackId, enabled := range previousRelationState.EdgeStacks {
// flag stack for update if stack is not in the updated relation state
// = stack has been removed for this relation
// or this relation has been deleted
if enabled && (updatedRelationState == nil || !updatedRelationState.EdgeStacks[stackId]) {
stacksToUpdate[stackId] = true
}
}
}
if updatedRelationState != nil {
for stackId, enabled := range updatedRelationState.EdgeStacks {
// flag stack for update if stack is not in the previous relation state
// = stack has been added for this relation
if enabled && (previousRelationState == nil || !previousRelationState.EdgeStacks[stackId]) {
stacksToUpdate[stackId] = true
}
}
}
// for each stack referenced by the updated relation
// list how many time this stack is referenced in all relations
// in order to update the stack deployments count
for refStackId, refStackEnabled := range stacksToUpdate {
if refStackEnabled {
numDeployments := 0
for _, r := range relations {
for sId, enabled := range r.EdgeStacks {
if enabled && sId == refStackId {
numDeployments += 1
}
}
}
service.service.updateStackFn(refStackId, func(edgeStack *portainer.EdgeStack) {
edgeStack.NumDeployments = numDeployments
})
}
}
}

View File

@@ -4,7 +4,8 @@ import "errors"
var (
// TODO: i'm pretty sure this needs wrapping at several levels
ErrObjectNotFound = errors.New("object not found inside the database")
ErrWrongDBEdition = errors.New("the Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/")
ErrDBImportFailed = errors.New("importing backup failed")
ErrObjectNotFound = errors.New("object not found inside the database")
ErrWrongDBEdition = errors.New("the Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/")
ErrDBImportFailed = errors.New("importing backup failed")
ErrDatabaseIsUpdating = errors.New("database is currently in updating state. Failed prior upgrade. Please restore from backup or delete the database and restart Portainer")
)

View File

@@ -1,7 +1,11 @@
package extension
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
const (
@@ -20,10 +24,10 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
// err := connection.SetServiceName(BucketName)
// if err != nil {
// return nil, err
// }
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
@@ -33,12 +37,12 @@ func NewService(connection portainer.Connection) (*Service, error) {
// Extension returns a extension by ID
func (service *Service) Extension(ID portainer.ExtensionID) (*portainer.Extension, error) {
var extension portainer.Extension
// identifier := service.connection.ConvertToKey(int(ID))
identifier := service.connection.ConvertToKey(int(ID))
// err := service.connection.GetObject(BucketName, identifier, &extension)
// if err != nil {
// return nil, err
// }
err := service.connection.GetObject(BucketName, identifier, &extension)
if err != nil {
return nil, err
}
return &extension, nil
}
@@ -47,33 +51,31 @@ func (service *Service) Extension(ID portainer.ExtensionID) (*portainer.Extensio
func (service *Service) Extensions() ([]portainer.Extension, error) {
var extensions = make([]portainer.Extension, 0)
// err := service.connection.GetAll(
// BucketName,
// &portainer.Extension{},
// func(obj interface{}) (interface{}, error) {
// extension, ok := obj.(*portainer.Extension)
// if !ok {
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Extension object")
// return nil, fmt.Errorf("Failed to convert to Extension object: %s", obj)
// }
err := service.connection.GetAll(
BucketName,
&portainer.Extension{},
func(obj interface{}) (interface{}, error) {
extension, ok := obj.(*portainer.Extension)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Extension object")
return nil, fmt.Errorf("Failed to convert to Extension object: %s", obj)
}
// extensions = append(extensions, *extension)
extensions = append(extensions, *extension)
// return &portainer.Extension{}, nil
// })
return &portainer.Extension{}, nil
})
return extensions, nil
return extensions, err
}
// Persist persists a extension inside the database.
func (service *Service) Persist(extension *portainer.Extension) error {
// return service.connection.CreateObjectWithId(BucketName, int(extension.ID), extension)
return nil
return service.connection.CreateObjectWithId(BucketName, int(extension.ID), extension)
}
// DeleteExtension deletes a Extension.
func (service *Service) DeleteExtension(ID portainer.ExtensionID) error {
// identifier := service.connection.ConvertToKey(int(ID))
// return service.connection.DeleteObject(BucketName, identifier)
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.DeleteObject(BucketName, identifier)
}

View File

@@ -1,7 +1,11 @@
package fdoprofile
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
const (
@@ -20,10 +24,10 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
// err := connection.SetServiceName(BucketName)
// if err != nil {
// return nil, err
// }
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
@@ -34,62 +38,58 @@ func NewService(connection portainer.Connection) (*Service, error) {
func (service *Service) FDOProfiles() ([]portainer.FDOProfile, error) {
var fdoProfiles = make([]portainer.FDOProfile, 0)
// err := service.connection.GetAll(
// BucketName,
// &portainer.FDOProfile{},
// func(obj interface{}) (interface{}, error) {
// fdoProfile, ok := obj.(*portainer.FDOProfile)
// if !ok {
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to FDOProfile object")
err := service.connection.GetAll(
BucketName,
&portainer.FDOProfile{},
func(obj interface{}) (interface{}, error) {
fdoProfile, ok := obj.(*portainer.FDOProfile)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to FDOProfile object")
// return nil, fmt.Errorf("Failed to convert to FDOProfile object: %s", obj)
// }
// fdoProfiles = append(fdoProfiles, *fdoProfile)
// return &portainer.FDOProfile{}, nil
// })
return nil, fmt.Errorf("Failed to convert to FDOProfile object: %s", obj)
}
fdoProfiles = append(fdoProfiles, *fdoProfile)
return &portainer.FDOProfile{}, nil
})
return fdoProfiles, nil
return fdoProfiles, err
}
// FDOProfile returns an FDO Profile by ID.
func (service *Service) FDOProfile(ID portainer.FDOProfileID) (*portainer.FDOProfile, error) {
var FDOProfile portainer.FDOProfile
// identifier := service.connection.ConvertToKey(int(ID))
identifier := service.connection.ConvertToKey(int(ID))
// err := service.connection.GetObject(BucketName, identifier, &FDOProfile)
// if err != nil {
// return nil, err
// }
err := service.connection.GetObject(BucketName, identifier, &FDOProfile)
if err != nil {
return nil, err
}
return &FDOProfile, nil
}
// Create assign an ID to a new FDO Profile and saves it.
func (service *Service) Create(FDOProfile *portainer.FDOProfile) error {
// return service.connection.CreateObjectWithId(
// BucketName,
// int(FDOProfile.ID),
// FDOProfile,
// )
return nil
return service.connection.CreateObjectWithId(
BucketName,
int(FDOProfile.ID),
FDOProfile,
)
}
// Update updates an FDO Profile.
func (service *Service) Update(ID portainer.FDOProfileID, FDOProfile *portainer.FDOProfile) error {
// identifier := service.connection.ConvertToKey(int(ID))
// return service.connection.UpdateObject(BucketName, identifier, FDOProfile)
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.UpdateObject(BucketName, identifier, FDOProfile)
}
// Delete deletes an FDO Profile.
func (service *Service) Delete(ID portainer.FDOProfileID) error {
// identifier := service.connection.ConvertToKey(int(ID))
// return service.connection.DeleteObject(BucketName, identifier)
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.DeleteObject(BucketName, identifier)
}
// GetNextIdentifier returns the next identifier for a FDO Profile.
func (service *Service) GetNextIdentifier() int {
// return service.connection.GetNextIdentifier(BucketName)
return 0
return service.connection.GetNextIdentifier(BucketName)
}

View File

@@ -1,7 +1,11 @@
package helmuserrepository
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
const (
@@ -20,10 +24,10 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
// err := connection.SetServiceName(BucketName)
// if err != nil {
// return nil, err
// }
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
@@ -34,70 +38,67 @@ func NewService(connection portainer.Connection) (*Service, error) {
func (service *Service) HelmUserRepositories() ([]portainer.HelmUserRepository, error) {
var repos = make([]portainer.HelmUserRepository, 0)
// err := service.connection.GetAll(
// BucketName,
// &portainer.HelmUserRepository{},
// func(obj interface{}) (interface{}, error) {
// r, ok := obj.(*portainer.HelmUserRepository)
// if !ok {
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to HelmUserRepository object")
// return nil, fmt.Errorf("Failed to convert to HelmUserRepository object: %s", obj)
// }
err := service.connection.GetAll(
BucketName,
&portainer.HelmUserRepository{},
func(obj interface{}) (interface{}, error) {
r, ok := obj.(*portainer.HelmUserRepository)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to HelmUserRepository object")
return nil, fmt.Errorf("Failed to convert to HelmUserRepository object: %s", obj)
}
// repos = append(repos, *r)
repos = append(repos, *r)
// return &portainer.HelmUserRepository{}, nil
// })
return &portainer.HelmUserRepository{}, nil
})
return repos, nil
return repos, err
}
// HelmUserRepositoryByUserID return an array containing all the HelmUserRepository objects where the specified userID is present.
func (service *Service) HelmUserRepositoryByUserID(userID portainer.UserID) ([]portainer.HelmUserRepository, error) {
var result = make([]portainer.HelmUserRepository, 0)
// err := service.connection.GetAll(
// BucketName,
// &portainer.HelmUserRepository{},
// func(obj interface{}) (interface{}, error) {
// record, ok := obj.(*portainer.HelmUserRepository)
// if !ok {
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to HelmUserRepository object")
// return nil, fmt.Errorf("Failed to convert to HelmUserRepository object: %s", obj)
// }
err := service.connection.GetAll(
BucketName,
&portainer.HelmUserRepository{},
func(obj interface{}) (interface{}, error) {
record, ok := obj.(*portainer.HelmUserRepository)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to HelmUserRepository object")
return nil, fmt.Errorf("Failed to convert to HelmUserRepository object: %s", obj)
}
// if record.UserID == userID {
// result = append(result, *record)
// }
if record.UserID == userID {
result = append(result, *record)
}
// return &portainer.HelmUserRepository{}, nil
// })
return &portainer.HelmUserRepository{}, nil
})
return result, nil
return result, err
}
// CreateHelmUserRepository creates a new HelmUserRepository object.
func (service *Service) Create(record *portainer.HelmUserRepository) error {
// return service.connection.CreateObject(
// BucketName,
// func(id uint64) (int, interface{}) {
// record.ID = portainer.HelmUserRepositoryID(id)
// return int(record.ID), record
// },
// )
return nil
return service.connection.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
record.ID = portainer.HelmUserRepositoryID(id)
return int(record.ID), record
},
)
}
// UpdateHelmUserRepostory updates an registry.
func (service *Service) UpdateHelmUserRepository(ID portainer.HelmUserRepositoryID, registry *portainer.HelmUserRepository) error {
// identifier := service.connection.ConvertToKey(int(ID))
// return service.connection.UpdateObject(BucketName, identifier, registry)
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.UpdateObject(BucketName, identifier, registry)
}
// DeleteHelmUserRepository deletes an registry.
func (service *Service) DeleteHelmUserRepository(ID portainer.HelmUserRepositoryID) error {
// identifier := service.connection.ConvertToKey(int(ID))
// return service.connection.DeleteObject(BucketName, identifier)
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.DeleteObject(BucketName, identifier)
}

View File

@@ -3,28 +3,22 @@ package dataservices
// "github.com/portainer/portainer/api/dataservices"
import (
"io"
"time"
"github.com/portainer/portainer/api/database/models"
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/portainer/portainer/api/edgetypes"
portainer "github.com/portainer/portainer/api"
)
type (
// DataStore defines the interface to manage the data
DataStore interface {
Open() (newStore bool, err error)
Init() error
Close() error
CheckCurrentEdition() error
DataStoreTx interface {
IsErrObjectNotFound(err error) bool
CustomTemplate() CustomTemplateService
EdgeGroup() EdgeGroupService
EdgeJob() EdgeJobService
EdgeStack() EdgeStackService
EdgeUpdateSchedule() EdgeUpdateScheduleService
Endpoint() EndpointService
EndpointGroup() EndpointGroupService
EndpointRelation() EndpointRelationService
@@ -47,6 +41,22 @@ type (
Webhook() WebhookService
}
// DataStore defines the interface to manage the data
DataStore interface {
Open() (newStore bool, err error)
Init() error
Close() error
UpdateTx(func(DataStoreTx) error) error
ViewTx(func(DataStoreTx) error) error
MigrateData() error
Rollback(force bool) error
CheckCurrentEdition() error
BackupTo(w io.Writer) error
Export(filename string) (err error)
DataStoreTx
}
// CustomTemplateService represents a service to manage custom templates
CustomTemplateService interface {
GetNextIdentifier() int
@@ -55,6 +65,7 @@ type (
Create(customTemplate *portainer.CustomTemplate) error
UpdateCustomTemplate(ID portainer.CustomTemplateID, customTemplate *portainer.CustomTemplate) error
DeleteCustomTemplate(ID portainer.CustomTemplateID) error
BucketName() string
}
// EdgeGroupService represents a service to manage Edge groups
@@ -63,7 +74,9 @@ type (
EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGroup, error)
Create(group *portainer.EdgeGroup) error
UpdateEdgeGroup(ID portainer.EdgeGroupID, group *portainer.EdgeGroup) error
UpdateEdgeGroupFunc(ID portainer.EdgeGroupID, updateFunc func(group *portainer.EdgeGroup)) error
DeleteEdgeGroup(ID portainer.EdgeGroupID) error
BucketName() string
}
// EdgeJobService represents a service to manage Edge jobs
@@ -72,38 +85,37 @@ type (
EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, error)
Create(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error
UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error
UpdateEdgeJobFunc(ID portainer.EdgeJobID, updateFunc func(edgeJob *portainer.EdgeJob)) error
DeleteEdgeJob(ID portainer.EdgeJobID) error
GetNextIdentifier() int
}
EdgeUpdateScheduleService interface {
ActiveSchedule(environmentID portainer.EndpointID) *edgetypes.EndpointUpdateScheduleRelation
ActiveSchedules(environmentIDs []portainer.EndpointID) []edgetypes.EndpointUpdateScheduleRelation
List() ([]edgetypes.UpdateSchedule, error)
Item(ID edgetypes.UpdateScheduleID) (*edgetypes.UpdateSchedule, error)
Create(edgeUpdateSchedule *edgetypes.UpdateSchedule) error
Update(ID edgetypes.UpdateScheduleID, edgeUpdateSchedule *edgetypes.UpdateSchedule) error
Delete(ID edgetypes.UpdateScheduleID) error
BucketName() string
}
// EdgeStackService represents a service to manage Edge stacks
EdgeStackService interface {
EdgeStacks() ([]portainer.EdgeStack, error)
EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStack, error)
EdgeStackVersion(ID portainer.EdgeStackID) (int, bool)
Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error
UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error
UpdateEdgeStackFunc(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error
DeleteEdgeStack(ID portainer.EdgeStackID) error
GetNextIdentifier() int
BucketName() string
}
// EndpointService represents a service for managing environment(endpoint) data
EndpointService interface {
Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error)
EndpointIDByEdgeID(edgeID string) (portainer.EndpointID, bool)
Heartbeat(endpointID portainer.EndpointID) (int64, bool)
UpdateHeartbeat(endpointID portainer.EndpointID)
Endpoints() ([]portainer.Endpoint, error)
Create(endpoint *portainer.Endpoint) error
UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error
DeleteEndpoint(ID portainer.EndpointID) error
GetNextIdentifier() int
BucketName() string
}
// EndpointGroupService represents a service for managing environment(endpoint) group data
@@ -113,6 +125,7 @@ type (
Create(group *portainer.EndpointGroup) error
UpdateEndpointGroup(ID portainer.EndpointGroupID, group *portainer.EndpointGroup) error
DeleteEndpointGroup(ID portainer.EndpointGroupID) error
BucketName() string
}
// EndpointRelationService represents a service for managing environment(endpoint) relations data
@@ -122,6 +135,7 @@ type (
Create(endpointRelation *portainer.EndpointRelation) error
UpdateEndpointRelation(EndpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error
DeleteEndpointRelation(EndpointID portainer.EndpointID) error
BucketName() string
}
// FDOProfileService represents a service to manage FDO Profiles
@@ -132,6 +146,7 @@ type (
Update(ID portainer.FDOProfileID, FDOProfile *portainer.FDOProfile) error
Delete(ID portainer.FDOProfileID) error
GetNextIdentifier() int
BucketName() string
}
// HelmUserRepositoryService represents a service to manage HelmUserRepositories
@@ -141,6 +156,7 @@ type (
Create(record *portainer.HelmUserRepository) error
UpdateHelmUserRepository(ID portainer.HelmUserRepositoryID, repository *portainer.HelmUserRepository) error
DeleteHelmUserRepository(ID portainer.HelmUserRepositoryID) error
BucketName() string
}
// JWTService represents a service for managing JWT tokens
@@ -159,6 +175,7 @@ type (
Create(registry *portainer.Registry) error
UpdateRegistry(ID portainer.RegistryID, registry *portainer.Registry) error
DeleteRegistry(ID portainer.RegistryID) error
BucketName() string
}
// ResourceControlService represents a service for managing resource control data
@@ -169,6 +186,7 @@ type (
Create(rc *portainer.ResourceControl) error
UpdateResourceControl(ID portainer.ResourceControlID, resourceControl *portainer.ResourceControl) error
DeleteResourceControl(ID portainer.ResourceControlID) error
BucketName() string
}
// RoleService represents a service for managing user roles
@@ -177,6 +195,7 @@ type (
Roles() ([]portainer.Role, error)
Create(role *portainer.Role) error
UpdateRole(ID portainer.RoleID, role *portainer.Role) error
BucketName() string
}
// APIKeyRepositoryService
@@ -193,7 +212,7 @@ type (
SettingsService interface {
Settings() (*portainer.Settings, error)
UpdateSettings(settings *portainer.Settings) error
IsFeatureFlagEnabled(feature portainer.Feature) bool
BucketName() string
}
SnapshotService interface {
@@ -202,12 +221,14 @@ type (
UpdateSnapshot(snapshot *portainer.Snapshot) error
DeleteSnapshot(endpointID portainer.EndpointID) error
Create(snapshot *portainer.Snapshot) error
BucketName() string
}
// SSLSettingsService represents a service for managing application settings
SSLSettingsService interface {
Settings() (*portainer.SSLSettings, error)
UpdateSettings(settings *portainer.SSLSettings) error
BucketName() string
}
// StackService represents a service for managing stack data
@@ -222,6 +243,7 @@ type (
GetNextIdentifier() int
StackByWebhookID(ID string) (*portainer.Stack, error)
RefreshableStacks() ([]portainer.Stack, error)
BucketName() string
}
// TagService represents a service for managing tag data
@@ -230,7 +252,9 @@ type (
Tag(ID portainer.TagID) (*portainer.Tag, error)
Create(tag *portainer.Tag) error
UpdateTag(ID portainer.TagID, tag *portainer.Tag) error
UpdateTagFunc(ID portainer.TagID, updateFunc func(tag *portainer.Tag)) error
DeleteTag(ID portainer.TagID) error
BucketName() string
}
// TeamService represents a service for managing user data
@@ -241,6 +265,7 @@ type (
Create(team *portainer.Team) error
UpdateTeam(ID portainer.TeamID, team *portainer.Team) error
DeleteTeam(ID portainer.TeamID) error
BucketName() string
}
// TeamMembershipService represents a service for managing team membership data
@@ -254,12 +279,15 @@ type (
DeleteTeamMembership(ID portainer.TeamMembershipID) error
DeleteTeamMembershipByUserID(userID portainer.UserID) error
DeleteTeamMembershipByTeamID(teamID portainer.TeamID) error
BucketName() string
DeleteTeamMembershipByTeamIDAndUserID(teamID portainer.TeamID, userID portainer.UserID) error
}
// TunnelServerService represents a service for managing data associated to the tunnel server
TunnelServerService interface {
Info() (*portainer.TunnelServerInfo, error)
UpdateInfo(info *portainer.TunnelServerInfo) error
BucketName() string
}
// UserService represents a service for managing user data
@@ -271,15 +299,16 @@ type (
Create(user *portainer.User) error
UpdateUser(ID portainer.UserID, user *portainer.User) error
DeleteUser(ID portainer.UserID) error
BucketName() string
}
// VersionService represents a service for managing version data
VersionService interface {
DBVersion() (int, error)
Edition() (portainer.SoftwareEdition, error)
InstanceID() (string, error)
StoreDBVersion(version int) error
StoreInstanceID(ID string) error
UpdateInstanceID(ID string) error
Version() (*models.Version, error)
UpdateVersion(*models.Version) error
}
// WebhookService represents a service for managing webhook data.
@@ -291,6 +320,7 @@ type (
WebhookByResourceID(resourceID string) (*portainer.Webhook, error)
WebhookByToken(token string) (*portainer.Webhook, error)
DeleteWebhook(ID portainer.WebhookID) error
BucketName() string
}
)

View File

@@ -1,7 +1,11 @@
package registry
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
const (
@@ -20,10 +24,10 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
// err := connection.SetServiceName(BucketName)
// if err != nil {
// return nil, err
// }
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
@@ -33,12 +37,12 @@ func NewService(connection portainer.Connection) (*Service, error) {
// Registry returns an registry by ID.
func (service *Service) Registry(ID portainer.RegistryID) (*portainer.Registry, error) {
var registry portainer.Registry
// identifier := service.connection.ConvertToKey(int(ID))
identifier := service.connection.ConvertToKey(int(ID))
// err := service.connection.GetObject(BucketName, identifier, &registry)
// if err != nil {
// return nil, err
// }
err := service.connection.GetObject(BucketName, identifier, &registry)
if err != nil {
return nil, err
}
return &registry, nil
}
@@ -47,46 +51,43 @@ func (service *Service) Registry(ID portainer.RegistryID) (*portainer.Registry,
func (service *Service) Registries() ([]portainer.Registry, error) {
var registries = make([]portainer.Registry, 0)
// err := service.connection.GetAll(
// BucketName,
// &portainer.Registry{},
// func(obj interface{}) (interface{}, error) {
// registry, ok := obj.(*portainer.Registry)
// if !ok {
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Registry object")
// return nil, fmt.Errorf("Failed to convert to Registry object: %s", obj)
// }
err := service.connection.GetAll(
BucketName,
&portainer.Registry{},
func(obj interface{}) (interface{}, error) {
registry, ok := obj.(*portainer.Registry)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Registry object")
return nil, fmt.Errorf("Failed to convert to Registry object: %s", obj)
}
// registries = append(registries, *registry)
registries = append(registries, *registry)
// return &portainer.Registry{}, nil
// })
return &portainer.Registry{}, nil
})
return registries, nil
return registries, err
}
// CreateRegistry creates a new registry.
func (service *Service) Create(registry *portainer.Registry) error {
// return service.connection.CreateObject(
// BucketName,
// func(id uint64) (int, interface{}) {
// registry.ID = portainer.RegistryID(id)
// return int(registry.ID), registry
// },
// )
return nil
return service.connection.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
registry.ID = portainer.RegistryID(id)
return int(registry.ID), registry
},
)
}
// UpdateRegistry updates an registry.
func (service *Service) UpdateRegistry(ID portainer.RegistryID, registry *portainer.Registry) error {
// identifier := service.connection.ConvertToKey(int(ID))
// return service.connection.UpdateObject(BucketName, identifier, registry)
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.UpdateObject(BucketName, identifier, registry)
}
// DeleteRegistry deletes an registry.
func (service *Service) DeleteRegistry(ID portainer.RegistryID) error {
// identifier := service.connection.ConvertToKey(int(ID))
// return service.connection.DeleteObject(BucketName, identifier)
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.DeleteObject(BucketName, identifier)
}

View File

@@ -1,7 +1,11 @@
package resourcecontrol
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
const (
@@ -20,10 +24,10 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
// err := connection.SetServiceName(BucketName)
// if err != nil {
// return nil, err
// }
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
@@ -33,12 +37,12 @@ func NewService(connection portainer.Connection) (*Service, error) {
// ResourceControl returns a ResourceControl object by ID
func (service *Service) ResourceControl(ID portainer.ResourceControlID) (*portainer.ResourceControl, error) {
var resourceControl portainer.ResourceControl
// identifier := service.connection.ConvertToKey(int(ID))
identifier := service.connection.ConvertToKey(int(ID))
// err := service.connection.GetObject(BucketName, identifier, &resourceControl)
// if err != nil {
// return nil, err
// }
err := service.connection.GetObject(BucketName, identifier, &resourceControl)
if err != nil {
return nil, err
}
return &resourceControl, nil
}
@@ -47,83 +51,80 @@ func (service *Service) ResourceControl(ID portainer.ResourceControlID) (*portai
// to the main ResourceID or in SubResourceIDs. It also performs a check on the resource type. Return nil
// if no ResourceControl was found.
func (service *Service) ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
// var resourceControl *portainer.ResourceControl
// stop := fmt.Errorf("ok")
// err := service.connection.GetAll(
// BucketName,
// &portainer.ResourceControl{},
// func(obj interface{}) (interface{}, error) {
// rc, ok := obj.(*portainer.ResourceControl)
// if !ok {
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to ResourceControl object")
// return nil, fmt.Errorf("Failed to convert to ResourceControl object: %s", obj)
// }
var resourceControl *portainer.ResourceControl
stop := fmt.Errorf("ok")
err := service.connection.GetAll(
BucketName,
&portainer.ResourceControl{},
func(obj interface{}) (interface{}, error) {
rc, ok := obj.(*portainer.ResourceControl)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to ResourceControl object")
return nil, fmt.Errorf("Failed to convert to ResourceControl object: %s", obj)
}
// if rc.ResourceID == resourceID && rc.Type == resourceType {
// resourceControl = rc
// return nil, stop
// }
if rc.ResourceID == resourceID && rc.Type == resourceType {
resourceControl = rc
return nil, stop
}
// for _, subResourceID := range rc.SubResourceIDs {
// if subResourceID == resourceID {
// resourceControl = rc
// return nil, stop
// }
// }
for _, subResourceID := range rc.SubResourceIDs {
if subResourceID == resourceID {
resourceControl = rc
return nil, stop
}
}
// return &portainer.ResourceControl{}, nil
// })
// if err == stop {
// return resourceControl, nil
// }
return &portainer.ResourceControl{}, nil
})
if err == stop {
return resourceControl, nil
}
return nil, nil
return nil, err
}
// ResourceControls returns all the ResourceControl objects
func (service *Service) ResourceControls() ([]portainer.ResourceControl, error) {
var rcs = make([]portainer.ResourceControl, 0)
// err := service.connection.GetAll(
// BucketName,
// &portainer.ResourceControl{},
// func(obj interface{}) (interface{}, error) {
// rc, ok := obj.(*portainer.ResourceControl)
// if !ok {
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to ResourceControl object")
// return nil, fmt.Errorf("Failed to convert to ResourceControl object: %s", obj)
// }
err := service.connection.GetAll(
BucketName,
&portainer.ResourceControl{},
func(obj interface{}) (interface{}, error) {
rc, ok := obj.(*portainer.ResourceControl)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to ResourceControl object")
return nil, fmt.Errorf("Failed to convert to ResourceControl object: %s", obj)
}
// rcs = append(rcs, *rc)
rcs = append(rcs, *rc)
// return &portainer.ResourceControl{}, nil
// })
return &portainer.ResourceControl{}, nil
})
return rcs, nil
return rcs, err
}
// CreateResourceControl creates a new ResourceControl object
func (service *Service) Create(resourceControl *portainer.ResourceControl) error {
// return service.connection.CreateObject(
// BucketName,
// func(id uint64) (int, interface{}) {
// resourceControl.ID = portainer.ResourceControlID(id)
// return int(resourceControl.ID), resourceControl
// },
// )
return nil
return service.connection.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
resourceControl.ID = portainer.ResourceControlID(id)
return int(resourceControl.ID), resourceControl
},
)
}
// UpdateResourceControl saves a ResourceControl object.
func (service *Service) UpdateResourceControl(ID portainer.ResourceControlID, resourceControl *portainer.ResourceControl) error {
// identifier := service.connection.ConvertToKey(int(ID))
// return service.connection.UpdateObject(BucketName, identifier, resourceControl)
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.UpdateObject(BucketName, identifier, resourceControl)
}
// DeleteResourceControl deletes a ResourceControl object by ID
func (service *Service) DeleteResourceControl(ID portainer.ResourceControlID) error {
// identifier := service.connection.ConvertToKey(int(ID))
// return service.connection.DeleteObject(BucketName, identifier)
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.DeleteObject(BucketName, identifier)
}

View File

@@ -1,7 +1,11 @@
package role
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
const (
@@ -20,10 +24,10 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
// err := connection.SetServiceName(BucketName)
// if err != nil {
// return nil, err
// }
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
@@ -33,12 +37,12 @@ func NewService(connection portainer.Connection) (*Service, error) {
// Role returns a Role by ID
func (service *Service) Role(ID portainer.RoleID) (*portainer.Role, error) {
var set portainer.Role
// identifier := service.connection.ConvertToKey(int(ID))
identifier := service.connection.ConvertToKey(int(ID))
// err := service.connection.GetObject(BucketName, identifier, &set)
// if err != nil {
// return nil, err
// }
err := service.connection.GetObject(BucketName, identifier, &set)
if err != nil {
return nil, err
}
return &set, nil
}
@@ -47,39 +51,37 @@ func (service *Service) Role(ID portainer.RoleID) (*portainer.Role, error) {
func (service *Service) Roles() ([]portainer.Role, error) {
var sets = make([]portainer.Role, 0)
// err := service.connection.GetAll(
// BucketName,
// &portainer.Role{},
// func(obj interface{}) (interface{}, error) {
// set, ok := obj.(*portainer.Role)
// if !ok {
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Role object")
// return nil, fmt.Errorf("Failed to convert to Role object: %s", obj)
// }
err := service.connection.GetAll(
BucketName,
&portainer.Role{},
func(obj interface{}) (interface{}, error) {
set, ok := obj.(*portainer.Role)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Role object")
return nil, fmt.Errorf("Failed to convert to Role object: %s", obj)
}
// sets = append(sets, *set)
sets = append(sets, *set)
// return &portainer.Role{}, nil
// })
return &portainer.Role{}, nil
})
return sets, nil
return sets, err
}
// CreateRole creates a new Role.
func (service *Service) Create(role *portainer.Role) error {
// return service.connection.CreateObject(
// BucketName,
// func(id uint64) (int, interface{}) {
// role.ID = portainer.RoleID(id)
// return int(role.ID), role
// },
// )
return nil
return service.connection.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
role.ID = portainer.RoleID(id)
return int(role.ID), role
},
)
}
// UpdateRole updates a role.
func (service *Service) UpdateRole(ID portainer.RoleID, role *portainer.Role) error {
// identifier := service.connection.ConvertToKey(int(ID))
// return service.connection.UpdateObject(BucketName, identifier, role)
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.UpdateObject(BucketName, identifier, role)
}

View File

@@ -1,7 +1,11 @@
package schedule
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
const (
@@ -20,10 +24,10 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
// err := connection.SetServiceName(BucketName)
// if err != nil {
// return nil, err
// }
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
@@ -33,33 +37,48 @@ func NewService(connection portainer.Connection) (*Service, error) {
// Schedule returns a schedule by ID.
func (service *Service) Schedule(ID portainer.ScheduleID) (*portainer.Schedule, error) {
var schedule portainer.Schedule
// identifier := service.connection.ConvertToKey(int(ID))
identifier := service.connection.ConvertToKey(int(ID))
// err := service.connection.GetObject(BucketName, identifier, &schedule)
// if err != nil {
// return nil, err
// }
err := service.connection.GetObject(BucketName, identifier, &schedule)
if err != nil {
return nil, err
}
return &schedule, nil
}
// UpdateSchedule updates a schedule.
func (service *Service) UpdateSchedule(ID portainer.ScheduleID, schedule *portainer.Schedule) error {
// identifier := service.connection.ConvertToKey(int(ID))
// return service.connection.UpdateObject(BucketName, identifier, schedule)
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.UpdateObject(BucketName, identifier, schedule)
}
// DeleteSchedule deletes a schedule.
func (service *Service) DeleteSchedule(ID portainer.ScheduleID) error {
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.DeleteObject(BucketName, identifier)
}
// Schedules return a array containing all the schedules.
func (service *Service) Schedules() ([]portainer.Schedule, error) {
var schedules = make([]portainer.Schedule, 0)
return schedules, nil
err := service.connection.GetAll(
BucketName,
&portainer.Schedule{},
func(obj interface{}) (interface{}, error) {
schedule, ok := obj.(*portainer.Schedule)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Schedule object")
return nil, fmt.Errorf("Failed to convert to Schedule object: %s", obj)
}
schedules = append(schedules, *schedule)
return &portainer.Schedule{}, nil
})
return schedules, err
}
// SchedulesByJobType return a array containing all the schedules
@@ -67,15 +86,32 @@ func (service *Service) Schedules() ([]portainer.Schedule, error) {
func (service *Service) SchedulesByJobType(jobType portainer.JobType) ([]portainer.Schedule, error) {
var schedules = make([]portainer.Schedule, 0)
return schedules, nil
err := service.connection.GetAll(
BucketName,
&portainer.Schedule{},
func(obj interface{}) (interface{}, error) {
schedule, ok := obj.(*portainer.Schedule)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Schedule object")
return nil, fmt.Errorf("Failed to convert to Schedule object: %s", obj)
}
if schedule.JobType == jobType {
schedules = append(schedules, *schedule)
}
return &portainer.Schedule{}, nil
})
return schedules, err
}
// Create assign an ID to a new schedule and saves it.
func (service *Service) CreateSchedule(schedule *portainer.Schedule) error {
return nil
return service.connection.CreateObjectWithId(BucketName, int(schedule.ID), schedule)
}
// GetNextIdentifier returns the next identifier for a schedule.
func (service *Service) GetNextIdentifier() int {
return 0
return service.connection.GetNextIdentifier(BucketName)
}

View File

@@ -21,6 +21,11 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
@@ -30,14 +35,15 @@ func NewService(connection portainer.Connection) (*Service, error) {
func (service *Service) Settings() (*portainer.Settings, error) {
var settings portainer.Settings
err := service.connection.GetObject(BucketName, []byte(settingsKey), &settings)
if err != nil {
return nil, err
}
return &settings, nil
}
// UpdateSettings persists a Settings object.
func (service *Service) UpdateSettings(settings *portainer.Settings) error {
return nil
}
func (service *Service) IsFeatureFlagEnabled(feature portainer.Feature) bool {
return false
return service.connection.UpdateObject(BucketName, []byte(settingsKey), settings)
}

View File

@@ -1,7 +1,11 @@
package snapshot
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
const (
@@ -17,14 +21,31 @@ func (service *Service) BucketName() string {
}
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
}
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
return ServiceTx{
service: service,
tx: tx,
}
}
func (service *Service) Snapshot(endpointID portainer.EndpointID) (*portainer.Snapshot, error) {
var snapshot portainer.Snapshot
identifier := service.connection.ConvertToKey(int(endpointID))
err := service.connection.GetObject(BucketName, identifier, &snapshot)
if err != nil {
return nil, err
}
return &snapshot, nil
}
@@ -32,17 +53,32 @@ func (service *Service) Snapshot(endpointID portainer.EndpointID) (*portainer.Sn
func (service *Service) Snapshots() ([]portainer.Snapshot, error) {
var snapshots = make([]portainer.Snapshot, 0)
return snapshots, nil
err := service.connection.GetAllWithJsoniter(
BucketName,
&portainer.Snapshot{},
func(obj interface{}) (interface{}, error) {
snapshot, ok := obj.(*portainer.Snapshot)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Snapshot object")
return nil, fmt.Errorf("failed to convert to Snapshot object: %s", obj)
}
snapshots = append(snapshots, *snapshot)
return &portainer.Snapshot{}, nil
})
return snapshots, err
}
func (service *Service) UpdateSnapshot(snapshot *portainer.Snapshot) error {
return nil
identifier := service.connection.ConvertToKey(int(snapshot.EndpointID))
return service.connection.UpdateObject(BucketName, identifier, snapshot)
}
func (service *Service) DeleteSnapshot(endpointID portainer.EndpointID) error {
return nil
identifier := service.connection.ConvertToKey(int(endpointID))
return service.connection.DeleteObject(BucketName, identifier)
}
func (service *Service) Create(snapshot *portainer.Snapshot) error {
return nil
return service.connection.CreateObjectWithId(BucketName, int(snapshot.EndpointID), snapshot)
}

View File

@@ -0,0 +1,63 @@
package snapshot
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
type ServiceTx struct {
service *Service
tx portainer.Transaction
}
func (service ServiceTx) BucketName() string {
return BucketName
}
func (service ServiceTx) Snapshot(endpointID portainer.EndpointID) (*portainer.Snapshot, error) {
var snapshot portainer.Snapshot
identifier := service.service.connection.ConvertToKey(int(endpointID))
err := service.tx.GetObject(BucketName, identifier, &snapshot)
if err != nil {
return nil, err
}
return &snapshot, nil
}
func (service ServiceTx) Snapshots() ([]portainer.Snapshot, error) {
var snapshots = make([]portainer.Snapshot, 0)
err := service.tx.GetAllWithJsoniter(
BucketName,
&portainer.Snapshot{},
func(obj interface{}) (interface{}, error) {
snapshot, ok := obj.(*portainer.Snapshot)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Snapshot object")
return nil, fmt.Errorf("failed to convert to Snapshot object: %s", obj)
}
snapshots = append(snapshots, *snapshot)
return &portainer.Snapshot{}, nil
})
return snapshots, err
}
func (service ServiceTx) UpdateSnapshot(snapshot *portainer.Snapshot) error {
identifier := service.service.connection.ConvertToKey(int(snapshot.EndpointID))
return service.tx.UpdateObject(BucketName, identifier, snapshot)
}
func (service ServiceTx) DeleteSnapshot(endpointID portainer.EndpointID) error {
identifier := service.service.connection.ConvertToKey(int(endpointID))
return service.tx.DeleteObject(BucketName, identifier)
}
func (service ServiceTx) Create(snapshot *portainer.Snapshot) error {
return service.tx.CreateObjectWithId(BucketName, int(snapshot.EndpointID), snapshot)
}

View File

@@ -21,6 +21,11 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
@@ -30,10 +35,15 @@ func NewService(connection portainer.Connection) (*Service, error) {
func (service *Service) Settings() (*portainer.SSLSettings, error) {
var settings portainer.SSLSettings
err := service.connection.GetObject(BucketName, []byte(key), &settings)
if err != nil {
return nil, err
}
return &settings, nil
}
// UpdateSettings persists a SSLSettings object.
func (service *Service) UpdateSettings(settings *portainer.SSLSettings) error {
return nil
return service.connection.UpdateObject(BucketName, []byte(key), settings)
}

View File

@@ -1,7 +1,13 @@
package stack
import (
"fmt"
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/rs/zerolog/log"
)
const (
@@ -20,6 +26,10 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
@@ -29,56 +39,171 @@ func NewService(connection portainer.Connection) (*Service, error) {
// Stack returns a stack object by ID.
func (service *Service) Stack(ID portainer.StackID) (*portainer.Stack, error) {
var stack portainer.Stack
identifier := service.connection.ConvertToKey(int(ID))
err := service.connection.GetObject(BucketName, identifier, &stack)
if err != nil {
return nil, err
}
return &stack, nil
}
// StackByName returns a stack object by name.
func (service *Service) StackByName(name string) (*portainer.Stack, error) {
return nil, nil
var s *portainer.Stack
stop := fmt.Errorf("ok")
err := service.connection.GetAll(
BucketName,
&portainer.Stack{},
func(obj interface{}) (interface{}, error) {
stack, ok := obj.(*portainer.Stack)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Stack object")
return nil, fmt.Errorf("Failed to convert to Stack object: %s", obj)
}
if stack.Name == name {
s = stack
return nil, stop
}
return &portainer.Stack{}, nil
})
if err == stop {
return s, nil
}
if err == nil {
return nil, errors.ErrObjectNotFound
}
return nil, err
}
// Stacks returns an array containing all the stacks with same name
func (service *Service) StacksByName(name string) ([]portainer.Stack, error) {
var stacks = make([]portainer.Stack, 0)
return stacks, nil
err := service.connection.GetAll(
BucketName,
&portainer.Stack{},
func(obj interface{}) (interface{}, error) {
stack, ok := obj.(portainer.Stack)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Stack object")
return nil, fmt.Errorf("Failed to convert to Stack object: %s", obj)
}
if stack.Name == name {
stacks = append(stacks, stack)
}
return &portainer.Stack{}, nil
})
return stacks, err
}
// Stacks returns an array containing all the stacks.
func (service *Service) Stacks() ([]portainer.Stack, error) {
var stacks = make([]portainer.Stack, 0)
return stacks, nil
err := service.connection.GetAll(
BucketName,
&portainer.Stack{},
func(obj interface{}) (interface{}, error) {
stack, ok := obj.(*portainer.Stack)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Stack object")
return nil, fmt.Errorf("Failed to convert to Stack object: %s", obj)
}
stacks = append(stacks, *stack)
return &portainer.Stack{}, nil
})
return stacks, err
}
// GetNextIdentifier returns the next identifier for a stack.
func (service *Service) GetNextIdentifier() int {
return 0
return service.connection.GetNextIdentifier(BucketName)
}
// CreateStack creates a new stack.
func (service *Service) Create(stack *portainer.Stack) error {
return nil
return service.connection.CreateObjectWithId(BucketName, int(stack.ID), stack)
}
// UpdateStack updates a stack.
func (service *Service) UpdateStack(ID portainer.StackID, stack *portainer.Stack) error {
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.UpdateObject(BucketName, identifier, stack)
}
// DeleteStack deletes a stack.
func (service *Service) DeleteStack(ID portainer.StackID) error {
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.DeleteObject(BucketName, identifier)
}
// StackByWebhookID returns a pointer to a stack object by webhook ID.
// It returns nil, errors.ErrObjectNotFound if there's no stack associated with the webhook ID.
func (service *Service) StackByWebhookID(id string) (*portainer.Stack, error) {
var s *portainer.Stack
stop := fmt.Errorf("ok")
err := service.connection.GetAll(
BucketName,
&portainer.Stack{},
func(obj interface{}) (interface{}, error) {
var ok bool
s, ok = obj.(*portainer.Stack)
return nil, nil
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Stack object")
return &portainer.Stack{}, nil
}
if s.AutoUpdate != nil && strings.EqualFold(s.AutoUpdate.Webhook, id) {
return nil, stop
}
return &portainer.Stack{}, nil
})
if err == stop {
return s, nil
}
if err == nil {
return nil, errors.ErrObjectNotFound
}
return nil, err
}
// RefreshableStacks returns stacks that are configured for a periodic update
func (service *Service) RefreshableStacks() ([]portainer.Stack, error) {
stacks := make([]portainer.Stack, 0)
return stacks, nil
err := service.connection.GetAll(
BucketName,
&portainer.Stack{},
func(obj interface{}) (interface{}, error) {
stack, ok := obj.(*portainer.Stack)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Stack object")
return nil, fmt.Errorf("Failed to convert to Stack object: %s", obj)
}
if stack.AutoUpdate != nil && stack.AutoUpdate.Interval != "" {
stacks = append(stacks, *stack)
}
return &portainer.Stack{}, nil
})
return stacks, err
}

View File

@@ -68,13 +68,13 @@ func (b *stackBuilder) createNewStack(webhookID string) portainer.Stack {
if webhookID == "" {
if b.count%2 == 0 {
stack.AutoUpdate = &portainer.StackAutoUpdate{
stack.AutoUpdate = &portainer.AutoUpdateSettings{
Interval: "",
Webhook: "",
}
} // else keep AutoUpdate nil
} else {
stack.AutoUpdate = &portainer.StackAutoUpdate{Webhook: webhookID}
stack.AutoUpdate = &portainer.AutoUpdateSettings{Webhook: webhookID}
}
err := b.store.StackService.Create(&stack)
@@ -91,8 +91,8 @@ func Test_RefreshableStacks(t *testing.T) {
defer teardown()
staticStack := portainer.Stack{ID: 1}
stackWithWebhook := portainer.Stack{ID: 2, AutoUpdate: &portainer.StackAutoUpdate{Webhook: "webhook"}}
refreshableStack := portainer.Stack{ID: 3, AutoUpdate: &portainer.StackAutoUpdate{Interval: "1m"}}
stackWithWebhook := portainer.Stack{ID: 2, AutoUpdate: &portainer.AutoUpdateSettings{Webhook: "webhook"}}
refreshableStack := portainer.Stack{ID: 3, AutoUpdate: &portainer.AutoUpdateSettings{Interval: "1m"}}
for _, stack := range []*portainer.Stack{&staticStack, &stackWithWebhook, &refreshableStack} {
err := store.Stack().Create(stack)

View File

@@ -1,7 +1,11 @@
package tag
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
const (
@@ -20,34 +24,87 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
}
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
return ServiceTx{
service: service,
tx: tx,
}
}
// Tags return an array containing all the tags.
func (service *Service) Tags() ([]portainer.Tag, error) {
var tags = make([]portainer.Tag, 0)
return tags, nil
err := service.connection.GetAll(
BucketName,
&portainer.Tag{},
func(obj interface{}) (interface{}, error) {
tag, ok := obj.(*portainer.Tag)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Tag object")
return nil, fmt.Errorf("Failed to convert to Tag object: %s", obj)
}
tags = append(tags, *tag)
return &portainer.Tag{}, nil
})
return tags, err
}
// Tag returns a tag by ID.
func (service *Service) Tag(ID portainer.TagID) (*portainer.Tag, error) {
var tag portainer.Tag
identifier := service.connection.ConvertToKey(int(ID))
err := service.connection.GetObject(BucketName, identifier, &tag)
if err != nil {
return nil, err
}
return &tag, nil
}
// CreateTag creates a new tag.
func (service *Service) Create(tag *portainer.Tag) error {
return nil
return service.connection.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
tag.ID = portainer.TagID(id)
return int(tag.ID), tag
},
)
}
// UpdateTag updates a tag.
// Deprecated: Use UpdateTagFunc instead.
func (service *Service) UpdateTag(ID portainer.TagID, tag *portainer.Tag) error {
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.UpdateObject(BucketName, identifier, tag)
}
// UpdateTagFunc updates a tag inside a transaction avoiding data races.
func (service *Service) UpdateTagFunc(ID portainer.TagID, updateFunc func(tag *portainer.Tag)) error {
id := service.connection.ConvertToKey(int(ID))
tag := &portainer.Tag{}
return service.connection.UpdateObjectFunc(BucketName, id, tag, func() {
updateFunc(tag)
})
}
// DeleteTag deletes a tag.
func (service *Service) DeleteTag(ID portainer.TagID) error {
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.DeleteObject(BucketName, identifier)
}

View File

@@ -0,0 +1,82 @@
package tag
import (
"errors"
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
type ServiceTx struct {
service *Service
tx portainer.Transaction
}
func (service ServiceTx) BucketName() string {
return BucketName
}
// Tags return an array containing all the tags.
func (service ServiceTx) Tags() ([]portainer.Tag, error) {
var tags = make([]portainer.Tag, 0)
err := service.tx.GetAll(
BucketName,
&portainer.Tag{},
func(obj interface{}) (interface{}, error) {
tag, ok := obj.(*portainer.Tag)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Tag object")
return nil, fmt.Errorf("failed to convert to Tag object: %s", obj)
}
tags = append(tags, *tag)
return &portainer.Tag{}, nil
})
return tags, err
}
// Tag returns a tag by ID.
func (service ServiceTx) Tag(ID portainer.TagID) (*portainer.Tag, error) {
var tag portainer.Tag
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.GetObject(BucketName, identifier, &tag)
if err != nil {
return nil, err
}
return &tag, nil
}
// CreateTag creates a new tag.
func (service ServiceTx) Create(tag *portainer.Tag) error {
return service.tx.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
tag.ID = portainer.TagID(id)
return int(tag.ID), tag
},
)
}
// UpdateTag updates a tag
func (service ServiceTx) UpdateTag(ID portainer.TagID, tag *portainer.Tag) error {
identifier := service.service.connection.ConvertToKey(int(ID))
return service.tx.UpdateObject(BucketName, identifier, tag)
}
// UpdateTagFunc is a no-op inside a transaction
func (service ServiceTx) UpdateTagFunc(ID portainer.TagID, updateFunc func(tag *portainer.Tag)) error {
return errors.New("cannot be called inside a transaction")
}
// DeleteTag deletes a tag.
func (service ServiceTx) DeleteTag(ID portainer.TagID) error {
identifier := service.service.connection.ConvertToKey(int(ID))
return service.tx.DeleteObject(BucketName, identifier)
}

View File

@@ -1,7 +1,13 @@
package team
import (
"fmt"
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/rs/zerolog/log"
)
const (
@@ -20,6 +26,11 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
@@ -28,31 +39,89 @@ func NewService(connection portainer.Connection) (*Service, error) {
// Team returns a Team by ID
func (service *Service) Team(ID portainer.TeamID) (*portainer.Team, error) {
var team portainer.Team
identifier := service.connection.ConvertToKey(int(ID))
err := service.connection.GetObject(BucketName, identifier, &team)
if err != nil {
return nil, err
}
return &team, nil
}
// TeamByName returns a team by name.
func (service *Service) TeamByName(name string) (*portainer.Team, error) {
return nil, nil
var t *portainer.Team
stop := fmt.Errorf("ok")
err := service.connection.GetAll(
BucketName,
&portainer.Team{},
func(obj interface{}) (interface{}, error) {
team, ok := obj.(*portainer.Team)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Team object")
return nil, fmt.Errorf("Failed to convert to Team object: %s", obj)
}
if strings.EqualFold(team.Name, name) {
t = team
return nil, stop
}
return &portainer.Team{}, nil
})
if err == stop {
return t, nil
}
if err == nil {
return nil, errors.ErrObjectNotFound
}
return nil, err
}
// Teams return an array containing all the teams.
func (service *Service) Teams() ([]portainer.Team, error) {
var teams = make([]portainer.Team, 0)
return teams, nil
err := service.connection.GetAll(
BucketName,
&portainer.Team{},
func(obj interface{}) (interface{}, error) {
team, ok := obj.(*portainer.Team)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Team object")
return nil, fmt.Errorf("Failed to convert to Team object: %s", obj)
}
teams = append(teams, *team)
return &portainer.Team{}, nil
})
return teams, err
}
// UpdateTeam saves a Team.
func (service *Service) UpdateTeam(ID portainer.TeamID, team *portainer.Team) error {
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.UpdateObject(BucketName, identifier, team)
}
// CreateTeam creates a new Team.
func (service *Service) Create(team *portainer.Team) error {
return nil
return service.connection.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
team.ID = portainer.TeamID(id)
return int(team.ID), team
},
)
}
// DeleteTeam deletes a Team.
func (service *Service) DeleteTeam(ID portainer.TeamID) error {
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.DeleteObject(BucketName, identifier)
}

View File

@@ -1,7 +1,11 @@
package teammembership
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
const (
@@ -20,6 +24,11 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
@@ -28,48 +37,167 @@ func NewService(connection portainer.Connection) (*Service, error) {
// TeamMembership returns a TeamMembership object by ID
func (service *Service) TeamMembership(ID portainer.TeamMembershipID) (*portainer.TeamMembership, error) {
var membership portainer.TeamMembership
identifier := service.connection.ConvertToKey(int(ID))
err := service.connection.GetObject(BucketName, identifier, &membership)
if err != nil {
return nil, err
}
return &membership, nil
}
// TeamMemberships return an array containing all the TeamMembership objects.
func (service *Service) TeamMemberships() ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
return memberships, nil
err := service.connection.GetAll(
BucketName,
&portainer.TeamMembership{},
func(obj interface{}) (interface{}, error) {
membership, ok := obj.(*portainer.TeamMembership)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
return nil, fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
}
memberships = append(memberships, *membership)
return &portainer.TeamMembership{}, nil
})
return memberships, err
}
// TeamMembershipsByUserID return an array containing all the TeamMembership objects where the specified userID is present.
func (service *Service) TeamMembershipsByUserID(userID portainer.UserID) ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
return memberships, nil
err := service.connection.GetAll(
BucketName,
&portainer.TeamMembership{},
func(obj interface{}) (interface{}, error) {
membership, ok := obj.(*portainer.TeamMembership)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
return nil, fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
}
if membership.UserID == userID {
memberships = append(memberships, *membership)
}
return &portainer.TeamMembership{}, nil
})
return memberships, err
}
// TeamMembershipsByTeamID return an array containing all the TeamMembership objects where the specified teamID is present.
func (service *Service) TeamMembershipsByTeamID(teamID portainer.TeamID) ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
return memberships, nil
err := service.connection.GetAll(
BucketName,
&portainer.TeamMembership{},
func(obj interface{}) (interface{}, error) {
membership, ok := obj.(*portainer.TeamMembership)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
return nil, fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
}
if membership.TeamID == teamID {
memberships = append(memberships, *membership)
}
return &portainer.TeamMembership{}, nil
})
return memberships, err
}
// UpdateTeamMembership saves a TeamMembership object.
func (service *Service) UpdateTeamMembership(ID portainer.TeamMembershipID, membership *portainer.TeamMembership) error {
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.UpdateObject(BucketName, identifier, membership)
}
// CreateTeamMembership creates a new TeamMembership object.
func (service *Service) Create(membership *portainer.TeamMembership) error {
return nil
return service.connection.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
membership.ID = portainer.TeamMembershipID(id)
return int(membership.ID), membership
},
)
}
// DeleteTeamMembership deletes a TeamMembership object.
func (service *Service) DeleteTeamMembership(ID portainer.TeamMembershipID) error {
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.DeleteObject(BucketName, identifier)
}
// DeleteTeamMembershipByUserID deletes all the TeamMembership object associated to a UserID.
func (service *Service) DeleteTeamMembershipByUserID(userID portainer.UserID) error {
return nil
return service.connection.DeleteAllObjects(
BucketName,
&portainer.TeamMembership{},
func(obj interface{}) (id int, ok bool) {
membership, ok := obj.(portainer.TeamMembership)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
//return fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
return -1, false
}
if membership.UserID == userID {
return int(membership.ID), true
}
return -1, false
})
}
// DeleteTeamMembershipByTeamID deletes all the TeamMembership object associated to a TeamID.
func (service *Service) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) error {
return nil
return service.connection.DeleteAllObjects(
BucketName,
&portainer.TeamMembership{},
func(obj interface{}) (id int, ok bool) {
membership, ok := obj.(portainer.TeamMembership)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
//return fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
return -1, false
}
if membership.TeamID == teamID {
return int(membership.ID), true
}
return -1, false
})
}
func (service *Service) DeleteTeamMembershipByTeamIDAndUserID(teamID portainer.TeamID, userID portainer.UserID) error {
return service.connection.DeleteAllObjects(
BucketName,
&portainer.TeamMembership{},
func(obj interface{}) (id int, ok bool) {
membership, ok := obj.(portainer.TeamMembership)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
//return fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
return -1, false
}
if membership.TeamID == teamID && membership.UserID == userID {
return int(membership.ID), true
}
return -1, false
})
}

View File

@@ -21,6 +21,11 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
@@ -30,10 +35,15 @@ func NewService(connection portainer.Connection) (*Service, error) {
func (service *Service) Info() (*portainer.TunnelServerInfo, error) {
var info portainer.TunnelServerInfo
err := service.connection.GetObject(BucketName, []byte(infoKey), &info)
if err != nil {
return nil, err
}
return &info, nil
}
// UpdateInfo persists a TunnelServerInfo object.
func (service *Service) UpdateInfo(settings *portainer.TunnelServerInfo) error {
return nil
return service.connection.UpdateObject(BucketName, []byte(infoKey), settings)
}

View File

@@ -1,7 +1,13 @@
package user
import (
"fmt"
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/rs/zerolog/log"
)
const (
@@ -20,6 +26,11 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
@@ -28,38 +39,120 @@ func NewService(connection portainer.Connection) (*Service, error) {
// User returns a user by ID
func (service *Service) User(ID portainer.UserID) (*portainer.User, error) {
var user portainer.User
identifier := service.connection.ConvertToKey(int(ID))
err := service.connection.GetObject(BucketName, identifier, &user)
if err != nil {
return nil, err
}
return &user, nil
}
// UserByUsername returns a user by username.
func (service *Service) UserByUsername(username string) (*portainer.User, error) {
return nil, nil
var u *portainer.User
stop := fmt.Errorf("ok")
err := service.connection.GetAll(
BucketName,
&portainer.User{},
func(obj interface{}) (interface{}, error) {
user, ok := obj.(*portainer.User)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to User object")
return nil, fmt.Errorf("Failed to convert to User object: %s", obj)
}
if strings.EqualFold(user.Username, username) {
u = user
return nil, stop
}
return &portainer.User{}, nil
})
if err == stop {
return u, nil
}
if err == nil {
return nil, errors.ErrObjectNotFound
}
return nil, err
}
// Users return an array containing all the users.
func (service *Service) Users() ([]portainer.User, error) {
var users = make([]portainer.User, 0)
return users, nil
err := service.connection.GetAll(
BucketName,
&portainer.User{},
func(obj interface{}) (interface{}, error) {
user, ok := obj.(*portainer.User)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to User object")
return nil, fmt.Errorf("Failed to convert to User object: %s", obj)
}
users = append(users, *user)
return &portainer.User{}, nil
})
return users, err
}
// UsersByRole return an array containing all the users with the specified role.
func (service *Service) UsersByRole(role portainer.UserRole) ([]portainer.User, error) {
var users = make([]portainer.User, 0)
return users, nil
err := service.connection.GetAll(
BucketName,
&portainer.User{},
func(obj interface{}) (interface{}, error) {
user, ok := obj.(*portainer.User)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to User object")
return nil, fmt.Errorf("Failed to convert to User object: %s", obj)
}
if user.Role == role {
users = append(users, *user)
}
return &portainer.User{}, nil
})
return users, err
}
// UpdateUser saves a user.
func (service *Service) UpdateUser(ID portainer.UserID, user *portainer.User) error {
return nil
identifier := service.connection.ConvertToKey(int(ID))
user.Username = strings.ToLower(user.Username)
return service.connection.UpdateObject(BucketName, identifier, user)
}
// CreateUser creates a new user.
func (service *Service) Create(user *portainer.User) error {
return nil
return service.connection.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
user.ID = portainer.UserID(id)
user.Username = strings.ToLower(user.Username)
return int(user.ID), user
},
)
}
// DeleteUser deletes a user.
func (service *Service) DeleteUser(ID portainer.UserID) error {
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.DeleteObject(BucketName, identifier)
}

View File

@@ -1,11 +1,19 @@
package version
import (
"fmt"
"strconv"
"errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/models"
"github.com/portainer/portainer/api/dataservices"
dserrors "github.com/portainer/portainer/api/dataservices/errors"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "version"
versionKey = "VERSION"
updatingKey = "DB_UPDATING"
)
// Service represents a service to manage stored versions.
@@ -15,88 +23,97 @@ type Service struct {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
}
// DBVersion retrieves the stored database version.
func (service *Service) DBVersion() (int, error) {
db := service.connection.GetDB()
var version models.Version
tx := db.First(&version, `key = ?`, models.VersionKey)
if tx.Error != nil {
return 0, tx.Error
func (service *Service) SchemaVersion() (string, error) {
v, err := service.Version()
if err != nil {
return "", err
}
return strconv.Atoi(version.Value)
return v.SchemaVersion, nil
}
// Edition retrieves the stored portainer edition.
func (service *Service) Edition() (portainer.SoftwareEdition, error) {
db := service.connection.GetDB()
var version models.Version
tx := db.First(&version, `key = ?`, models.EditionKey)
if tx.Error != nil {
return 0, tx.Error
func (service *Service) UpdateSchemaVersion(version string) error {
v, err := service.Version()
if err != nil {
return err
}
e, err := strconv.Atoi(version.Value)
v.SchemaVersion = version
return service.UpdateVersion(v)
}
func (service *Service) Edition() (portainer.SoftwareEdition, error) {
v, err := service.Version()
if err != nil {
return 0, err
}
return portainer.SoftwareEdition(e), nil
}
// StoreDBVersion store the database version.
func (service *Service) StoreDBVersion(v int) error {
db := service.connection.GetDB()
version := &models.Version{Key: models.VersionKey}
fmt.Printf("## version %s", version)
tx := db.Model(version).Update("value", strconv.FormatInt(int64(v), 10))
if tx.Error != nil {
return tx.Error
}
return nil
return portainer.SoftwareEdition(v.Edition), nil
}
// IsUpdating retrieves the database updating status.
func (service *Service) IsUpdating() (bool, error) {
db := service.connection.GetDB()
var version models.Version
tx := db.First(&version, `key = ?`, models.UpdatingKey)
if tx.Error != nil {
return false, tx.Error
var isUpdating bool
err := service.connection.GetObject(BucketName, []byte(updatingKey), &isUpdating)
if err != nil && errors.Is(err, dserrors.ErrObjectNotFound) {
return false, nil
}
return version.Value == "true", nil
return isUpdating, err
}
// StoreIsUpdating store the database updating status.
func (service *Service) StoreIsUpdating(isUpdating bool) error {
db := service.connection.GetDB()
tx := db.Model(&models.Version{Key: models.UpdatingKey}).Update("value", strconv.FormatBool(isUpdating))
if tx.Error != nil {
return tx.Error
}
return nil
return service.connection.DeleteObject(BucketName, []byte(updatingKey))
}
// InstanceID retrieves the stored instance ID.
func (service *Service) InstanceID() (string, error) {
db := service.connection.GetDB()
var version models.Version
tx := db.First(&version, `key = ?`, models.InstanceKey)
if tx.Error != nil {
return "", tx.Error
v, err := service.Version()
if err != nil {
return "", err
}
return version.Value, nil
return v.InstanceID, nil
}
// StoreInstanceID store the instance ID.
func (service *Service) StoreInstanceID(ID string) error {
db := service.connection.GetDB()
tx := db.FirstOrCreate(&models.Version{Key: models.InstanceKey, Value: ID})
if tx.Error != nil {
return tx.Error
func (service *Service) UpdateInstanceID(id string) error {
v, err := service.Version()
if err != nil {
if !dataservices.IsErrObjectNotFound(err) {
return err
}
v = &models.Version{}
}
return nil
v.InstanceID = id
return service.UpdateVersion(v)
}
// Version retrieve the version object.
func (service *Service) Version() (*models.Version, error) {
var v models.Version
err := service.connection.GetObject(BucketName, []byte(versionKey), &v)
if err != nil {
return nil, err
}
return &v, nil
}
// UpdateVersion persists a Version object.
func (service *Service) UpdateVersion(version *models.Version) error {
return service.connection.UpdateObject(BucketName, []byte(versionKey), version)
}

View File

@@ -1,7 +1,12 @@
package webhook
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/rs/zerolog/log"
)
const (
@@ -20,6 +25,11 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
@@ -28,36 +38,125 @@ func NewService(connection portainer.Connection) (*Service, error) {
// Webhooks returns an array of all webhooks
func (service *Service) Webhooks() ([]portainer.Webhook, error) {
var webhooks = make([]portainer.Webhook, 0)
return webhooks, nil
err := service.connection.GetAll(
BucketName,
&portainer.Webhook{},
func(obj interface{}) (interface{}, error) {
webhook, ok := obj.(*portainer.Webhook)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Webhook object")
return nil, fmt.Errorf("Failed to convert to Webhook object: %s", obj)
}
webhooks = append(webhooks, *webhook)
return &portainer.Webhook{}, nil
})
return webhooks, err
}
// Webhook returns a webhook by ID.
func (service *Service) Webhook(ID portainer.WebhookID) (*portainer.Webhook, error) {
var webhook portainer.Webhook
identifier := service.connection.ConvertToKey(int(ID))
err := service.connection.GetObject(BucketName, identifier, &webhook)
if err != nil {
return nil, err
}
return &webhook, nil
}
// WebhookByResourceID returns a webhook by the ResourceID it is associated with.
func (service *Service) WebhookByResourceID(ID string) (*portainer.Webhook, error) {
return nil, nil
var w *portainer.Webhook
stop := fmt.Errorf("ok")
err := service.connection.GetAll(
BucketName,
&portainer.Webhook{},
func(obj interface{}) (interface{}, error) {
webhook, ok := obj.(*portainer.Webhook)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Webhook object")
return nil, fmt.Errorf("Failed to convert to Webhook object: %s", obj)
}
if webhook.ResourceID == ID {
w = webhook
return nil, stop
}
return &portainer.Webhook{}, nil
})
if err == stop {
return w, nil
}
if err == nil {
return nil, errors.ErrObjectNotFound
}
return nil, err
}
// WebhookByToken returns a webhook by the random token it is associated with.
func (service *Service) WebhookByToken(token string) (*portainer.Webhook, error) {
return nil, nil
var w *portainer.Webhook
stop := fmt.Errorf("ok")
err := service.connection.GetAll(
BucketName,
&portainer.Webhook{},
func(obj interface{}) (interface{}, error) {
webhook, ok := obj.(*portainer.Webhook)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Webhook object")
return nil, fmt.Errorf("Failed to convert to Webhook object: %s", obj)
}
if webhook.Token == token {
w = webhook
return nil, stop
}
return &portainer.Webhook{}, nil
})
if err == stop {
return w, nil
}
if err == nil {
return nil, errors.ErrObjectNotFound
}
return nil, err
}
// DeleteWebhook deletes a webhook.
func (service *Service) DeleteWebhook(ID portainer.WebhookID) error {
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.DeleteObject(BucketName, identifier)
}
// CreateWebhook assign an ID to a new webhook and saves it.
func (service *Service) Create(webhook *portainer.Webhook) error {
return nil
return service.connection.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
webhook.ID = portainer.WebhookID(id)
return int(webhook.ID), webhook
},
)
}
// UpdateWebhook update a webhook.
func (service *Service) UpdateWebhook(ID portainer.WebhookID, webhook *portainer.Webhook) error {
return nil
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.UpdateObject(BucketName, identifier, webhook)
}

189
api/datastore/backup.go Normal file
View File

@@ -0,0 +1,189 @@
package datastore
import (
"fmt"
"os"
"path"
"time"
"github.com/portainer/portainer/api/database/models"
"github.com/rs/zerolog/log"
)
var backupDefaults = struct {
backupDir string
commonDir string
}{
"backups",
"common",
}
//
// Backup Helpers
//
// createBackupFolders create initial folders for backups
func (store *Store) createBackupFolders() {
// create common dir
commonDir := store.commonBackupDir()
if exists, _ := store.fileService.FileExists(commonDir); !exists {
if err := os.MkdirAll(commonDir, 0700); err != nil {
log.Error().Err(err).Msg("error while creating common backup folder")
}
}
}
func (store *Store) databasePath() string {
return store.connection.GetDatabaseFilePath()
}
func (store *Store) commonBackupDir() string {
return path.Join(store.connection.GetStorePath(), backupDefaults.backupDir, backupDefaults.commonDir)
}
func (store *Store) copyDBFile(from string, to string) error {
log.Info().Str("from", from).Str("to", to).Msg("copying DB file")
err := store.fileService.Copy(from, to, true)
if err != nil {
log.Error().Err(err).Msg("failed")
}
return err
}
// BackupOptions provide a helper to inject backup options
type BackupOptions struct {
Version string
BackupDir string
BackupFileName string
BackupPath string
}
// getBackupRestoreOptions returns options to store db at common backup dir location; used by:
// - db backup prior to version upgrade
// - db rollback
func getBackupRestoreOptions(backupDir string) *BackupOptions {
return &BackupOptions{
BackupDir: backupDir, //connection.commonBackupDir(),
BackupFileName: beforePortainerVersionUpgradeBackup,
}
}
// Backup current database with default options
func (store *Store) Backup(version *models.Version) (string, error) {
if version == nil {
return store.backupWithOptions(nil)
}
return store.backupWithOptions(&BackupOptions{
Version: version.SchemaVersion,
})
}
func (store *Store) setupOptions(options *BackupOptions) *BackupOptions {
if options == nil {
options = &BackupOptions{}
}
if options.Version == "" {
v, err := store.VersionService.Version()
if err != nil {
options.Version = ""
}
options.Version = v.SchemaVersion
}
if options.BackupDir == "" {
options.BackupDir = store.commonBackupDir()
}
if options.BackupFileName == "" {
options.BackupFileName = fmt.Sprintf("%s.%s.%s", store.connection.GetDatabaseFileName(), options.Version, time.Now().Format("20060102150405"))
}
if options.BackupPath == "" {
options.BackupPath = path.Join(options.BackupDir, options.BackupFileName)
}
return options
}
// BackupWithOptions backup current database with options
func (store *Store) backupWithOptions(options *BackupOptions) (string, error) {
log.Info().Msg("creating DB backup")
store.createBackupFolders()
options = store.setupOptions(options)
dbPath := store.databasePath()
if err := store.Close(); err != nil {
return options.BackupPath, fmt.Errorf(
"error closing datastore before creating backup: %v",
err,
)
}
if err := store.copyDBFile(dbPath, options.BackupPath); err != nil {
return options.BackupPath, err
}
if _, err := store.Open(); err != nil {
return options.BackupPath, fmt.Errorf(
"error opening datastore after creating backup: %v",
err,
)
}
return options.BackupPath, 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 {
options = store.setupOptions(options)
// Check if backup file exist before restoring
_, err := os.Stat(options.BackupPath)
if os.IsNotExist(err) {
log.Error().Str("path", options.BackupPath).Err(err).Msg("backup file to restore does not exist %s")
return err
}
err = store.Close()
if err != nil {
log.Error().Err(err).Msg("error while closing store before restore")
return err
}
log.Info().Msg("restoring DB backup")
err = store.copyDBFile(options.BackupPath, store.databasePath())
if err != nil {
return err
}
_, err = store.Open()
return err
}
// RemoveWithOptions removes backup database based on supplied options
func (store *Store) removeWithOptions(options *BackupOptions) error {
log.Info().Msg("removing DB backup")
options = store.setupOptions(options)
_, err := os.Stat(options.BackupPath)
if os.IsNotExist(err) {
log.Error().Str("path", options.BackupPath).Err(err).Msg("backup file to remove does not exist")
return err
}
log.Info().Str("path", options.BackupPath).Msg("removing DB file")
err = os.Remove(options.BackupPath)
if err != nil {
log.Error().Err(err).Msg("failed")
return err
}
return nil
}

View File

@@ -0,0 +1,112 @@
package datastore
import (
"fmt"
"os"
"path"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/models"
)
func TestCreateBackupFolders(t *testing.T) {
_, store, teardown := MustNewTestStore(t, true, true)
defer teardown()
connection := store.GetConnection()
backupPath := path.Join(connection.GetStorePath(), backupDefaults.backupDir)
if isFileExist(backupPath) {
t.Error("Expect backups folder to not exist")
}
store.createBackupFolders()
if !isFileExist(backupPath) {
t.Error("Expect backups folder to exist")
}
}
func TestStoreCreation(t *testing.T) {
_, store, teardown := MustNewTestStore(t, true, true)
defer teardown()
if store == nil {
t.Error("Expect to create a store")
}
if store.CheckCurrentEdition() != nil {
t.Error("Expect to get CE Edition")
}
}
func TestBackup(t *testing.T) {
_, store, teardown := MustNewTestStore(t, true, true)
connection := store.GetConnection()
defer teardown()
t.Run("Backup should create default db backup", func(t *testing.T) {
v := models.Version{
SchemaVersion: portainer.APIVersion,
}
store.VersionService.UpdateVersion(&v)
store.backupWithOptions(nil)
backupFileName := path.Join(connection.GetStorePath(), "backups", "common", fmt.Sprintf("portainer.edb.%s.*", portainer.APIVersion))
if !isFileExist(backupFileName) {
t.Errorf("Expect backup file to be created %s", backupFileName)
}
})
t.Run("BackupWithOption should create a name specific backup at common path", func(t *testing.T) {
store.backupWithOptions(&BackupOptions{
BackupFileName: beforePortainerVersionUpgradeBackup,
BackupDir: store.commonBackupDir(),
})
backupFileName := path.Join(connection.GetStorePath(), "backups", "common", beforePortainerVersionUpgradeBackup)
if !isFileExist(backupFileName) {
t.Errorf("Expect backup file to be created %s", backupFileName)
}
})
}
func TestRemoveWithOptions(t *testing.T) {
_, store, teardown := MustNewTestStore(t, true, true)
defer teardown()
t.Run("successfully removes file if existent", func(t *testing.T) {
store.createBackupFolders()
options := &BackupOptions{
BackupDir: store.commonBackupDir(),
BackupFileName: "test.txt",
}
filePath := path.Join(options.BackupDir, options.BackupFileName)
f, err := os.Create(filePath)
if err != nil {
t.Fatalf("file should be created; err=%s", err)
}
f.Close()
err = store.removeWithOptions(options)
if err != nil {
t.Errorf("RemoveWithOptions should successfully remove file; err=%v", err)
}
if isFileExist(f.Name()) {
t.Errorf("RemoveWithOptions should successfully remove file; file=%s", f.Name())
}
})
t.Run("fails to removes file if non-existent", func(t *testing.T) {
options := &BackupOptions{
BackupDir: store.commonBackupDir(),
BackupFileName: "test.txt",
}
err := store.removeWithOptions(options)
if err == nil {
t.Error("RemoveWithOptions should fail for non-existent file")
}
})
}

View File

@@ -1,27 +1,19 @@
package datastore
import (
"fmt"
"io"
"os"
"path"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
portainerErrors "github.com/portainer/portainer/api/dataservices/errors"
"gorm.io/gorm"
"github.com/rs/zerolog/log"
)
func (store *Store) version() (int, error) {
version, err := store.VersionService.DBVersion()
if store.IsErrObjectNotFound(err) {
version = 0
}
return version, err
}
func (store *Store) edition() portainer.SoftwareEdition {
edition, err := store.VersionService.Edition()
if store.IsErrObjectNotFound(err) {
edition = portainer.PortainerCE
}
return edition
}
// NewStore initializes a new Store and the associated services
func NewStore(storePath string, fileService portainer.FileService, connection portainer.Connection) *Store {
return &Store{
@@ -32,50 +24,162 @@ func NewStore(storePath string, fileService portainer.FileService, connection po
// Open opens and initializes the BoltDB database.
func (store *Store) Open() (newStore bool, err error) {
newStore = true
encryptionReq, err := store.connection.NeedsEncryptionMigration()
if err != nil {
return false, err
}
if encryptionReq {
err = store.encryptDB()
if err != nil {
return false, err
}
}
err = store.connection.Open()
if err != nil {
return newStore, err
return false, err
}
err = store.initServices()
if err != nil {
return newStore, err
return false, err
}
// if we have DBVersion in the database then ensure we flag this as NOT a new store
// version, err := store.VersionService.DBVersion()
// if err != nil {
// if store.IsErrObjectNotFound(err) {
// return newStore, nil
// }
// If no settings object exists then assume we have a new store
_, err = store.SettingsService.Settings()
if err != nil {
if store.IsErrObjectNotFound(err) {
return true, nil
}
return false, err
}
// return newStore, err
// }
// if version > 0 {
// log.Debug().Int("version", version).Msg("opened existing store")
// return false, nil
// }
return newStore, nil
return false, nil
}
func (store *Store) Close() error {
return store.connection.Close()
}
func (store *Store) UpdateTx(fn func(dataservices.DataStoreTx) error) error {
return store.connection.UpdateTx(func(tx portainer.Transaction) error {
return fn(&StoreTx{
store: store,
tx: tx,
})
})
}
func (store *Store) ViewTx(fn func(dataservices.DataStoreTx) error) error {
return store.connection.ViewTx(func(tx portainer.Transaction) error {
return fn(&StoreTx{
store: store,
tx: tx,
})
})
}
// 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.BackupTo(w)
}
// CheckCurrentEdition checks if current edition is community edition
func (store *Store) CheckCurrentEdition() error {
if store.edition() != portainer.PortainerCE {
if store.edition() != portainer.Edition {
return portainerErrors.ErrWrongDBEdition
}
return nil
}
func (store *Store) edition() portainer.SoftwareEdition {
edition, err := store.VersionService.Edition()
if store.IsErrObjectNotFound(err) {
edition = portainer.PortainerCE
}
return edition
}
// TODO: move the use of this to dataservices.IsErrObjectNotFound()?
func (store *Store) IsErrObjectNotFound(e error) bool {
return e == gorm.ErrRecordNotFound
return e == portainerErrors.ErrObjectNotFound
}
func (store *Store) Connection() portainer.Connection {
return store.connection
}
func (store *Store) Rollback(force bool) error {
return store.connectionRollback(force)
}
func (store *Store) encryptDB() error {
store.connection.SetEncrypted(false)
err := store.connection.Open()
if err != nil {
return err
}
err = store.initServices()
if err != nil {
return err
}
// The DB is not currently encrypted. First save the encrypted db filename
oldFilename := store.connection.GetDatabaseFilePath()
log.Info().Msg("encrypting database")
// export file path for backup
exportFilename := path.Join(store.databasePath() + "." + fmt.Sprintf("backup-%d.json", time.Now().Unix()))
log.Info().Str("filename", exportFilename).Msg("exporting database backup")
err = store.Export(exportFilename)
if err != nil {
log.Error().Str("filename", exportFilename).Err(err).Msg("failed to export")
return err
}
log.Info().Msg("database backup exported")
// Close existing un-encrypted db so that we can delete the file later
store.connection.Close()
// Tell the db layer to create an encrypted db when opened
store.connection.SetEncrypted(true)
store.connection.Open()
// We have to init services before import
err = store.initServices()
if err != nil {
return err
}
err = store.Import(exportFilename)
if err != nil {
// Remove the new encrypted file that we failed to import
os.Remove(store.connection.GetDatabaseFilePath())
log.Fatal().Err(portainerErrors.ErrDBImportFailed).Msg("")
}
err = os.Remove(oldFilename)
if err != nil {
log.Error().Msg("failed to remove the un-encrypted db file")
}
err = os.Remove(exportFilename)
if err != nil {
log.Error().Msg("failed to remove the json backup file")
}
// Close db connection
store.connection.Close()
log.Info().Msg("database successfully encrypted")
return nil
}

View File

@@ -0,0 +1,417 @@
package datastore
import (
"fmt"
"runtime"
"strings"
"testing"
"github.com/dchest/uniuri"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/chisel"
"github.com/portainer/portainer/api/crypto"
"github.com/stretchr/testify/assert"
)
const (
adminUsername = "admin"
adminPassword = "password"
standardUsername = "standard"
standardPassword = "password"
agentOnDockerEnvironmentUrl = "tcp://192.168.167.207:30775"
edgeAgentOnKubernetesEnvironmentUrl = "tcp://192.168.167.207"
kubernetesLocalEnvironmentUrl = "https://kubernetes.default.svc"
)
// TestStoreFull an eventually comprehensive set of tests for the Store.
// The idea is what we write to the store, we should read back.
func TestStoreFull(t *testing.T) {
_, store, teardown := MustNewTestStore(t, true, true)
defer teardown()
testCases := map[string]func(t *testing.T){
"User Accounts": func(t *testing.T) {
store.testUserAccounts(t)
},
"Environments": func(t *testing.T) {
store.testEnvironments(t)
},
"Settings": func(t *testing.T) {
store.testSettings(t)
},
"SSL Settings": func(t *testing.T) {
store.testSSLSettings(t)
},
"Tunnel Server": func(t *testing.T) {
store.testTunnelServer(t)
},
"Custom Templates": func(t *testing.T) {
store.testCustomTemplates(t)
},
"Registries": func(t *testing.T) {
store.testRegistries(t)
},
"Resource Control": func(t *testing.T) {
store.testResourceControl(t)
},
"Schedules": func(t *testing.T) {
store.testSchedules(t)
},
"Tags": func(t *testing.T) {
store.testTags(t)
},
// "Test Title": func(t *testing.T) {
// },
}
for name, test := range testCases {
t.Run(name, test)
}
}
func (store *Store) testEnvironments(t *testing.T) {
id := store.CreateEndpoint(t, "local", portainer.KubernetesLocalEnvironment, "", true)
store.CreateEndpointRelation(id)
id = store.CreateEndpoint(t, "agent", portainer.AgentOnDockerEnvironment, agentOnDockerEnvironmentUrl, true)
store.CreateEndpointRelation(id)
id = store.CreateEndpoint(t, "edge", portainer.EdgeAgentOnKubernetesEnvironment, edgeAgentOnKubernetesEnvironmentUrl, true)
store.CreateEndpointRelation(id)
}
func newEndpoint(endpointType portainer.EndpointType, id portainer.EndpointID, name, URL string, TLS bool) *portainer.Endpoint {
endpoint := &portainer.Endpoint{
ID: id,
Name: name,
URL: URL,
Type: endpointType,
GroupID: portainer.EndpointGroupID(1),
PublicURL: "",
TLSConfig: portainer.TLSConfiguration{
TLS: false,
},
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
TagIDs: []portainer.TagID{},
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.DockerSnapshot{},
Kubernetes: portainer.KubernetesDefault(),
}
if TLS {
endpoint.TLSConfig = portainer.TLSConfiguration{
TLS: true,
TLSSkipVerify: true,
}
}
return endpoint
}
func setEndpointAuthorizations(endpoint *portainer.Endpoint) {
endpoint.SecuritySettings = portainer.EndpointSecuritySettings{
AllowVolumeBrowserForRegularUsers: false,
EnableHostManagementFeatures: false,
AllowSysctlSettingForRegularUsers: true,
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowHostNamespaceForRegularUsers: true,
AllowContainerCapabilitiesForRegularUsers: true,
AllowDeviceMappingForRegularUsers: true,
AllowStackManagementForRegularUsers: true,
}
}
func (store *Store) CreateEndpoint(t *testing.T, name string, endpointType portainer.EndpointType, URL string, tls bool) portainer.EndpointID {
is := assert.New(t)
var expectedEndpoint *portainer.Endpoint
id := portainer.EndpointID(store.Endpoint().GetNextIdentifier())
switch endpointType {
case portainer.DockerEnvironment:
if URL == "" {
URL = "unix:///var/run/docker.sock"
if runtime.GOOS == "windows" {
URL = "npipe:////./pipe/docker_engine"
}
}
expectedEndpoint = newEndpoint(endpointType, id, name, URL, tls)
case portainer.AgentOnDockerEnvironment:
expectedEndpoint = newEndpoint(endpointType, id, name, URL, tls)
case portainer.AgentOnKubernetesEnvironment:
URL = strings.TrimPrefix(URL, "tcp://")
expectedEndpoint = newEndpoint(endpointType, id, name, URL, tls)
case portainer.EdgeAgentOnKubernetesEnvironment:
cs := chisel.NewService(store, nil)
expectedEndpoint = newEndpoint(endpointType, id, name, URL, tls)
edgeKey := cs.GenerateEdgeKey(URL, "", int(id))
expectedEndpoint.EdgeKey = edgeKey
store.testTunnelServer(t)
case portainer.KubernetesLocalEnvironment:
if URL == "" {
URL = kubernetesLocalEnvironmentUrl
}
expectedEndpoint = newEndpoint(endpointType, id, name, URL, tls)
}
setEndpointAuthorizations(expectedEndpoint)
store.Endpoint().Create(expectedEndpoint)
endpoint, err := store.Endpoint().Endpoint(id)
is.NoError(err, "Endpoint() should not return an error")
is.Equal(expectedEndpoint, endpoint, "endpoint should be the same")
return endpoint.ID
}
func (store *Store) CreateEndpointRelation(id portainer.EndpointID) {
relation := &portainer.EndpointRelation{
EndpointID: id,
EdgeStacks: map[portainer.EdgeStackID]bool{},
}
store.EndpointRelation().Create(relation)
}
func (store *Store) testSSLSettings(t *testing.T) {
is := assert.New(t)
ssl := &portainer.SSLSettings{
CertPath: "/data/certs/cert.pem",
HTTPEnabled: true,
KeyPath: "/data/certs/key.pem",
SelfSigned: true,
}
store.SSLSettings().UpdateSettings(ssl)
settings, err := store.SSLSettings().Settings()
is.NoError(err, "Get sslsettings should succeed")
is.Equal(ssl, settings, "Stored SSLSettings should be the same as what is read out")
}
func (store *Store) testTunnelServer(t *testing.T) {
is := assert.New(t)
expectPrivateKeySeed := uniuri.NewLen(16)
err := store.TunnelServer().UpdateInfo(&portainer.TunnelServerInfo{PrivateKeySeed: expectPrivateKeySeed})
is.NoError(err, "UpdateInfo should have succeeded")
serverInfo, err := store.TunnelServer().Info()
is.NoError(err, "Info should have succeeded")
is.Equal(expectPrivateKeySeed, serverInfo.PrivateKeySeed, "hashed passwords should not differ")
}
// add users, read them back and check the details are unchanged
func (store *Store) testUserAccounts(t *testing.T) {
is := assert.New(t)
err := store.createAccount(adminUsername, adminPassword, portainer.AdministratorRole)
is.NoError(err, "CreateAccount should succeed")
store.checkAccount(adminUsername, adminPassword, portainer.AdministratorRole)
is.NoError(err, "Account failure")
err = store.createAccount(standardUsername, standardPassword, portainer.StandardUserRole)
is.NoError(err, "CreateAccount should succeed")
store.checkAccount(standardUsername, standardPassword, portainer.StandardUserRole)
is.NoError(err, "Account failure")
}
// create an account with the provided details
func (store *Store) createAccount(username, password string, role portainer.UserRole) error {
var err error
user := &portainer.User{Username: username, Role: role}
// encrypt the password
cs := &crypto.Service{}
user.Password, err = cs.Hash(password)
if err != nil {
return err
}
err = store.User().Create(user)
if err != nil {
return err
}
return nil
}
func (store *Store) checkAccount(username, expectPassword string, expectRole portainer.UserRole) error {
// Read the account for username. Check password and role is what we expect
user, err := store.User().UserByUsername(username)
if err != nil {
return errors.Wrap(err, "failed to find user")
}
if user.Username != username || user.Role != expectRole {
return fmt.Errorf("%s user details do not match", user.Username)
}
// Check the password
cs := &crypto.Service{}
expectPasswordHash, err := cs.Hash(expectPassword)
if err != nil {
return errors.Wrap(err, "hash failed")
}
if user.Password != expectPasswordHash {
return fmt.Errorf("%s user password hash failure", user.Username)
}
return nil
}
func (store *Store) testSettings(t *testing.T) {
is := assert.New(t)
// since many settings are default and basically nil, I'm going to update some and read them back
expectedSettings, err := store.Settings().Settings()
is.NoError(err, "Settings() should not return an error")
expectedSettings.TemplatesURL = "http://portainer.io/application-templates"
expectedSettings.HelmRepositoryURL = "http://portainer.io/helm-repository"
expectedSettings.EdgeAgentCheckinInterval = 60
expectedSettings.AuthenticationMethod = portainer.AuthenticationLDAP
expectedSettings.LDAPSettings = portainer.LDAPSettings{
AnonymousMode: true,
StartTLS: true,
AutoCreateUsers: true,
Password: "random",
}
expectedSettings.SnapshotInterval = "10m"
err = store.Settings().UpdateSettings(expectedSettings)
is.NoError(err, "UpdateSettings() should succeed")
settings, err := store.Settings().Settings()
is.NoError(err, "Settings() should not return an error")
is.Equal(expectedSettings, settings, "stored settings should match")
}
func (store *Store) testCustomTemplates(t *testing.T) {
is := assert.New(t)
customTemplate := store.CustomTemplate()
is.NotNil(customTemplate, "customTemplate Service shouldn't be nil")
expectedTemplate := &portainer.CustomTemplate{
ID: portainer.CustomTemplateID(customTemplate.GetNextIdentifier()),
Title: "Custom Title",
Description: "Custom Template Description",
ProjectPath: "/data/custom_template/1",
Note: "A note about this custom template",
EntryPoint: "docker-compose.yaml",
CreatedByUserID: 10,
}
customTemplate.Create(expectedTemplate)
actualTemplate, err := customTemplate.CustomTemplate(expectedTemplate.ID)
is.NoError(err, "CustomTemplate should not return an error")
is.Equal(expectedTemplate, actualTemplate, "expected and actual template do not match")
}
func (store *Store) testRegistries(t *testing.T) {
is := assert.New(t)
regService := store.RegistryService
is.NotNil(regService, "RegistryService shouldn't be nil")
reg1 := &portainer.Registry{
ID: 1,
Type: portainer.DockerHubRegistry,
Name: "Dockerhub Registry Test",
}
reg2 := &portainer.Registry{
ID: 2,
Type: portainer.GitlabRegistry,
Name: "Gitlab Registry Test",
Gitlab: portainer.GitlabRegistryData{
ProjectID: 12345,
InstanceURL: "http://gitlab.com/12345",
ProjectPath: "mytestproject",
},
}
err := regService.Create(reg1)
is.NoError(err)
err = regService.Create(reg2)
is.NoError(err)
actualReg1, err := regService.Registry(reg1.ID)
is.NoError(err)
is.Equal(reg1, actualReg1, "registries differ")
actualReg2, err := regService.Registry(reg2.ID)
is.NoError(err)
is.Equal(reg2, actualReg2, "registries differ")
}
func (store *Store) testResourceControl(t *testing.T) {
// is := assert.New(t)
// resControl := store.ResourceControl()
// ctrl := &portainer.ResourceControl{
// }
// resControl().Create()
}
func (store *Store) testSchedules(t *testing.T) {
is := assert.New(t)
schedule := store.ScheduleService
s := &portainer.Schedule{
ID: portainer.ScheduleID(schedule.GetNextIdentifier()),
Name: "My Custom Schedule 1",
CronExpression: "*/5 * * * * portainer /bin/sh -c echo 'hello world'",
}
err := schedule.CreateSchedule(s)
is.NoError(err, "CreateSchedule should succeed")
actual, err := schedule.Schedule(s.ID)
is.NoError(err, "schedule should be found")
is.Equal(s, actual, "schedules differ")
}
func (store *Store) testTags(t *testing.T) {
is := assert.New(t)
tags := store.TagService
tag1 := &portainer.Tag{
ID: 1,
Name: "Tag 1",
}
tag2 := &portainer.Tag{
ID: 2,
Name: "Tag 1",
}
err := tags.Create(tag1)
is.NoError(err, "Tags.Create should succeed")
err = tags.Create(tag2)
is.NoError(err, "Tags.Create should succeed")
actual, err := tags.Tag(tag1.ID)
is.NoError(err, "tag1 should be found")
is.Equal(tag1, actual, "tags differ")
actual, err = tags.Tag(tag2.ID)
is.NoError(err, "tag2 should be found")
is.Equal(tag2, actual, "tags differ")
}

View File

@@ -1,80 +1,22 @@
package datastore
import (
"fmt"
"github.com/gofrs/uuid"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/models"
)
// Init creates the default data set.
func (store *Store) Init() error {
conn := store.GetConnection()
err := conn.Init()
err := store.checkOrCreateDefaultSettings()
if err != nil {
return err
}
err = store.initVersions()
err = store.checkOrCreateDefaultSSLSettings()
if err != nil {
return err
}
// err = store.checkOrCreateDefaultSettings()
// if err != nil {
// return err
// }
// err = store.checkOrCreateDefaultSSLSettings()
// if err != nil {
// return err
// }
// return store.checkOrCreateDefaultData()
return nil
}
func (store *Store) initVersions() error {
_, err := store.VersionService.InstanceID()
if store.IsErrObjectNotFound(err) {
uid, err := uuid.NewV4()
if err != nil {
return err
}
instanceID := uid.String()
db := store.connection.GetDB()
fmt.Println("updating instance id")
tx := db.Create(&models.Version{Key: models.InstanceKey, Value: instanceID})
if tx.Error != nil {
return tx.Error
}
fmt.Printf("updating version id %s\n", models.VersionKey)
tx = db.Create(&models.Version{Key: models.VersionKey, Value: fmt.Sprint(portainer.DBVersion)})
if tx.Error != nil {
return tx.Error
}
fmt.Println("updating updating id")
tx = db.Create(&models.Version{Key: models.UpdatingKey, Value: "false"})
if tx.Error != nil {
return tx.Error
}
fmt.Println("updating edition id")
tx = db.Create(&models.Version{Key: models.EditionKey, Value: fmt.Sprint(portainer.PortainerCE)})
if tx.Error != nil {
return tx.Error
}
} else {
return err
}
return nil
return store.checkOrCreateDefaultData()
}
func (store *Store) checkOrCreateDefaultSettings() error {

View File

@@ -0,0 +1,142 @@
package datastore
import (
"fmt"
"runtime/debug"
portaineree "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/cli"
"github.com/portainer/portainer/api/database/models"
dserrors "github.com/portainer/portainer/api/dataservices/errors"
"github.com/portainer/portainer/api/datastore/migrator"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
const beforePortainerVersionUpgradeBackup = "portainer.db.bak"
func (store *Store) MigrateData() error {
updating, err := store.VersionService.IsUpdating()
if err != nil {
return errors.Wrap(err, "while checking if the store is updating")
}
if updating {
return dserrors.ErrDatabaseIsUpdating
}
// migrate new version bucket if required (doesn't write anything to db yet)
version, err := store.getOrMigrateLegacyVersion()
if err != nil {
return errors.Wrap(err, "while migrating legacy version")
}
migratorParams := store.newMigratorParameters(version)
migrator := migrator.NewMigrator(migratorParams)
if !migrator.NeedsMigration() {
return nil
}
// before we alter anything in the DB, create a backup
backupPath, err := store.Backup(version)
if err != nil {
return errors.Wrap(err, "while backing up database")
}
err = store.FailSafeMigrate(migrator, version)
if err != nil {
err = store.restoreWithOptions(&BackupOptions{BackupPath: backupPath})
if err != nil {
return errors.Wrap(err, "failed to restore database")
}
log.Info().Msg("database restored to previous version")
return errors.Wrap(err, "failed to migrate database")
}
return nil
}
func (store *Store) newMigratorParameters(version *models.Version) *migrator.MigratorParameters {
return &migrator.MigratorParameters{
CurrentDBVersion: version,
EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService,
EndpointRelationService: store.EndpointRelationService,
ExtensionService: store.ExtensionService,
RegistryService: store.RegistryService,
ResourceControlService: store.ResourceControlService,
RoleService: store.RoleService,
ScheduleService: store.ScheduleService,
SettingsService: store.SettingsService,
SnapshotService: store.SnapshotService,
StackService: store.StackService,
TagService: store.TagService,
TeamMembershipService: store.TeamMembershipService,
UserService: store.UserService,
VersionService: store.VersionService,
FileService: store.fileService,
DockerhubService: store.DockerHubService,
AuthorizationService: authorization.NewService(store),
EdgeStackService: store.EdgeStackService,
EdgeJobService: store.EdgeJobService,
}
}
// FailSafeMigrate backup and restore DB if migration fail
func (store *Store) FailSafeMigrate(migrator *migrator.Migrator, version *models.Version) (err error) {
defer func() {
if e := recover(); e != nil {
// return error with cause and stacktrace (recover() doesn't include a stacktrace)
err = fmt.Errorf("%v %s", e, string(debug.Stack()))
}
}()
err = store.VersionService.StoreIsUpdating(true)
if err != nil {
return errors.Wrap(err, "while updating the store")
}
// now update the version to the new struct (if required)
err = store.finishMigrateLegacyVersion(version)
if err != nil {
return errors.Wrap(err, "while updating version")
}
log.Info().Msg("migrating database from version " + version.SchemaVersion + " to " + portaineree.APIVersion)
err = migrator.Migrate()
if err != nil {
return err
}
err = store.VersionService.StoreIsUpdating(false)
if err != nil {
return errors.Wrap(err, "failed to update the store")
}
return nil
}
// Rollback to a pre-upgrade backup copy/snapshot of portainer.db
func (store *Store) connectionRollback(force bool) error {
if !force {
confirmed, err := cli.Confirm("Are you sure you want to rollback your database to the previous backup?")
if err != nil || !confirmed {
return err
}
}
options := getBackupRestoreOptions(store.commonBackupDir())
err := store.restoreWithOptions(options)
if err != nil {
return err
}
return store.connection.Close()
}

View File

@@ -0,0 +1,520 @@
package datastore
import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"
"github.com/portainer/portainer/api/database/boltdb"
"github.com/google/go-cmp/cmp"
"github.com/portainer/portainer/api/database/models"
"github.com/rs/zerolog/log"
)
// testVersion is a helper which tests current store version against wanted version
func testVersion(store *Store, versionWant string, t *testing.T) {
v, err := store.VersionService.Version()
if err != nil {
t.Errorf("Expect store version to be %s but was %s with error: %s", versionWant, v.SchemaVersion, err)
}
if v.SchemaVersion != versionWant {
t.Errorf("Expect store version to be %s but was %s", versionWant, v.SchemaVersion)
}
}
func TestMigrateData(t *testing.T) {
snapshotTests := []struct {
testName string
srcPath string
wantPath string
overrideInstanceId bool
}{
{
testName: "migrate version 24 to latest",
srcPath: "test_data/input_24.json",
wantPath: "test_data/output_24_to_latest.json",
overrideInstanceId: true,
},
}
for _, test := range snapshotTests {
t.Run(test.testName, func(t *testing.T) {
err := migrateDBTestHelper(t, test.srcPath, test.wantPath, test.overrideInstanceId)
if err != nil {
t.Errorf(
"Failed migrating mock database %v: %v",
test.srcPath,
err,
)
}
})
}
// t.Run("MigrateData for New Store & Re-Open Check", func(t *testing.T) {
// newStore, store, teardown := MustNewTestStore(t, true, false)
// defer teardown()
// if !newStore {
// t.Error("Expect a new DB")
// }
// testVersion(store, portainer.APIVersion, t)
// store.Close()
// newStore, _ = store.Open()
// if newStore {
// t.Error("Expect store to NOT be new DB")
// }
// })
// tests := []struct {
// version string
// expectedVersion string
// }{
// {version: "1.24.1", expectedVersion: portainer.APIVersion},
// {version: "2.0.0", expectedVersion: portainer.APIVersion},
// }
// for _, tc := range tests {
// _, store, teardown := MustNewTestStore(t, true, true)
// defer teardown()
// // Setup data
// v := models.Version{SchemaVersion: tc.version}
// store.VersionService.UpdateVersion(&v)
// // Required roles by migrations 22.2
// store.RoleService.Create(&portainer.Role{ID: 1})
// store.RoleService.Create(&portainer.Role{ID: 2})
// store.RoleService.Create(&portainer.Role{ID: 3})
// store.RoleService.Create(&portainer.Role{ID: 4})
// t.Run(fmt.Sprintf("MigrateData for version %s", tc.version), func(t *testing.T) {
// store.MigrateData()
// testVersion(store, tc.expectedVersion, t)
// })
// t.Run(fmt.Sprintf("Restoring DB after migrateData for version %s", tc.version), func(t *testing.T) {
// store.Rollback(true)
// store.Open()
// testVersion(store, tc.version, t)
// })
// }
// t.Run("Error in MigrateData should restore backup before MigrateData", func(t *testing.T) {
// _, store, teardown := MustNewTestStore(t, false, true)
// defer teardown()
// v := models.Version{SchemaVersion: "1.24.1"}
// store.VersionService.UpdateVersion(&v)
// store.MigrateData()
// testVersion(store, v.SchemaVersion, t)
// })
// t.Run("MigrateData should create backup file upon update", func(t *testing.T) {
// _, store, teardown := MustNewTestStore(t, false, true)
// defer teardown()
// v := models.Version{SchemaVersion: "0.0.0"}
// store.VersionService.UpdateVersion(&v)
// store.MigrateData()
// options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir()))
// if !isFileExist(options.BackupPath) {
// t.Errorf("Backup file should exist; file=%s", options.BackupPath)
// }
// })
// t.Run("MigrateData should fail to create backup if database file is set to updating", func(t *testing.T) {
// _, store, teardown := MustNewTestStore(t, false, true)
// defer teardown()
// store.VersionService.StoreIsUpdating(true)
// store.MigrateData()
// options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir()))
// if isFileExist(options.BackupPath) {
// t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath)
// }
// })
// t.Run("MigrateData should not create backup on startup if portainer version matches db", func(t *testing.T) {
// _, store, teardown := MustNewTestStore(t, false, true)
// defer teardown()
// store.MigrateData()
// options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir()))
// if isFileExist(options.BackupPath) {
// t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath)
// }
// })
}
func Test_getBackupRestoreOptions(t *testing.T) {
_, store, teardown := MustNewTestStore(t, false, true)
defer teardown()
options := getBackupRestoreOptions(store.commonBackupDir())
wantDir := store.commonBackupDir()
if !strings.HasSuffix(options.BackupDir, wantDir) {
log.Fatal().Str("got", options.BackupDir).Str("want", wantDir).Msg("incorrect backup dir")
}
wantFilename := "portainer.db.bak"
if options.BackupFileName != wantFilename {
log.Fatal().Str("got", options.BackupFileName).Str("want", wantFilename).Msg("incorrect backup file")
}
}
func TestRollback(t *testing.T) {
t.Run("Rollback should restore upgrade after backup", func(t *testing.T) {
version := models.Version{SchemaVersion: "2.4.0"}
_, store, teardown := MustNewTestStore(t, true, false)
defer teardown()
err := store.VersionService.UpdateVersion(&version)
if err != nil {
t.Errorf("Failed updating version: %v", err)
}
_, err = store.backupWithOptions(getBackupRestoreOptions(store.commonBackupDir()))
if err != nil {
log.Fatal().Err(err).Msg("")
}
// Change the current version
version2 := models.Version{SchemaVersion: "2.6.0"}
err = store.VersionService.UpdateVersion(&version2)
if err != nil {
log.Fatal().Err(err).Msg("")
}
err = store.Rollback(true)
if err != nil {
t.Logf("Rollback failed: %s", err)
t.Fail()
return
}
_, err = store.Open()
if err != nil {
t.Logf("Open failed: %s", err)
t.Fail()
return
}
testVersion(store, version.SchemaVersion, t)
})
}
// isFileExist is helper function to check for file existence
func isFileExist(path string) bool {
matches, err := filepath.Glob(path)
if err != nil {
return false
}
return len(matches) > 0
}
// migrateDBTestHelper loads a json representation of a bolt database from srcPath,
// parses it into a database, runs a migration on that database, and then
// compares it with an expected output database.
func migrateDBTestHelper(t *testing.T, srcPath, wantPath string, overrideInstanceId bool) error {
srcJSON, err := os.ReadFile(srcPath)
if err != nil {
t.Fatalf("failed loading source JSON file %v: %v", srcPath, err)
}
// Parse source json to db.
// When we create a new test store, it sets its version field automatically to latest.
_, store, _ := MustNewTestStore(t, true, false)
fmt.Println("store.path=", store.GetConnection().GetDatabaseFilePath())
store.connection.DeleteObject("version", []byte("VERSION"))
// defer teardown()
err = importJSON(t, bytes.NewReader(srcJSON), store)
if err != nil {
return err
}
// Run the actual migrations on our input database.
err = store.MigrateData()
if err != nil {
return err
}
if overrideInstanceId {
// old versions of portainer did not have instance-id. Because this gets generated
// we need to override the expected output to match the expected value to pass the test
v, err := store.VersionService.Version()
if err != nil {
return err
}
v.InstanceID = "463d5c47-0ea5-4aca-85b1-405ceefee254"
err = store.VersionService.UpdateVersion(v)
if err != nil {
return err
}
}
// Assert that our database connection is using bolt so we can call
// exportJson rather than ExportRaw. The exportJson function allows us to
// strip out the metadata which we don't want for our tests.
// TODO: update connection interface in CE to allow us to use ExportRaw and pass meta false
err = store.connection.Close()
if err != nil {
t.Fatalf("err closing bolt connection: %v", err)
}
con, ok := store.connection.(*boltdb.DbConnection)
if !ok {
t.Fatalf("backing database is not using boltdb, but the migrations test requires it")
}
// Convert database back to json.
databasePath := con.GetDatabaseFilePath()
if _, err := os.Stat(databasePath); err != nil {
return fmt.Errorf("stat on %s failed: %s", databasePath, err)
}
gotJSON, err := con.ExportJSON(databasePath, false)
if err != nil {
t.Logf(
"failed re-exporting database %s to JSON: %v",
databasePath,
err,
)
}
wantJSON, err := os.ReadFile(wantPath)
if err != nil {
t.Fatalf("failed loading want JSON file %v: %v", wantPath, err)
}
// Compare the result we got with the one we wanted.
if diff := cmp.Diff(wantJSON, gotJSON); diff != "" {
gotPath := filepath.Join(os.TempDir(), "portainer-migrator-test-fail.json")
os.WriteFile(
gotPath,
gotJSON,
0600,
)
t.Errorf(
"migrate data from %s to %s failed\nwrote migrated input to %s\nmismatch (-want +got):\n%s",
srcPath,
wantPath,
gotPath,
diff,
)
}
return nil
}
// importJSON reads input JSON and commits it to a portainer datastore.Store.
// Errors are logged with the testing package.
func importJSON(t *testing.T, r io.Reader, store *Store) error {
objects := make(map[string]interface{})
// Parse json into map of objects.
d := json.NewDecoder(r)
d.UseNumber()
err := d.Decode(&objects)
if err != nil {
return err
}
// Get database connection from store.
con := store.connection
for k, v := range objects {
switch k {
case "version":
versions, ok := v.(map[string]interface{})
if !ok {
t.Logf("failed casting %s to map[string]interface{}", k)
}
// New format db
version, ok := versions["VERSION"]
if ok {
err := con.CreateObjectWithStringId(
k,
[]byte("VERSION"),
version,
)
if err != nil {
t.Logf("failed writing VERSION in %s: %v", k, err)
}
}
// old format db
dbVersion, ok := versions["DB_VERSION"]
if ok {
numDBVersion, ok := dbVersion.(json.Number)
if !ok {
t.Logf("failed parsing DB_VERSION as json number from %s", k)
}
intDBVersion, err := numDBVersion.Int64()
if err != nil {
t.Logf("failed casting %v to int: %v", numDBVersion, intDBVersion)
}
err = con.CreateObjectWithStringId(
k,
[]byte("DB_VERSION"),
int(intDBVersion),
)
if err != nil {
t.Logf("failed writing DB_VERSION in %s: %v", k, err)
}
}
instanceID, ok := versions["INSTANCE_ID"]
if ok {
err = con.CreateObjectWithStringId(
k,
[]byte("INSTANCE_ID"),
instanceID,
)
if err != nil {
t.Logf("failed writing INSTANCE_ID in %s: %v", k, err)
}
}
edition, ok := versions["EDITION"]
if ok {
err = con.CreateObjectWithStringId(
k,
[]byte("EDITION"),
edition,
)
if err != nil {
t.Logf("failed writing EDITION in %s: %v", k, err)
}
}
case "dockerhub":
obj, ok := v.([]interface{})
if !ok {
t.Logf("failed to cast %s to []interface{}", k)
}
err := con.CreateObjectWithStringId(
k,
[]byte("DOCKERHUB"),
obj[0],
)
if err != nil {
t.Logf("failed writing DOCKERHUB in %s: %v", k, err)
}
case "ssl":
obj, ok := v.(map[string]interface{})
if !ok {
t.Logf("failed to case %s to map[string]interface{}", k)
}
err := con.CreateObjectWithStringId(
k,
[]byte("SSL"),
obj,
)
if err != nil {
t.Logf("failed writing SSL in %s: %v", k, err)
}
case "settings":
obj, ok := v.(map[string]interface{})
if !ok {
t.Logf("failed to case %s to map[string]interface{}", k)
}
err := con.CreateObjectWithStringId(
k,
[]byte("SETTINGS"),
obj,
)
if err != nil {
t.Logf("failed writing SETTINGS in %s: %v", k, err)
}
case "tunnel_server":
obj, ok := v.(map[string]interface{})
if !ok {
t.Logf("failed to case %s to map[string]interface{}", k)
}
err := con.CreateObjectWithStringId(
k,
[]byte("INFO"),
obj,
)
if err != nil {
t.Logf("failed writing INFO in %s: %v", k, err)
}
case "templates":
continue
default:
objlist, ok := v.([]interface{})
if !ok {
t.Logf("failed to cast %s to []interface{}", k)
}
for _, obj := range objlist {
value, ok := obj.(map[string]interface{})
if !ok {
t.Logf("failed to cast %v to map[string]interface{}", obj)
} else {
var ok bool
var id interface{}
switch k {
case "endpoint_relations":
// TODO: need to make into an int, then do that weird
// stringification
id, ok = value["EndpointID"]
default:
id, ok = value["Id"]
}
if !ok {
// endpoint_relations: EndpointID
t.Logf("missing Id field: %s", k)
id = "error"
}
n, ok := id.(json.Number)
if !ok {
t.Logf("failed to cast %v to json.Number in %s", id, k)
} else {
key, err := n.Int64()
if err != nil {
t.Logf("failed to cast %v to int in %s", n, k)
} else {
err := con.CreateObjectWithId(
k,
int(key),
value,
)
if err != nil {
t.Logf("failed writing %v in %s: %v", key, k, err)
}
}
}
}
}
}
}
return nil
}

View File

@@ -0,0 +1,90 @@
package datastore
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore/migrator"
"github.com/portainer/portainer/api/internal/authorization"
)
const dummyLogoURL = "example.com"
// initTestingDBConn creates a settings service with raw database DB connection
// for unit testing usage only since using NewStore will cause cycle import inside migrator pkg
func initTestingSettingsService(dbConn portainer.Connection, preSetObj map[string]interface{}) error {
//insert a obj
if err := dbConn.UpdateObject("settings", []byte("SETTINGS"), preSetObj); err != nil {
return err
}
return nil
}
func setup(store *Store) error {
var err error
dummySettingsObj := map[string]interface{}{
"LogoURL": dummyLogoURL,
}
err = initTestingSettingsService(store.connection, dummySettingsObj)
if err != nil {
return err
}
return nil
}
func TestMigrateSettings(t *testing.T) {
_, store, teardown := MustNewTestStore(t, false, true)
defer teardown()
err := setup(store)
if err != nil {
t.Errorf("failed to complete testing setups, err: %v", err)
}
updatedSettings, err := store.SettingsService.Settings()
// SO -basically, this test _isn't_ testing migration, its testing golang type defaults.
if updatedSettings.LogoURL != dummyLogoURL { // ensure a pre-migrate setting isn't unset
t.Errorf("unexpected value changes in the updated settings, want LogoURL value: %s, got LogoURL value: %s", dummyLogoURL, updatedSettings.LogoURL)
}
if updatedSettings.OAuthSettings.SSO != false { // I recon golang defaulting will make this false
t.Errorf("unexpected default OAuth SSO setting, want: false, got: %t", updatedSettings.OAuthSettings.SSO)
}
if updatedSettings.OAuthSettings.LogoutURI != "" {
t.Errorf("unexpected default OAuth HideInternalAuth setting, want:, got: %s", updatedSettings.OAuthSettings.LogoutURI)
}
m := migrator.NewMigrator(&migrator.MigratorParameters{
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,
DockerhubService: store.DockerHubService,
AuthorizationService: authorization.NewService(store),
})
if err := m.MigrateSettingsToDB30(); err != nil {
t.Errorf("failed to update settings: %v", err)
}
if err != nil {
t.Errorf("failed to retrieve the updated settings: %v", err)
}
if updatedSettings.LogoURL != dummyLogoURL {
t.Errorf("unexpected value changes in the updated settings, want LogoURL value: %s, got LogoURL value: %s", dummyLogoURL, updatedSettings.LogoURL)
}
if updatedSettings.OAuthSettings.SSO != false {
t.Errorf("unexpected default OAuth SSO setting, want: false, got: %t", updatedSettings.OAuthSettings.SSO)
}
if updatedSettings.OAuthSettings.LogoutURI != "" {
t.Errorf("unexpected default OAuth HideInternalAuth setting, want:, got: %s", updatedSettings.OAuthSettings.LogoutURI)
}
}

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