Compare commits

..

151 Commits

Author SHA1 Message Date
cheloRydel
3903fdbfd4 remove feature flag 2022-01-12 17:40:24 -03:00
cheloRydel
4c5a108965 fix(edge): fix Add New device visibility 2022-01-12 16:31:29 -03:00
cheloRydel
c9f71e2e33 merge upstream 2022-01-12 16:08:32 -03:00
cheloRydel
a43983cc1e fixed husky version 2022-01-12 16:01:13 -03:00
cheloRydel
6d5975b1f8 merge develop 2022-01-12 15:54:16 -03:00
cheloRydel
0ae8bc10f3 remove log 2022-01-12 14:48:34 -03:00
cheloRydel
259faec4ff fix ColumnVisibilityMenu columns prop 2022-01-12 13:15:02 -03:00
cheloRydel
f57c54e836 peer review in createEndpoint 2022-01-12 13:03:12 -03:00
cheloRydel
e5ee5e0bec peer review pt 3 2022-01-12 12:58:43 -03:00
cheloRydel
b597eb2618 update husky version 2022-01-11 17:07:03 -03:00
cheloRydel
3e0a7e64eb change selection and actions columns width 2022-01-11 16:58:18 -03:00
cheloRydel
7c36a275a8 peer review pt 2 2022-01-11 16:48:16 -03:00
cheloRydel
1e63ab33cb peer review pt 1 2022-01-11 15:24:50 -03:00
cheloRydel
cff5bb82c3 peer review pt 1 2022-01-11 15:22:44 -03:00
andres-portainer
f4c9cce60b fix(fdo): fix incorrect profile URL INT-45 (#6377) 2022-01-11 12:52:47 -03:00
cheloRydel
927d4d5ed4 try format 2022-01-11 11:38:24 -03:00
cheloRydel
28841f04bd try format 2022-01-11 11:37:59 -03:00
cheloRydel
29b35a6e3c fix labels 2022-01-11 09:44:11 -03:00
cheloRydel
1fa5dc2bb7 fix imports order 2022-01-11 09:39:23 -03:00
cheloRydel
0734aaaf45 fix import 2022-01-11 09:25:46 -03:00
cheloRydel
17a5df42f9 yarn format 2022-01-11 09:20:39 -03:00
Chaim Lev-Ari
389561eb28 fix(registries): sync code with ee [EE-2176] (#6355)
fixes [EE-2176]
2022-01-11 07:35:09 +02:00
cheloRydel
bdd2a5901c remove logs 2022-01-10 18:30:33 -03:00
Dmitry Salakhov
bc54d687be refactor: unit tests (#6367) 2022-01-11 10:26:41 +13:00
cheloRydel
54491e3d51 fix open-amt feature flag value 2022-01-10 17:36:55 -03:00
cheloRydel
157f851920 improve styles 2022-01-10 17:26:19 -03:00
cheloRydel
5a4edbcf9f improve styles 2022-01-10 17:13:36 -03:00
cheloRydel
aabc20bd24 feat(edge): remove unused angular views 2022-01-10 16:50:15 -03:00
cheloRydel
6ca05eed22 fix useQuery 2022-01-10 16:26:13 -03:00
cheloRydel
3002711852 feat(edge): add AMT actions 2022-01-10 16:14:58 -03:00
cheloRydel
6eba0f5f53 feat(edge): add IsEdgeDevice attribute 2022-01-10 12:00:53 -03:00
cheloRydel
a75e8517c9 feat(edge): proper heartnbeat and group columns 2022-01-10 10:50:40 -03:00
Chaim Lev-Ari
8e45076f35 feat(i18n): add support for multiple languages (#6270)
feat(users): add i18n to create access token

chore(app): remove test code
2022-01-10 15:22:21 +02:00
Chaim Lev-Ari
87dda810fc fix(edgestacks): create new stack [EE-2178] (#6311)
* fix(edgestacks): create new stack [EE-2178]

[EE-2178]

* refactor(edgestacks): id is required on create
2022-01-10 11:36:46 +02:00
Chao Geng
4e77d2d772 fix(download-plugin): Image name not available when using watchtower or similar (#6225)
* make plugin version 1.0.22 and correct download-file name

* updated to v2.0.0-rc.2

* rollback download_docker_compose_binary.sh
2022-01-10 10:07:46 +08:00
Dmitry Salakhov
0b62a3d664 feat: bump golang version to 1.17.6 (#6366) 2022-01-10 13:10:02 +13:00
Richard Wei
84f354452b feat(k8s): add ingressClassName to payload EE-2129 (#6265)
* add ingressClassName to payload

* add IngressClass.Name into formValues
2022-01-10 09:02:02 +13:00
cheloRydel
beb46cb767 add ActionsMenu component 2022-01-07 17:35:56 -03:00
andres-portainer
1be0694222 feat(fdo): add FDO profiles INT-22 (#6363)
feat(fdo): add FDO profiles INT-22
2022-01-07 14:46:29 -03:00
cheloRydel
db8e4e8c9a useQuery for amt devices 2022-01-07 14:03:53 -03:00
cheloRydel
9e20979e73 init load devices using react-query 2022-01-07 13:02:27 -03:00
Chaim Lev-Ari
c24d8fab0f chore(tests): update AccessControlForm snapshots [EE-2348] (#6361) 2022-01-07 12:14:36 -03:00
cheloRydel
00f39db0c0 revert husky version update 2022-01-07 10:49:26 -03:00
cheloRydel
e1453c0ce7 parse amt devices values 2022-01-07 10:27:04 -03:00
Chaim Lev-Ari
5362e15624 fix(ldap): show BE border correctly (#6357) 2022-01-07 12:58:15 +02:00
cheloRydel
af30b4d088 merge upstream 2022-01-06 17:16:55 -03:00
cheloRydel
efe8c9f45f merge develop 2022-01-06 16:46:21 -03:00
cheloRydel
8b459adad4 feat(edge): init AMTDevicesDatatable in react show devices 2022-01-06 15:54:27 -03:00
cheloRydel
862a9f56ea feat(edge): init AMTDevicesDatatable in react 2022-01-06 14:15:25 -03:00
cheloRydel
5c8f5e840c feat(edge): init useExpand hook 2022-01-06 13:42:54 -03:00
Chaim Lev-Ari
07c6ce84c2 refactor(environments): remove angular dep from service [EE-2346] (#6360)
refactor(environments): parse axios error
2022-01-06 18:31:47 +02:00
cheloRydel
3260b057d8 feat(edge): add edge devices actions 2022-01-06 12:06:25 -03:00
cheloRydel
7ecfb89c71 fix state column 2022-01-05 19:31:12 -03:00
cheloRydel
352de4a436 fix module 2022-01-05 19:25:15 -03:00
cheloRydel
42a850331b move files 2022-01-05 19:10:54 -03:00
cheloRydel
595b1db085 init add edge devices table view 2022-01-05 18:47:29 -03:00
cheloRydel
09478b56a0 init add edge devices view 2022-01-05 14:30:51 -03:00
Chaim Lev-Ari
ecd0eb6170 refactor(app): create access-control-form react component [EE-2332] (#6346)
* refactor(app): create access-control-form react component [EE-2332]

fix [EE-2332]

* chore(tests): setup msw for async tests and stories

chore(sb): add msw support for storybook

* refactor(access-control): move loading into component

* fix(app): fix users and teams selector stories

* chore(access-control): write test for validation
2022-01-05 18:28:56 +02:00
Marcelo Rydel
e9113865dd refactor(intel): Add Edge Compute Settings view (#6351) 2022-01-05 13:17:05 -03:00
cheloRydel
1440ae68fa Merge branch 'develop' into feat/poc-intel 2022-01-05 12:29:26 -03:00
Marcelo Rydel
8dbb802fb1 feat(react): add FileUploadField and FileUploadForm components [EE-2336] (#6350) 2022-01-05 10:39:34 -03:00
cheloRydel
9bbc2f24ef merge develop 2022-01-04 11:36:38 -03:00
Chaim Lev-Ari
07e7fbd270 refactor(containers): replace containers datatable with react component [EE-1815] (#6059) 2022-01-04 14:16:09 +02:00
cheloRydel
bb5ffb7cc9 Merge branch 'develop' into feat/poc-intel 2022-01-03 19:18:36 -03:00
fhanportainer
65821aaccc feat(react): migrate analytics interface to react. (#6296) [EE-2100] 2022-01-03 17:49:59 +02:00
andres-portainer
ca2259270e fix(fdo): move the FDO client code to the hostmanagement folder INT-44 (#6345) 2022-01-03 11:52:36 -03:00
Chaim Lev-Ari
d33ac8c588 refactor(app): create a composed header component [EE-2329] (#6326)
* refactor(app): create a composed header component

refactor(app): support single child breadcrumbs

fix(app): fix breadcrumbs warning

* refactor(app): import breadcrumbs

* refactor(app): support object breadcrumbs

* chore(app): write tests for header components
2021-12-30 16:46:12 +01:00
andres-portainer
fce6fd27c9 Minor code cleanup. 2021-12-24 11:50:50 -03:00
Marcelo Rydel
102a07346a fix(kubeconfig): fix modal inputType [EE-2325] (#6317) 2021-12-23 10:44:56 -03:00
cheloRydel
f0a197b648 Merge branch 'develop' into feat/poc-intel 2021-12-23 10:25:57 -03:00
Marcelo Rydel
80000806e1 feat(openmt): use .ts services with axios for OpenAMT (#6312) 2021-12-23 10:22:56 -03:00
Chaim Lev-Ari
8fc5a5e8a1 fix(teams): create more then one team [EE-2184] (#6305)
fixes [EE-2184]
2021-12-23 07:57:32 +02:00
andres-portainer
cdfa9b25a8 fix(home): display tags properly [EE-2153] (#6275)
fix(home): display tags properly EE-2153
2021-12-22 19:39:23 -03:00
Richard Wei
e7fc996424 fix scroolbar shown in confirmation dialogs (#6264) 2021-12-22 11:32:04 +08:00
sunportainer
1c374b9fd2 Fix(UI): disable autofill username input EE-2140 (#6252)
* fix/ee-2140/disable-autofill-username
2021-12-22 10:34:55 +08:00
cheloRydel
ec170ae2b4 merge develop 2021-12-21 10:16:39 -03:00
Chaim Lev-Ari
d9db789511 chore(build): add script to analyze webpack bundle [EE-2132] (#6259)
* chore(build): add script to analyze webpack bundle

* chore(build): use single dep (lodash,moment)
2021-12-21 14:32:48 +02:00
Chaim Lev-Ari
5a3687a564 fix(app): main services [EE-1896] (#6279)
[EE-1896]
2021-12-21 12:08:44 +02:00
Chao Geng
6e53bf5dc7 support upgrading (#6256) 2021-12-21 08:45:05 +08:00
Chaim Lev-Ari
e25141d899 fix(modals): upgrade jquery versions (#6303) 2021-12-21 11:51:48 +13:00
cheloRydel
a73977c321 merge develop 2021-12-20 15:24:10 -03:00
Chaim Lev-Ari
4f7b432f44 feat(app): introduce form framework [EE-1946] (#6272) 2021-12-20 19:21:19 +02:00
cheloRydel
f8c1f6ee11 merge develop 2021-12-20 10:15:40 -03:00
Marcelo Rydel
e1f7411926 feat(openamt): change kvm redirection for pop up, always enable features [INT-37] (#6293) 2021-12-17 17:07:58 -03:00
Marcelo Rydel
2fcd238320 feat(openamt): change kvm redirection for pop up, always enable features [INT-37] (#6292) 2021-12-17 12:35:48 -03:00
cheloRydel
24b5fce26d yarn install 2021-12-17 11:00:59 -03:00
Hao Zhang
c5fe994cd2 feat(service): duplication validation for configs and secrets EE-1974 (#6266)
feat(service): check if configs or secrets are duplicated
2021-12-17 20:22:50 +08:00
Hao Zhang
c30292cedd feat(service): rebase and recommit (#6245) 2021-12-17 20:22:13 +08:00
Matt Hook
33a29159d2 fix(db): fix marshalling code so that we're compatible with the existing db (#6286)
* special handling for non-json types

* added tests for json MarshalObject

* another attempt

* Fix marshal/unmarshal code for VERSION bucket

* use short form

* don't discard err

* fix the json_test.go

* remove duplicate string

* added uuid tests

* updated case for strings

Co-authored-by: zees-dev <dev.786zshan@gmail.com>
2021-12-17 08:43:10 +13:00
Marcelo Rydel
ea49a192da feat(openamt): Remove wireless config related code [INT-41] (#6291) 2021-12-16 16:26:38 -03:00
Marcelo Rydel
11e486019a feat(openamt): Better UI/UX for AMT activation loading [INT-39] (#6290) 2021-12-16 16:01:49 -03:00
Richard Wei
187b66f5cb feat(frontend): upgrade frontend dependencies DTD-11 (#6244)
* upgrade webpack, eslint, storybook and other dependencies
2021-12-17 07:52:54 +13:00
Marcelo Rydel
1af028df4f Merge branch 'develop' into feat/poc-intel 2021-12-16 09:32:43 -03:00
Marcelo Rydel
a84ec025e8 feat(openamt): preload existing AMT settings (#6283) 2021-12-16 09:29:03 -03:00
Chaim Lev-Ari
730fdb160d fix(intel): fix switches params [EE-2166] (#6284)
* fix(intel): fix switches params

* feat(settings): prevent openamt panel to render
2021-12-16 11:19:12 +02:00
wheresolivia
efa125790f feat(cy): add data-cy to add kube volume views (#6285) 2021-12-16 16:12:55 +13:00
Marcelo Rydel
6dfe8ad97a fix(intel): Fix switches params (#6282) 2021-12-15 10:05:04 -03:00
Marcelo Rydel
a6d9e566ba feat(openamt): Do not fetch OpenAMT details for an unassociated Edge endpoint (#6273) 2021-12-15 09:32:31 -03:00
deviantony
867168cac7 refactor(fdo): fix develop merge issues 2021-12-15 10:07:00 +00:00
Anthony Lapenna
184db846c2 Merge branch 'develop' into feat/poc-intel 2021-12-15 09:54:52 +00:00
Richard Wei
ac9ca7d5e3 add switch for react query devtools based on .env (#6280) 2021-12-15 11:43:49 +02:00
Sven Dowideit
f99329eb7e chore(store) EE-1981: Refactor/store/error checking, and other refactoring (#6173)
* use the Store interface IsErrObjectNotFound() to avoid revealing internal errors

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* what happens when you extract the datastore interfaces into their own package

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* Start renaming Storage methods

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* extract the boltdb specific code from the Portainer storage code (example, the others need the same)

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* more extract bolt.Tx from datastore code

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* minimise imports by putting moving the struct definition into the file that needs the Service imports

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* more extraction of boltdb.Tx

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* extract the use of bucket.SetSequence

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* almost done - just endpoint.Synchonise :/

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* so, endpoint.Synchonize looks hard, but i can't find where we use it, so 'delete first refactoring'

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* fix test compile errors

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* test compile fixes after rebase

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* fix a mis-remembering I had wrt deserialisation - last time i used AnyData - jsoniter's bindTo looks interesting for the same reason

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* set us up to make the connection an interface

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* make the db connection a datastore interface, and separate out our datastore services from the bolt ones

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* rename methods to something less oltdb internals specific

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* these errors are not boltdb secific

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* start using the db-backend factory method too

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* export boltdb raw in case we can't export from the service layer

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* add a raw export from boltdb to yaml for broken db's, and an export services to yaml in backup

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* add the version info by hand for now

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* actually, the export from services can be fully typed - its the import that needs to do more work

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* redo raw export, and make import capable of using it

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* add DockerHub

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* migration from anything older than v1.21.0 has been broken for quite a while, deleting the un-tested code

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* fix go test ./... again

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* my goland wasn't setup to gofmt

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* move the two extremely dubious migration tests down into store, so they can use the test store code

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* the migrator is now free of boltdb

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* reverse goland overzealous replcement of internal with boltdb

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* more undo over-zealous goland internal->boltdb

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* yay, now bolt is only mentioned inside the api/database/ dir

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* and this might be the last of the boltdb references?

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* add todo

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* extract the store code into a separate module too

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* don't need the fileService in boltdb anymore

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* use IsErrObjectNotFound()

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* use a string to select what database backend we use

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* make isNew store an ephemeral bool that doesn't stay true after we've initialised it

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* move the import.json wip to a separate file so its more obvious - we'll be using it for testing, emergency fixups, and in the next part of the store work, when we improve migrations and data model lifecycles

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* undo vscode formatting html

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* fix app templates symbol (#6221)

* feat(webhook) EE-2125 send registry auth haeder when update swarms service via webhook (#6220)

* feat(webhook) EE-2125 add some helpers to registry utils

* feat(webhook) EE-2125 persist registryID when creating a webhook

* feat(webhook) EE-2125 send registry auth header when executing a webhook

* feat(webhook) EE-2125 send registryID to backend when creating a service with webhook

* feat(webhook) EE-2125 use the initial registry ID to create webhook on editing service screen

* feat(webhook) EE-2125 update webhook when update registry

* feat(webhook) EE-2125 add endpoint of update webhook

* feat(webhook) EE-2125 code cleanup

* feat(webhook) EE-2125 fix a typo

* feat(webhook) EE-2125 fix circle import issue with unit test

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

* fix(kubeconfig): show kubeconfig download button for non admin users [EE-2123] (#6204)

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

* fix data-cy for k8s cluster menu (#6226)

LGTM

* feat(stack): make stack created from app template editable EE-1941 (#6104)

feat(stack): make stack from app template editable

* fix(container):disable Duplicate/Edit button when the container is portainer (#6223)

* fix/ee-1909/show-pull-image-error (#6195)

Co-authored-by: sunportainer <ericsun@SG1.local>

* feat(cy): add data-cy to helm install button (#6241)

* feat(cy): add data-cy to add registry button (#6242)

* refactor(app): convert root folder files to es6 (#4159)

* refactor(app): duplicate constants as es6 exports (#4158)

* fix(docker): provide workaround to save network name variable  (#6080)

* fix/EE-1862/unable-to-stop-or-remove-stack workaround for var without default value in yaml file

* fix/EE-1862/unable-to-stop-or-remove-stack check yaml file

* fixed func and var names

* wrapper error and used bool for stringset

* UT case for createNetworkEnvFile

* UT case for %s=%s

* powerful StringSet

* wrapper error for extract network name

* wrapper all the return err

* store more env

* put to env file

* make default value None

* feat: gzip static resources (#6258)

* fix(ssl)//handle --sslcert and --sslkey ee-2106 (#6203)

* fix/ee-2106/handle-sslcert-sslkey

Co-authored-by: sunportainer <ericsun@SG1.local>

* fix(server):support disable https only ee-2068 (#6232)

* fix/ee-2068/disable-forcely-https

* feat(store): implement store tests EE-2112 (#6224)

* add store tests

* add some more tests

* Update missing helm user repo methods

* remove redundant comments

* add webhook export

* update webhooks

* use the Store interface IsErrObjectNotFound() to avoid revealing internal errors

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* what happens when you extract the datastore interfaces into their own package

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* Start renaming Storage methods

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* extract the boltdb specific code from the Portainer storage code (example, the others need the same)

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* more extract bolt.Tx from datastore code

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* minimise imports by putting moving the struct definition into the file that needs the Service imports

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* more extraction of boltdb.Tx

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* extract the use of bucket.SetSequence

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* almost done - just endpoint.Synchonise :/

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* so, endpoint.Synchonize looks hard, but i can't find where we use it, so 'delete first refactoring'

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* fix test compile errors

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* test compile fixes after rebase

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* fix a mis-remembering I had wrt deserialisation - last time i used AnyData - jsoniter's bindTo looks interesting for the same reason

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* set us up to make the connection an interface

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* make the db connection a datastore interface, and separate out our datastore services from the bolt ones

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* rename methods to something less oltdb internals specific

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* these errors are not boltdb secific

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* start using the db-backend factory method too

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* export boltdb raw in case we can't export from the service layer

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* add a raw export from boltdb to yaml for broken db's, and an export services to yaml in backup

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* add the version info by hand for now

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* actually, the export from services can be fully typed - its the import that needs to do more work

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* redo raw export, and make import capable of using it

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* add DockerHub

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* migration from anything older than v1.21.0 has been broken for quite a while, deleting the un-tested code

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* fix go test ./... again

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* my goland wasn't setup to gofmt

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* move the two extremely dubious migration tests down into store, so they can use the test store code

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* the migrator is now free of boltdb

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* reverse goland overzealous replcement of internal with boltdb

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* more undo over-zealous goland internal->boltdb

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* yay, now bolt is only mentioned inside the api/database/ dir

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* and this might be the last of the boltdb references?

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* add todo

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* extract the store code into a separate module too

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* don't need the fileService in boltdb anymore

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* use IsErrObjectNotFound()

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* use a string to select what database backend we use

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* make isNew store an ephemeral bool that doesn't stay true after we've initialised it

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* move the import.json wip to a separate file so its more obvious - we'll be using it for testing, emergency fixups, and in the next part of the store work, when we improve migrations and data model lifecycles

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* undo vscode formatting html

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* Update missing helm user repo methods

* feat(store): implement store tests EE-2112 (#6224)

* add store tests

* add some more tests

* remove redundant comments

* add webhook export

* update webhooks

* fix build issues after rebasing

* move migratorparams

* remove unneeded integer type conversions

* disable the db import/export for now

Co-authored-by: Richard Wei <54336863+WaysonWei@users.noreply.github.com>
Co-authored-by: cong meng <mcpacino@gmail.com>
Co-authored-by: Simon Meng <simon.meng@portainer.io>
Co-authored-by: Marcelo Rydel <marcelorydel26@gmail.com>
Co-authored-by: Hao Zhang <hao.zhang@portainer.io>
Co-authored-by: sunportainer <93502624+sunportainer@users.noreply.github.com>
Co-authored-by: sunportainer <ericsun@SG1.local>
Co-authored-by: wheresolivia <78844659+wheresolivia@users.noreply.github.com>
Co-authored-by: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Co-authored-by: Chao Geng <93526589+chaogeng77977@users.noreply.github.com>
Co-authored-by: Dmitry Salakhov <to@dimasalakhov.com>
Co-authored-by: Matt Hook <hookenz@gmail.com>
2021-12-15 15:26:09 +13:00
Matt Hook
b02bf0c9d7 release 2.11 2021-12-15 14:28:55 +13:00
Marcelo Rydel
738ec4316d feat(fdo): add import device UI [INT-20] (#6240)
feat(fdo): add import device UI INT-20
2021-12-14 19:51:16 -03:00
Chaim Lev-Ari
7ae5a3042c feat(app): introduce component library in react [EE-1816] (#6236)
* refactor(app): replace notification with es6 service (#6015) [EE-1897]

chore(app): format

* refactor(containers): remove the dependency on angular modal service (#6017) [EE-1898]

* refactor(app): remove angular from http-request [EE-1899] (#6016)

* feat(app): add axios [EE-2035](#6077)

* refactor(feature): remove angular dependency from feature service [EE-2034] (#6078)

* refactor(app): replace box-selector with react component (#6046)

fix: rename angular2react

refactor(app): make box-selector type generic

feat(app): add story for box-selector

feat(app): test box-selector

feat(app): add stories for box selector item

fix(app): remove unneccesary element

refactor(app): remove assign

* feat(feature): add be-indicator in react [EE-2005] (#6106)

* refactor(app): add react components for headers [EE-1949] (#6023)

* feat(auth): provide user context

* feat(app): added base header component [EE-1949]

style(app): reformat

refactor(app/header): use same api as angular

* feat(app): add breadcrumbs component [EE-2024]

* feat(app): remove u element from user links

* fix(users): handle axios errors

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

* refactor(app): convert switch component to react [EE-2005] (#6025)

Co-authored-by: Marcelo Rydel <marcelorydel26@gmail.com>
2021-12-15 08:14:53 +13:00
Chaim Lev-Ari
eb9f6c77f4 refactor(endpoints): remove endpointProvider from views [EE-1136] (#5359)
[EE-1136]
2021-12-14 09:34:54 +02:00
sunportainer
7088da5157 fix(server):support disable https only ee-2068 (#6232)
* fix/ee-2068/disable-forcely-https
2021-12-14 08:40:44 +08:00
sunportainer
da422d6ed6 fix(ssl)//handle --sslcert and --sslkey ee-2106 (#6203)
* fix/ee-2106/handle-sslcert-sslkey

Co-authored-by: sunportainer <ericsun@SG1.local>
2021-12-13 23:43:55 +08:00
Dmitry Salakhov
eb517c2e12 feat: gzip static resources (#6258) 2021-12-13 22:34:55 +13:00
Anthony Lapenna
8567c4051a Merge branch 'develop' into feat/poc-intel 2021-12-13 07:27:19 +00:00
Marcelo Rydel
415af981f8 feat(openamt): Disable the ability to use KVM and OOB actions on a MPS disconnected device [INT-36] (#6254) 2021-12-10 17:05:38 -03:00
Marcelo Rydel
3acaee1489 feat(openamt): Increase OpenAMT timeouts [INT-30] (#6253) 2021-12-11 07:45:12 +13:00
Marcelo Rydel
27ced894fd feat(openamt): hide wireless config in OpenAMT form (#6250) 2021-12-09 17:15:57 -03:00
Chao Geng
76916b0ad6 fix(docker): provide workaround to save network name variable (#6080)
* fix/EE-1862/unable-to-stop-or-remove-stack workaround for var without default value in yaml file

* fix/EE-1862/unable-to-stop-or-remove-stack check yaml file

* fixed func and var names

* wrapper error and used bool for stringset

* UT case for createNetworkEnvFile

* UT case for %s=%s

* powerful StringSet

* wrapper error for extract network name

* wrapper all the return err

* store more env

* put to env file

* make default value None
2021-12-09 23:09:34 +08:00
Chaim Lev-Ari
19a09b4730 refactor(app): duplicate constants as es6 exports (#4158) 2021-12-09 10:48:47 +02:00
Chaim Lev-Ari
8f32517baa refactor(app): convert root folder files to es6 (#4159) 2021-12-09 09:38:07 +02:00
wheresolivia
f864b1bf69 feat(cy): add data-cy to add registry button (#6242) 2021-12-09 18:38:12 +13:00
wheresolivia
e57454cd7c feat(cy): add data-cy to helm install button (#6241) 2021-12-09 12:39:49 +13:00
andres-portainer
cdf954a5e5 feat(fdo): implement Owner client INT-17 (#6231)
feat(fdo): implement Owner client INT-17
2021-12-08 19:33:23 -03:00
andres-portainer
dbe17b9425 feat(fdo): implement the FDO configuration settings INT-19 (#6238)
feat(fdo): implement the FDO configuration settings INT-19
2021-12-08 15:08:42 -03:00
sunportainer
b3e04adee3 fix/ee-1909/show-pull-image-error (#6195)
Co-authored-by: sunportainer <ericsun@SG1.local>
2021-12-08 12:07:45 +08:00
Hao Zhang
a78d8a4ff1 fix(container):disable Duplicate/Edit button when the container is portainer (#6223) 2021-12-07 23:25:20 +08:00
Marcelo Rydel
b36a0ec258 feat(openamt): Enable KVM by default [INT-25] (#6228) 2021-12-07 09:14:16 -03:00
Hao Zhang
9f5ac154aa feat(stack): make stack created from app template editable EE-1941 (#6104)
feat(stack): make stack from app template editable
2021-12-07 19:46:58 +08:00
Richard Wei
0627e16b35 fix data-cy for k8s cluster menu (#6226)
LGTM
2021-12-07 14:25:20 +13:00
Marcelo Rydel
2a1b8efaed fix(kubeconfig): show kubeconfig download button for non admin users [EE-2123] (#6204)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-12-06 18:40:59 -03:00
cong meng
98972dec0d feat(webhook) EE-2125 send registry auth haeder when update swarms service via webhook (#6220)
* feat(webhook) EE-2125 add some helpers to registry utils

* feat(webhook) EE-2125 persist registryID when creating a webhook

* feat(webhook) EE-2125 send registry auth header when executing a webhook

* feat(webhook) EE-2125 send registryID to backend when creating a service with webhook

* feat(webhook) EE-2125 use the initial registry ID to create webhook on editing service screen

* feat(webhook) EE-2125 update webhook when update registry

* feat(webhook) EE-2125 add endpoint of update webhook

* feat(webhook) EE-2125 code cleanup

* feat(webhook) EE-2125 fix a typo

* feat(webhook) EE-2125 fix circle import issue with unit test

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-12-07 09:11:44 +13:00
Richard Wei
aa8fc52106 fix app templates symbol (#6221) 2021-12-06 19:15:18 +13:00
zees-dev
5839f96787 - standard user cannot delete another users api-keys (#6208) (#6217)
- added new method to get api key by ID
- added tests
2021-12-06 10:21:33 +13:00
zees-dev
7cc28b10a0 fallback to depracted copy text if clipboard api not available (#6200) (#6218) 2021-12-06 10:01:54 +13:00
Marcelo Rydel
7ddea7e09e feat(openamt): Enhance the Environments MX to activate OpenAMT on compatible environments [INT-7] (#6196) 2021-12-03 15:27:52 -03:00
Marcelo Rydel
4173702662 feat(openamt): add AMT Devices KVM Connection [INT-10] (#6179) 2021-12-03 13:00:59 -03:00
Marcelo Rydel
11268e7816 feat(openamt): add AMT Devices Ouf of Band Managamenet actions [INT-9] (#6171) 2021-12-03 12:44:51 -03:00
Marcelo Rydel
e2bb76ff58 feat(openamt): add AMT Devices information in Environments view [INT-8] (#6169) 2021-12-03 12:26:53 -03:00
Prabhat Khera
4aea5690a8 feat(config): add base url support EE-506 (#5999) 2021-12-03 14:34:45 +13:00
sunportainer
335f951e6b Fix(stack)/update StackUpdateGit swagger info to POST EE-2019 (#6176)
* fix/EE-2019/Fix-stackgitupdate-swagger

Co-authored-by: sunportainer <ericsun@SG1.local>
2021-12-02 09:54:38 +08:00
Hao Zhang
42e782452c fix(container): prevent user from editing the portainer container it self EE-917 (#6093)
* fix(container): prevent from editing portainer container

* fix(container): prevent from editing portainer container

* Missing kill operation

* fix(container): enhance creating stack from template

* fix(docker): prevent user from editing the portainer container itself EE-917

* fix(docker): enhance code style

* fix(container): fix issues from code review

* fix(container): enhance creating stack from template

* fix(container): some code review issues

* fix(container): disable leave network when the container is portainer

* fix(container): disable leave network when the container is portainer
2021-12-02 08:41:05 +08:00
Chaim Lev-Ari
d2fe76368a fix(environments): show kubeconfig env list in dark mode (#6156) 2021-12-01 13:58:55 +13:00
Prabhat Khera
aa7d7845c1 verify repositry URL from template json when coping (#6036) (#6111) 2021-12-01 13:54:47 +13:00
cong meng
a86c7046df feat(registry) EE-806 add support for AWS ECR (#6165)
* feat(ecr) EE-806 add support for aws ecr

* feat(ecr) EE-806 fix wrong doc for Ecr Region

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-12-01 13:18:57 +13:00
Matt Hook
ff6185cc81 fix(openamt): fix IsFeatureFlagEnabled, rename MPS Url to MPS Server (#6185)
Co-authored-by: cheloRydel <marcelorydel26@gmail.com>
2021-12-01 12:35:47 +13:00
Matt Hook
f360392d39 Revert "fix(openamt): fix IsFeatureFlagEnabled, rename MPS Url to MPS Server [INT-6] (#6172)" (#6182)
This reverts commit c267355759.
2021-12-01 11:20:20 +13:00
Marcelo Rydel
fa44a62c4a fix(react): use ctrl directive in WidgetTitle component [EE-2118] (#6181) 2021-11-30 18:22:39 -03:00
huib-portainer
2a384d4c64 Update endpointItem.html (#6142)
feat(home): show cpu and ram for non local endpoints EE-2077
2021-11-30 18:46:38 +13:00
LP B
b6fbf8eecc fix(k8s/ingress): ensure new ports are only added to ingress only if app is published via ingress (#6153)
* fix(k8s/ingress): ensure new ports are only added to ingress only if app is published via ingress

* refactor(k8s/ingress): removed deleted ports of ingress in a single pass
2021-11-30 17:14:52 +13:00
zees-dev
69c17986d9 feat(api-key/backend): introducing support for api-key based auth EE-978 (#6079)
* feat(access-token): Multi-auth middleware support EE-1891 (#5936)

* AnyAuth middleware initial implementation with tests

* using mux.MiddlewareFunc instead of custom definition

* removed redundant comments

* - ExtractBearerToken bouncer func made private
- changed helm token handling functionality to use jwt service to convert token to jwt string
- updated tests
- fixed helm list broken test due to missing token in request context

* rename mwCheckAuthentication -> mwCheckJWTAuthentication

* - introduce initial api-key auth support using X-API-KEY header
- added tests to validate x-api-key request header presence

* updated core mwAuthenticatedUser middleware to support multiple auth paradigms

* - simplified anyAuth middleware
- enforcing authmiddleware to implement verificationFunc interface
- created tests for middleware

* simplify bouncer

Co-authored-by: Dmitry Salakhov <to@dimasalakhov.com>

* feat(api-key): user-access-token generation endpoint EE-1889 EE-1888 EE-1895 (#6012)

* user-access-token generation endpoint

* fix comment

* - introduction of apikey service
- seperation of repository from service logic - called in handler

* fixed tests

* - fixed api key prefix
- added tests

* added another test for digest matching

* updated swagger spec for access token creation

* api key response returns raw key and struct - easing testability

* test for api key prefix length

* added another TODO to middleware

* - api-key prefix rune -> string (rune does not auto-encode when response sent back to client)
- digest -> pointer as we want to allow nil values and omit digest in responses (when nil)

* - updated apikey struct
- updated apikey service to support all common operations
- updated apikey repo
- integration of apikey service into bouncer
- added test for all apikey service functions
- boilerplate code for apikey service integration

* - user access token generation tests
- apiKeyLookup updated to support query params
- added api-key tests for query params
- added api-key tests for apiKeyLookup

* get and remove access token handlers

* get and remove access token handler tests

* - delete user deletes all associated api keys
- tests for this functionality

* removed redundant []byte cast

* automatic api-key eviction set within cache for 1 hour

* fixed bug with loop var using final value

* fixed service comment

* ignore bolt error responses

* case-insensitive query param check

* simplified query var assignment

* - added GetAPIKey func to get by unique id
- updated DeleteAPIKey func to not require user ID
- updated tests

* GenerateRandomKey helper func from github.com/gorilla/securecookie moved to codebase

* json response casing for api-keys fixed

* updating api-key will update the cache

* updated golang LRU cache

* using hashicorps golang-LRU cache for api keys

* simplified jwt check in create user access token

* fixed api-key update logic on cache miss

* Prefix generated api-keys with `ptr_` (#6067)

* prefix api-keys with 'ptr_'

* updated apikey description

* refactor

Co-authored-by: Dmitry Salakhov <to@dimasalakhov.com>

* helm list test refactor

* fixed user delete test

* reduce test nil pointer errors

* using correct http 201 created status code for token creation; updated tests

* fixed swagger doc user id path param for user access token based endpoints

* added api-key security openapi spec to existing jwt secured endpoints (#6091)

* fixed flaky test

* apikey datecreated and lastused attrs converted to unix timestamp

* feat(user): added access token datatable. (#6124)

* feat(user): added access token datatable.

* feat(tokens): only display lastUsed time when it is not the default date

* Update app/portainer/views/account/accountController.js

Co-authored-by: zees-dev <63374656+zees-dev@users.noreply.github.com>

* Update app/portainer/views/account/accountController.js

Co-authored-by: zees-dev <63374656+zees-dev@users.noreply.github.com>

* Update app/portainer/views/account/accountController.js

Co-authored-by: zees-dev <63374656+zees-dev@users.noreply.github.com>

* Update app/portainer/components/datatables/access-tokens-datatable/accessTokensDatatableController.js

Co-authored-by: zees-dev <63374656+zees-dev@users.noreply.github.com>

* Update app/portainer/services/api/userService.js

Co-authored-by: zees-dev <63374656+zees-dev@users.noreply.github.com>

* feat(improvements): proposed datatable improvements to speed up dev time (#6138)

* modal code update

* updated datatable filenames, updated controller to be default class export

* fix(access-token): code improvement.

Co-authored-by: zees-dev <63374656+zees-dev@users.noreply.github.com>

* feat(apikeys): create access token view initial implementation EE-1886 (#6129)

* CopyButton implementation

* Code component implementation

* ToolTip component migration to another folder

* TextTip component implementation - continued

* form Heading component

* Button component updated to be more dynamic

* copybutton - small size

* form control pass tip error

* texttip small text

* CreateAccessToken react feature initial implementation

* create user access token angularjs view implementation

* registration of CreateAccessToken component in AngularJS

* user token generation API request moved to angular service, method passed down instead

* consistent naming of access token operations; clustered similar code together

* any user can add access token

* create access token page routing

* moved code component to the correct location

* removed isadmin check as all functionality applicable to all users

* create access token angular view moved up a level

* fixed PR issues, updated PR

* addressed PR issues/improvements

* explicit hr for horizontal line

* fixed merge conflict storybook build breaking

* - apikey test
- cache test

* addressed testing issues:
- description validations
- remove token description link on table

* fix(api-keys): user role change evicts user keys in cache EE-2113 (#6168)

* user role change evicts user api keys in cache

* EvictUserKeyCache -> InvalidateUserKeyCache

* godoc for InvalidateUserKeyCache func

* additional test line

* disable add access token button after adding token to prevent spam

Co-authored-by: Dmitry Salakhov <to@dimasalakhov.com>
Co-authored-by: fhanportainer <79428273+fhanportainer@users.noreply.github.com>
2021-11-30 15:31:16 +13:00
Sven Dowideit
120584909c fix(docker-event-display): EE-1968: support (event_name)[:extra info] for all event Actions, and append it to the output details (#6092)
Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>
2021-11-30 09:59:55 +10:00
Richard Wei
c24dc3112b fix(registry): fix order of registries in drop down menu EE-1939 (#5960)
Co-authored-by: Prabhat Khera <prabhat.khera@portainer.io>
2021-11-30 11:03:08 +13:00
Prabhat Khera
1e80061186 feat(docker): allow docker container resource settings without restart EE-1942 (#6065)
Co-authored-by: sam <sam@allofword>
Co-authored-by: sam@gemibook <huapox@126.com>
Co-authored-by: Prabhat Khera <prabhat.khera@gmail.com>
2021-11-30 11:01:09 +13:00
Marcelo Rydel
c267355759 fix(openamt): fix IsFeatureFlagEnabled, rename MPS Url to MPS Server [INT-6] (#6172) 2021-11-29 18:44:33 -03:00
Marcelo Rydel
47c1af93ea feat(openamt): Configuration of the OpenAMT capability [INT-6] (#6071)
Co-authored-by: Sven Dowideit <sven.dowideit@portainer.io>
2021-11-29 10:06:50 -03:00
476 changed files with 10291 additions and 13459 deletions

View File

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

View File

@@ -99,7 +99,3 @@ overrides:
'jest/globals': true
rules:
'react/jsx-no-constructed-context-values': off
- files:
- app/**/*.stories.*
rules:
'no-alert': off

View File

@@ -1,4 +0,0 @@
# prettier
cf5056d9c03b62d91a25c3b9127caac838695f98
# prettier v2 (put here after fix/EE-2344/fix-eslint-issues is merged)

View File

@@ -1,41 +0,0 @@
name: Lint
on:
push:
branches:
- master
- develop
- release/*
pull_request:
branches:
- master
- develop
- release/*
jobs:
run-linters:
name: Run linters
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v1
with:
node-version: 12
# ESLint and Prettier must be in `package.json`
- name: Install Node.js dependencies
run: yarn
- name: Run linters
uses: wearerequired/lint-action@v1
with:
eslint: true
eslint_extensions: ts,tsx,js,jsx
prettier: true
prettier_dir: app/
gofmt: true
gofmt_dir: api/

View File

@@ -1,11 +0,0 @@
name: Test Frontend
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install modules
run: yarn
- name: Run tests
run: yarn test:client

View File

@@ -4,16 +4,21 @@
"htmlWhitespaceSensitivity": "strict",
"overrides": [
{
"files": ["*.html"],
"files": [
"*.html"
],
"options": {
"parser": "angular"
}
},
{
"files": ["*.{j,t}sx", "*.ts"],
"files": [
"*.{j,t}sx",
"*.ts"
],
"options": {
"printWidth": 80
}
}
]
}
}

View File

@@ -1,5 +1,4 @@
{
"go.lintTool": "golangci-lint",
"go.lintFlags": ["--fast", "-E", "exportloopref"],
"gitlens.advanced.blame.customArguments": ["–ignore-revs-file", ".git-blame-ignore-revs"]
"go.lintFlags": ["--fast", "-E", "exportloopref"]
}

View File

@@ -80,8 +80,7 @@ func CreateBackupArchive(password string, gate *offlinegate.OfflineGate, datasto
}
func backupDb(backupDirPath string, datastore dataservices.DataStore) error {
dbFileName := datastore.Connection().GetDatabaseFileName()
backupWriter, err := os.Create(filepath.Join(backupDirPath, dbFileName))
backupWriter, err := os.Create(filepath.Join(backupDirPath, "portainer.db"))
if err != nil {
return err
}

View File

@@ -10,13 +10,12 @@ import (
"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 = filesToBackup
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 {
@@ -66,20 +65,5 @@ func restoreFiles(srcDir string, destinationDir string) error {
return err
}
}
// TODO: This is very boltdb module specific once again due to the filename. Move to bolt module? Refactor for another day
// Prevent the possibility of having both databases. Remove any default new instance
os.Remove(filepath.Join(destinationDir, boltdb.DatabaseFileName))
os.Remove(filepath.Join(destinationDir, boltdb.EncryptedDatabaseFileName))
// Now copy the database. It'll be either portainer.db or portainer.edb
// Note: CopyPath does not return an error if the source file doesn't exist
err := filesystem.CopyPath(filepath.Join(srcDir, boltdb.EncryptedDatabaseFileName), destinationDir)
if err != nil {
return err
}
return filesystem.CopyPath(filepath.Join(srcDir, boltdb.DatabaseFileName), destinationDir)
return nil
}

View File

@@ -50,17 +50,13 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").String(),
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").String(),
Rollback: kingpin.Flag("rollback", "Rollback the database store to the previous version").Bool(),
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each environment snapshot job").String(),
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each environment snapshot job").Default(defaultSnapshotInterval).String(),
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
Templates: kingpin.Flag("templates", "URL to the templates definitions.").Short('t').String(),
BaseURL: kingpin.Flag("base-url", "Base URL parameter such as portainer if running portainer as http://yourdomain.com/portainer/.").Short('b').Default(defaultBaseURL).String(),
InitialMmapSize: kingpin.Flag("initial-mmap-size", "Initial mmap size of the database in bytes").Int(),
MaxBatchSize: kingpin.Flag("max-batch-size", "Maximum size of a batch").Int(),
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(),
}
kingpin.Parse()
@@ -129,7 +125,7 @@ func validateEndpointURL(endpointURL string) error {
}
func validateSnapshotInterval(snapshotInterval string) error {
if snapshotInterval != "" {
if snapshotInterval != defaultSnapshotInterval {
_, err := time.ParseDuration(snapshotInterval)
if err != nil {
return errInvalidSnapshotInterval

View File

@@ -20,6 +20,6 @@ const (
defaultSSL = "false"
defaultSSLCertPath = "/certs/portainer.crt"
defaultSSLKeyPath = "/certs/portainer.key"
defaultSnapshotInterval = "5m"
defaultBaseURL = "/"
defaultSecretKeyName = "portainer"
)

View File

@@ -19,5 +19,4 @@ const (
defaultSSLKeyPath = "C:\\certs\\portainer.key"
defaultSnapshotInterval = "5m"
defaultBaseURL = "/"
defaultSecretKeyName = "portainer"
)

View File

@@ -13,7 +13,7 @@ func importFromJson(fileService portainer.FileService, store *datastore.Store) {
importFile := "/data/import.json"
if exists, _ := fileService.FileExists(importFile); exists {
if err := store.Import(importFile); err != nil {
logrus.WithError(err).Debugf("Import %s failed", importFile)
logrus.WithError(err).Debugf("import %s failed", importFile)
// TODO: should really rollback on failure, but then we have nothing.
} else {
@@ -23,7 +23,7 @@ func importFromJson(fileService portainer.FileService, store *datastore.Store) {
// I also suspect that everything from "Init to Init" is potentially a migration
err := store.Init()
if err != nil {
log.Fatalf("Failed initializing data store: %v", err)
log.Fatalf("failed initializing data store: %v", err)
}
}
}

View File

@@ -1,41 +1,18 @@
package main
import (
"fmt"
"log"
"strings"
"github.com/sirupsen/logrus"
)
type portainerFormatter struct {
logrus.TextFormatter
}
func (f *portainerFormatter) Format(entry *logrus.Entry) ([]byte, error) {
var levelColor int
switch entry.Level {
case logrus.DebugLevel, logrus.TraceLevel:
levelColor = 31 // gray
case logrus.WarnLevel:
levelColor = 33 // yellow
case logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel:
levelColor = 31 // red
default:
levelColor = 36 // blue
}
return []byte(fmt.Sprintf("\x1b[%dm%s\x1b[0m %s %s\n", levelColor, strings.ToUpper(entry.Level.String()), entry.Time.Format(f.TimestampFormat), entry.Message)), nil
}
func configureLogger() {
logger := logrus.New() // logger is to implicitly substitute stdlib's log
log.SetOutput(logger.Writer())
formatter := &logrus.TextFormatter{DisableTimestamp: true, DisableLevelTruncation: true}
formatterLogrus := &portainerFormatter{logrus.TextFormatter{DisableTimestamp: false, DisableLevelTruncation: true, TimestampFormat: "2006/01/02 15:04:05", FullTimestamp: true}}
logger.SetFormatter(formatter)
logrus.SetFormatter(formatterLogrus)
logrus.SetFormatter(formatter)
logger.SetLevel(logrus.DebugLevel)
logrus.SetLevel(logrus.DebugLevel)

View File

@@ -2,12 +2,13 @@ package main
import (
"context"
"crypto/sha256"
"fmt"
"log"
"os"
"path"
"strconv"
"strings"
"time"
"github.com/sirupsen/logrus"
@@ -18,7 +19,6 @@ 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/dataservices"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/docker"
@@ -47,12 +47,12 @@ func initCLI() *portainer.CLIFlags {
var cliService portainer.CLIService = &cli.Service{}
flags, err := cliService.ParseFlags(portainer.APIVersion)
if err != nil {
logrus.Fatalf("Failed parsing flags: %v", err)
log.Fatalf("failed parsing flags: %v", err)
}
err = cliService.ValidateFlags(flags)
if err != nil {
logrus.Fatalf("Failed validating flags:%v", err)
log.Fatalf("failed validating flags:%v", err)
}
return flags
}
@@ -60,88 +60,99 @@ func initCLI() *portainer.CLIFlags {
func initFileService(dataStorePath string) portainer.FileService {
fileService, err := filesystem.NewService(dataStorePath, "")
if err != nil {
logrus.Fatalf("Failed creating file service: %v", err)
log.Fatalf("failed creating file service: %v", err)
}
return fileService
}
func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService portainer.FileService, shutdownCtx context.Context) dataservices.DataStore {
connection, err := database.NewDatabase("boltdb", *flags.Data, secretKey)
func initDataStore(flags *portainer.CLIFlags, fileService portainer.FileService, shutdownCtx context.Context) dataservices.DataStore {
connection, err := database.NewDatabase("boltdb", *flags.Data)
if err != nil {
logrus.Fatalf("failed creating database connection: %s", err)
panic(err)
}
if bconn, ok := connection.(*boltdb.DbConnection); ok {
bconn.MaxBatchSize = *flags.MaxBatchSize
bconn.MaxBatchDelay = *flags.MaxBatchDelay
bconn.InitialMmapSize = *flags.InitialMmapSize
} else {
logrus.Fatalf("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 {
logrus.Fatalf("Failed opening store: %v", err)
log.Fatalf("failed opening store: %v", err)
}
if *flags.Rollback {
err := store.Rollback(false)
if err != nil {
logrus.Fatalf("Failed rolling back: %v", err)
log.Fatalf("failed rolling back: %s", err)
}
logrus.Println("Exiting rollback")
log.Println("Exiting rollback")
os.Exit(0)
return nil
}
// Init sets some defaults - it's basically a migration
// Init sets some defaults - its basically a migration
err = store.Init()
if err != nil {
logrus.Fatalf("Failed initializing data store: %v", err)
log.Fatalf("failed initializing data store: %v", err)
}
if isNew {
// from MigrateData
store.VersionService.StoreDBVersion(portainer.DBVersion)
} else {
storedVersion, err := store.VersionService.DBVersion()
// Disabled for now. Can't use feature flags due to the way that works
// EXPERIMENTAL, will only activate if `/data/import.json` exists
//importFromJson(fileService, store)
err := updateSettingsFromFlags(store, flags)
if err != nil {
logrus.Fatalf("Something Failed during creation of new database: %v", err)
}
if storedVersion != portainer.DBVersion {
err = store.MigrateData()
if err != nil {
logrus.Fatalf("Failed migration: %v", err)
}
log.Fatalf("failed updating settings from flags: %v", err)
}
}
err = updateSettingsFromFlags(store, flags)
storedVersion, err := store.VersionService.DBVersion()
if err != nil {
logrus.Fatalf("Failed updating settings from flags: %v", err)
log.Fatalf("Something failed during creation of new database: %v", err)
}
if storedVersion != portainer.DBVersion {
err = store.MigrateData()
if err != nil {
log.Fatalf("failed migration: %v", err)
}
}
// this is for the db restore functionality - needs more tests.
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 {
logrus.WithError(err).Debugf("failed to export to %s", exportFilename)
} else {
logrus.Debugf("exported to %s", exportFilename)
}
connection.Close()
}()
return store
}
func initComposeStackManager(assetsPath string, configPath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
composeWrapper, err := exec.NewComposeStackManager(assetsPath, configPath, proxyManager)
if err != nil {
logrus.Fatalf("Failed creating compose manager: %v", err)
log.Fatalf("failed creating compose manager: %s", err)
}
return composeWrapper
}
func initSwarmStackManager(assetsPath string, configPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService, dataStore dataservices.DataStore) (portainer.SwarmStackManager, error) {
func initSwarmStackManager(
assetsPath string,
configPath string,
signatureService portainer.DigitalSignatureService,
fileService portainer.FileService,
reverseTunnelService portainer.ReverseTunnelService,
dataStore dataservices.DataStore,
) (portainer.SwarmStackManager, error) {
return exec.NewSwarmStackManager(assetsPath, configPath, signatureService, fileService, reverseTunnelService, dataStore)
}
@@ -210,11 +221,11 @@ func initKubernetesClientFactory(signatureService portainer.DigitalSignatureServ
return kubecli.NewClientFactory(signatureService, reverseTunnelService, instanceID, dataStore)
}
func initSnapshotService(snapshotIntervalFromFlag string, dataStore dataservices.DataStore, dockerClientFactory *docker.ClientFactory, kubernetesClientFactory *kubecli.ClientFactory, shutdownCtx context.Context) (portainer.SnapshotService, error) {
func initSnapshotService(snapshotInterval string, dataStore dataservices.DataStore, dockerClientFactory *docker.ClientFactory, kubernetesClientFactory *kubecli.ClientFactory, shutdownCtx context.Context) (portainer.SnapshotService, error) {
dockerSnapshotter := docker.NewSnapshotter(dockerClientFactory)
kubernetesSnapshotter := kubernetes.NewSnapshotter(kubernetesClientFactory)
snapshotService, err := snapshot.NewService(snapshotIntervalFromFlag, dataStore, dockerSnapshotter, kubernetesSnapshotter, shutdownCtx)
snapshotService, err := snapshot.NewService(snapshotInterval, dataStore, dockerSnapshotter, kubernetesSnapshotter, shutdownCtx)
if err != nil {
return nil, err
}
@@ -235,17 +246,11 @@ func updateSettingsFromFlags(dataStore dataservices.DataStore, flags *portainer.
return err
}
if *flags.SnapshotInterval != "" {
settings.SnapshotInterval = *flags.SnapshotInterval
}
if *flags.Logo != "" {
settings.LogoURL = *flags.Logo
}
if *flags.EnableEdgeComputeFeatures {
settings.EnableEdgeComputeFeatures = *flags.EnableEdgeComputeFeatures
}
settings.LogoURL = *flags.Logo
settings.SnapshotInterval = *flags.SnapshotInterval
settings.EnableEdgeComputeFeatures = *flags.EnableEdgeComputeFeatures
settings.EnableTelemetry = true
settings.OAuthSettings.SSO = true
if *flags.Templates != "" {
settings.TemplatesURL = *flags.Templates
@@ -267,8 +272,8 @@ func updateSettingsFromFlags(dataStore dataservices.DataStore, flags *portainer.
if *flags.HTTPDisabled {
sslSettings.HTTPEnabled = false
} else if *flags.HTTPEnabled {
sslSettings.HTTPEnabled = true
} else {
sslSettings.HTTPEnabled = *flags.HTTPEnabled || sslSettings.HTTPEnabled
}
err = dataStore.SSLSettings().UpdateSettings(sslSettings)
@@ -311,9 +316,9 @@ func enableFeaturesFromFlags(dataStore dataservices.DataStore, flags *portainer.
}
if featureState {
logrus.Printf("Feature %v : on", *correspondingFeature)
log.Printf("Feature %v : on", *correspondingFeature)
} else {
logrus.Printf("Feature %v : off", *correspondingFeature)
log.Printf("Feature %v : off", *correspondingFeature)
}
settings.FeatureFlagSettings[*correspondingFeature] = featureState
@@ -342,7 +347,7 @@ func generateAndStoreKeyPair(fileService portainer.FileService, signatureService
func initKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
existingKeyPair, err := fileService.KeyPairFilesExist()
if err != nil {
logrus.Fatalf("Failed checking for existing key pair: %v", err)
log.Fatalf("failed checking for existing key pair: %v", err)
}
if existingKeyPair {
@@ -413,7 +418,7 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore dataservices.
err := snapshotService.SnapshotEndpoint(endpoint)
if err != nil {
logrus.Printf("http error: environment snapshot error (environment=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
log.Printf("http error: environment snapshot error (environment=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
}
return dataStore.Endpoint().Create(endpoint)
@@ -459,7 +464,7 @@ func createUnsecuredEndpoint(endpointURL string, dataStore dataservices.DataStor
err := snapshotService.SnapshotEndpoint(endpoint)
if err != nil {
logrus.Printf("http error: environment snapshot error (environment=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
log.Printf("http error: environment snapshot error (environment=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
}
return dataStore.Endpoint().Create(endpoint)
@@ -476,7 +481,7 @@ func initEndpoint(flags *portainer.CLIFlags, dataStore dataservices.DataStore, s
}
if len(endpoints) > 0 {
logrus.Println("Instance already has defined environments. Skipping the environment defined via CLI.")
log.Println("Instance already has defined environments. Skipping the environment defined via CLI.")
return nil
}
@@ -486,80 +491,59 @@ func initEndpoint(flags *portainer.CLIFlags, dataStore dataservices.DataStore, s
return createUnsecuredEndpoint(*flags.EndpointURL, dataStore, snapshotService)
}
func loadEncryptionSecretKey(keyfilename string) []byte {
content, err := os.ReadFile(path.Join("/run/secrets", keyfilename))
if err != nil {
if os.IsNotExist(err) {
logrus.Printf("Encryption key file `%s` not present", keyfilename)
} else {
logrus.Printf("Error reading encryption key file: %v", err)
}
return nil
}
// return a 32 byte hash of the secret (required for AES)
hash := sha256.Sum256(content)
return hash[:]
}
func buildServer(flags *portainer.CLIFlags) portainer.Server {
shutdownCtx, shutdownTrigger := context.WithCancel(context.Background())
fileService := initFileService(*flags.Data)
encryptionKey := loadEncryptionSecretKey(*flags.SecretKeyName)
if encryptionKey == nil {
logrus.Println("Proceeding without encryption key")
}
dataStore := initDataStore(flags, encryptionKey, fileService, shutdownCtx)
dataStore := initDataStore(flags, fileService, shutdownCtx)
if err := dataStore.CheckCurrentEdition(); err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
instanceID, err := dataStore.Version().InstanceID()
if err != nil {
logrus.Fatalf("Failed getting instance id: %v", err)
log.Fatalf("failed getting instance id: %v", err)
}
apiKeyService := initAPIKeyService(dataStore)
settings, err := dataStore.Settings().Settings()
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
jwtService, err := initJWTService(settings.UserSessionTimeout, dataStore)
if err != nil {
logrus.Fatalf("Failed initializing JWT service: %v", err)
log.Fatalf("failed initializing JWT service: %v", err)
}
err = enableFeaturesFromFlags(dataStore, flags)
if err != nil {
logrus.Fatalf("Failed enabling feature flag: %v", err)
log.Fatalf("failed enabling feature flag: %v", err)
}
ldapService := initLDAPService()
oauthService := initOAuthService()
gitService := initGitService()
openAMTService := openamt.NewService()
openAMTService := openamt.NewService(dataStore)
cryptoService := initCryptoService()
digitalSignatureService := initDigitalSignatureService()
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.Data, *flags.SSLCert, *flags.SSLKey, fileService, dataStore, shutdownTrigger)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
sslSettings, err := sslService.GetSSLSettings()
if err != nil {
logrus.Fatalf("Failed to get ssl settings: %s", err)
log.Fatalf("failed to get ssl settings: %s", err)
}
err = initKeyPair(fileService, digitalSignatureService)
if err != nil {
logrus.Fatalf("Failed initializing key pair: %v", err)
log.Fatalf("failed initializing key pair: %v", err)
}
reverseTunnelService := chisel.NewService(dataStore, shutdownCtx)
@@ -569,7 +553,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx)
if err != nil {
logrus.Fatalf("Failed initializing snapshot service: %v", err)
log.Fatalf("failed initializing snapshot service: %v", err)
}
snapshotService.Start()
@@ -590,37 +574,37 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, digitalSignatureService, fileService, reverseTunnelService, dataStore)
if err != nil {
logrus.Fatalf("Failed initializing swarm stack manager: %v", err)
log.Fatalf("failed initializing swarm stack manager: %s", err)
}
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, proxyManager, *flags.Assets)
helmPackageManager, err := initHelmPackageManager(*flags.Assets)
if err != nil {
logrus.Fatalf("Failed initializing helm package manager: %v", err)
log.Fatalf("failed initializing helm package manager: %s", err)
}
err = edge.LoadEdgeJobs(dataStore, reverseTunnelService)
if err != nil {
logrus.Fatalf("Failed loading edge jobs from database: %v", err)
log.Fatalf("failed loading edge jobs from database: %v", err)
}
applicationStatus := initStatus(instanceID)
err = initEndpoint(flags, dataStore, snapshotService)
if err != nil {
logrus.Fatalf("Failed initializing environment: %v", err)
log.Fatalf("failed initializing environment: %v", err)
}
adminPasswordHash := ""
if *flags.AdminPasswordFile != "" {
content, err := fileService.GetFileContent(*flags.AdminPasswordFile, "")
if err != nil {
logrus.Fatalf("Failed getting admin password file: %v", err)
log.Fatalf("failed getting admin password file: %v", err)
}
adminPasswordHash, err = cryptoService.Hash(strings.TrimSuffix(string(content), "\n"))
if err != nil {
logrus.Fatalf("Failed hashing admin password: %v", err)
log.Fatalf("failed hashing admin password: %v", err)
}
} else if *flags.AdminPassword != "" {
adminPasswordHash = *flags.AdminPassword
@@ -629,11 +613,11 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
if adminPasswordHash != "" {
users, err := dataStore.User().UsersByRole(portainer.AdministratorRole)
if err != nil {
logrus.Fatalf("Failed getting admin user: %v", err)
log.Fatalf("failed getting admin user: %v", err)
}
if len(users) == 0 {
logrus.Println("Created admin user with the given password.")
log.Println("Created admin user with the given password.")
user := &portainer.User{
Username: "admin",
Role: portainer.AdministratorRole,
@@ -641,21 +625,21 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
}
err := dataStore.User().Create(user)
if err != nil {
logrus.Fatalf("Failed creating admin user: %v", err)
log.Fatalf("failed creating admin user: %v", err)
}
} else {
logrus.Println("Instance already has an administrator user defined. Skipping admin password related flags.")
log.Println("Instance already has an administrator user defined. Skipping admin password related flags.")
}
}
err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService)
if err != nil {
logrus.Fatalf("Failed starting tunnel server: %v", err)
log.Fatalf("failed starting tunnel server: %s", err)
}
sslDBSettings, err := dataStore.SSLSettings().Settings()
if err != nil {
logrus.Fatalf("Failed to fetch ssl settings from DB")
log.Fatalf("failed to fetch ssl settings from DB")
}
scheduler := scheduler.NewScheduler(shutdownCtx)
@@ -706,8 +690,8 @@ func main() {
for {
server := buildServer(flags)
logrus.Printf("[INFO] [cmd,main] Starting Portainer version %s\n", portainer.APIVersion)
log.Printf("[INFO] [cmd,main] Starting Portainer version %s\n", portainer.APIVersion)
err := server.Start()
logrus.Printf("[INFO] [cmd,main] Http server exited: %v\n", err)
log.Printf("[INFO] [cmd,main] Http server exited: %s\n", err)
}
}

View File

@@ -29,6 +29,11 @@ func Test_enableFeaturesFromFlags(t *testing.T) {
isSupported bool
}{
{"test", false},
{"openamt", false},
{"open-amt", true},
{"oPeN-amT", true},
{"fdo", true},
{"FDO", true},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%s succeeds:%v", test.featureFlag, test.isSupported), func(t *testing.T) {

View File

@@ -11,16 +11,14 @@ type Connection interface {
// write the db contents to filename as json (the schema needs defining)
ExportRaw(filename string) error
//Rollback(force bool) error
//MigrateData(migratorParams *database.MigratorParameters, force bool) error
// TODO: this one is very database specific atm
BackupTo(w io.Writer) error
GetDatabaseFileName() string
GetDatabaseFilePath() string
GetDatabaseFilename() string
GetStorePath() string
IsEncryptedStore() bool
NeedsEncryptionMigration() (bool, error)
SetEncrypted(encrypted bool)
SetServiceName(bucketName string) error
GetObject(bucketName string, key []byte, object interface{}) error
UpdateObject(bucketName string, key []byte, object interface{}) error

View File

@@ -2,7 +2,6 @@ package boltdb
import (
"encoding/binary"
"errors"
"fmt"
"io"
"io/ioutil"
@@ -11,128 +10,44 @@ import (
"time"
"github.com/boltdb/bolt"
dserrors "github.com/portainer/portainer/api/dataservices/errors"
"github.com/sirupsen/logrus"
"github.com/portainer/portainer/api/dataservices/errors"
)
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")
DatabaseFileName = "portainer.db"
)
type DbConnection struct {
Path string
MaxBatchSize int
MaxBatchDelay time.Duration
InitialMmapSize int
EncryptionKey []byte
isEncrypted bool
Path string
*bolt.DB
}
// GetDatabaseFileName get the database filename
func (connection *DbConnection) GetDatabaseFileName() string {
if connection.IsEncryptedStore() {
return EncryptedDatabaseFileName
}
func (connection *DbConnection) GetDatabaseFilename() string {
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 {
logrus.Infof("Loading PortainerDB: %s", connection.GetDatabaseFileName())
// Disabled for now. Can't use feature flags due to the way that works
// databaseExportPath := path.Join(connection.Path, fmt.Sprintf("raw-%s-%d.json", DatabaseFileName, time.Now().Unix()))
// if err := connection.ExportRaw(databaseExportPath); err != nil {
// log.Printf("raw export to %s error: %s", databaseExportPath, err)
// } else {
// log.Printf("raw export to %s success", databaseExportPath)
// }
// Now we open the db
databasePath := connection.GetDatabaseFilePath()
db, err := bolt.Open(databasePath, 0600, &bolt.Options{
Timeout: 1 * time.Second,
InitialMmapSize: connection.InitialMmapSize,
})
databasePath := path.Join(connection.Path, DatabaseFileName)
db, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second})
if err != nil {
return err
}
db.MaxBatchSize = connection.MaxBatchSize
db.MaxBatchDelay = connection.MaxBatchDelay
connection.DB = db
return nil
}
@@ -156,12 +71,12 @@ func (connection *DbConnection) BackupTo(w io.Writer) error {
}
func (connection *DbConnection) ExportRaw(filename string) error {
databasePath := connection.GetDatabaseFilePath()
databasePath := path.Join(connection.Path, DatabaseFileName)
if _, err := os.Stat(databasePath); err != nil {
return fmt.Errorf("stat on %s failed: %s", databasePath, err)
}
b, err := connection.exportJson(databasePath)
b, err := exportJson(databasePath)
if err != nil {
return err
}
@@ -179,7 +94,7 @@ func (connection *DbConnection) ConvertToKey(v int) []byte {
// CreateBucket is a generic function used to create a bucket inside a database database.
func (connection *DbConnection) SetServiceName(bucketName string) error {
return connection.Batch(func(tx *bolt.Tx) error {
return connection.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(bucketName))
if err != nil {
return err
@@ -197,7 +112,7 @@ func (connection *DbConnection) GetObject(bucketName string, key []byte, object
value := bucket.Get(key)
if value == nil {
return dserrors.ErrObjectNotFound
return errors.ErrObjectNotFound
}
data = make([]byte, len(value))
@@ -209,33 +124,31 @@ func (connection *DbConnection) GetObject(bucketName string, key []byte, object
return err
}
return connection.UnmarshalObjectWithJsoniter(data, object)
}
func (connection *DbConnection) getEncryptionKey() []byte {
if !connection.isEncrypted {
return nil
}
return connection.EncryptionKey
return UnmarshalObject(data, object)
}
// UpdateObject is a generic function used to update an object inside a database database.
func (connection *DbConnection) UpdateObject(bucketName string, key []byte, object interface{}) error {
data, err := connection.MarshalObject(object)
if err != nil {
return err
}
return connection.Batch(func(tx *bolt.Tx) error {
return connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
return bucket.Put(key, data)
data, err := MarshalObject(object)
if err != nil {
return err
}
err = bucket.Put(key, data)
if err != nil {
return err
}
return nil
})
}
// DeleteObject is a generic function used to delete an object inside a database database.
func (connection *DbConnection) DeleteObject(bucketName string, key []byte) error {
return connection.Batch(func(tx *bolt.Tx) error {
return connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
return bucket.Delete(key)
})
@@ -244,13 +157,13 @@ func (connection *DbConnection) DeleteObject(bucketName string, key []byte) erro
// 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, matching func(o interface{}) (id int, ok bool)) error {
return connection.Batch(func(tx *bolt.Tx) error {
return connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var obj interface{}
err := connection.UnmarshalObject(v, &obj)
err := UnmarshalObject(v, &obj)
if err != nil {
return err
}
@@ -271,7 +184,7 @@ func (connection *DbConnection) DeleteAllObjects(bucketName string, matching fun
func (connection *DbConnection) GetNextIdentifier(bucketName string) int {
var identifier int
connection.Batch(func(tx *bolt.Tx) error {
connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
id, err := bucket.NextSequence()
if err != nil {
@@ -286,13 +199,13 @@ func (connection *DbConnection) GetNextIdentifier(bucketName string) int {
// 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.Batch(func(tx *bolt.Tx) error {
return connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
seqId, _ := bucket.NextSequence()
id, obj := fn(seqId)
data, err := connection.MarshalObject(obj)
data, err := MarshalObject(obj)
if err != nil {
return err
}
@@ -303,9 +216,10 @@ func (connection *DbConnection) CreateObject(bucketName string, fn func(uint64)
// 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.Batch(func(tx *bolt.Tx) error {
return connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
data, err := connection.MarshalObject(obj)
data, err := MarshalObject(obj)
if err != nil {
return err
}
@@ -317,7 +231,7 @@ func (connection *DbConnection) CreateObjectWithId(bucketName string, id int, ob
// CreateObjectWithSetSequence creates a new object in the bucket, using the specified id, and sets the bucket sequence
// avoid this :)
func (connection *DbConnection) CreateObjectWithSetSequence(bucketName string, id int, obj interface{}) error {
return connection.Batch(func(tx *bolt.Tx) error {
return connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
// We manually manage sequences for schedules
@@ -326,7 +240,7 @@ func (connection *DbConnection) CreateObjectWithSetSequence(bucketName string, i
return err
}
data, err := connection.MarshalObject(obj)
data, err := MarshalObject(obj)
if err != nil {
return err
}
@@ -338,9 +252,10 @@ func (connection *DbConnection) CreateObjectWithSetSequence(bucketName string, i
func (connection *DbConnection) GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error {
err := connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
err := connection.UnmarshalObject(v, obj)
err := UnmarshalObject(v, obj)
if err != nil {
return err
}
@@ -362,7 +277,7 @@ func (connection *DbConnection) GetAllWithJsoniter(bucketName string, obj interf
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
err := connection.UnmarshalObjectWithJsoniter(v, obj)
err := UnmarshalObjectWithJsoniter(v, obj)
if err != nil {
return err
}

View File

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

@@ -10,9 +10,8 @@ import (
// 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) ([]byte, error) {
logrus.WithField("databasePath", databasePath).Infof("exportJson")
func exportJson(databasePath string) ([]byte, error) {
connection, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second, ReadOnly: true})
if err != nil {
return []byte("{}"), err
@@ -32,7 +31,7 @@ func (c *DbConnection) exportJson(databasePath string) ([]byte, error) {
continue
}
var obj interface{}
err := c.UnmarshalObject(v, &obj)
err := UnmarshalObject(v, &obj)
if err != nil {
logrus.WithError(err).Errorf("Failed to unmarshal (bucket %s): %v", bucketName, string(v))
obj = v

View File

@@ -1,72 +1,26 @@
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) {
func MarshalObject(object interface{}) ([]byte, 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
}
return []byte(v), nil
}
if connection.getEncryptionKey() == nil {
return data, nil
}
return encrypt(data, connection.getEncryptionKey())
return json.Marshal(object)
}
// 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)
func UnmarshalObject(data []byte, object interface{}) error {
// Special case for the VERSION bucket. Here we're not using json
// So we need to return it as a string
err := json.Unmarshal(data, object)
if err != nil {
if s, ok := object.(*string); ok {
*s = string(data)
@@ -79,55 +33,10 @@ func (connection *DbConnection) UnmarshalObjectWithJsoniter(data []byte, object
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
// UnmarshalObjectWithJsoniter decodes an object from binary data
// using the jsoniter library. It is mainly used to accelerate environment(endpoint)
// decoding at the moment.
func UnmarshalObjectWithJsoniter(data []byte, object interface{}) error {
var jsoni = jsoniter.ConfigCompatibleWithStandardLibrary
return jsoni.Unmarshal(data, &object)
}

View File

@@ -1,7 +1,6 @@
package boltdb
import (
"crypto/sha256"
"fmt"
"testing"
@@ -9,17 +8,9 @@ import (
"github.com/stretchr/testify/assert"
)
const (
jsonobject = `{"LogoURL":"","BlackListedLabels":[],"AuthenticationMethod":1,"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"
)
const jsonobject = `{"LogoURL":"","BlackListedLabels":[],"AuthenticationMethod":1,"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}`
func secretToEncryptionKey(passphrase string) []byte {
hash := sha256.Sum256([]byte(passphrase))
return hash[:]
}
func Test_MarshalObjectUnencrypted(t *testing.T) {
func Test_MarshalObject(t *testing.T) {
is := assert.New(t)
uuid := uuid.Must(uuid.NewV4())
@@ -82,18 +73,16 @@ func Test_MarshalObjectUnencrypted(t *testing.T) {
},
}
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)
data, err := MarshalObject(test.object)
is.NoError(err)
is.Equal(test.expected, string(data))
})
}
}
func Test_UnMarshalObjectUnencrypted(t *testing.T) {
func Test_UnMarshalObject(t *testing.T) {
is := assert.New(t)
// Based on actual data entering and what we expect out of the function
@@ -116,62 +105,18 @@ func Test_UnMarshalObjectUnencrypted(t *testing.T) {
expected: "9ca4a1dd-a439-4593-b386-a7dfdc2e9fc6",
},
{
// An un-marshalled json object string should return the same as a string without error also
// An unmarshalled 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)
err := 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)
})
}
}

View File

@@ -2,19 +2,15 @@ package database
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"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) {
func NewDatabase(storeType, storePath string) (connection portainer.Connection, err error) {
switch storeType {
case "boltdb":
return &boltdb.DbConnection{
Path: storePath,
EncryptionKey: encryptionKey,
}, nil
return &boltdb.DbConnection{Path: storePath}, nil
}
return nil, fmt.Errorf("unknown storage database: %s", storeType)
return nil, fmt.Errorf("Unknown storage database: %s", storeType)
}

View File

@@ -69,7 +69,7 @@ func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
endpoint, ok := obj.(*portainer.Endpoint)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to Endpoint object")
return nil, fmt.Errorf("failed to convert to Endpoint object: %s", obj)
return nil, fmt.Errorf("Failed to convert to Endpoint object: %s", obj)
}
endpoints = append(endpoints, *endpoint)
return &portainer.Endpoint{}, nil

View File

@@ -4,7 +4,6 @@ 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/")
)

View File

@@ -1,92 +0,0 @@
package fdoprofile
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "fdo_profiles"
)
// Service represents a service for managingFDO Profiles data.
type Service struct {
connection portainer.Connection
}
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
}
return &Service{
connection: connection,
}, nil
}
// FDOProfiles return an array containing all the FDO Profiles.
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 {
logrus.WithField("obj", obj).Errorf("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 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))
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,
)
}
// 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)
}
// 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)
}
// GetNextIdentifier returns the next identifier for a FDO Profile.
func (service *Service) GetNextIdentifier() int {
return service.connection.GetNextIdentifier(BucketName)
}

View File

@@ -34,7 +34,7 @@ func NewService(connection portainer.Connection) (*Service, error) {
}
//HelmUserRepository returns an array of all HelmUserRepository
func (service *Service) HelmUserRepositories() ([]portainer.HelmUserRepository, error) {
func (service *Service) HelmUserRepositorys() ([]portainer.HelmUserRepository, error) {
var repos = make([]portainer.HelmUserRepository, 0)
err := service.connection.GetAll(

View File

@@ -23,7 +23,7 @@ type (
BackupTo(w io.Writer) error
Export(filename string) (err error)
IsErrObjectNotFound(err error) bool
Connection() portainer.Connection
CustomTemplate() CustomTemplateService
EdgeGroup() EdgeGroupService
EdgeJob() EdgeJobService
@@ -31,7 +31,6 @@ type (
Endpoint() EndpointService
EndpointGroup() EndpointGroupService
EndpointRelation() EndpointRelationService
FDOProfile() FDOProfileService
HelmUserRepository() HelmUserRepositoryService
Registry() RegistryService
ResourceControl() ResourceControlService
@@ -123,20 +122,9 @@ type (
BucketName() string
}
// FDOProfileService represents a service to manage FDO Profiles
FDOProfileService interface {
FDOProfiles() ([]portainer.FDOProfile, error)
FDOProfile(ID portainer.FDOProfileID) (*portainer.FDOProfile, error)
Create(FDOProfile *portainer.FDOProfile) error
Update(ID portainer.FDOProfileID, FDOProfile *portainer.FDOProfile) error
Delete(ID portainer.FDOProfileID) error
GetNextIdentifier() int
BucketName() string
}
// HelmUserRepositoryService represents a service to manage HelmUserRepositories
HelmUserRepositoryService interface {
HelmUserRepositories() ([]portainer.HelmUserRepository, error)
HelmUserRepositorys() ([]portainer.HelmUserRepository, error)
HelmUserRepositoryByUserID(userID portainer.UserID) ([]portainer.HelmUserRepository, error)
Create(record *portainer.HelmUserRepository) error
UpdateHelmUserRepository(ID portainer.HelmUserRepositoryID, repository *portainer.HelmUserRepository) error

View File

@@ -4,6 +4,7 @@ import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/sirupsen/logrus"
)
@@ -78,6 +79,9 @@ func (service *Service) ResourceControlByResourceIDAndType(resourceID string, re
if err == stop {
return resourceControl, nil
}
if err == nil {
return nil, errors.ErrObjectNotFound
}
return nil, err
}

View File

@@ -35,7 +35,7 @@ func (store *Store) createBackupFolders() {
}
func (store *Store) databasePath() string {
return store.connection.GetDatabaseFilePath()
return path.Join(store.connection.GetStorePath(), store.connection.GetDatabaseFilename())
}
func (store *Store) commonBackupDir() string {
@@ -84,7 +84,7 @@ func (store *Store) setupOptions(options *BackupOptions) *BackupOptions {
options.BackupDir = store.commonBackupDir()
}
if options.BackupFileName == "" {
options.BackupFileName = fmt.Sprintf("%s.%s.%s", store.connection.GetDatabaseFileName(), fmt.Sprintf("%03d", options.Version), time.Now().Format("20060102150405"))
options.BackupFileName = fmt.Sprintf("%s.%s.%s", store.connection.GetDatabaseFilename(), fmt.Sprintf("%03d", options.Version), time.Now().Format("20060102150405"))
}
if options.BackupPath == "" {
options.BackupPath = path.Join(options.BackupDir, options.BackupFileName)

View File

@@ -48,7 +48,7 @@ func TestBackup(t *testing.T) {
store.VersionService.StoreDBVersion(portainer.DBVersion)
store.backupWithOptions(nil)
backupFileName := path.Join(connection.GetStorePath(), "backups", "common", fmt.Sprintf("portainer.edb.%03d.*", portainer.DBVersion))
backupFileName := path.Join(connection.GetStorePath(), "backups", "common", fmt.Sprintf("portainer.db.%03d.*", portainer.DBVersion))
if !isFileExist(backupFileName) {
t.Errorf("Expect backup file to be created %s", backupFileName)
}

View File

@@ -1,15 +1,10 @@
package datastore
import (
"fmt"
"io"
"os"
"path"
"time"
portainer "github.com/portainer/portainer/api"
portainerErrors "github.com/portainer/portainer/api/dataservices/errors"
"github.com/sirupsen/logrus"
"github.com/portainer/portainer/api/dataservices/errors"
)
func (store *Store) version() (int, error) {
@@ -39,19 +34,6 @@ 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
@@ -63,18 +45,8 @@ func (store *Store) Open() (newStore bool, err error) {
}
// 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
}
return newStore, err
}
if version > 0 {
logrus.WithField("version", version).Infof("Opened existing store")
return false, nil
if _, err := store.VersionService.DBVersion(); err == nil {
newStore = false
}
return newStore, nil
@@ -93,85 +65,16 @@ func (store *Store) BackupTo(w io.Writer) error {
// CheckCurrentEdition checks if current edition is community edition
func (store *Store) CheckCurrentEdition() error {
if store.edition() != portainer.PortainerCE {
return portainerErrors.ErrWrongDBEdition
return errors.ErrWrongDBEdition
}
return nil
}
// TODO: move the use of this to dataservices.IsErrObjectNotFound()?
func (store *Store) IsErrObjectNotFound(e error) bool {
return e == portainerErrors.ErrObjectNotFound
}
func (store *Store) Connection() portainer.Connection {
return store.connection
return e == errors.ErrObjectNotFound
}
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()
logrus.Infof("Encrypting database")
// export file path for backup
exportFilename := path.Join(store.databasePath() + "." + fmt.Sprintf("backup-%d.json", time.Now().Unix()))
logrus.Infof("Exporting database backup to %s", exportFilename)
err = store.Export(exportFilename)
if err != nil {
logrus.WithError(err).Debugf("Failed to export to %s", exportFilename)
return err
}
logrus.Infof("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())
logrus.Fatal(portainerErrors.ErrDBImportFailed.Error())
}
err = os.Remove(oldFilename)
if err != nil {
logrus.Errorf("Failed to remove the un-encrypted db file")
}
err = os.Remove(exportFilename)
if err != nil {
logrus.Errorf("Failed to remove the json backup file")
}
// Close db connection
store.connection.Close()
logrus.Info("Database successfully encrypted")
return nil
}

View File

@@ -7,30 +7,6 @@ import (
// Init creates the default data set.
func (store *Store) Init() error {
err := store.checkOrCreateInstanceID()
if err != nil {
return err
}
err = store.checkOrCreateDefaultSettings()
if err != nil {
return err
}
err = store.checkOrCreateDefaultSSLSettings()
if err != nil {
return err
}
err = store.checkOrCreateDefaultData()
if err != nil {
return err
}
return nil
}
func (store *Store) checkOrCreateInstanceID() error {
instanceID, err := store.VersionService.InstanceID()
if store.IsErrObjectNotFound(err) {
uid, err := uuid.NewV4()
@@ -39,17 +15,18 @@ func (store *Store) checkOrCreateInstanceID() error {
}
instanceID = uid.String()
return store.VersionService.StoreInstanceID(instanceID)
err = store.VersionService.StoreInstanceID(instanceID)
if err != nil {
return err
}
} else if err != nil {
return err
}
return err
}
func (store *Store) checkOrCreateDefaultSettings() error {
// TODO: these need to also be applied when importing
settings, err := store.SettingsService.Settings()
if store.IsErrObjectNotFound(err) {
defaultSettings := &portainer.Settings{
EnableTelemetry: true,
AuthenticationMethod: portainer.AuthenticationInternal,
BlackListedLabels: make([]portainer.Pair, 0),
LDAPSettings: portainer.LDAPSettings{
@@ -57,16 +34,14 @@ func (store *Store) checkOrCreateDefaultSettings() error {
AutoCreateUsers: true,
TLSConfig: portainer.TLSConfiguration{},
SearchSettings: []portainer.LDAPSearchSettings{
{},
portainer.LDAPSearchSettings{},
},
GroupSearchSettings: []portainer.LDAPGroupSearchSettings{
{},
portainer.LDAPGroupSearchSettings{},
},
},
OAuthSettings: portainer.OAuthSettings{
SSO: true,
},
SnapshotInterval: portainer.DefaultSnapshotInterval,
OAuthSettings: portainer.OAuthSettings{},
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
TemplatesURL: portainer.DefaultTemplatesURL,
HelmRepositoryURL: portainer.DefaultHelmRepositoryURL,
@@ -75,33 +50,35 @@ func (store *Store) checkOrCreateDefaultSettings() error {
KubectlShellImage: portainer.DefaultKubectlShellImage,
}
return store.SettingsService.UpdateSettings(defaultSettings)
}
if err != nil {
err = store.SettingsService.UpdateSettings(defaultSettings)
if err != nil {
return err
}
} else if err != nil {
return err
} else if err == nil {
if settings.UserSessionTimeout == "" {
settings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
store.Settings().UpdateSettings(settings)
}
}
if settings.UserSessionTimeout == "" {
settings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
return store.Settings().UpdateSettings(settings)
}
return nil
}
_, err = store.SSLSettings().Settings()
if err != nil {
if !store.IsErrObjectNotFound(err) {
return err
}
func (store *Store) checkOrCreateDefaultSSLSettings() error {
_, err := store.SSLSettings().Settings()
if store.IsErrObjectNotFound(err) {
defaultSSLSettings := &portainer.SSLSettings{
HTTPEnabled: true,
}
return store.SSLSettings().UpdateSettings(defaultSSLSettings)
err = store.SSLSettings().UpdateSettings(defaultSSLSettings)
if err != nil {
return err
}
}
return err
}
func (store *Store) checkOrCreateDefaultData() error {
groups, err := store.EndpointGroupService.EndpointGroups()
if err != nil {
return err
@@ -122,5 +99,6 @@ func (store *Store) checkOrCreateDefaultData() error {
return err
}
}
return nil
}

View File

@@ -30,7 +30,6 @@ func (store *Store) MigrateData() error {
EndpointService: store.EndpointService,
EndpointRelationService: store.EndpointRelationService,
ExtensionService: store.ExtensionService,
FDOProfilesService: store.FDOProfilesService,
RegistryService: store.RegistryService,
ResourceControlService: store.ResourceControlService,
RoleService: store.RoleService,

View File

@@ -7,7 +7,6 @@ import (
"github.com/portainer/portainer/api/dataservices/endpointgroup"
"github.com/portainer/portainer/api/dataservices/endpointrelation"
"github.com/portainer/portainer/api/dataservices/extension"
"github.com/portainer/portainer/api/dataservices/fdoprofile"
"github.com/portainer/portainer/api/dataservices/registry"
"github.com/portainer/portainer/api/dataservices/resourcecontrol"
"github.com/portainer/portainer/api/dataservices/role"
@@ -27,12 +26,12 @@ var migrateLog = plog.NewScopedLog("database, migrate")
type (
// Migrator defines a service to migrate data after a Portainer version update.
Migrator struct {
currentDBVersion int
currentDBVersion int
endpointGroupService *endpointgroup.Service
endpointService *endpoint.Service
endpointRelationService *endpointrelation.Service
extensionService *extension.Service
fdoProfilesService *fdoprofile.Service
registryService *registry.Service
resourceControlService *resourcecontrol.Service
roleService *role.Service
@@ -55,7 +54,6 @@ type (
EndpointService *endpoint.Service
EndpointRelationService *endpointrelation.Service
ExtensionService *extension.Service
FDOProfilesService *fdoprofile.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
RoleService *role.Service
@@ -80,7 +78,6 @@ func NewMigrator(parameters *MigratorParameters) *Migrator {
endpointService: parameters.EndpointService,
endpointRelationService: parameters.EndpointRelationService,
extensionService: parameters.ExtensionService,
fdoProfilesService: parameters.FDOProfilesService,
registryService: parameters.RegistryService,
resourceControlService: parameters.ResourceControlService,
roleService: parameters.RoleService,

View File

@@ -17,7 +17,6 @@ import (
"github.com/portainer/portainer/api/dataservices/endpointgroup"
"github.com/portainer/portainer/api/dataservices/endpointrelation"
"github.com/portainer/portainer/api/dataservices/extension"
"github.com/portainer/portainer/api/dataservices/fdoprofile"
"github.com/portainer/portainer/api/dataservices/helmuserrepository"
"github.com/portainer/portainer/api/dataservices/registry"
"github.com/portainer/portainer/api/dataservices/resourcecontrol"
@@ -51,7 +50,6 @@ type Store struct {
EndpointService *endpoint.Service
EndpointRelationService *endpointrelation.Service
ExtensionService *extension.Service
FDOProfilesService *fdoprofile.Service
HelmUserRepositoryService *helmuserrepository.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
@@ -131,12 +129,6 @@ func (store *Store) initServices() error {
}
store.ExtensionService = extensionService
fdoProfilesService, err := fdoprofile.NewService(store.connection)
if err != nil {
return err
}
store.FDOProfilesService = fdoProfilesService
helmUserRepositoryService, err := helmuserrepository.NewService(store.connection)
if err != nil {
return err
@@ -265,11 +257,6 @@ func (store *Store) EndpointRelation() dataservices.EndpointRelationService {
return store.EndpointRelationService
}
// FDOProfile gives access to the FDOProfile data management layer
func (store *Store) FDOProfile() dataservices.FDOProfileService {
return store.FDOProfilesService
}
// HelmUserRepository access the helm user repository settings
func (store *Store) HelmUserRepository() dataservices.HelmUserRepositoryService {
return store.HelmUserRepositoryService
@@ -376,184 +363,118 @@ func (store *Store) Export(filename string) (err error) {
backup := storeExport{}
if c, err := store.CustomTemplate().CustomTemplates(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Custom Templates")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.CustomTemplate = c
}
if e, err := store.EdgeGroup().EdgeGroups(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Edge Groups")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.EdgeGroup = e
}
if e, err := store.EdgeJob().EdgeJobs(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Edge Jobs")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.EdgeJob = e
}
if e, err := store.EdgeStack().EdgeStacks(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Edge Stacks")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.EdgeStack = e
}
if e, err := store.Endpoint().Endpoints(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Endpoints")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Endpoint = e
}
if e, err := store.EndpointGroup().EndpointGroups(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Endpoint Groups")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.EndpointGroup = e
}
if r, err := store.EndpointRelation().EndpointRelations(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Endpoint Relations")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.EndpointRelation = r
}
if r, err := store.ExtensionService.Extensions(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Extensions")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Extensions = r
}
if r, err := store.HelmUserRepository().HelmUserRepositories(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Helm User Repositories")
}
if r, err := store.HelmUserRepository().HelmUserRepositorys(); err != nil {
logrus.WithError(err).Debugf("Export boom")
} else {
backup.HelmUserRepository = r
}
if r, err := store.Registry().Registries(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Registries")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Registry = r
}
if c, err := store.ResourceControl().ResourceControls(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Resource Controls")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.ResourceControl = c
}
if role, err := store.Role().Roles(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Roles")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Role = role
}
if r, err := store.ScheduleService.Schedules(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Schedules")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Schedules = r
}
if settings, err := store.Settings().Settings(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Settings")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Settings = *settings
}
if settings, err := store.SSLSettings().Settings(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting SSL Settings")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.SSLSettings = *settings
}
if t, err := store.Stack().Stacks(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Stacks")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Stack = t
}
if t, err := store.Tag().Tags(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Tags")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Tag = t
}
if t, err := store.TeamMembership().TeamMemberships(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Team Memberships")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.TeamMembership = t
}
if t, err := store.Team().Teams(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Teams")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Team = t
}
if info, err := store.TunnelServer().Info(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Tunnel Server")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.TunnelServer = *info
}
if users, err := store.User().Users(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Users")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.User = users
}
if webhooks, err := store.Webhook().Webhooks(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Webhooks")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Webhook = webhooks
}
v, err := store.Version().DBVersion()
if err != nil && !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting DB version")
if err != nil {
logrus.WithError(err).Debugf("Export boom")
}
instance, _ := store.Version().InstanceID()
backup.Version = map[string]string{
@@ -597,66 +518,50 @@ func (store *Store) Import(filename string) (err error) {
for _, v := range backup.CustomTemplate {
store.CustomTemplate().UpdateCustomTemplate(v.ID, &v)
}
for _, v := range backup.EdgeGroup {
store.EdgeGroup().UpdateEdgeGroup(v.ID, &v)
}
for _, v := range backup.EdgeJob {
store.EdgeJob().UpdateEdgeJob(v.ID, &v)
}
for _, v := range backup.EdgeStack {
store.EdgeStack().UpdateEdgeStack(v.ID, &v)
}
for _, v := range backup.Endpoint {
store.Endpoint().UpdateEndpoint(v.ID, &v)
}
for _, v := range backup.EndpointGroup {
store.EndpointGroup().UpdateEndpointGroup(v.ID, &v)
}
for _, v := range backup.EndpointRelation {
store.EndpointRelation().UpdateEndpointRelation(v.EndpointID, &v)
}
for _, v := range backup.HelmUserRepository {
store.HelmUserRepository().UpdateHelmUserRepository(v.ID, &v)
}
for _, v := range backup.Registry {
store.Registry().UpdateRegistry(v.ID, &v)
}
for _, v := range backup.ResourceControl {
store.ResourceControl().UpdateResourceControl(v.ID, &v)
}
for _, v := range backup.Role {
store.Role().UpdateRole(v.ID, &v)
}
store.Settings().UpdateSettings(&backup.Settings)
store.SSLSettings().UpdateSettings(&backup.SSLSettings)
for _, v := range backup.Stack {
store.Stack().UpdateStack(v.ID, &v)
}
for _, v := range backup.Tag {
store.Tag().UpdateTag(v.ID, &v)
}
for _, v := range backup.TeamMembership {
store.TeamMembership().UpdateTeamMembership(v.ID, &v)
}
for _, v := range backup.Team {
store.Team().UpdateTeam(v.ID, &v)
}
store.TunnelServer().UpdateInfo(&backup.TunnelServer)
for _, user := range backup.User {
@@ -665,9 +570,10 @@ func (store *Store) Import(filename string) (err error) {
}
}
for _, v := range backup.Webhook {
store.Webhook().UpdateWebhook(v.ID, &v)
}
// backup[store.Webhook().BucketName()], err = store.Webhook().Webhooks()
// if err != nil {
// logrus.WithError(err).Debugf("Export boom")
// }
return nil
}

View File

@@ -42,7 +42,7 @@ func NewTestStore(init bool) (bool, *Store, func(), error) {
return false, nil, nil, err
}
connection, err := database.NewDatabase("boltdb", storePath, []byte("apassphrasewhichneedstobe32bytes"))
connection, err := database.NewDatabase("boltdb", storePath)
if err != nil {
panic(err)
}

View File

@@ -33,7 +33,7 @@ func (s StringSet) List() []string {
list := make([]string, s.Len())
i := 0
for k := range s {
for k, _ := range s {
list[i] = k
i++
}

View File

@@ -46,7 +46,7 @@ func (manager *ComposeStackManager) ComposeSyntaxMaxVersion() string {
}
// Up builds, (re)creates and starts containers in the background. Wraps `docker-compose up -d` command
func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, forceRereate bool) error {
func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
url, proxy, err := manager.fetchEndpointProxy(endpoint)
if err != nil {
return errors.Wrap(err, "failed to fetch environment proxy")
@@ -62,7 +62,7 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
}
filePaths := stackutils.GetStackFilePaths(stack)
err = manager.deployer.Deploy(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFilePath, forceRereate)
err = manager.deployer.Deploy(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFilePath)
return errors.Wrap(err, "failed to deploy a stack")
}

View File

@@ -50,7 +50,7 @@ func Test_UpAndDown(t *testing.T) {
ctx := context.TODO()
err = w.Up(ctx, stack, endpoint, false)
err = w.Up(ctx, stack, endpoint)
if err != nil {
t.Fatalf("Error calling docker-compose up: %s", err)
}

View File

@@ -35,8 +35,6 @@ const (
ManifestFileDefaultName = "k8s-deployment.yml"
// EdgeStackStorePath represents the subfolder where edge stack files are stored in the file store folder.
EdgeStackStorePath = "edge_stacks"
// FDOProfileStorePath represents the subfolder where FDO profiles files are stored in the file store folder.
FDOProfileStorePath = "fdo_profiles"
// PrivateKeyFile represents the name on disk of the file containing the private key.
PrivateKeyFile = "portainer.key"
// PublicKeyFile represents the name on disk of the file containing the public key.
@@ -654,20 +652,3 @@ func MoveDirectory(originalPath, newPath string) error {
return os.Rename(originalPath, newPath)
}
// StoreFDOProfileFileFromBytes creates a subfolder in the FDOProfileStorePath and stores a new file from bytes.
// It returns the path to the folder where the file is stored.
func (service *Service) StoreFDOProfileFileFromBytes(fdoProfileIdentifier string, data []byte) (string, error) {
err := service.createDirectoryInStore(FDOProfileStorePath)
if err != nil {
return "", err
}
filePath := JoinPaths(FDOProfileStorePath, fdoProfileIdentifier)
err = service.createFileInStore(filePath, bytes.NewReader(data))
if err != nil {
return "", err
}
return service.wrapFileStore(filePath), nil
}

View File

@@ -30,7 +30,7 @@ require (
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
github.com/pkg/errors v0.9.1
github.com/portainer/docker-compose-wrapper v0.0.0-20220113045708-6569596db840
github.com/portainer/docker-compose-wrapper v0.0.0-20211018221743-10a04c9d4f19
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108
github.com/portainer/libhttp v0.0.0-20211021135806-13e6c55c5fbc
@@ -96,7 +96,6 @@ require (
github.com/sergi/go-diff v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect

View File

@@ -613,8 +613,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/portainer/docker-compose-wrapper v0.0.0-20220113045708-6569596db840 h1:Nciddt8Y8G8nTMmyDfWxeN23PZUcsqbZE2zOFB/F1xg=
github.com/portainer/docker-compose-wrapper v0.0.0-20220113045708-6569596db840/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c=
github.com/portainer/docker-compose-wrapper v0.0.0-20211018221743-10a04c9d4f19 h1:tG2gU4mkm5yElj35XpU3lgllOYQxN3kaM1Jab7AqTDs=
github.com/portainer/docker-compose-wrapper v0.0.0-20211018221743-10a04c9d4f19/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c=
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a h1:qY8TbocN75n5PDl16o0uVr5MevtM5IhdwSelXEd4nFM=
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a/go.mod h1:n54EEIq+MM0NNtqLeCby8ljL+l275VpolXO0ibHegLE=
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108 h1:5e8KAnDa2G3cEHK7aV/ue8lOaoQwBZUzoALslwWkR04=

View File

@@ -6,12 +6,13 @@ import (
"encoding/json"
"errors"
"fmt"
"golang.org/x/sync/errgroup"
"io/ioutil"
"net/http"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"golang.org/x/sync/errgroup"
)
const (
@@ -31,7 +32,7 @@ type Service struct {
}
// NewService initializes a new service.
func NewService() *Service {
func NewService(dataStore dataservices.DataStore) *Service {
return &Service{
httpsClient: &http.Client{
Timeout: httpClientTimeout,

View File

@@ -12,7 +12,6 @@ import (
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/internal/authorization"
)
type authenticatePayload struct {
@@ -50,7 +49,7 @@ func (payload *authenticatePayload) Validate(r *http.Request) error {
// @failure 422 "Invalid Credentials"
// @failure 500 "Server error"
// @router /auth [post]
func (handler *Handler) authenticate(rw http.ResponseWriter, r *http.Request) *httperror.HandlerError {
func (handler *Handler) authenticate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload authenticatePayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
@@ -62,36 +61,39 @@ func (handler *Handler) authenticate(rw http.ResponseWriter, r *http.Request) *h
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
}
user, err := handler.DataStore.User().UserByUsername(payload.Username)
if err != nil {
if !handler.DataStore.IsErrObjectNotFound(err) {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err}
}
if settings.AuthenticationMethod == portainer.AuthenticationInternal ||
settings.AuthenticationMethod == portainer.AuthenticationOAuth ||
(settings.AuthenticationMethod == portainer.AuthenticationLDAP && !settings.LDAPSettings.AutoCreateUsers) {
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized}
}
u, err := handler.DataStore.User().UserByUsername(payload.Username)
if err != nil && !handler.DataStore.IsErrObjectNotFound(err) {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err}
}
if user != nil && isUserInitialAdmin(user) || settings.AuthenticationMethod == portainer.AuthenticationInternal {
return handler.authenticateInternal(rw, user, payload.Password)
}
if settings.AuthenticationMethod == portainer.AuthenticationOAuth {
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Only initial admin is allowed to login without oauth", httperrors.ErrUnauthorized}
if handler.DataStore.IsErrObjectNotFound(err) && (settings.AuthenticationMethod == portainer.AuthenticationInternal || settings.AuthenticationMethod == portainer.AuthenticationOAuth) {
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized}
}
if settings.AuthenticationMethod == portainer.AuthenticationLDAP {
return handler.authenticateLDAP(rw, user, payload.Username, payload.Password, &settings.LDAPSettings)
if u == nil && settings.LDAPSettings.AutoCreateUsers {
return handler.authenticateLDAPAndCreateUser(w, payload.Username, payload.Password, &settings.LDAPSettings)
} else if u == nil && !settings.LDAPSettings.AutoCreateUsers {
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized}
}
return handler.authenticateLDAP(w, u, payload.Password, &settings.LDAPSettings)
}
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Login method is not supported", httperrors.ErrUnauthorized}
return handler.authenticateInternal(w, u, payload.Password)
}
func isUserInitialAdmin(user *portainer.User) bool {
return int(user.ID) == 1
func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.User, password string, ldapSettings *portainer.LDAPSettings) *httperror.HandlerError {
err := handler.LDAPService.AuthenticateUser(user.Username, password, ldapSettings)
if err != nil {
return handler.authenticateInternal(w, user, password)
}
err = handler.addUserIntoTeams(user, ldapSettings)
if err != nil {
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
}
return handler.writeToken(w, user)
}
func (handler *Handler) authenticateInternal(w http.ResponseWriter, user *portainer.User, password string) *httperror.HandlerError {
@@ -103,27 +105,20 @@ func (handler *Handler) authenticateInternal(w http.ResponseWriter, user *portai
return handler.writeToken(w, user)
}
func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.User, username, password string, ldapSettings *portainer.LDAPSettings) *httperror.HandlerError {
func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, username, password string, ldapSettings *portainer.LDAPSettings) *httperror.HandlerError {
err := handler.LDAPService.AuthenticateUser(username, password, ldapSettings)
if err != nil {
return &httperror.HandlerError{
StatusCode: http.StatusForbidden,
Message: "Only initial admin is allowed to login without oauth",
Err: err,
}
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", err}
}
if user == nil {
user = &portainer.User{
Username: username,
Role: portainer.StandardUserRole,
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
}
user := &portainer.User{
Username: username,
Role: portainer.StandardUserRole,
}
err = handler.DataStore.User().Create(user)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err}
}
err = handler.DataStore.User().Create(user)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err}
}
err = handler.addUserIntoTeams(user, ldapSettings)
@@ -135,9 +130,7 @@ func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.
}
func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError {
tokenData := composeTokenData(user)
return handler.persistAndWriteToken(w, tokenData)
return handler.persistAndWriteToken(w, composeTokenData(user))
}
func (handler *Handler) persistAndWriteToken(w http.ResponseWriter, tokenData *portainer.TokenData) *httperror.HandlerError {

View File

@@ -4,9 +4,9 @@ import (
"errors"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
portainer "github.com/portainer/portainer/api"
"strconv"
"strings"
portainer "github.com/portainer/portainer/api"
"net/http"
)

View File

@@ -11,7 +11,6 @@ import (
"strings"
"time"
"github.com/gofrs/uuid"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
@@ -340,20 +339,6 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
IsEdgeDevice: payload.IsEdgeDevice,
}
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve the settings from the database", err}
}
if settings.EnforceEdgeID {
edgeID, err := uuid.NewV4()
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Cannot generate the Edge ID", err}
}
endpoint.EdgeID = edgeID.String()
}
err = handler.saveEndpointAndUpdateAuthorizations(endpoint)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the environment", err}

View File

@@ -89,10 +89,8 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
filteredEndpoints = filterEndpointsByGroupID(filteredEndpoints, portainer.EndpointGroupID(groupID))
}
edgeDeviceFilter, edgeDeviceFilterErr := request.RetrieveBooleanQueryParameter(r, "edgeDeviceFilter", false)
if edgeDeviceFilterErr == nil {
filteredEndpoints = filterEndpointsByEdgeDevice(filteredEndpoints, edgeDeviceFilter)
}
edgeDeviceFilter, _ := request.RetrieveBooleanQueryParameter(r, "edgeDeviceFilter", true)
filteredEndpoints = filterEndpointsByEdgeDevice(filteredEndpoints, edgeDeviceFilter)
if search != "" {
tags, err := handler.DataStore.Tag().Tags()

View File

@@ -1,7 +1,6 @@
package endpoints
import (
"errors"
"net/http"
httperror "github.com/portainer/libhttp/error"
@@ -38,7 +37,7 @@ func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request)
}
if !snapshot.SupportDirectSnapshot(endpoint) {
return &httperror.HandlerError{http.StatusBadRequest, "Snapshots not supported for this environment", errors.New("Snapshots not supported for this environment")}
return &httperror.HandlerError{http.StatusBadRequest, "Snapshots not supported for this environment", err}
}
snapshotError := handler.SnapshotService.SnapshotEndpoint(endpoint)

View File

@@ -103,15 +103,6 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req
}
}
if endpoint.EdgeCheckinInterval == 0 {
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
}
endpoint.EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
}
endpoint.LastCheckInDate = time.Now().Unix()
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
@@ -119,8 +110,18 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to Unable to persist environment changes inside the database", err}
}
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
}
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
checkinInterval := settings.EdgeAgentCheckinInterval
if endpoint.EdgeCheckinInterval != 0 {
checkinInterval = endpoint.EdgeCheckinInterval
}
schedules := []edgeJobResponse{}
for _, job := range tunnel.Jobs {
schedule := edgeJobResponse{
@@ -145,7 +146,7 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req
Status: tunnel.Status,
Port: tunnel.Port,
Schedules: schedules,
CheckinInterval: endpoint.EdgeCheckinInterval,
CheckinInterval: checkinInterval,
Credentials: tunnel.Credentials,
}

View File

@@ -46,8 +46,6 @@ type endpointUpdatePayload struct {
EdgeCheckinInterval *int `example:"5"`
// Associated Kubernetes data
Kubernetes *portainer.KubernetesData
// Whether the device has been trusted or not by the user
UserTrusted *bool
}
func (payload *endpointUpdatePayload) Validate(r *http.Request) error {
@@ -272,10 +270,6 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
}
}
if payload.UserTrusted != nil {
endpoint.UserTrusted = *payload.UserTrusted
}
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment changes inside the database", err}

View File

@@ -80,7 +80,7 @@ type Handler struct {
}
// @title PortainerCE API
// @version 2.11.1
// @version 2.11.0
// @description.markdown api-description.md
// @termsOfService
@@ -230,9 +230,13 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case strings.HasPrefix(r.URL.Path, "/api/ssl"):
http.StripPrefix("/api", h.SSLHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/open_amt"):
http.StripPrefix("/api", h.OpenAMTHandler).ServeHTTP(w, r)
if h.OpenAMTHandler != nil {
http.StripPrefix("/api", h.OpenAMTHandler).ServeHTTP(w, r)
}
case strings.HasPrefix(r.URL.Path, "/api/fdo"):
http.StripPrefix("/api", h.FDOHandler).ServeHTTP(w, r)
if h.FDOHandler != nil {
http.StripPrefix("/api", h.FDOHandler).ServeHTTP(w, r)
}
case strings.HasPrefix(r.URL.Path, "/api/teams"):
http.StripPrefix("/api", h.TeamHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/team_memberships"):

View File

@@ -3,35 +3,28 @@ package fdo
import (
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"path"
"strings"
"github.com/fxamacker/cbor/v2"
cbor "github.com/fxamacker/cbor/v2"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
)
const (
deploymentScriptName = "fdo.sh"
)
type deviceConfigurePayload struct {
EdgeID string `json:"edgeID"`
EdgeKey string `json:"edgeKey"`
Name string `json:"name"`
ProfileID int `json:"profile"`
EdgeKey string `json:"edgeKey"`
Name string `json:"name"`
ProfileURL string `json:"profile"`
}
func (payload *deviceConfigurePayload) Validate(r *http.Request) error {
if payload.EdgeID == "" {
return errors.New("invalid edge ID provided")
}
if payload.EdgeKey == "" {
return errors.New("invalid edge key provided")
}
@@ -40,16 +33,26 @@ func (payload *deviceConfigurePayload) Validate(r *http.Request) error {
return errors.New("the device name cannot be empty")
}
if payload.ProfileID < 1 {
return errors.New("invalid profile id provided")
if err := validateURL(payload.ProfileURL); err != nil {
return fmt.Errorf("FDO profile URL: %w", err)
}
return nil
}
func fetchProfileContents(profileURL string) ([]byte, error) {
resp, err := http.Get(profileURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
// @id fdoConfigureDevice
// @summary configures an FDO device
// @description configures an FDO device
// @summary configure an FDO device
// @description configure an FDO device
// @description **Access policy**: administrator
// @tags intel
// @security jwt
@@ -75,17 +78,14 @@ func (handler *Handler) fdoConfigureDevice(w http.ResponseWriter, r *http.Reques
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
profile, err := handler.DataStore.FDOProfile().FDOProfile(portainer.FDOProfileID(payload.ProfileID))
if handler.DataStore.IsErrObjectNotFound(err) {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a FDO Profile with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a FDO Profile with the specified identifier inside the database", err}
profileUrl, err := url.Parse(payload.ProfileURL)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "fdoConfigureDevice: invalid FDO profile URL", Err: err}
}
fileContent, err := handler.FileService.GetFileContent(profile.FilePath, "")
profileContents, err := fetchProfileContents(payload.ProfileURL)
if err != nil {
logrus.WithError(err).Info("fdoConfigureDevice: GetFileContent")
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: GetFileContent", Err: err}
return &httperror.HandlerError{StatusCode: http.StatusBadGateway, Message: "fdoConfigureDevice: could not retrieve the FDO profile", Err: err}
}
fdoClient, err := handler.newFDOClient()
@@ -106,17 +106,6 @@ func (handler *Handler) fdoConfigureDevice(w http.ResponseWriter, r *http.Reques
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw()", Err: err}
}
if err = fdoClient.PutDeviceSVIRaw(url.Values{
"guid": []string{guid},
"priority": []string{"1"},
"module": []string{"fdo_sys"},
"var": []string{"filedesc"},
"filename": []string{"DEVICE_edgeid.txt"},
}, []byte(payload.EdgeID)); err != nil {
logrus.WithError(err).Info("fdoConfigureDevice: PutDeviceSVIRaw(edgeid)")
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw(edgeid)", Err: err}
}
// write down the edgekey
if err = fdoClient.PutDeviceSVIRaw(url.Values{
"guid": []string{guid},
@@ -153,13 +142,15 @@ func (handler *Handler) fdoConfigureDevice(w http.ResponseWriter, r *http.Reques
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw()", Err: err}
}
// onboarding script - this would get selected by the profile name
deploymentScriptName := path.Base(profileUrl.Path)
if err = fdoClient.PutDeviceSVIRaw(url.Values{
"guid": []string{guid},
"priority": []string{"1"},
"module": []string{"fdo_sys"},
"var": []string{"filedesc"},
"filename": []string{deploymentScriptName},
}, fileContent); err != nil {
}, profileContents); err != nil {
logrus.WithError(err).Info("fdoConfigureDevice: PutDeviceSVIRaw()")
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw()", Err: err}
}
@@ -170,15 +161,15 @@ func (handler *Handler) fdoConfigureDevice(w http.ResponseWriter, r *http.Reques
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw() failed to encode", Err: err}
}
cborBytes := strings.ToUpper(hex.EncodeToString(b))
logrus.WithField("cbor", cborBytes).WithField("string", deploymentScriptName).Info("converted to CBOR")
cbor := strings.ToUpper(hex.EncodeToString(b))
logrus.WithField("cbor", cbor).WithField("string", deploymentScriptName).Info("converted to CBOR")
if err = fdoClient.PutDeviceSVIRaw(url.Values{
"guid": []string{guid},
"priority": []string{"2"},
"module": []string{"fdo_sys"},
"var": []string{"exec"},
"bytes": []string{cborBytes},
"bytes": []string{cbor},
}, []byte("")); err != nil {
logrus.WithError(err).Info("fdoConfigureDevice: PutDeviceSVIRaw()")
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw()", Err: err}

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"net/http"
"net/url"
"strconv"
"time"
httperror "github.com/portainer/libhttp/error"
@@ -40,6 +39,10 @@ func (payload *fdoConfigurePayload) Validate(r *http.Request) error {
if err := validateURL(payload.OwnerURL); err != nil {
return fmt.Errorf("owner server URL: %w", err)
}
if err := validateURL(payload.ProfilesURL); err != nil {
return fmt.Errorf("profile list URL: %w", err)
}
}
return nil
@@ -97,68 +100,5 @@ func (handler *Handler) fdoConfigure(w http.ResponseWriter, r *http.Request) *ht
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Error saving FDO settings", Err: err}
}
profiles, err := handler.DataStore.FDOProfile().FDOProfiles()
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Error saving FDO settings", Err: err}
}
if len(profiles) == 0 {
err = handler.addDefaultProfile()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
}
return response.Empty(w)
}
func (handler *Handler) addDefaultProfile() error {
profileID := handler.DataStore.FDOProfile().GetNextIdentifier()
profile := &portainer.FDOProfile{
ID: portainer.FDOProfileID(profileID),
Name: "Docker Standalone + Edge",
}
filePath, err := handler.FileService.StoreFDOProfileFileFromBytes(strconv.Itoa(int(profile.ID)), []byte(defaultProfileFileContent))
if err != nil {
return err
}
profile.FilePath = filePath
profile.DateCreated = time.Now().Unix()
err = handler.DataStore.FDOProfile().Create(profile)
if err != nil {
return err
}
return nil
}
const defaultProfileFileContent = `
#!/bin/bash -ex
env > env.log
export AGENT_IMAGE=portainer/agent:2.11.0
export GUID=$(cat DEVICE_GUID.txt)
export DEVICE_NAME=$(cat DEVICE_name.txt)
export EDGE_ID=$(cat DEVICE_edgeid.txt)
export EDGE_KEY=$(cat DEVICE_edgekey.txt)
export AGENTVOLUME=$(pwd)/data/portainer_agent_data/
mkdir -p ${AGENTVOLUME}
docker pull ${AGENT_IMAGE}
docker run -d \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /var/lib/docker/volumes:/var/lib/docker/volumes \
-v /:/host \
-v ${AGENTVOLUME}:/data \
--restart always \
-e EDGE=1 \
-e EDGE_ID=${EDGE_ID} \
-e EDGE_KEY=${EDGE_KEY} \
-e CAP_HOST_MANAGEMENT=1 \
-e EDGE_INSECURE_POLL=1 \
--name portainer_edge_agent \
${AGENT_IMAGE}
`

View File

@@ -6,35 +6,26 @@ import (
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/security"
)
type Handler struct {
*mux.Router
DataStore dataservices.DataStore
FileService portainer.FileService
DataStore dataservices.DataStore
}
func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataStore, fileService portainer.FileService) *Handler {
func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataStore) *Handler {
h := &Handler{
Router: mux.NewRouter(),
DataStore: dataStore,
FileService: fileService,
Router: mux.NewRouter(),
DataStore: dataStore,
}
h.Handle("/fdo/configure", bouncer.AdminAccess(httperror.LoggerHandler(h.fdoConfigure))).Methods(http.MethodPost)
h.Handle("/fdo/list", bouncer.AdminAccess(httperror.LoggerHandler(h.fdoListAll))).Methods(http.MethodGet)
h.Handle("/fdo/register", bouncer.AdminAccess(httperror.LoggerHandler(h.fdoRegisterDevice))).Methods(http.MethodPost)
h.Handle("/fdo/configure/{guid}", bouncer.AdminAccess(httperror.LoggerHandler(h.fdoConfigureDevice))).Methods(http.MethodPost)
h.Handle("/fdo/profiles", bouncer.AdminAccess(httperror.LoggerHandler(h.fdoProfileList))).Methods(http.MethodGet)
h.Handle("/fdo/profiles", bouncer.AdminAccess(httperror.LoggerHandler(h.createProfile))).Methods(http.MethodPost)
h.Handle("/fdo/profiles/{id}", bouncer.AdminAccess(httperror.LoggerHandler(h.fdoProfileInspect))).Methods(http.MethodGet)
h.Handle("/fdo/profiles/{id}", bouncer.AdminAccess(httperror.LoggerHandler(h.updateProfile))).Methods(http.MethodPut)
h.Handle("/fdo/profiles/{id}", bouncer.AdminAccess(httperror.LoggerHandler(h.deleteProfile))).Methods(http.MethodDelete)
h.Handle("/fdo/profiles/{id}/duplicate", bouncer.AdminAccess(httperror.LoggerHandler(h.duplicateProfile))).Methods(http.MethodPost)
h.Handle("/fdo/profiles", bouncer.AdminAccess(httperror.LoggerHandler(h.fdoProfiles))).Methods(http.MethodGet)
return h
}

View File

@@ -1,92 +0,0 @@
package fdo
import (
"errors"
"fmt"
"net/http"
"strconv"
"time"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
)
type createProfileFromFileContentPayload struct {
Name string
ProfileFileContent string
}
func (payload *createProfileFromFileContentPayload) Validate(r *http.Request) error {
if payload.Name == "" {
return errors.New("profile name must be provided")
}
if payload.ProfileFileContent == "" {
return errors.New("profile file content must be provided")
}
return nil
}
// @id createProfile
// @summary creates a new FDO Profile
// @description creates a new FDO Profile
// @description **Access policy**: administrator
// @tags intel
// @security jwt
// @produce json
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 409 "Profile name already exists"
// @failure 500 "Server error"
// @router /fdo/profiles [post]
func (handler *Handler) createProfile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
method, err := request.RetrieveQueryParameter(r, "method", false)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: method", err}
}
switch method {
case "editor":
return handler.createFDOProfileFromFileContent(w, r)
}
return &httperror.HandlerError{http.StatusBadRequest, "Invalid method. Value must be one of: editor", errors.New("invalid method")}
}
func (handler *Handler) createFDOProfileFromFileContent(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload createProfileFromFileContentPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
isUnique, err := handler.checkUniqueProfileName(payload.Name, -1)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
if !isUnique {
return &httperror.HandlerError{http.StatusConflict, fmt.Sprintf("A profile with the name '%s' already exists", payload.Name), errors.New("a profile already exists with this name")}
}
profileID := handler.DataStore.FDOProfile().GetNextIdentifier()
profile := &portainer.FDOProfile{
ID: portainer.FDOProfileID(profileID),
Name: payload.Name,
}
filePath, err := handler.FileService.StoreFDOProfileFileFromBytes(strconv.Itoa(int(profile.ID)), []byte(payload.ProfileFileContent))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist profile file on disk", err}
}
profile.FilePath = filePath
profile.DateCreated = time.Now().Unix()
err = handler.DataStore.FDOProfile().Create(profile)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the profile inside the database", err}
}
return response.JSON(w, profile)
}

View File

@@ -1,36 +0,0 @@
package fdo
import (
"errors"
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
)
// @id deleteProfile
// @summary deletes a FDO Profile
// @description deletes a FDO Profile
// @description **Access policy**: administrator
// @tags intel
// @security jwt
// @produce json
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /fdo/profiles/{id} [delete]
func (handler *Handler) deleteProfile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
id, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Bad request", errors.New("missing 'id' query parameter")}
}
err = handler.DataStore.FDOProfile().Delete(portainer.FDOProfileID(id))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete Profile", err}
}
return response.Empty(w)
}

View File

@@ -1,67 +0,0 @@
package fdo
import (
"errors"
"fmt"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"net/http"
"strconv"
"time"
)
// @id duplicate
// @summary duplicated an existing FDO Profile
// @description duplicated an existing FDO Profile
// @description **Access policy**: administrator
// @tags intel
// @security jwt
// @produce json
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /fdo/profiles/{id}/duplicate [post]
func (handler *Handler) duplicateProfile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
id, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Bad request", errors.New("missing 'id' query parameter")}
}
originalProfile, err := handler.DataStore.FDOProfile().FDOProfile(portainer.FDOProfileID(id))
if handler.DataStore.IsErrObjectNotFound(err) {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a FDO Profile with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a FDO Profile with the specified identifier inside the database", err}
}
fileContent, err := handler.FileService.GetFileContent(originalProfile.FilePath, "")
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Profile file content", err}
}
profileID := handler.DataStore.FDOProfile().GetNextIdentifier()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to duplicate Profile", err}
}
newProfile := &portainer.FDOProfile{
ID: portainer.FDOProfileID(profileID),
Name: fmt.Sprintf("%s - copy", originalProfile.Name),
}
filePath, err := handler.FileService.StoreFDOProfileFileFromBytes(strconv.Itoa(int(newProfile.ID)), fileContent)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist profile file on disk", err}
}
newProfile.FilePath = filePath
newProfile.DateCreated = time.Now().Unix()
err = handler.DataStore.FDOProfile().Create(newProfile)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the profile inside the database", err}
}
return response.JSON(w, newProfile)
}

View File

@@ -1,49 +0,0 @@
package fdo
import (
"errors"
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
)
type fdoProfileResponse struct {
Name string `json:"name"`
FileContent string `json:"fileContent"`
}
// @id fdoProfileInspect
// @summary retrieves a given FDO profile information and content
// @description retrieves a given FDO profile information and content
// @description **Access policy**: administrator
// @tags intel
// @security jwt
// @produce json
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /fdo/profiles/{id} [get]
func (handler *Handler) fdoProfileInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
id, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Bad request", errors.New("missing 'id' query parameter")}
}
profile, err := handler.DataStore.FDOProfile().FDOProfile(portainer.FDOProfileID(id))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Profile", err}
}
fileContent, err := handler.FileService.GetFileContent(profile.FilePath, "")
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Profile file content", err}
}
return response.JSON(w, fdoProfileResponse{
Name: profile.Name,
FileContent: string(fileContent),
})
}

View File

@@ -1,31 +0,0 @@
package fdo
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
)
// @id fdoProfileList
// @summary retrieves all FDO profiles
// @description retrieves all FDO profiles
// @description **Access policy**: administrator
// @tags intel
// @security jwt
// @produce json
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied to access settings"
// @failure 500 "Server error"
// @failure 500 "Bad gateway"
// @router /fdo/profiles [get]
func (handler *Handler) fdoProfileList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
profiles, err := handler.DataStore.FDOProfile().FDOProfiles()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
return response.JSON(w, profiles)
}

View File

@@ -1,67 +0,0 @@
package fdo
import (
"errors"
"fmt"
"net/http"
"strconv"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
)
// @id updateProfile
// @summary updates an existing FDO Profile
// @description updates an existing FDO Profile
// @description **Access policy**: administrator
// @tags intel
// @security jwt
// @produce json
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 409 "Profile name already exists"
// @failure 500 "Server error"
// @router /fdo/profiles/{id} [put]
func (handler *Handler) updateProfile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
id, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Bad request", errors.New("missing 'id' query parameter")}
}
var payload createProfileFromFileContentPayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
profile, err := handler.DataStore.FDOProfile().FDOProfile(portainer.FDOProfileID(id))
if handler.DataStore.IsErrObjectNotFound(err) {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a FDO Profile with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a FDO Profile with the specified identifier inside the database", err}
}
isUnique, err := handler.checkUniqueProfileName(payload.Name, id)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
if !isUnique {
return &httperror.HandlerError{http.StatusConflict, fmt.Sprintf("A profile with the name '%s' already exists", payload.Name), errors.New("a profile already exists with this name")}
}
filePath, err := handler.FileService.StoreFDOProfileFileFromBytes(strconv.Itoa(int(profile.ID)), []byte(payload.ProfileFileContent))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update profile", err}
}
profile.FilePath = filePath
profile.Name = payload.Name
err = handler.DataStore.FDOProfile().Update(profile.ID, profile)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update profile", err}
}
return response.JSON(w, profile)
}

View File

@@ -0,0 +1,40 @@
package fdo
import (
"io"
"net/http"
httperror "github.com/portainer/libhttp/error"
)
// @id fdoProfiles
// @summary retrieve a list of FDO profiles
// @description retrieve a list of FDO profiles
// @description **Access policy**: administrator
// @tags intel
// @security jwt
// @produce json
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied to access settings"
// @failure 500 "Server error"
// @failure 500 "Bad gateway"
// @router /fdo/profiles [get]
func (handler *Handler) fdoProfiles(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
}
profiles, err := http.Get(settings.FDOConfiguration.ProfilesURL)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadGateway, Message: "Unabled to retrieve the profile list", Err: err}
}
defer profiles.Body.Close()
if _, err := io.Copy(w, profiles.Body); err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadGateway, Message: "Unabled to retrieve the profile list", Err: err}
}
return nil
}

View File

@@ -1,16 +0,0 @@
package fdo
func (handler *Handler) checkUniqueProfileName(name string, id int) (bool, error) {
profiles, err := handler.DataStore.FDOProfile().FDOProfiles()
if err != nil {
return false, err
}
for _, profile := range profiles {
if profile.Name == name && (id == -1 || id != int(profile.ID)) {
return false, nil
}
}
return true, nil
}

View File

@@ -21,7 +21,7 @@ type Handler struct {
}
// NewHandler returns a new Handler
func NewHandler(bouncer *security.RequestBouncer) *Handler {
func NewHandler(bouncer *security.RequestBouncer) (*Handler, error) {
h := &Handler{
Router: mux.NewRouter(),
}
@@ -33,5 +33,5 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
h.Handle("/open_amt/{id}/devices/{deviceId}/action", bouncer.AdminAccess(httperror.LoggerHandler(h.deviceAction))).Methods(http.MethodPost)
h.Handle("/open_amt/{id}/devices/{deviceId}/features", bouncer.AdminAccess(httperror.LoggerHandler(h.deviceFeatures))).Methods(http.MethodPost)
return h
return h, nil
}

View File

@@ -21,7 +21,7 @@ type registryConfigurePayload struct {
// Password used to authenticate against this registry. required when Authentication is true
Password string `example:"registry_password"`
// ECR region
Region string
Region string
// Use TLS
TLS bool `example:"true"`
// Skip the verification of the server TLS certificate

View File

@@ -2,9 +2,8 @@ package registries
import (
"errors"
"net/http"
"github.com/portainer/portainer/api/internal/endpointutils"
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@@ -26,13 +25,13 @@ type registryUpdatePayload struct {
// Username used to authenticate against this registry. Required when Authentication is true
Username *string `example:"registry_user"`
// Password used to authenticate against this registry. required when Authentication is true
Password *string `example:"registry_password"`
Password *string `example:"registry_password"`
// Quay data
Quay *portainer.QuayRegistryData
Quay *portainer.QuayRegistryData
// Registry access control
RegistryAccesses *portainer.RegistryAccesses `json:",omitempty"`
// ECR data
Ecr *portainer.EcrData `json:",omitempty"`
Ecr *portainer.EcrData `json:",omitempty"`
}
func (payload *registryUpdatePayload) Validate(r *http.Request) error {

View File

@@ -42,10 +42,6 @@ type settingsUpdatePayload struct {
HelmRepositoryURL *string `example:"https://charts.bitnami.com/bitnami"`
// Kubectl Shell Image
KubectlShellImage *string `example:"portainer/kubectl-shell:latest"`
// DisableTrustOnFirstConnect makes Portainer require explicit user trust of the edge agent before accepting the connection
DisableTrustOnFirstConnect *bool `example:"false"`
// EnforceEdgeID makes Portainer store the Edge ID instead of accepting anyone
EnforceEdgeID *bool `example:"false"`
}
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
@@ -166,14 +162,6 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
settings.EnableEdgeComputeFeatures = *payload.EnableEdgeComputeFeatures
}
if payload.DisableTrustOnFirstConnect != nil {
settings.DisableTrustOnFirstConnect = *payload.DisableTrustOnFirstConnect
}
if payload.EnforceEdgeID != nil {
settings.EnforceEdgeID = *payload.EnforceEdgeID
}
if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval {
err := handler.updateSnapshotInterval(settings, *payload.SnapshotInterval)
if err != nil {

View File

@@ -129,7 +129,7 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
return configErr
}
err = handler.deployComposeStack(config, false)
err = handler.deployComposeStack(config)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
}
@@ -283,7 +283,7 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
return configErr
}
err = handler.deployComposeStack(config, false)
err = handler.deployComposeStack(config)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
}
@@ -394,7 +394,7 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
return configErr
}
err = handler.deployComposeStack(config, false)
err = handler.deployComposeStack(config)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
}
@@ -451,7 +451,7 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai
// to login/logout, which will generate the required data in the config.json file and then
// clean it. Hence the use of the mutex.
// We should contribute to libcompose to support authentication without using the config.json file.
func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig, forceCreate bool) error {
func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) error {
isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
if err != nil {
return errors.Wrap(err, "failed to check user priviliges deploying a stack")
@@ -480,5 +480,5 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig,
}
}
return handler.StackDeployer.DeployComposeStack(config.stack, config.endpoint, config.registries, forceCreate)
return handler.StackDeployer.DeployComposeStack(config.stack, config.endpoint, config.registries)
}

View File

@@ -156,14 +156,6 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack changes inside the database", Err: err}
}
if resourceControl != nil {
resourceControl.ResourceID = stackutils.ResourceControlID(stack.EndpointID, stack.Name)
err := handler.DataStore.ResourceControl().UpdateResourceControl(resourceControl.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the resource control changes", err}
}
}
if stack.GitConfig != nil && stack.GitConfig.Authentication != nil && stack.GitConfig.Authentication.Password != "" {
// sanitize password in the http response to minimise possible security leaks
stack.GitConfig.Authentication.Password = ""
@@ -185,7 +177,7 @@ func (handler *Handler) migrateComposeStack(r *http.Request, stack *portainer.St
return configErr
}
err := handler.deployComposeStack(config, false)
err := handler.deployComposeStack(config)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
}

View File

@@ -123,7 +123,7 @@ func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *http
func (handler *Handler) startStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
switch stack.Type {
case portainer.DockerComposeStack:
return handler.ComposeStackManager.Up(context.TODO(), stack, endpoint, false)
return handler.ComposeStackManager.Up(context.TODO(), stack, endpoint)
case portainer.DockerSwarmStack:
return handler.SwarmStackManager.Deploy(stack, true, endpoint)
}

View File

@@ -49,7 +49,7 @@ func (payload *updateSwarmStackPayload) Validate(r *http.Request) error {
// @id StackUpdate
// @summary Update a stack
// @description Update a stack, only for file based stacks.
// @description Update a stack.
// @description **Access policy**: authenticated
// @tags stacks
// @security ApiKeyAuth
@@ -123,15 +123,6 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
}
}
// Must not be git based stack. stop the auto update job if there is any
if stack.AutoUpdate != nil {
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)
stack.AutoUpdate = nil
}
if stack.GitConfig != nil {
stack.FromAppTemplate = true
}
updateError := handler.updateAndDeployStack(r, stack, endpoint)
if updateError != nil {
return updateError
@@ -190,7 +181,7 @@ func (handler *Handler) updateComposeStack(r *http.Request, stack *portainer.Sta
return configErr
}
err = handler.deployComposeStack(config, false)
err = handler.deployComposeStack(config)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
}

View File

@@ -206,7 +206,7 @@ func (handler *Handler) deployStack(r *http.Request, stack *portainer.Stack, end
return httpErr
}
if err := handler.deployComposeStack(config, true); err != nil {
if err := handler.deployComposeStack(config); err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
}

View File

@@ -87,12 +87,6 @@ func (handler *Handler) userCreate(w http.ResponseWriter, r *http.Request) *http
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
}
// when ldap/oauth is on, can only add users without password
if (settings.AuthenticationMethod == portainer.AuthenticationLDAP || settings.AuthenticationMethod == portainer.AuthenticationOAuth) && payload.Password != "" {
errMsg := "A user with password can not be created when authentication method is Oauth or LDAP"
return &httperror.HandlerError{http.StatusBadRequest, errMsg, errors.New(errMsg)}
}
if settings.AuthenticationMethod == portainer.AuthenticationInternal {
user.Password, err = handler.CryptoService.Hash(payload.Password)
if err != nil {

View File

@@ -2,10 +2,9 @@ package webhooks
import (
"errors"
"net/http"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/registryutils/access"
"net/http"
"github.com/asaskevich/govalidator"
"github.com/gofrs/uuid"
@@ -18,7 +17,7 @@ import (
type webhookCreatePayload struct {
ResourceID string
EndpointID int
RegistryID portainer.RegistryID
RegistryID portainer.RegistryID
WebhookType int
}

View File

@@ -80,13 +80,13 @@ func (handler *Handler) executeServiceWebhook(
}
service.Spec.TaskTemplate.ForceUpdate++
var imageName = strings.Split(service.Spec.TaskTemplate.ContainerSpec.Image, "@sha")[0]
if imageTag != "" {
var tagIndex = strings.LastIndex(imageName, ":")
if tagIndex == -1 {
tagIndex = len(imageName)
tagIndex = len(imageName)
}
service.Spec.TaskTemplate.ContainerSpec.Image = imageName[:tagIndex] + ":" + imageTag
} else {

View File

@@ -45,11 +45,7 @@ func (transport *Transport) proxyContainerGroupPutRequest(request *http.Request)
}
if validationResponse.StatusCode >= 200 && validationResponse.StatusCode < 300 {
resp := &http.Response{
Header: http.Header{
http.CanonicalHeaderKey("content-type"): []string{"application/json"},
},
}
resp := &http.Response{}
errObj := map[string]string{
"message": "A container instance with the same name already exists inside the selected resource group",
}

View File

@@ -16,7 +16,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
dataerrors "github.com/portainer/portainer/api/dataservices/errors"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/http/proxy/factory/utils"
"github.com/portainer/portainer/api/http/security"
@@ -602,10 +601,6 @@ func (transport *Transport) executeGenericResourceDeletionOperation(request *htt
if response.StatusCode == http.StatusNoContent || response.StatusCode == http.StatusOK {
resourceControl, err := transport.dataStore.ResourceControl().ResourceControlByResourceIDAndType(resourceIdentifierAttribute, resourceType)
if err != nil {
if err == dataerrors.ErrObjectNotFound {
return response, nil
}
return response, err
}

View File

@@ -12,4 +12,4 @@ func (transport *baseTransport) proxyDeploymentsRequest(request *http.Request, n
default:
return transport.executeKubernetesRequest(request)
}
}
}

View File

@@ -12,4 +12,4 @@ func (transport *baseTransport) proxyPodsRequest(request *http.Request, namespac
default:
return transport.executeKubernetesRequest(request)
}
}
}

View File

@@ -1,9 +1,8 @@
package kubernetes
import (
"net/http"
"github.com/portainer/portainer/api/internal/registryutils"
"net/http"
)
func (transport *baseTransport) refreshRegistry(request *http.Request, namespace string) (err error) {
@@ -15,4 +14,4 @@ func (transport *baseTransport) refreshRegistry(request *http.Request, namespace
err = registryutils.RefreshEcrSecret(cli, transport.endpoint, transport.dataStore, namespace)
return
}
}

View File

@@ -2,7 +2,6 @@ package security
import (
"errors"
"fmt"
"net/http"
"strings"
"time"
@@ -135,19 +134,6 @@ func (bouncer *RequestBouncer) AuthorizedEdgeEndpointOperation(r *http.Request,
return errors.New("invalid Edge identifier")
}
if endpoint.LastCheckInDate > 0 || endpoint.UserTrusted {
return nil
}
settings, err := bouncer.dataStore.Settings().Settings()
if err != nil {
return fmt.Errorf("could not retrieve the settings: %w", err)
}
if settings.DisableTrustOnFirstConnect {
return errors.New("the device has not been trusted yet")
}
return nil
}

View File

@@ -210,12 +210,17 @@ func (server *Server) Start() error {
var sslHandler = sslhandler.NewHandler(requestBouncer)
sslHandler.SSLService = server.SSLService
openAMTHandler := openamt.NewHandler(requestBouncer)
openAMTHandler.OpenAMTService = server.OpenAMTService
openAMTHandler.DataStore = server.DataStore
openAMTHandler.DockerClientFactory = server.DockerClientFactory
openAMTHandler, err := openamt.NewHandler(requestBouncer)
if err != nil {
return err
}
if openAMTHandler != nil {
openAMTHandler.OpenAMTService = server.OpenAMTService
openAMTHandler.DataStore = server.DataStore
openAMTHandler.DockerClientFactory = server.DockerClientFactory
}
fdoHandler := fdo.NewHandler(requestBouncer, server.DataStore, server.FileService)
fdoHandler := fdo.NewHandler(requestBouncer, server.DataStore)
var stackHandler = stacks.NewHandler(requestBouncer)
stackHandler.DataStore = server.DataStore

View File

@@ -23,39 +23,21 @@ type Service struct {
}
// NewService creates a new instance of a service
func NewService(snapshotIntervalFromFlag string, dataStore dataservices.DataStore, dockerSnapshotter portainer.DockerSnapshotter, kubernetesSnapshotter portainer.KubernetesSnapshotter, shutdownCtx context.Context) (*Service, error) {
snapshotFrequency, err := parseSnapshotFrequency(snapshotIntervalFromFlag, dataStore)
func NewService(snapshotInterval string, dataStore dataservices.DataStore, dockerSnapshotter portainer.DockerSnapshotter, kubernetesSnapshotter portainer.KubernetesSnapshotter, shutdownCtx context.Context) (*Service, error) {
snapshotFrequency, err := time.ParseDuration(snapshotInterval)
if err != nil {
return nil, err
}
return &Service{
dataStore: dataStore,
snapshotIntervalInSeconds: snapshotFrequency,
snapshotIntervalInSeconds: snapshotFrequency.Seconds(),
dockerSnapshotter: dockerSnapshotter,
kubernetesSnapshotter: kubernetesSnapshotter,
shutdownCtx: shutdownCtx,
}, nil
}
func parseSnapshotFrequency(snapshotInterval string, dataStore dataservices.DataStore) (float64, error) {
if snapshotInterval == "" {
settings, err := dataStore.Settings().Settings()
if err != nil {
return 0, err
}
snapshotInterval = settings.SnapshotInterval
if snapshotInterval == "" {
snapshotInterval = portainer.DefaultSnapshotInterval
}
}
snapshotFrequency, err := time.ParseDuration(snapshotInterval)
if err != nil {
return 0, err
}
return snapshotFrequency.Seconds(), nil
}
// Start will start a background routine to execute periodic snapshots of environments(endpoints)
func (service *Service) Start() {
if service.refreshSignal != nil {

View File

@@ -4,7 +4,6 @@ import (
"io"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/dataservices/errors"
)
@@ -17,7 +16,6 @@ type testDatastore struct {
endpoint dataservices.EndpointService
endpointGroup dataservices.EndpointGroupService
endpointRelation dataservices.EndpointRelationService
fdoProfile dataservices.FDOProfileService
helmUserRepository dataservices.HelmUserRepositoryService
registry dataservices.RegistryService
resourceControl dataservices.ResourceControlService
@@ -33,7 +31,6 @@ type testDatastore struct {
user dataservices.UserService
version dataservices.VersionService
webhook dataservices.WebhookService
connection portainer.Connection
}
func (d *testDatastore) BackupTo(io.Writer) error { return nil }
@@ -49,9 +46,6 @@ func (d *testDatastore) EdgeJob() dataservices.EdgeJobService { re
func (d *testDatastore) EdgeStack() dataservices.EdgeStackService { return d.edgeStack }
func (d *testDatastore) Endpoint() dataservices.EndpointService { return d.endpoint }
func (d *testDatastore) EndpointGroup() dataservices.EndpointGroupService { return d.endpointGroup }
func (d *testDatastore) FDOProfile() dataservices.FDOProfileService {
return d.fdoProfile
}
func (d *testDatastore) EndpointRelation() dataservices.EndpointRelationService {
return d.endpointRelation
}
@@ -81,10 +75,6 @@ func (d *testDatastore) IsErrObjectNotFound(e error) bool {
return false
}
func (d *testDatastore) Connection() portainer.Connection {
return d.connection
}
func (d *testDatastore) Export(filename string) (err error) {
return nil
}
@@ -97,12 +87,10 @@ type datastoreOption = func(d *testDatastore)
// NewDatastore creates new instance of testDatastore.
// Will apply options before returning, opts will be applied from left to right.
func NewDatastore(options ...datastoreOption) *testDatastore {
conn, _ := database.NewDatabase("boltdb", "", nil)
d := testDatastore{connection: conn}
d := testDatastore{}
for _, o := range options {
o(&d)
}
return &d
}

View File

@@ -50,7 +50,7 @@ func (factory *ClientFactory) GetInstanceID() (instanceID string) {
}
// Remove the cached kube client so a new one can be created
func (factory *ClientFactory) RemoveKubeClient(endpointID portainer.EndpointID) {
func (factory *ClientFactory) RemoveKubeClient(endpointID portainer.EndpointID) {
factory.endpointClients.Remove(strconv.Itoa(int(endpointID)))
}

View File

@@ -16,8 +16,8 @@ import (
const (
secretDockerConfigKey = ".dockerconfigjson"
labelRegistryType = "io.portainer.kubernetes.registry.type"
annotationRegistryID = "portainer.io/registry.id"
labelRegistryType = "io.portainer.kubernetes.registry.type"
annotationRegistryID = "portainer.io/registry.id"
)
type (

View File

@@ -78,17 +78,7 @@ type (
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"`
ProfilesURL string `json:"profilesURL"`
}
// CLIFlags represents the available flags on the CLI
@@ -121,10 +111,6 @@ type (
Rollback *bool
SnapshotInterval *string
BaseURL *string
InitialMmapSize *int
MaxBatchSize *int
MaxBatchDelay *time.Duration
SecretKeyName *string
}
// CustomTemplate represents a custom template
@@ -328,8 +314,6 @@ type (
LastCheckInDate int64
// IsEdgeDevice marks if the environment was created as an EdgeDevice
IsEdgeDevice bool
// Whether the device has been trusted or not by the user
UserTrusted bool
// Deprecated fields
// Deprecated in DBVersion == 4
@@ -819,10 +803,6 @@ type (
HelmRepositoryURL string `json:"HelmRepositoryURL" example:"https://charts.bitnami.com/bitnami"`
// KubectlImage, defaults to portainer/kubectl-shell
KubectlShellImage string `json:"KubectlShellImage" example:"portainer/kubectl-shell"`
// DisableTrustOnFirstConnect makes Portainer require explicit user trust of the edge agent before accepting the connection
DisableTrustOnFirstConnect bool `json:"DisableTrustOnFirstConnect" example:"false"`
// EnforceEdgeID makes Portainer store the Edge ID instead of accepting anyone
EnforceEdgeID bool `json:"EnforceEdgeID" example:"false"`
// Deprecated fields
DisplayDonationHeader bool
@@ -1189,7 +1169,7 @@ type (
ComposeStackManager interface {
ComposeSyntaxMaxVersion() string
NormalizeStackName(name string) string
Up(ctx context.Context, stack *Stack, endpoint *Endpoint, forceRereate bool) error
Up(ctx context.Context, stack *Stack, endpoint *Endpoint) error
Down(ctx context.Context, stack *Stack, endpoint *Endpoint) error
}
@@ -1247,7 +1227,6 @@ type (
GetDefaultSSLCertsPath() (string, string)
StoreSSLCertPair(cert, key []byte) (string, string, error)
CopySSLCertPair(certPath, keyPath string) (string, string, error)
StoreFDOProfileFileFromBytes(fdoProfileIdentifier string, data []byte) (string, error)
}
// GitService represents a service for managing Git
@@ -1347,7 +1326,7 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.11.1"
APIVersion = "2.11.0"
// DBVersion is the version number of the Portainer database
DBVersion = 35
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
@@ -1375,8 +1354,6 @@ const (
// PortainerAgentSignatureMessage represents the message used to create a digital signature
// to be used when communicating with an agent
PortainerAgentSignatureMessage = "Portainer-App"
// DefaultSnapshotInterval represents the default interval between each environment snapshot job
DefaultSnapshotInterval = "5m"
// DefaultEdgeAgentCheckinIntervalInSeconds represents the default interval (in seconds) used by Edge agents to checkin with the Portainer instance
DefaultEdgeAgentCheckinIntervalInSeconds = 5
// DefaultTemplatesURL represents the URL to the official templates supported by Portainer

View File

@@ -21,8 +21,6 @@ func (e *StackAuthorMissingErr) Error() string {
return fmt.Sprintf("stack's %v author %s is missing", e.stackID, e.authorName)
}
// RedeployWhenChanged pull and redeploy the stack when git repo changed
// Stack will always be redeployed if force deployment is set to true
func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, datastore dataservices.DataStore, gitService portainer.GitService) error {
logger := log.WithFields(log.Fields{"stackID": stackID})
logger.Debug("redeploying stack")
@@ -89,7 +87,7 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data
switch stack.Type {
case portainer.DockerComposeStack:
err := deployer.DeployComposeStack(stack, endpoint, registries, false)
err := deployer.DeployComposeStack(stack, endpoint, registries)
if err != nil {
return errors.WithMessagef(err, "failed to deploy a docker compose stack %v", stackID)
}

View File

@@ -32,7 +32,7 @@ func (s *noopDeployer) DeploySwarmStack(stack *portainer.Stack, endpoint *portai
return nil
}
func (s *noopDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forceRereate bool) error {
func (s *noopDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry) error {
return nil
}

View File

@@ -14,7 +14,7 @@ import (
type StackDeployer interface {
DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool) error
DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forceRereate bool) error
DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry) error
DeployKubernetesStack(stack *portainer.Stack, endpoint *portainer.Endpoint, user *portainer.User) error
}
@@ -45,14 +45,14 @@ func (d *stackDeployer) DeploySwarmStack(stack *portainer.Stack, endpoint *porta
return d.swarmStackManager.Deploy(stack, prune, endpoint)
}
func (d *stackDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forceRereate bool) error {
func (d *stackDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry) error {
d.lock.Lock()
defer d.lock.Unlock()
d.swarmStackManager.Login(registries, endpoint)
defer d.swarmStackManager.Logout(endpoint)
return d.composeStackManager.Up(context.TODO(), stack, endpoint, forceRereate)
return d.composeStackManager.Up(context.TODO(), stack, endpoint)
}
func (d *stackDeployer) DeployKubernetesStack(stack *portainer.Stack, endpoint *portainer.Endpoint, user *portainer.User) error {

View File

@@ -1,3 +1,3 @@
<button type="button" ngf-select="$ctrl.onFileSelected($file)" class="btn ng-scope" button-spinner="$ctrl.state.uploadInProgress">
<i style="margin: 0" class="fa fa-upload" ng-if="!$ctrl.state.uploadInProgress"></i>
<i style="margin: 0;" class="fa fa-upload" ng-if="!$ctrl.state.uploadInProgress"></i>
</button>

View File

@@ -41,7 +41,9 @@
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ModTime' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th> Actions </th>
<th>
Actions
</th>
</tr>
</thead>
<tbody>

View File

@@ -680,7 +680,6 @@ a[ng-click] {
.image-zoom-modal .modal-dialog {
width: 80%;
}
/*!bootbox override*/
/*angular-multi-select override*/
@@ -903,7 +902,6 @@ json-tree .branch-preview {
margin-left: 0.25rem;
}
/* used for bootbox prompt with inputType radio */
.form-check.radio {
margin-left: 15px;
}

View File

@@ -339,7 +339,7 @@ html {
--text-body-color: var(--white-color);
--text-sidebar-title-color: var(--grey-8);
--text-widget-header-color: var(--white-color);
--text-form-control-color: var(--white-color);
--text-form-control-color: var(--grey-8);
--text-muted-color: var(--grey-8);
--text-link-color: var(--blue-9);
--text-link-hover-color: var(--blue-2);
@@ -365,7 +365,7 @@ html {
--text-blocklist-item-selected-color: var(--white-color);
--text-progress-bar-color: var(--white-color);
--text-pagination-color: var(--white-color);
--text-pagination-span-color: var(--blue-2);
--text-pagination-span-color: var(--white-color);
--text-pagination-span-hover-color: var(--white-color);
--text-ui-select-color: var(--white-color);
--text-ui-select-hover-color: var(--white-color);
@@ -547,8 +547,6 @@ html {
--text-button-hover-color: var(--white-color);
--text-btn-default-color: var(--white-color);
--text-small-select-color: var(--white-color);
--text-multiselect-item-color: var(--white-color);
--text-pagination-span-color: var(--blue-2);
--border-color: var(--grey-55);
--border-widget-color: var(--white-color);

View File

@@ -1,18 +1,18 @@
{
"name": "Portainer",
"icons": [
{
"src": "ico/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "ico/android-chrome-256x256.png",
"sizes": "256x256",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
"name": "Portainer",
"icons": [
{
"src": "ico/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "ico/android-chrome-256x256.png",
"sizes": "256x256",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@@ -1,5 +1,7 @@
<div>
<div class="col-sm-12 form-section-title"> Azure configuration </div>
<div class="col-sm-12 form-section-title">
Azure configuration
</div>
<!-- applicationId-input -->
<div class="form-group">
<label for="azure_credential_appid" class="col-sm-3 col-lg-2 control-label text-left">Application ID</label>

View File

@@ -2,7 +2,7 @@
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px"></i> {{ $ctrl.titleText }} </div>
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
</div>
<div class="actionBar">
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
@@ -46,7 +46,9 @@
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Location' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th> Published Ports </th>
<th>
Published Ports
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('ResourceControl.Ownership')">
Ownership
@@ -96,7 +98,9 @@
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px"> Items per page </span>
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()" data-cy="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>

View File

@@ -8,7 +8,9 @@
<rd-widget>
<rd-widget-body>
<div class="form-horizontal" autocomplete="off">
<div class="col-sm-12 form-section-title"> Azure settings </div>
<div class="col-sm-12 form-section-title">
Azure settings
</div>
<!-- subscription-input -->
<div class="form-group">
<label for="azure_subscription" class="col-sm-2 control-label text-left">Subscription</label>
@@ -33,7 +35,9 @@
</div>
</div>
<!-- !location-input -->
<div class="col-sm-12 form-section-title"> Container configuration </div>
<div class="col-sm-12 form-section-title">
Container configuration
</div>
<!-- name-input -->
<div class="form-group">
<label for="container_name" class="col-sm-2 control-label text-left">Name</label>
@@ -64,15 +68,15 @@
<label class="control-label text-left">Port mapping</label>
</div>
<!-- port-mapping-input-list -->
<div class="col-sm-12 form-inline" style="margin-top: 10px">
<div ng-repeat="binding in $ctrl.container.Ports" style="margin-top: 2px">
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="binding in $ctrl.container.Ports" style="margin-top: 2px;">
<!-- host-port -->
<div class="input-group col-sm-4 input-group-sm">
<span class="input-group-addon">host</span>
<input type="text" class="form-control" ng-model="binding.host" placeholder="e.g. 80" disabled />
</div>
<!-- !host-port -->
<span style="margin: 0 10px 0 10px">
<span style="margin: 0 10px 0 10px;">
<i class="fa fa-long-arrow-alt-right" aria-hidden="true"></i>
</span>
<!-- container-port -->
@@ -96,13 +100,17 @@
<!-- !port-mapping -->
<!-- public-ip -->
<div class="form-group" ng-if="$ctrl.container.AllocatePublicIP">
<label for="public_ip" class="col-sm-2 control-label text-left"> Public IP </label>
<label for="public_ip" class="col-sm-2 control-label text-left">
Public IP
</label>
<div class="col-sm-10">
<input type="text" class="form-control" ng-model="$ctrl.container.IPAddress" disabled />
</div>
</div>
<!-- public-ip -->
<div class="col-sm-12 form-section-title"> Container resources </div>
<div class="col-sm-12 form-section-title">
Container resources
</div>
<!-- cpu-input -->
<div class="form-group">
<label for="container_cpu" class="col-sm-2 control-label text-left">CPU</label>

View File

@@ -8,7 +8,9 @@
<rd-widget>
<rd-widget-body>
<form class="form-horizontal" autocomplete="off" name="aciForm">
<div class="col-sm-12 form-section-title"> Azure settings </div>
<div class="col-sm-12 form-section-title">
Azure settings
</div>
<!-- subscription-input -->
<div class="form-group">
<label for="azure_subscription" class="col-sm-1 control-label text-left">Subscription</label>
@@ -44,7 +46,9 @@
</div>
</div>
<!-- !location-input -->
<div class="col-sm-12 form-section-title"> Container configuration </div>
<div class="col-sm-12 form-section-title">
Container configuration
</div>
<!-- name-input -->
<div class="form-group">
<label for="container_name" class="col-sm-1 control-label text-left">Name</label>
@@ -90,20 +94,20 @@
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">Port mapping</label>
<span class="label label-default interactive" style="margin-left: 10px" ng-click="addPortBinding()">
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addPortBinding()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> map additional port
</span>
</div>
<!-- port-mapping-input-list -->
<div class="col-sm-12 form-inline" style="margin-top: 10px">
<div ng-repeat="binding in model.Ports" style="margin-top: 2px">
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="binding in model.Ports" style="margin-top: 2px;">
<!-- host-port -->
<div class="input-group col-sm-4 input-group-sm">
<span class="input-group-addon">host</span>
<input type="text" class="form-control" ng-model="binding.host" placeholder="e.g. 80" />
</div>
<!-- !host-port -->
<span style="margin: 0 10px 0 10px">
<span style="margin: 0 10px 0 10px;">
<i class="fa fa-long-arrow-alt-right" aria-hidden="true"></i>
</span>
<!-- container-port -->
@@ -134,7 +138,9 @@
</div>
<!-- public-ip -->
<div class="col-sm-12 form-section-title"> Container resources </div>
<div class="col-sm-12 form-section-title">
Container resources
</div>
<!-- cpu-input -->
<div class="form-group">
<label for="container_cpu" class="col-sm-1 control-label text-left">CPU</label>
@@ -155,14 +161,16 @@
<por-access-control-form form-data="model.AccessControlData"></por-access-control-form>
<!-- !access-control -->
<!-- actions -->
<div class="col-sm-12 form-section-title"> Actions </div>
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress" ng-click="create()" button-spinner="state.actionInProgress">
<span ng-hide="state.actionInProgress">Deploy the container</span>
<span ng-show="state.actionInProgress">Deployment in progress...</span>
</button>
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px">{{ state.formValidationError }}</span>
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
</div>
</div>
<!-- !actions -->

View File

@@ -1,9 +1,8 @@
import angular from 'angular';
import containersModule from './containers';
import { componentsModule } from './components';
angular.module('portainer.docker', ['portainer.app', containersModule, componentsModule]).config([
angular.module('portainer.docker', ['portainer.app', containersModule]).config([
'$stateRegistryProvider',
function ($stateRegistryProvider) {
'use strict';

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