Compare commits

...

452 Commits

Author SHA1 Message Date
Anthony Lapenna a171e540c5 Merge branch 'release/1.20.2' 2019-03-05 17:34:28 +13:00
Anthony Lapenna cb858f0412 chore(version): bump version number 2019-03-05 17:34:19 +13:00
Anthony Lapenna 82078a8d8f style(extensions): update extensions information panel 2019-03-05 16:09:03 +13:00
Anthony Lapenna 2b31f489d9 feat(api): add support for an externally fetched title for motd (#2755)
* feat(api): add support for an externally fetched title for motd

* refactor(api): gofmt motd.go

* refactor(api): update go comment
2019-03-05 16:05:15 +13:00
Anthony Lapenna e2a17480af Merge branch 'develop' of github.com:portainer/portainer into develop 2019-03-04 13:48:01 +13:00
Anthony Lapenna 0670079566 feat(api): update ExtensionDefinitionsURL 2019-03-04 13:46:27 +13:00
Anthony Lapenna 5ca9501540 dep(api): update docker binary version to 18.09.3 (#2749) 2019-03-01 14:45:36 +13:00
Anthony Lapenna 415c1759d1 Merge branch 'oath-poc' into develop 2019-03-01 14:16:04 +13:00
Anthony Lapenna db0091b46d feat(api): revert extension URLs to correct one 2019-03-01 13:58:55 +13:00
linquize 42529cc5ea feat(volumes): show volume creation date (#2745) 2019-03-01 11:59:11 +13:00
Anthony Lapenna 60fbfeba23 fix(oauth): fix settings displaying issue for custom OAuth configuration 2019-03-01 11:24:47 +13:00
Anthony Lapenna f5091ce5fb fix(auth): fix invalid condition to display OAuth login button 2019-03-01 10:58:18 +13:00
Anthony Lapenna 58962de20e Merge branch 'develop' into oath-poc 2019-03-01 09:42:38 +13:00
Anthony Lapenna 1eb7e6bacc fix(auth): rollback changes introduced via #2591 (#2747) 2019-02-28 11:38:02 +13:00
Anthony Lapenna 130baddea0 fix(api): fix an issue when removing non local administrators 2019-02-25 18:54:21 +13:00
Tim van den Eijnden 9cbf1f34a7 feat(networks): prevent removal of predefined networks (#2697)
* fix(networks): disable removing predefined networks (#1838)

*  fix(networks): disable select all for predefined networks (#1838)

* fix(networks): do not allow delete in network-details & use constant (#1838)
2019-02-25 14:25:48 +13:00
linquize c152d3f62e fix(stacks): update web editor to set tab key to insert spaces (#2735) 2019-02-25 14:19:53 +13:00
linquize da44f14e07 fix(auth): prevent redirect parameter to use state portainer.auth (#2701) 2019-02-25 13:57:11 +13:00
Anthony Lapenna 49516e2c3f style(oauth): update Azure UI elements 2019-02-25 13:38:27 +13:00
Anthony Lapenna 9c4c782a90 style(container-creation): review auto remove element position 2019-02-25 13:09:09 +13:00
baron_l 7aa6a30614 feat(registry-manager): allow regular users to use the registry browse feature (#2664)
* feat(registries): registries accessibility to all authorized people and not only admins

* feat(registry): dockerhub settings for admin only

* feat(registry): remove registry config access for non admin users

* feat(api): use AuthenticatedAccess policy instead of RestrictedAccess for extensionList operation

* refactor(api): minor update to security package

* refactor(api): revert unexporting function changes

* refactor(api): apply gofmt
2019-02-25 13:02:49 +13:00
linquize 99e50370bd feat(container-creation): support auto remove option (docker run --rm) (#2684) 2019-02-25 09:48:31 +13:00
Anthony Lapenna dc2a8cf1f4 feat(oauth): update OAuth configuration UX 2019-02-21 14:02:25 +13:00
Anthony Lapenna b9ac3d4286 feat(oauth): fix the double refresh issue 2019-02-21 11:09:57 +13:00
Anthony Lapenna 6711e6c969 feat(oauth): update configuration override UX 2019-02-21 10:30:09 +13:00
Anthony Lapenna 4a5fa211a7 feat(account): display a warning message in the account view 2019-02-20 13:57:13 +13:00
Anthony Lapenna d510d23408 feat(oauth): improve Azure OAuth support 2019-02-20 13:53:25 +13:00
Anthony Lapenna ce9e009e22 feat(oauth): update UI/UX 2019-02-19 14:38:42 +13:00
Anthony Lapenna 9918c1260b feat(oauth): update authentication panel with OAuth provider details 2019-02-19 09:54:02 +13:00
Anthony Lapenna e325ad10dd fix(oauth): fix an UX issue when updating microsoft oauth settings 2019-02-18 16:18:48 +13:00
Anthony Lapenna 73f20b5157 refactor(oauth): remove console log statement 2019-02-18 15:21:34 +13:00
Anthony Lapenna b6f04c5e0d fix(oauth): fix missing scopes for microsoft provider 2019-02-18 15:21:06 +13:00
Anthony Lapenna 2ef8c0b33e fix(app): rewrite URLHelper to avoid an issue with minification 2019-02-18 15:08:54 +13:00
Anthony Lapenna 7643f8d08c feat(oauth): dev build supporting Oauth extension 2019-02-18 14:46:34 +13:00
Anthony Lapenna 086bad2956 Merge branch 'develop' into oath-poc 2019-02-18 09:58:51 +13:00
Anthony Lapenna d5dfc889bb docs(README): remove gitter badges 2019-02-18 09:51:20 +13:00
Montana Flynn ef926dce33 docs(README): update logo src (#2719)
The current logo src is 404: https://portainer.io/images/logo_alt.png

The repo already includes the logo: https://github.com/portainer/portainer/blob/develop/assets/images/logo_alt.png?raw=true
2019-02-18 09:49:34 +13:00
Anthony Lapenna d768e72a21 feat(oauth): add support for default team 2019-02-17 19:01:42 +13:00
Anthony Lapenna 78e2aaf7d4 feat(oauth): update OAuth UX 2019-02-17 17:01:36 +13:00
Anthony Lapenna 17cf374c30 Merge branch 'develop' into oath-poc 2019-02-17 16:39:23 +13:00
Nathan Baum 165096bef0 refactor(api): fix a typo (#2712)
Just a trivial spelling error.
2019-02-15 09:12:53 +13:00
Anthony Lapenna de76ba4e67 feat(oauth): update OAuth UX 2019-02-14 15:58:45 +13:00
linquize b1e048e218 feat(build-system): prefix some dependencies with "semver:" (#2690)
This makes both npm and yarn to work
2019-02-14 12:13:48 +13:00
linquize 8f32d58fae fix(templates): redirect to home if endpoint not yet selected #2709 (#2710) 2019-02-14 12:08:46 +13:00
Anthony Lapenna 16226b1202 Merge branch 'oath-poc' of github.com:portainer/pportainer into oath-poc 2019-02-13 10:01:06 +13:00
baron_l 8f568c8699 style(oauth): oauth loading + oauth config rework 2019-02-08 16:07:16 +01:00
Anthony Lapenna af34b99cd4 Merge branch 'develop' into oath-poc 2019-02-08 13:32:53 +13:00
baron_l 2755527d28 feat(oauth): default team for user on oauth settings 2019-02-07 19:32:02 +01:00
baron_l 4d8133f696 feat(oauth): spinner on code evaluation after sucessfull oauth 2019-02-07 15:07:10 +01:00
Anthony Lapenna fdc11dbe3a feat(build-system): update build system (#2682) 2019-02-07 12:00:47 +13:00
Anthony Lapenna 508352f4ea Merge branch 'develop' into oath-poc 2019-02-04 09:19:12 +13:00
Daniel Cardoza 9b6b6e09ae fix(endpoints): correct agent stack download url (#2667)
* 2584 fix(endpoints): correct agent stack download url

The directions for installing the agent stack from the endpoints
view used an old url. Update to the new url.

* Drop the portainer- prefix for the download path and filename

Co-Authored-By: dang3r <danielpcardoza@gmail.com>
2019-02-04 09:06:07 +13:00
Anthony Lapenna 899cd5f279 fix(home): fix an issue when trying to connect to an Azure ACI endpoint (#2671) 2019-02-04 09:04:52 +13:00
Anthony Lapenna 2eec8b75d0 Merge tag '1.20.1' into develop
Release 1.20.1
2019-01-31 13:15:28 +13:00
Anthony Lapenna 048c74a0dc Merge branch 'release/1.20.1' 2019-01-31 13:15:23 +13:00
Anthony Lapenna 6b1c476b63 chore(version): bump version number 2019-01-31 13:15:18 +13:00
Anthony Lapenna c5b5f80bea docs(README): update build badge 2019-01-31 12:02:12 +13:00
Anthony Lapenna cea2c60b55 refactor(build-system): fix lint issues 2019-01-31 11:38:27 +13:00
Steven Kang 576f369152 feat(build-system): introduce Azure DevOps support (#2666) 2019-01-31 11:37:16 +13:00
Anthony Lapenna fca4f619b5 fix(api): re-use previous password when ldap settings update use empty password (#2659) 2019-01-30 14:53:14 +13:00
Chaim Lev Ari 90281fd7f0 feat(oauth): add providers to providers-selector 2019-01-25 10:57:40 +02:00
Chaim Lev Ari c1939f6070 feature(oauth): add provider selector 2019-01-25 10:46:17 +02:00
Chaim Lev Ari 50c604ee4c fix(auth): use the right function to oauth validate 2019-01-25 10:44:31 +02:00
Chaim Lev Ari 41ded64037 Revert "refactor(auth): extract oauth login mechanism to service"
This reverts commit 0a439b3893.
2019-01-25 10:37:23 +02:00
baron_l 801336336f fix(registry-manager): add repositories pagination support (#2641)
* fix(registry-management): add support for repositories list with multiple requests

* refactor(registry-management): change regex usage to a reusable interceptor function

* refactor(registry-management): change interceptor to transformResponse function
2019-01-24 13:38:36 +13:00
Anthony Lapenna 90a0998502 feat(templates): add sonatype nexus 3 template 2019-01-23 16:05:07 +13:00
Anthony Lapenna 1a4dff536d fix(container-creation): fix an issue with command parsing (#2642)
* fix(container-creation): fix an issue with command parsing

* refactor(container-creation): remove indentation update
2019-01-23 12:25:42 +13:00
Chaim Lev-Ari f772cd31cb feat(auth): preserve url when redirected to login (#2591)
* feat(auth): preserve url when redirected to login

* feat(auth): add redirect also to unauthenticated flow

* style(app): remove style changes from files

* fix(app): remove reference to otpLogin

* style(auth): remove semicolon
2019-01-23 12:22:56 +13:00
Chaim Lev-Ari 8160fe4717 feat(app): redirect to home if no endpoint is set (#2601)
* refactor(stacks): set newstack state as a child state of stacks

* fix(docker): add check on docker states for endpoint

* refactor(app): remove redirect notification
2019-01-23 12:21:48 +13:00
Anthony Lapenna 86c60807cd feat(endpoint-creation): fix invalid link (#2644) 2019-01-23 12:18:18 +13:00
baron_l c1f2d90997 fix(container-creation): fix missing capabilities on duplicate (#2635) 2019-01-23 09:28:44 +13:00
Chaim Lev Ari 3699b794eb feat(oauth): add providers selectors 2019-01-18 12:14:12 +02:00
Chaim Lev Ari 69252a8377 refactour(auth): move information body to each setting 2019-01-18 12:08:18 +02:00
Chaim Lev Ari 193e7eb3f8 refactor(oauth): remove separation of strings 2019-01-18 11:53:44 +02:00
Chaim Lev Ari de5f6086d0 refactor(oauth): return parse content error 2019-01-18 11:51:41 +02:00
Chaim Lev Ari 46e8f10aea refactor(ouath): use oauth2 library to get token 2019-01-18 10:56:16 +02:00
Chaim Lev Ari 60040e90d0 refactor(oauth): move build url logic to service 2019-01-18 10:24:42 +02:00
Chaim Lev Ari c5c06b307a refactor(oauth): rename authenticate function 2019-01-18 10:15:02 +02:00
Chaim Lev Ari c28274667d refactor(oauth): use oauth2 to generate login url 2019-01-18 10:13:33 +02:00
Anthony Lapenna 54163e3b92 fix(extensions): fix an issue with extensions with expired licenses (#2628)
* fix(extensions): fix an issue with extensions with expired licenses

* fix(api): fix invalid log call

* fix(api): allow to re-enable an extension
2019-01-18 10:00:18 +13:00
Chaim Lev-Ari 62eb47b3cb fix(container-creation): revert container state if creation failed (#2565)
* fix(container): rename old container only if exist

* fix(container): remove new container only if created

* style(container): fix typo

Co-Authored-By: chiptus <chiptus@users.noreply.github.com>
2019-01-18 08:59:43 +13:00
Anthony Lapenna 808eb7d341 dep(bootstrap): update bootstrap version to 3.4.0 (#2632) 2019-01-18 08:51:12 +13:00
hiyao a33eca4bbb fix(registry-manager): fix an issue when removing all tags of a repository (#2545)
* fix repository reload got error in remove tags

When I remove all tags, removeTags() will reload and do initView() again, but data.tags response null, that trigger data.tags.length got error.

* Revert "fix repository reload got error in remove tags"

This reverts commit 5d9b1778ef91aefd7969909d60b68ca55cbcc705.

* fix(registry-management): change response repository tags type to array by force

* feat(registry-management): redirect to repositories page when no tag in the repository after delete tags
2019-01-18 08:01:47 +13:00
baron_l 50e77d2bf1 fix(network-details): displaying all subnets and gateways on network details (#2629) 2019-01-17 11:39:15 +13:00
DevHugo 50a3b08209 feat(app): add driver name in the volume selector for container/service creation (#2534)
* Feat(containers): add driver name in the volume selector

* Feat(services): add driver name in the volume selector
2019-01-17 11:28:40 +13:00
Chaim Lev Ari 0a439b3893 refactor(auth): extract oauth login mechanism to service 2019-01-16 18:57:15 +02:00
Chaim Lev Ari 0d4e1d00f0 refactor(login): move oauth button to right 2019-01-16 18:00:01 +02:00
Chaim Lev Ari b09f491f62 style(auth): remove comments and change error 2019-01-16 17:53:10 +02:00
Chaim Lev Ari dc067b3308 refactor(http): remove old oauth handler 2019-01-16 17:41:56 +02:00
Chaim Lev Ari b121f975fa refactor(settings): remove duplicate settings 2019-01-16 17:38:07 +02:00
Chaim Lev Ari 3f44925d7e fix(auth): fix typo - missing function 2019-01-16 17:37:50 +02:00
Chaim Lev Ari 80d570861d refactor(auth): move public settings into view model 2019-01-16 17:34:12 +02:00
Chaim Lev Ari 317bd53e43 Merge branch 'oath-poc' of github.com:portainer/pportainer into oath-poc 2019-01-16 17:26:29 +02:00
Chaim Lev Ari 24f066716b refactor(auth): expose only the login url 2019-01-16 17:25:16 +02:00
Chaim Lev Ari 4cbde7bb0d refactor(auth): move oauth handler under auth 2019-01-16 17:24:58 +02:00
Chaim Lev Ari f6bdc5c2b3 refactor(auth): move oauth handler code to its own file 2019-01-16 17:01:38 +02:00
Anthony Lapenna c650fe56c2 fix(auth): fix typos
Co-Authored-By: chiptus <chiptus@users.noreply.github.com>
2019-01-16 16:53:24 +02:00
Anthony Lapenna fc8938e871 fix(auth): change oauth error type
Co-Authored-By: chiptus <chiptus@users.noreply.github.com>
2019-01-16 16:50:19 +02:00
Anthony Lapenna 44b7e0fdca fix(auth): change error type
Co-Authored-By: chiptus <chiptus@users.noreply.github.com>
2019-01-16 16:49:33 +02:00
Chaim Lev-Ari fe63b4a156 fix(container-creation): populate logger config from existing container (#2602)
* refactor(container): change map function to lodash

* style(container): add semicolon
2019-01-16 13:34:28 +13:00
Chaim Lev-Ari 42365a52b1 feat(container-details): change network identifier to name (#2623) 2019-01-16 08:05:55 +13:00
Mark Stansberry d6aafceba8 docs(api): update swagger definitions 2019-01-16 08:04:47 +13:00
baron_l c7983d8993 fix(app): remove endpoint status update on 502/503 http return
* refactor(app): removing unused dep and function
2019-01-16 07:58:35 +13:00
Anthony Lapenna 34667bd3b3 fix(network-creation): force overlay network creation on manager node (#2622)
* fix(network-creation): force overlay network creation on manager node

* fix(app): fix function override

* fix(app): use portainerAgentManagerOperation in interceptor
2019-01-15 12:10:29 +13:00
Anthony Lapenna 3a3577754e fix(home): only display group name if available (#2621) 2019-01-15 08:52:26 +13:00
Anthony Lapenna bed49c37e4 fix(teams): remove name sanitization when creating a team (#2619) 2019-01-14 17:27:55 +13:00
Anthony Lapenna dedc02cc8d docs(api): fix invalid example value for AutoCreateUsers property (#2618) 2019-01-14 16:50:53 +13:00
Chaim Lev Ari 17ac3e5ed1 refactor(oauth): move enpoint constant to extension 2019-01-03 13:36:17 +02:00
Chaim Lev Ari 25620c5008 refactor(auth): refactor get url params 2019-01-02 20:49:25 +02:00
Chaim Lev Ari 9bebe9dee7 refactor(auth): move user setter into function 2019-01-02 20:01:23 +02:00
Chaim Lev Ari 81e3ace232 fix(auth): fix oauh enabled function 2019-01-02 20:01:06 +02:00
Chaim Lev Ari 15b6941872 refactor(oauth): move oauth rest service to extension 2019-01-02 20:00:41 +02:00
Chaim Lev Ari 7aaa9e58e9 refactor(auth): move oauth info to component 2019-01-02 16:24:10 +02:00
Chaim Lev Ari 515daf6dba refactor(auth): exprt oauth settings into extension 2019-01-02 16:21:36 +02:00
Chaim Lev Ari 0a1643bbcf style(auth): remove added spaces 2019-01-02 16:01:10 +02:00
Chaim Lev Ari 38f24683a6 refactor(auth): remove empty $q.deffered 2019-01-02 15:59:38 +02:00
Chaim Lev Ari 7494101a4d refactor(auth): refactor auth controller 2019-01-02 15:56:08 +02:00
Chaim Lev Ari 996319d299 feat(auth): don't clear client secret on update 2018-12-30 18:39:16 +02:00
Chaim Lev Ari 2ee6f2780b refactor(oauth): add debug logs 2018-12-30 18:25:30 +02:00
Chaim Lev Ari 241a701eca feat(oauth): merge pr from https://github.com/portainer/portainer/pull/2515 2018-12-30 18:02:22 +02:00
Anthony Lapenna 463b379876 docs(README): remove broken badges and links 2018-12-27 09:03:13 +01:00
Chaim Lev-Ari f2cd33e831 feat(container-creation): call stopAndRename after pullImage (#2564)
* refactor(container): remove bind of function
2018-12-21 00:37:35 +09:00
Anthony Lapenna 6b05a35881 fix(api): set a default value for potentially empty snapshot interval (#2543) 2018-12-12 21:16:44 +13:00
Anthony Lapenna 6648c0bbe7 Merge tag '1.20.0' into develop
Release 1.20.0
2018-12-12 17:03:36 +13:00
Anthony Lapenna dbda568481 Merge branch 'release/1.20.0' 2018-12-12 17:03:31 +13:00
Anthony Lapenna 189d131105 chore(version): bump version number 2018-12-12 17:03:25 +13:00
Anthony Lapenna 1384359baf fix(api): fix snapshot hanging 2018-12-12 17:00:15 +13:00
Anthony Lapenna 6c26cf1f39 style(support): update support pricing 2018-12-12 16:03:20 +13:00
Anthony Lapenna 8780b0a901 feat(api): update extension path on Windows arch 2018-12-12 14:19:58 +13:00
Anthony Lapenna f5ada3085e fix(api): fix an issue with schedule update 2018-12-12 14:11:40 +13:00
Anthony Lapenna acc5218c16 fix(api): fix snapshot schedule loading 2018-12-12 12:31:55 +13:00
Anthony Lapenna 8a186b4024 feat(api): update DigitalSignatureService (#2539) 2018-12-12 11:19:23 +13:00
Anthony Lapenna 5c2e714e69 style(extensions): minor update to extension UX/UI (#2538)
* style(extensions): update extension icons

* style(extensions): style update

* feat(extensions): update extension UX

* style(extensions): update extension style

* style(extension-details): update screenshot default size

* style(extensions): update overview diagram image

* refactor(support): fix support URLs
2018-12-12 10:28:21 +13:00
Anthony Lapenna f222b3cb1a feat(templates): update logo URLs 2018-12-12 09:47:28 +13:00
Anthony Lapenna e440ba53cb feat(api): migrate template data logo URLs (#2537) 2018-12-12 09:46:05 +13:00
Anthony Lapenna 17d85fdc15 fix(registry-creation): fix registry creation request being fired twice on firefox 2018-12-10 21:56:07 +13:00
Anthony Lapenna 42a357f863 fix(support-details): fix a js error 2018-12-09 17:03:26 +13:00
Anthony Lapenna 6fd5ddc802 feat(extensions): introduce extension support (#2527)
* wip

* wip: missing repository & tags removal

* feat(registry): private registry management

* style(plugin-details): update view

* wip

* wip

* wip

* feat(plugins): add license info

* feat(plugins): browse feature preview

* feat(registry-configure): add the ability to configure registry management

* style(app): update text in app

* feat(plugins): add plugin version number

* feat(plugins): wip plugin upgrade process

* feat(plugins): wip plugin upgrade

* feat(plugins): add the ability to update a plugin

* feat(plugins): init plugins at startup time

* feat(plugins): add the ability to remove a plugin

* feat(plugins): update to latest plugin definitions

* feat(plugins): introduce plugin-tooltip component

* refactor(app): relocate plugin files to app/plugins

* feat(plugins): introduce PluginDefinitionsURL constant

* feat(plugins): update the flags used by the plugins

* feat(plugins): wip

* feat(plugins): display a label when a plugin has expired

* wip

* feat(registry-creation): update registry creation logic

* refactor(registry-creation): change name/ids for inputs

* feat(api): pass registry type to management configuration

* feat(api): unstrip /v2 in regsitry proxy

* docs(api): add TODO

* feat(store): mockup-1

* feat(store): mockup 2

* feat(store): mockup 2

* feat(store): update mockup-2

* feat(app): add unauthenticated event check

* update gruntfile

* style(support): update support views

* style(support): update product views

* refactor(extensions): refactor plugins to extensions

* feat(extensions): add a deal property

* feat(extensions): introduce ExtensionManager

* style(extensions): update extension details style

* feat(extensions): display license/company when enabling extension

* feat(extensions): update extensions views

* feat(extensions): use ProductId defined in extension schema

* style(app): remove padding left for form section title elements

* style(support): use per host model

* refactor(extensions): multiple refactors related to extensions mecanism

* feat(extensions): update tls file path for registry extension

* feat(extensions): update registry management configuration

* feat(extensions): send license in header to extension proxy

* fix(proxy): fix invalid default loopback address

* feat(extensions): add header X-RegistryManagement-ForceNew for specific operations

* feat(extensions): add the ability to display screenshots

* feat(extensions): center screenshots

* style(extensions): tune style

* feat(extensions-details): open full screen image on click (#2517)

* feat(extension-details): show magnifying glass on images

* feat(extensions): support extension logo

* feat(extensions): update support logos

* refactor(lint): fix lint issues
2018-12-09 16:49:27 +13:00
Anthony Lapenna f5dc663879 fix(build-system): revert appveyor integration
* Revert "fix(build-system): fix local build system after appveyor introduction (#2528)"

This reverts commit 79c24ced96.

* Revert "feat(build-system): add support for AppVeyor CI (#2449)"

This reverts commit 65979709e9.
2018-12-09 16:32:12 +13:00
Anthony Lapenna 79c24ced96 fix(build-system): fix local build system after appveyor introduction (#2528) 2018-12-09 16:08:36 +13:00
Steven Kang 65979709e9 feat(build-system): add support for AppVeyor CI (#2449) 2018-12-07 16:19:58 +13:00
Olli Janatuinen 2541f4daea feat(UX): persist search criterias (#2425)
* feat(ui): persist search criteria

* fix(ui): trying make templates search working correctly

* fix(ui): corrected search persistance on home and templates

* fix(ui): corrected javascript errors
2018-12-07 08:54:34 +13:00
baron_l 1a94158f77 * feat(UX): schedule creation UX overhaul (#2485)
* feat(api): add a new Recurring property on Schedule

* feat(schedules): date to cron convert + recurring flag

* feat(schedules): update angularjs-datetime-picker from v1 to v2

* chore(app): use minified dependency for angularjs-datetime-picker

* chore(vendor): rollback version of angularjs-datetime-picker

* * feat(ux): replace datepicker for schedule creation/details

* feat(container-stats): add refresh rate of 1 and 3 seconds (#2493)

* fix(templates): set var to default value if no value selected (#2323)

* fix(templates): set preset to true iff var type is preset

* fix(templates): add env var value when changing type

* feat(security): shutdown instance after 5minutes if no admin account created (#2500)

* feat(security): skip admin check if --no-auth

* fix(security): change error message

* fix(vendor): use datepicker minified version

* feat(schedule-creation): replace angular-datetime-picker

* feat(schedule): parse cron to datetime

* fix(schedule): fix zero based months
2018-12-07 08:53:23 +13:00
Anthony Lapenna 9e1800e2ec style(settings): update host management tooltip 2018-12-06 14:01:49 +13:00
baron_l a9b107dbb5 feat(app): add the capability to enable/disable host management features (#2472)
* feat(settings): add the capability to enable/disable the host management features

* feat(settings): remove the validation of EnableHostManagementFeatures in frontend

* feat(api): disable schedules API when HostManagementFeatures is false + DB migration

* style(settings): update host management settings tooltip

* refacot(schedules): update DBVersion to 15
2018-12-06 11:36:25 +13:00
Chaim Lev-Ari 101bb41587 feat(security): shutdown instance after 5minutes if no admin account created (#2500)
* feat(security): skip admin check if --no-auth

* fix(security): change error message
2018-12-04 16:50:41 +13:00
Chaim Lev-Ari acce5e0023 fix(templates): set var to default value if no value selected (#2323)
* fix(templates): set preset to true iff var type is preset

* 

* fix(templates): add env var value when changing type
2018-12-04 09:52:59 +13:00
linquize 5fa4403d20 feat(container-stats): add refresh rate of 1 and 3 seconds (#2493) 2018-12-03 21:49:02 +13:00
Anthony Lapenna dc9a878f4b chore(docker): update docker binary version to 18.09.0 (#2510) 2018-12-03 12:10:55 +13:00
baron_l 969f70edeb fix(image-upload): uploading a tar with multiple images wont display an error anymore (#2503) 2018-11-29 07:00:58 +13:00
baron_l c778e79004 fix(container-console): close the console when selected shell does not exist inside the container (#2502) 2018-11-29 06:57:36 +13:00
Chaim Lev-Ari 34b886d690 chore(build-system): add start and start:server scripts (#2495) 2018-11-27 10:05:13 +13:00
Andreas Roussos b809177147 feat(dashboard): use plural form only when required
* fix(endpoint-item): use plural form only when required

* refactor(endpoint-item): use clearer patterns

* refactor(dashboard): use clearer patterns
2018-11-25 09:46:13 +13:00
baron_l 52788029ed feat(container-details): add visual feedback when creating image from container (#2487) 2018-11-24 11:11:58 +13:00
Anthony Lapenna d510bbbcfd feat(api): filter LDAP password from settings response (#2488) 2018-11-24 08:40:56 +13:00
Olli Janatuinen 17d63ae3ca chore(dependencies): updated xterm to 3.8.0 version (#2452) 2018-11-23 22:00:30 +13:00
baron_l 5e49f934b9 fix(containers-stats): accessing a down container stats wont display a js error anymore (#2484) 2018-11-23 21:44:34 +13:00
Anthony Lapenna d03fd5805a feat(api): support AGENT_SECRET environment variable (#2486) 2018-11-23 11:46:51 +13:00
baron_l fe8dfee69a feat(home): display each endpoint URL (#2471) 2018-11-19 19:07:38 +13:00
baron_l 488dc5f9db fix(network-creation): macvlan availability for standalone endpoints (#2441) 2018-11-16 13:26:56 +13:00
Anthony Lapenna 0ef25a4cbd fix(schedules): add schedule name validation and remove endpoint name prefix (#2470) 2018-11-14 16:10:49 +13:00
Anthony Lapenna 94d3d7bde2 feat(motd): relocate motd file URL and always return 200 (#2466) 2018-11-14 12:20:33 +13:00
Christer Warén 40e0c3879c style(dashboard): change blocklist-item border color (#2465)
Changing blocklist-item border color to more confortable color that makes UI look more consistence
2018-11-14 10:01:36 +13:00
baron_l d455ab3fc7 feat(endpoints): enhance offline browsing (#2454)
* feat(api): rewrite error response when trying to query a down endpoint

* feat(interceptors): adding custom backend return code on offline fastfail
2018-11-13 16:08:12 +13:00
Anthony Lapenna 0825d05546 feat(endpoints): improve offline banner UX (#2462)
* feat(endpoints): add the last snapshot timestamp in offline banner

* feat(endpoints): add the ability to refresh a snapshot in the offline banner
2018-11-13 16:02:49 +13:00
Anthony Lapenna cf370f6a4c refactor(endpoints): remove time.Sleep call 2018-11-13 15:19:29 +13:00
Anthony Lapenna 381ab81fdd fix(endpoints): ensure endpoint is up to date after snapshot (#2460)
* feat(snapshots): fix a potential concurrency issue with endpoint snapshots

* fix(endpoints): ensure endpoint is up to date after snapshot
2018-11-13 15:18:38 +13:00
Anthony Lapenna 64c29f7402 feat(schedules): add the ability to list tasks from snapshots (#2458)
* feat(schedules): add the ability to list tasks from snapshots

* feat(schedules): update schedules

* refactor(schedules): fix linting issue
2018-11-13 14:39:26 +13:00
Anthony Lapenna a2d9f591a7 feat(schedules): add retry policy to script schedules (#2445) 2018-11-09 15:22:08 +13:00
Anthony Lapenna e7ab057c81 feat(sidebar): add a new Scheduler top entry 2018-11-08 14:09:21 +13:00
Yassir Hannoun 309620545c fix(container-stat): fix cpu/mem charts on Windows containers
* Fixing the CPU and Memory charts on Windows containers

* Fixing the CPU and Memory charts on Windows containers
2018-11-08 13:31:33 +13:00
Dmitriy Larionov 55b50c2a49 feat(container-creation): allow escaped quotes in command field (#2419) 2018-11-08 09:53:19 +13:00
Anthony Lapenna 807c830db0 feat(schedules): add the ability to update a schedule script (#2438) 2018-11-07 17:19:10 +13:00
Anthony Lapenna 695c28d4f8 fix(host): fix a typo in job history clear notification 2018-11-07 16:06:27 +13:00
Anthony Lapenna 4740375ba5 feat(schedules): add schedules UI (#2414)
* feat(schedules): add schedules UI mockups

* feat(schedules): update controller pattern

* feat(schedules): leverages API

* feat(schedules): add the ability create/edit a script execution job schedule

* feat(schedules): add form validation and details about cron expression
2018-11-07 11:59:21 +13:00
Anthony Lapenna 7d32a6619d feat(api): add created property for schedules (#2435) 2018-11-07 09:22:30 +13:00
Anthony Lapenna 110fcc46a6 feat(api): revamp scheduling to introduce system schedules (#2433)
* feat(api): revamp scheduling to introduce system schedules

* fix(api): fix linting issues

* fix(api): fix lint issues

* refactor(api): fix lint issues
2018-11-06 22:49:48 +13:00
Chaim Lev-Ari dbbea0a20f feat(schedules): add the schedule API
* feat(jobs): add job service interface

* feat(jobs): create job execution api

* style(jobs): remove comment

* feat(jobs): add bindings

* feat(jobs): validate payload different cases

* refactor(jobs): rename endpointJob method

* refactor(jobs): return original error

* feat(jobs): pull image before creating container

* feat(jobs): run jobs with sh

* style(jobs): remove comment

* refactor(jobs): change error names

* feat(jobs): sync pull image

* fix(jobs): close image reader after error check

* style(jobs): remove comment and add docs

* refactor(jobs): inline script command

* fix(jobs): handle pul image error

* refactor(jobs): handle image pull output

* fix(docker): set http client timeout to 100s

* feat(api): create schedule type

* feat(agent): add basic schedule api

* feat(schedules): add schedule service in bolt

* feat(schedule): add schedule service to handler

* feat(schedule): add and list schedules from db

* feat(agent): get schedule from db

* feat(schedule): update schedule in db

* feat(agent): delete schedule

* fix(bolt): remove sync method from scheduleService

* feat(schedules): save/delete script in fs

* feat(schedules): schedules cron service implementation

* feat(schedule): integrate handler with cron

* feat(schedules): schedules API overhaul

* refactor(project): remove .idea folder

* fix(schedules): fix script task execute call

* refactor(schedules): refactor/fix golint issues

* refactor(schedules): update SnapshotTask documentation

* refactor(schedules): validate image name in ScheduleCreate operation
2018-11-06 09:58:15 +13:00
Anthony Lapenna e94d6ad6b2 docs(swagger): update EndpointCreate operation 2018-11-01 07:32:41 +13:00
Jan Jansen 78bf374548 feat(ux): normalize quick actions buttons (#2389)
* feat(ux): normalize quick actions buttons

Fixes #2013

* fix(ux): fix wrong naming of variable
2018-10-31 15:50:38 +13:00
pc 8df64031e8 feat(log-viewer): change line count default to 100 and add a since parameter (#2377)
* chore(log-viewer): add the ability to use`since` parameter #1942

https://github.com/portainer/portainer/issues/1942#issuecomment-430246378

* chore(log-viewer): change lineCount to 100 #1942

https://github.com/portainer/portainer/issues/1942#issuecomment-430246378

* fix(log-viewer): js syntax typo for `;` and `'`

forget to lint the code, reported by codeclimate

* fix(log-viewer): use mementjs to format timestamp

1. use moment lib instead of define a function in filter.js(not the right place for this function, removed)
2. set sinceTimestamp init value to `24 hours ago`, as we just need to focus on the relative latest logs after the log-viewer loading, not all the logs(to speedup the process)
3. use moment().unix() to convert the `sinceTimestamp`  to local unix timestamp(not utc)

* chore(log-viewer): add the ability to select the datetime for `since`

* chore(log-viewer): add the ability to fetch logs from specific time
2018-10-29 17:49:35 +13:00
baron_l a61654a35d feat(endpoints): add the ability to browse offline endpoints (#2253)
* feat(back): saved data in snapshot

* feat(endpoints): adding interceptors to retrieve saved data on offline endpoints

* feat(endpoints): offline dashboard working - need tests on offline views

* refactor(endpoints): interceptors cleaning and saving/loading offline endpoints data in/from localstorage

* feat(endpoints): browsing offline endpoints

* feat(endpoints): removing all the link in offline mode - sidebar not working when switching between off and on modes w/ stateManager logic

* feat(endpoints): endpoint status detection in real time

* fix(endpoints): offline swarm endpoint are not accessible anymore

* fix(endpoints): refactor message + disable offline browsing for an endpoint when no snapshot is available for it

* fix(endpoints): adding timeout and enabling loading bar for offline requests

* fix(endpoints): trying to access a down endpoint wont remove sidebar items if it fails

* feat(endpoints): disable checkboxes on offline views for offline mode

* feat(endpoints): updating endpoint status when detecting a change

* refactor(host): moved offline status panel from engine view to new host view

* fix(endpoints): missing endpoint update on ping from home view

* fix(api): rework EndpointUpdate operation

* refactor(offline): moved endpoint status to EndpointProvider and refactor the status-changed detection

* fix(offline): moved status detection to callback on views -> prevent displaying the offline message when endpoint is back online on view change

* fix(offline): offline message is now displayed online when browsing an offline endpoint

* fix(offline): sidebar updates correctly on endpoint status change

* fix(offline): offline panel not displayed and hidden on online mode

* refactor(offline): rework of OfflineMode management

* refactor(offline): extract information-panel for offlineMode into a component

* refactor(offline): remove redundant binding of informationPanel + endpointStatusInterceptor patter as service

* refactor(interceptors): moved interceptors pattern to service pattern

* feat(stacks): prevent inspection of a stack in offline mode

* feat(host): hide devices/disk panels in offline mode

* feat(host): disable browse action in offline mode

* refactor(home): remove comments
2018-10-28 22:27:06 +13:00
baron_l 354fda31f1 feat(jobs): add the ability to run a job on a target endpoint #2374
* feat(jobs): adding the ability to run scripts on endpoints

fix(job): click on containerId in JobsDatatable redirects to container's logs
refactor(job): remove the jobs datatable settings + texts changes on JobCreation view
fix(jobs): jobs payloads are now following API rules and case
feat(jobs): adding the capability to run scripts on hosts

* feat(jobs): adding the ability to purge jobs containers

* refactor(job): apply review changes

* feat(job-creation): store image name in local storage

* feat(host): disable job exec link in non-agent Swarm setup

* feat(host): only display execute job in agent setups or standalone

* feat(job): job execution overhaul

* docs(swagger): update EndpointJob documentation
2018-10-28 19:06:50 +13:00
Mark Stansberry 6ab510e5cb docs(api): update swagger related files to support swagger-codegen (#2404)
* Linting updates to api/swagger.yaml

* Security updates to api/swagger.yml

* Add api/swagger_config.json for swagger-codegen

* Add swagger_config.json packageVersion to match swagger.yml
2018-10-28 16:05:54 +13:00
Damian Czaja 7e6c647e93 feat(container-creation): add the ability to override the logging driver (#2384) 2018-10-28 16:00:56 +13:00
Yassir Hannoun 07c1e1bc3e feat(container-stats): display cache in memory usage chart (#2383) 2018-10-28 15:45:02 +13:00
Ricardo Cardona Ramirez fe6ca042f3 feat(ux): Alphabetically sort configs and secrets in service details/creation (#2396)
* fix(sorting): Alphabetically sort configs in service details select box
* fix(sorting): Alphabetically sort configs and secrets  for service creation
2018-10-28 15:39:09 +13:00
Chaim Lev-Ari 9813099aa4 feat(app): toggle features based on agent API version (#2378)
* feat(agent): get agent's version from ping

* feat(agent): add version to api url

* feat(agent): query agent with api version

* feat(agent): rename agent api version name on state

* feat(agent): disable feature based on agent's api version

* style(agent): rename ping rest service + remove whitespaces

* style(state): remove whitespace

* style(agent): add whitespace

* fix(agent): remove check for error status 403

* refactor(agent): rename ping file name

* refactor(agent): move old services to v1 folder

* refactor(agent): turn ping service to usual pattern

* refactor(agent): change version to a global variable

* refactor(agent): move ping to version2

* refactor(agent): restore ping to use root ping

* fix(volumes): add volumeID to browse api path

* feat(volume): add upload button to volume browser
2018-10-26 16:16:29 +13:00
Yassir Hannoun cca378b2e8 docs(README): fix semaphore badge 2018-10-24 08:55:30 +13:00
Anthony Lapenna b5dfaff292 refactor(app): refactor unauthenticated state management (#2393)
* refactor(app): refactor Authentication service

* refactor(app): refactor unauthenticated state management
2018-10-23 17:28:59 +13:00
Anthony Lapenna 4f9a8180f9 docs(swagger): document the endpoint job execution (#2392) 2018-10-23 11:59:43 +13:00
Anthony Lapenna 14d2bf4ebb refactor(api): fix typo (#2391)
* refactor(api): fix typo

* refactor(api): remove newline
2018-10-23 10:07:39 +13:00
Chaim Lev-Ari 65291c68e9 feat(jobs): add the job execution API
* feat(jobs): add job service interface

* feat(jobs): create job execution api

* style(jobs): remove comment

* feat(jobs): add bindings

* feat(jobs): validate payload different cases

* refactor(jobs): rename endpointJob method

* refactor(jobs): return original error

* feat(jobs): pull image before creating container

* feat(jobs): run jobs with sh

* style(jobs): remove comment

* refactor(jobs): change error names

* feat(jobs): sync pull image

* fix(jobs): close image reader after error check

* style(jobs): remove comment and add docs

* refactor(jobs): inline script command

* fix(jobs): handle pul image error

* refactor(jobs): handle image pull output

* fix(docker): set http client timeout to 100s

* fix(client): remove timeout from http client
2018-10-23 10:03:30 +13:00
Yassir Hannoun 719299d75b fix(container-stat) : exclude cache from the Memory Usage chart to avoid misinterpret… (#2371) 2018-10-18 10:00:45 +13:00
Jan Jansen d6ba46ed7f feat(ux): Redirect from init/admin to home when admin already exists (#2340)
Fixes #1853
2018-10-13 19:29:44 +13:00
Chaim Lev-Ari c5aecfe6f3 feat(host): Add host file browser with upload/download files (#2337)
* feat(agent): add new host page

* feat(agent): convert volume-browser to files-datatable

* fix(agent): browse folders in file-datatable

* feat(engine-details): replace engine view with host view

* feat(engine-details): remove old panels

* feat(engine-details): add basic engine-details-panel component

* feat(engine-details): pass details to the different components

* feat(engine-details): replace host-view with host-overview

* feat(engine-details): add commaseperated filter

* feat(engine-details): add host-view container component

* feat(engine-details): add host-details component

* feat(engine-details): build host details object

* feat(engine-details): format engine version

* feat(engine-details): get details for one node

* feat(engine-details): pass is-agent from view

* feat(engine-details): replace old node view with a new component

* feat(engine-details): add swarm-node-details component

* feat(engine-details): remove isSwarm binding

* feat(engine-details): remove node-details and include in parent

* feat(engine-details): add labels-table component

* feat(engine-details): add update node service

* feat(engine-details): add update label functionality

* style(engine-details): remove whitespaces

* feat(engine-details): remove old node page

* feat(engine-details): pass is agent to host details

* feat(host-details): hide missing info

* feat(host-details): update node availability

* style(host-details): remove obsolete event object

* feat(host-details): fix labels not sending

* feat(host-details): remove flags for hiding data

* feat(host-details): create mock call to server for agent host info

* style(host-details): fix spelling mistake in filter's name

* feat(host-details): get info from agent

* feat(host-details): hide engine labels when empty

* feat(node-details): move labels table and save button

* feat(host-info): add different urls for refresh

* feat(host-details): show disk/devices info for agent

* feat(host-view): add loading indicator to devices-panel

* feat(host-details): add loading indicator to disks panel

* feat(agent): fix browse volume

* feat(agent): browse files

* feat(agent): enable rename

* feat(agent): download file

* fix(agent): download file from root

* feat(agent): delete file

* style(agent): remove whitespaces

* fix(agent): fix link on node browser

* feat(agent): basic file uploader

* feat(agent): add basic file upload

* fix(volume-browser): move volume id to query params

* feat(node-browser): moved uploader into browser

* feat(node-browser): add upload spinner

* feat(agent): browse files relative to root

* feat(agent): browse standalone agent

* feat(agent): move browse button from header

* fix(agent): fix url of browser view

* fix(agent): fix breadcrumb on title of host-browser

* feat(agent): fix url on node-browser breadcrumb

* refactor(agent): remove unused controller

* refactor(docker): remove unused filter

* refactor(docker): remove unused controllers

* refactor(docker): remove isAgent binding
2018-10-12 11:32:17 +13:00
Anthony Lapenna 5341ad33af docs(swagger): update StackUpdateRequest model (#2360) 2018-10-11 13:09:51 +13:00
baron_l e948d606f4 fix(container-creation): set a default runtime value (#2325)
* fix(containers): creating a container with default runtime let the docker daemon assume the correct value

* refactor(containers): implementation simplification of default runtime value
2018-10-09 09:28:26 +13:00
Chaim Lev-Ari ca08b2fa2a feat(host): replace engine view with host view (#2255)
* feat(engine-details): remove old panels

* feat(engine-details): add basic engine-details-panel component

* feat(engine-details): pass details to the different components

* feat(engine-details): replace host-view with host-overview

* feat(engine-details): add commaseperated filter

* feat(engine-details): add host-view container component

* feat(engine-details): add host-details component

* feat(engine-details): build host details object

* feat(engine-details): format engine version

* feat(engine-details): get details for one node

* feat(engine-details): pass is-agent from view

* feat(engine-details): replace old node view with a new component

* feat(engine-details): add swarm-node-details component

* feat(engine-details): remove isSwarm binding

* feat(engine-details): remove node-details and include in parent

* feat(engine-details): add labels-table component

* feat(engine-details): add update node service

* feat(engine-details): add update label functionality

* style(engine-details): remove whitespaces

* feat(engine-details): remove old node page

* feat(engine-details): pass is agent to host details

* feat(host-details): hide missing info

* feat(host-details): update node availability

* style(host-details): remove obsolete event object

* feat(host-details): fix labels not sending

* feat(host-details): remove flags for hiding data

* feat(host-details): create mock call to server for agent host info

* style(host-details): fix spelling mistake in filter's name

* feat(host-details): get info from agent

* feat(host-details): hide engine labels when empty

* feat(node-details): move labels table and save button

* feat(host-info): add different urls for refresh

* feat(host-details): show disk/devices info for agent

* feat(host-view): add loading indicator to devices-panel

* feat(host-details): add loading indicator to disks panel

* feat(host-details): show devices/disks on standalone agent

* refactor(host-details): remove default value

* refactor(host-details): remove redundant commaSeperated filter

* refactor(host-details): remove unused functions

* style(host-details): remove whitespace
2018-10-08 11:44:08 +13:00
Chaim Lev-Ari 275fcf5587 fix(volume-browser): move volume id to query params (#2338) 2018-10-08 11:34:47 +13:00
Anthony Lapenna 3422662191 fix(app): fix invalid state name (#2330)
* fix(app): fix invalid state name

* fix(app): update ui-sref
2018-10-04 13:28:39 +13:00
Brian Kabiro f6d9a4c7c1 feat(nodes): display node name when available (#2328)
- check if the name of a node is available, otherwise default to the Hostname
2018-10-04 12:07:31 +13:00
Ricardo Cardona Ramirez 575735a6f7 feat(ux): sort networks alphabetically in network selection dropdowns (#2326)
* Sort network lists
2018-10-04 12:04:38 +13:00
Brian Kabiro b7c48fcbed feat(visualizer): sort tasks in alphabetical order on refresh (#2329)
- sort the tasks on each node in alphabetical order to make it easier to track what has changed
2018-10-04 11:57:07 +13:00
Tolik Litovsky 6e8a10d72f fix(api): remove x-frame-options header (#2322) 2018-10-03 14:18:03 +13:00
Chaim Lev-Ari bad95987ec feat(backend): trigger startup snapshot job in a goroutine (#2309)
* feat(backend): wrap init enpoint with goroutine

* feat(backend): wrap job snapshot with goroutine

* feat(snapshots): reset changes for main and job_endpoint

* feat(snapshot): run first job.snapshot as a goroutine
2018-10-01 14:38:14 +13:00
Chaim Lev-Ari 9b4870d57e feat(stack-details): Add the ability to duplicate a stack (#2278)
* feat(stack-details): add duplicate-stack button

* feat(stack-details): add stack-duplication-form component

* feat(stack-details): add duplicate stack method on controller

* feat(stack-details): add duplicate stack method

* feat(stack-details): remove old duplication in progress flag

* feat(stack-details): combine migration and duplication forms

* feat(stack-details): pass new stack name to server

* feat(stack-details): add option to rename migrated stack

* feat(stack-details): disable both migrate/duplicate buttons

* feat(stack-details): disable migration button on same endpoint

* feat(stack-details): change duplicate icon

* style(stack-details): remove whitespaces and fix pattern

* feat(stack-details): add name to migration payload in swagger.yml

* style(stack-details): add semicolon

* bug(stack-details): toggle endpoints before and after duplication
2018-10-01 14:36:49 +13:00
Chaim Lev-Ari 6e262e6e89 feat(home): support search in multiple fields (name, group, tag, status) (#2285)
* feat(home): search multiple fields (group/tag)

* feat(home): change search from "OR" to "AND"

* feat(home): search only for a tag or a group

* feat(home): search by keywords in name,group,tag

* feat(home): support case insensitive search

* style(home): remove unused $filter

* feat(home): search state

* style(home): update search input placeholder
2018-10-01 09:06:58 +13:00
Chaim Lev-Ari 5be2684442 feat(home): add the ability to edit an endpoint (#2305)
* feat(home): add edit button

* feat(home): style edit button

* feat(home): make endpoint editable on admin only
2018-09-30 11:20:10 +13:00
Chaim Lev-Ari 226c45f035 fix(template-creation): fix an issue related to the network setting (#2312)
* bug(template): pass network name on creation

* bug(templates): choose network object on update

* fix(templates): set network only when available
2018-09-28 15:06:47 +12:00
Angele 92b15523f0 feat(containers): add container name in error notification
* containersDatable: add containers name if error on executeActionOnContainerList

* Update containersDatatableActionsController.js

* Update containersDatatableActionsController.js
2018-09-28 10:49:30 +12:00
Anthony Lapenna f0f01c33bd feat(endpoint-creation): add requirement message for agent endpoint (#2303) 2018-09-26 18:59:50 +12:00
Lukas Joergensen 94b202fedc fix(authentication): escape LDAP filters (#2209) 2018-09-25 11:10:41 +12:00
Anthony Lapenna d5dd362d53 feat(api): update client.Get with a new timeout parameter and default… (#2297)
* feat(api): update client.Get with a new timeout parameter and default to 5s

* fix(api): fix invalid type
2018-09-24 12:09:12 +12:00
Anthony Lapenna c3d80a1b21 docs(project): update CONTRIBUTING.md 2018-09-19 11:40:06 +08:00
Anthony Lapenna b192b098ca feat(build-system): update shippedDockerVersion to 18.06.1-ce (#2281) 2018-09-17 09:26:37 +08:00
Anthony Lapenna 22450bbdeb chore(build): update build script and add grunt yarn script (#2276) 2018-09-16 10:34:46 +08:00
Anthony Lapenna 313c8be997 chore(version): bump version number 2018-09-15 19:26:03 +08:00
Anthony Lapenna 885c61fb7b Merge tag '1.19.2' into develop
Release 1.19.2
2018-09-15 16:40:43 +08:00
Anthony Lapenna 02362defde Merge branch 'release/1.19.2' 2018-09-15 16:40:38 +08:00
Anthony Lapenna 57bd82ba85 chore(version): bump version number 2018-09-15 16:40:26 +08:00
Anthony Lapenna e2258f98cc fix(services): only display logs action when container has ID in agent proxy mode 2018-09-15 10:33:33 +08:00
Anthony Lapenna bab02f2b91 fix(container-details): update container restart policy init 2018-09-15 10:19:51 +08:00
Anthony Lapenna 77913543b1 feat(container-details): update container-restart-policy component (#2273) 2018-09-15 09:53:35 +08:00
Anthony Lapenna b24891a6bc refactor(api): introduce libhttp usage (#2263) 2018-09-10 12:01:38 +02:00
Anthony Lapenna 42f5aec6a5 feat(container-console): increase hijacked tcp connection reader size (#2259) 2018-09-07 11:24:18 +02:00
Anthony Lapenna 7ba19ee1f9 fix(api): change user password update flow (#2247)
* fix(api): change password update flow

* feat(update-password): add current password confirmation
2018-09-05 08:49:43 +02:00
Anthony Lapenna 736f61dc2f fix(snapshots): close Docker client after snapshot (#2235) 2018-09-05 08:44:04 +02:00
Anthony Lapenna 0b8f7f6cea refactor(api): update error message for /users/admin/init 2018-09-03 20:18:04 +02:00
Kendrick 0efeeaf185 feat(webhooks): add support for service update webhooks (#2161)
* Initial pass at adding webhook controller and routes

* Moving some objects around

* Cleaning up comments

* Fixing syntax, switching to using the docker sdk over building an http client

* Adding delete and list functionality

* Updating the handler to use the correct permissions. Updating some comments

* Fixing some comments

* Code cleanup per pull request comments

* Cleanup per PR feedback. Syntax error fix

* Initial creation of webhook app code

* Moving ClientFactory creation out of handler code and instead using the one created by the main process. Removing webhookInspect method and updating the list function to use json filters

* Delete now works on the webhook ID vs service ID

* WIP - Service creates a webhook. Display will show an existing webhook URL.

* Adding the webhook field to the service view. There is now the ability to add or remove a webhook from a service

* Moving all api calls to be webhooks vs webhook

* Code cleanup. Moving all api calls to be webhooks vs webhook

* More conversion of webhook to webhooks?

* Moving UI elements around. Starting function for copying to clipboard

* Finalizing function for copying to clipboard. Adding button that calls function and copies webhook to clipboard.

* Fixing UI issues. Hiding field entirely when there is no webhook

* Moving URL crafting to a helper method. The edit pane for service now creates/deletes webhooks immidiately.

* style(service-details): update webhook line

* feat(api): strip sha when updating an image via the update webhook

* Fixing up some copy. Only displying the port if it is not http or https

* Fixing tooltip copy. Setting the forceupdate to be true to require an update to occur

* Fixing code climate errors

* Adding WebhookType field and setting to ServiceWebhook for new webhooks. Renaming ServiceID to resourceID so future work can add new types of webhooks in other resource areas.

* Adding the webhook type to the payload to support more types of webhooks in the future. Setting the type correctly when creating one for a service

* feat(webhooks): changes related to webhook management

* API code cleanup, removing unneeded functions, and updating validation logic

* Incorrectly ignoring the error that the webhook did not exist

* Re-adding missing error handling. Changing error response to be a 404 vs 500 when token can't find an object

* fix(webhooks): close Docker client after service webhook execution
2018-09-03 12:08:03 +02:00
Anthony Lapenna d5facde9d4 fix(api): fix invalid error message in endpoint creation handler (#2233) 2018-09-02 10:35:05 +02:00
classmember e17c873e73 refactor(build-system): update build_in_container.sh (#2230)
wrapped `$(pwd)/api:/src` in `"` quotes to prevent word splitting on the `-tv` option
2018-09-01 10:09:24 +02:00
Anthony Lapenna 84fc3119a0 docs(swagger): update StackCreate operation parameter 2018-08-30 13:11:15 +02:00
Anthony Lapenna 887c16c580 feat(api): display details in error response (#2228) 2018-08-30 12:21:53 +02:00
Anthony Lapenna a5d6ab0410 refactor(app): remove unused params in templates state declaration 2018-08-28 10:50:15 +02:00
Anthony Lapenna 812f3e3e85 feat(auth): remove sanitization calls and ask for password update if needed (#2222)
* wip

* feat(auth): remove sanitization calls and ask for password update if needed
2018-08-28 10:13:01 +02:00
aksappy bfccf55729 fix(images): Fix upload modal to allow both tar and tar.gz images (#2218) 2018-08-27 21:43:58 +02:00
Anthony Lapenna 538a2b5ee2 fix(service-details): disable auto-focus on task datatable (#2214)
* fix(service-details): disable auto-focus on task datatable

* refactor(api): gofmt main.go
2018-08-24 14:30:41 +02:00
William Easton c941fac2cc fix(api): set templatesURL in settings when using the --templates flag
Re-add the CLI for external template management
2018-08-24 13:08:46 +02:00
Anthony Lapenna 4b05699e66 chore(codeclimate): update .codeclimate.yml (#2212)
* chore(codeclimate): update .codeclimate.yml

* chore(codeclimate): update .codeclimate.yml

* chore(codeclimate): update .codeclimate.yml
2018-08-24 10:40:05 +02:00
Anthony Lapenna 8cd3964d75 feat(security): update secured headers and sanitize team name (#2167) 2018-08-23 17:10:18 +02:00
Chaim Lev-Ari e58acd7dd6 * chore(eslint): update esllint and remove unused variables
* chore(eslint-config): change no-unused-vars to warn

* chore(eslint): remove unused variables

* chore(eslint): allow unused globals

* fixup! chore(eslint): allow unused globals

* chore(eslint): remove commented unused vars

* fixup! chore(eslint): remove commented unused vars
2018-08-22 17:33:06 +02:00
Anthony Lapenna 46da95ecfb feat(motd): ignore loading for motd 2018-08-22 13:18:02 +02:00
Luca 68d77e5e0e feat(networks): add details about the attachable/internal properties (#2200) 2018-08-22 08:45:14 +02:00
Luca e8ab89ae79 feat(config-details): add the ability to clone a config (#2189) 2018-08-22 08:41:02 +02:00
Anthony Lapenna 6ab6cfafb7 feat(motd): add the ability to display motd and dimiss information panels (#2191)
* feat(api): add motd handler

* feat(app): add the motd api layer

* feat(motd): display motd and add the ability to dismiss information messages

* style(home): relocate important message before info01

* feat(api): silently fail when an error occurs during motd retrieval
2018-08-21 20:40:42 +02:00
Chaim Lev-Ari 74ca908759 fix(stack-details): pass agentProxy as an argument (#2196) 2018-08-21 12:11:39 +02:00
Anthony Lapenna e60d809154 fix(container-creation): fix an issue with container-edition and UAC 2018-08-20 21:06:30 +02:00
Anthony Lapenna 64beaaa279 feat(container-details): update re-creation flow (#2193) 2018-08-20 20:55:12 +02:00
baron_l 1b51daf9c4 fix(services): fix invalid replica count (#1990) (#2127)
* fix(services): replicas numbers display is now correct with constraints and down nodes

* refactor(helpers): constraint helper has less complexity

* feat(services): constraints on node/engine labels are now supported

* refactor(helpers): ConstraintsHelper - remove regex patterns and improve code lisibility

* refactor(helpers): rework matchesConstraint() for better code lisibility and lodash find() instead for IE compatibility
2018-08-19 08:05:16 +02:00
Ricardo Cardona Ramirez e1e263d8c8 feat(UAC): change default ownership to admininstrators (#2137)
* #960 feat(UAC): change ownership to admins for externally created ressources

* feat(UAC): change ownership to admins for externally created resources

Deprecated AdministratorsOnly js and go backend

* #960 feat(UAC): remove AdministratorsOnly property and minor GUI  fixes

Update swagger definition changing AdministratorsOnly to Public

* #960 feat(UAC): fix create resource with access control data

* #960 feat(UAC): authorization of non-admin users for restricted operations

On stacks, containers networks, services , tasks and volumes.

* #960 feat(UAC): database migration to version 14

 The administrator resources are deleted and Public resources are now managed by admins

* #960 feat(UAC):  small fixes from PR #2137

* #960 feat(UAC): improve the readability of the source code

* feat(UAC) fix displayed ownership for Swarm related  resources  (#960)
2018-08-19 07:57:28 +02:00
Hasnat 31c2a6d9e7 feat(container-console): Adds custom commands based on container labels (#2159)
* feat(console): Adds custom commands based on container labels

* feat(console): Update custom commands label prefix
2018-08-18 10:31:01 +02:00
Chaim Lev-Ari 102e63e1e5 refactor(container-creation): change order of container re-creation/duplication steps
* refactor(container-creation): change order of container creation steps

* refactor(container-creation):  remove nested methods

* fix(container-creation): skip actions if old container missing

* fix(container-creation): reject if user is not authorized

* fix(container-creation): remove rejection on invalid form

* refactor(container-creation): start container after duplicate

* fix(container-creation): add form validation error message

* fix(container-creation): pass correct id to create resource control

* fix(container-creation): set action in progress after confirmation
2018-08-18 10:27:24 +02:00
Chaim Lev-Ari 7e08227ddb feat(build-system): add build-offline script (#2169) 2018-08-17 08:37:31 +02:00
baron_l bda5eac0c1 feat(network-creation): enhance UX with macvlan driver for swarm mode (#2082) (#2122)
* feat(network-creation): macvlan driver for swarm

* refactor(network-creation): layout rework to make it simpler with MACVLAN and keep it consistent with other drivers

* fix(network-creation): MACVLAN - parent network card is now properly saved, names are not prefixed anymore and the --attachable option is now supported

* refactor(network-creation): PR macvlan review - rework of macvlan view + code optimisation

* fix(network-creation): disable attachable and internal options on macvlan config creation
2018-08-16 12:29:15 +02:00
Chaim Lev-Ari 8769fadd5c feat(container-details): add the ability to update restart policy 2018-08-16 11:31:00 +02:00
Ru Fan de9f99d030 feat(container-creation): add runtime option in (#2162) (#2163) 2018-08-16 11:28:06 +02:00
Anthony Lapenna 55f719128b docs(README): update build badge 2018-08-15 21:02:01 +02:00
Chaim Lev-Ari 594daf0de8 fix(home): Show correct number of cpus and total memory for swarm (#2147)
* fix(home): show cpu/mem for swarm

* fix(home): add nodes data to snapshot

* fix(dashboard): get cpus/mem from snapshot

* refactor(home): remove temp variable
2018-08-13 21:20:56 +02:00
Chaim Lev-Ari f3dc67a852 fix(container-details): change order of container recreation 2018-08-13 21:13:42 +02:00
Anthony Lapenna 1233cb7f08 chore(project): update lodash version to 4.17.10 (#2156) 2018-08-13 19:10:09 +02:00
Chaim Lev-Ari d4e4d34ea4 chore(build-system): add dev, clean, build scripts (#2146) 2018-08-13 17:28:59 +02:00
Anthony Lapenna df1592a3d2 feat(templates): add datadog agent templates 2018-08-13 14:06:54 +02:00
salcedo cbe4cc92db feat(templates): update file browser image (#2152) 2018-08-13 09:11:54 +02:00
Anthony Lapenna 80c2adfc53 chore(ci): remove codefresh workflows (#2144) 2018-08-09 17:53:25 +02:00
baron_l 9c0b568773 feat(container-creation): container add/drop capabilities on creation (#468) (#2078)
* feat(container-creation): container add/drop capabilities on creation

* feat(container-creation): capabilities are now loaded on edit/duplicate/update
2018-08-09 10:40:06 +02:00
baron_l 5222413532 feat(volume-creation) : NFS volume creation (#2083) (#2108)
* feat(volume-creation): NFS support for volume creation - layout

* feat(volume-creation): NFS support for volume creation

* fix(volume-creation): NFS style, display and check on submit

* refactor(volume-creation): remove useless controller + refactor var naming

* refactor(volume-creation): NFS wording, help and style
2018-08-09 10:33:16 +02:00
Anthony Lapenna ee9c8d7d1a feat(templates): re-introduce external template management (#2119)
* feat(templates): re-introduce external template management

* refactor(api): review error handling
2018-08-07 17:43:36 +02:00
Chaim Lev-Ari 09cb8e7350 chore(gitignore): add .vscode to .gitignore (#2130) 2018-08-06 15:32:27 +02:00
Chaim Lev-Ari 8dfa129129 fix(dashboard): update stopped/running container filters 2018-08-06 15:09:23 +02:00
Kendrick 0ae10c6f82 feat(container-details): add the image name to the container details in addition to the sha (#1369) (#2121) 2018-08-02 21:00:58 +02:00
Olli Janatuinen 892276b105 feat(build-system): add Dockerfile for Windows server 2016 (#2117) 2018-08-02 16:52:36 +02:00
Anthony Lapenna aa36adc5fd chore(project): update CONTRIBUTING.md 2018-08-02 09:39:43 +02:00
Anthony Lapenna 2216bd6e80 style(home): only display CPU/MEM for standalone endpoints 2018-07-31 11:58:08 +02:00
Anthony Lapenna 5f79547138 fix(api): filter sensitive information from API response (#2103) 2018-07-31 11:50:04 +02:00
Anthony Lapenna b8ed6d3d4a chore(version): bump version number 2018-07-28 20:42:17 +02:00
Anthony Lapenna 252af86cea fix(build-system): fix an invalid condition in shell_downloadDockerBinary task 2018-07-28 20:35:01 +02:00
Anthony Lapenna 8c5b80cefd Merge tag '1.19.1' into develop
Release 1.19.1
2018-07-28 19:46:20 +02:00
Anthony Lapenna e94a725a8a Merge branch 'release/1.19.1' 2018-07-28 19:46:14 +02:00
Anthony Lapenna b15af67552 chore(version): bump version number 2018-07-28 19:44:01 +02:00
Anthony Lapenna 29cd952a0b feat(home): display refresh button if --no-auth enabled 2018-07-28 18:12:03 +02:00
Anthony Lapenna 6e072dbcdf fix(build-system): fix the downloadDockerBinary task 2018-07-28 16:45:44 +02:00
Anthony Lapenna 024739f9f1 fix(authentication): fix an issue with the --no-auth flag (#2090) 2018-07-28 16:38:26 +02:00
Anthony Lapenna 2e0d1f289c fix(build-system): fix invalid template copy step (#2089) 2018-07-28 16:12:24 +02:00
Anthony Lapenna 8cca3de70b Merge tag '1.19.0' into develop
Release 1.19.0
2018-07-27 16:06:07 +02:00
Anthony Lapenna dc9512f25c Merge branch 'release/1.19.0' 2018-07-27 16:06:03 +02:00
Anthony Lapenna 8964dad73b chore(version): bump version number 2018-07-27 16:05:57 +02:00
Anthony Lapenna 9ab2da1018 style(home): add a group prefix in front of endpoint group 2018-07-27 16:04:36 +02:00
baron_l 5bca9560c9 feat(images): add the ability to export/import Docker images (#935) (#2073) 2018-07-26 15:09:48 +02:00
Anthony Lapenna d2702d6d7b fix(api): fix invalid endpoint create payload 2018-07-26 10:13:18 +02:00
Anthony Lapenna ab77f149fa feat(home): add the ability to refresh endpoint information (#2080)
* feat(home): add the ability to refresh endpoint information

* style(home): update refresh confirmation message
2018-07-25 21:52:17 +02:00
Anthony Lapenna 52f71b0813 style(home): display information about endpoint CPU/RAM 2018-07-25 20:51:21 +02:00
Anthony Lapenna 134a38a566 style(dashboard): update dashboard information (#2079)
* style(dashboard): update dashboard information

* docs(swagger): update swagger.yml
2018-07-25 20:47:33 +02:00
Anthony Lapenna 3306cbaa27 feat(api): do not set down status if an error is raised during snapshot at startup 2018-07-24 21:39:56 +02:00
Jan Jansen 76e1aa97e2 feat(stack-creation): add the ability to specify git reference (#1948) (#2063) 2018-07-24 16:11:35 +02:00
Anthony Lapenna 1f24320fa7 fix(api): fix endpoint snapshot process at endpoint creation time (#2072)
* fix(api): fix endpoint snapshot process at endpoint creation time

* refactor(api): remove comments
2018-07-24 14:47:19 +02:00
Anthony Lapenna 1cf77bf9e9 fix(libcompose): fix an issue with TLS enabled endpoints (#2071) 2018-07-24 11:11:47 +02:00
hiyao 4de83f793f fix(container-stats): fix invalid component closing tag (#2069) 2018-07-24 09:25:46 +02:00
Anthony Lapenna 113da93145 feat(authentication): add a setting to toggle automatic user provisioning when u… (#2068)
* feat(api): add a setting to toggle automatic user provisioning when using LDAP authentication

* fix(auth): fix an issue with AutoCreateUsers disabled
2018-07-24 08:49:17 +02:00
Anthony Lapenna c7cb515035 fix(api): fix invalid build related filenames (#2067) 2018-07-23 18:50:45 +02:00
Anthony Lapenna 98b0ab50fc feat(api): rewrite SwarmInspect operation (#2065)
* feat(api): rewrite SwarmInspect operation

* refactor(api): remove useless statements
2018-07-23 18:04:11 +02:00
Anthony Lapenna b1227b17e1 fix(api): fix invalid platform build statements (#2064) 2018-07-23 16:49:04 +02:00
Anthony Lapenna f62b40dc3f fix(api): fix an issue when using websocketExec with a standalone agent 2018-07-23 16:07:18 +02:00
Anthony Lapenna 7225619456 feat(agent): support agent deployed on standalone engine endpoint (#2061) 2018-07-23 11:31:21 +02:00
Anthony Lapenna 3c6f6cf5bf feat(home): update endpoint list (#2060) 2018-07-23 09:51:33 +02:00
Anthony Lapenna 48179b9e3d feat(volume-browser): add the ability to browse volume content (#2051) 2018-07-23 07:01:03 +02:00
Olli Janatuinen cec878b01d feat(authentication/ldap): Auto create and assign LDAP users (#2042) 2018-07-23 06:57:38 +02:00
Anthony Lapenna ea7615d71c refactor(api): remove log statement 2018-07-22 20:51:43 +02:00
baron_l 0f63326bd5 fix(app): wrap long text in tables cells (#1920) (#2052)
* fix(style): wrap long text in tables cells (#1920)

* fix(style): <code> tags are now wrapped correctly (PR #2052)

* fix(style): revert #1770 style-related content and apply nowrap on datatables (PR#2052)
2018-07-20 18:31:34 +02:00
Anthony Lapenna 509e3fa795 fix(api): fix an issue with optional numeric query parameter parsing 2018-07-20 16:11:45 +02:00
Olli Janatuinen 4129550d44 feat(api): Add npipe support (#2018) 2018-07-20 11:02:06 +02:00
Johann Schmitz 0368c4e937 feat(ux): make Images and Volumes datatable more readable (#2047)
Raise cutoff level and provide tooltips for links in images and volumes datatables to avoid having to open the detail view to see the full name.
2018-07-16 09:06:41 +02:00
Anthony Lapenna 391ad7b74d feat(templates): replace Wordpress container template with a stack template 2018-07-12 09:24:21 +02:00
Anthony Lapenna e15da005a5 feat(templates): support env variables in Compose stacks 2018-07-12 09:17:07 +02:00
Anthony Lapenna c8c54cf991 fix(templates): fix an issue when deploying a swarm stack template 2018-07-12 07:22:02 +02:00
Anthony Lapenna 80ee25d817 fix(api): fix an issue with snapshots and agent endpoints 2018-07-12 07:16:53 +02:00
Anthony Lapenna 6e2e643f1f fix(containers): fix an issue when removing a container with agent proxy (#2036) 2018-07-12 07:09:27 +02:00
Anthony Lapenna e156aa202e feat(ux): update form input validation (#2035) 2018-07-11 16:22:02 +02:00
Anthony Lapenna cdf79c731b feat(ux): always display search bar in datatables (#2034) 2018-07-11 16:18:44 +02:00
Anthony Lapenna b6792461a4 feat(home): add a new home view (#2033) 2018-07-11 10:39:20 +02:00
Hasnat a94f2ee7b8 feat(log-viewer): add the ability to wrap lines (#1972)
* feat(log-viewer): Split auto scrolling & log refresh + adds wrap lines option

* feat(log-viewer): Get rid of scroll lock changes

* feat(log-viewer): remove function call in view [code review changes]
2018-07-10 21:06:45 +02:00
Anthony Lapenna 85d50d7566 Merge branch 'develop' of github.com:portainer/portainer into develop 2018-07-06 12:15:10 +02:00
Anthony Lapenna 2ad7ca969f fix(codefresh): fix invalid alpine image 2018-07-06 12:14:58 +02:00
Anthony Lapenna 7acaf4b35a fix(cli): fix default template file path on Windows (#2024) 2018-07-06 08:07:43 +02:00
Parag Jayant Datar 50020dae89 feat(containers): add column visibility dropdown in containers view (#1977) 2018-07-05 09:24:53 +02:00
Anthony Lapenna 863d917acc feat(services): default value for update image to false when updating a service (#2023) 2018-07-05 09:21:26 +02:00
Anthony Lapenna 61c285bd2e feat(templates): introduce templates management (#2017) 2018-07-03 20:31:02 +02:00
Anthony Lapenna e7939a5384 chore(version): bump version number 2018-06-25 18:33:08 +03:00
Anthony Lapenna 686712e042 chore(version): bump version number 2018-06-25 16:49:50 +03:00
Anthony Lapenna 71f407af73 Merge tag '1.18.1' into develop
Release 1.18.1
2018-06-25 15:13:13 +03:00
Anthony Lapenna 64b21d6f9c Merge branch 'release/1.18.1' 2018-06-25 15:13:08 +03:00
Anthony Lapenna b19356be6f chore(version): bump version number 2018-06-25 15:13:01 +03:00
Anthony Lapenna dbcc6a9624 fix(stack-creation): use numeric value for stack root folder name (#2000) 2018-06-25 14:48:28 +03:00
Anthony Lapenna f3925cb3ae docs(swagger): update missing stack documentation 2018-06-22 08:51:40 +03:00
Anthony Lapenna 3782761d04 chore(version): bump version number 2018-06-21 16:59:05 +03:00
Anthony Lapenna 6e0deab553 Merge tag '1.18.0' into develop
Release 1.18.0
2018-06-21 14:28:19 +03:00
Anthony Lapenna 7f9644b55e Merge branch 'release/1.18.0' 2018-06-21 14:28:14 +03:00
Anthony Lapenna decb67f4d9 chore(version): bump version number 2018-06-21 14:28:07 +03:00
Anthony Lapenna 0a9eab53d0 feat(containers): do not remember selected items (#1988) 2018-06-21 13:09:57 +02:00
Anthony Lapenna d3a26a4ade refactor(images): relocate tag/digest replacement 2018-06-21 13:59:50 +03:00
Anthony Lapenna 23b0d6f1dc fix(stack): fix an issue with stack migration 2018-06-20 21:02:53 +03:00
Anthony Lapenna a5bd2743f3 fix(stacks): fix an issue with stack update 2018-06-20 20:55:00 +03:00
Anthony Lapenna 48f963398f refactor(api): remove useless log.printf statement 2018-06-20 20:43:39 +03:00
Anthony Lapenna 115c1608b9 feat(libcompose): set RemoveVolume to false 2018-06-20 18:20:16 +03:00
Anthony Lapenna 413ab44dc0 refactor(stacks): remove unused component 2018-06-20 17:08:31 +03:00
Anthony Lapenna 165ca3ce3e fix(services): fix invalid published ports link 2018-06-20 17:03:53 +03:00
Duvel f8370a1421 fix(images): create tags from RepoDigests when no tags are available (#1522) 2018-06-20 15:58:56 +02:00
Anthony Lapenna 61c74e22f0 feat(services): add the ability to pull latest image when updating a … (#1984)
* feat(services): add the ability to pull latest image when updating a service

* feat(services): update version header value

* refactor(services): remove TODO

* feat(services): rollback version header value to 1.29
2018-06-20 15:53:58 +02:00
Anthony Lapenna 0da9e564b9 feat(stacks): add the ability to migrate stacks to another endpoint (#1976)
* feat(stacks): add the ability to migrate stacks to another endpoint

* feat(stack-details): do not redirect to alternate endpoint after migration

* fix(api): fix merge conflicts

* feat(stack-details): add a modal to confirm stack migration
2018-06-19 17:28:40 +02:00
Anthony Lapenna 9cab961d87 fix(about): fix missing widget headers 2018-06-19 14:20:34 +03:00
Anthony Lapenna d7ff14777f refactor(api): restructure bolt package (#1981)
* refactor(api): bolt package refactor

* refactor(api): refactor bolt package
2018-06-19 13:15:10 +02:00
Anthony Lapenna 6698173bf5 fix(api): fix endpointExtensionAddPayload validation 2018-06-18 15:30:44 +03:00
Anthony Lapenna b4c2820ad7 refactor(api): use a standard stack identifier (#1980) 2018-06-18 12:07:56 +02:00
Anthony Lapenna da5a430b8c fix(api): add an authenticated access policy to the websocket endpoint (#1979)
* fix(api): add an authenticated access policy to the websocket endpoint

* refactor(api): centralize EndpointAccess validation

* feat(api): validate id query parameter for the /websocket/exec endpoint
2018-06-18 11:56:31 +02:00
Anthony Lapenna f3ce5c25de refactor(api): use generic marshal/unmarshal functions in bolt package 2018-06-17 19:57:22 +03:00
Anthony Lapenna 783f838171 feat(containers): add a tooltip with full container name on hover (#1978) 2018-06-17 10:00:15 +03:00
Anthony Lapenna e1345416b4 feat(stacks): migrate stack data from previous portainer version 2018-06-15 18:14:01 +03:00
Anthony Lapenna 5e73a49473 feat(tags): add the ability to manage tags (#1971)
* feat(tags): add the ability to manage tags

* feat(tags): update tag selector UX

* refactor(app): remove unused ui-select library
2018-06-15 09:18:25 +02:00
cedric-crouzet-penbase b349f16090 fix(containers): remove hardcoded container stop/restart timeout
REST call to stop/restart a container overrides the default stop timeout (before kill) with hardcoded 5 seconds.
Containers already have a default stop timeout handled by the engine API (https://github.com/moby/moby/blob/master/client/container_stop.go).
With this hardcoded 5 seconds, the containers get killed after 5 seconds even if they define a custom greater stop timeout.
Another solution would be to not hardcode the 5 seconds but rather use a global editable setting.
2018-06-13 16:04:24 +02:00
Anthony Lapenna 1e12057cdd fix(api): review security policies when creating/updating a resource control (#1964) 2018-06-11 17:58:46 +02:00
Anthony Lapenna e3d564325b feat(stacks): support compose v2.0 stack (#1963) 2018-06-11 15:13:19 +02:00
Anthony Lapenna ef15cd30eb style(app): update widget title property (#1952)
* style(app): update widget title property

* style(containerinstances): fix invalid component title
2018-06-06 18:12:35 +02:00
Anthony Lapenna 3ace184069 feat(dashboard): update dashboard info (#1944) 2018-06-04 10:30:53 +02:00
Konstantin Azizov 4429c6a160 fix(container-details): recreate container with multiple networks (#1907)
* fix(container): Use first network's Mac address by default

* fix(container): Connect additional networks to container after creation

* fix(container): Remove warning message
2018-06-02 08:44:18 +02:00
Anthony Lapenna 9bb885629a feat(endpoints): UX enhancements (#1943)
* feat(endpoints): add details about endpoints in datatable

* feat(endpoint-details): add the ability to inspect/update azure endpoint

* feat(endpoint-selector): disable placeholder selection
2018-06-01 16:13:24 +02:00
Anthony Lapenna bfc49574b7 style(endpoints): update Azure endpoint type description 2018-06-01 09:11:56 +02:00
Anthony Lapenna 1cc31f8956 fix(app): fix a state URL conflict between azure and docker modules 2018-06-01 09:09:36 +02:00
Anthony Lapenna e15856c62c fix(init-endpoint): fix an issue preventing the init of a remote endpoint 2018-05-31 22:00:18 +02:00
valkheim c4576e9e2f feat(api): update admin deletion policy (#1935) 2018-05-31 21:24:15 +02:00
Anthony Lapenna 9ff4b21616 feat(support): add support view (#1937) 2018-05-28 16:40:59 +02:00
Anthony Lapenna 9ad9cc5e2d feat(azure): add experimental Azure endpoint support (#1936) 2018-05-28 16:40:33 +02:00
Sawood Alam 415c6ce5e1 docs(README): drop support for Standalone Docker Swarm (#1934)
* Dropped support for standalone Docker Swarm documented

* A more verbose explaination of standalone Docker Swarm Support
2018-05-25 18:00:47 +02:00
Andrea Kao 6c520907ad chore(license): update license info so that GitHub recognizes it (#1924)
GitHub uses a library called Licensee to identify a project's license
type. It shows this information in the status bar and via the API if it
can unambiguously identify the license.

This commit modifies a few of Portainer's docs so that Licensee is able
to recognize the repository's license type. It updates LICENSE so that
it contains only the text of the zlib license. It also moves the info
concerning 3rd-party software to a new "Licensing" section in the
README.

Collectively, these changes allow Licensee to successfully identify the
license type of Portainer as zlib.

Signed-off-by: Andrea Kao <eirinikos@gmail.com>
2018-05-23 14:47:43 +02:00
Anthony Lapenna 9a071a57f2 chore(version): bump version number 2018-05-21 13:58:47 +02:00
Anthony Lapenna 67d729c992 Merge tag '1.17.1' into develop
Release 1.17.1
2018-05-21 11:03:59 +02:00
Anthony Lapenna f42733b74c Merge branch 'release/1.17.1' 2018-05-21 11:03:55 +02:00
Anthony Lapenna 19f9840c8c chore(version): bump version number 2018-05-21 11:03:48 +02:00
Anthony Lapenna fe7a88697b feat(service): automatically focus replica input after clicking on scale (#1916) 2018-05-21 10:59:02 +02:00
kirdia 19c3fa276b feat(log-viewer): Add the ability to specify displayed line count (#1914) 2018-05-21 10:51:56 +02:00
Anthony Lapenna 63d338c4da fix(api): refactor TLS support (#1909)
* refactor(api): refactor TLS support

* feat(api): migrate endpoint data

* refactor(api): remove unused code and rename functions

* refactor(app): remove console.log statement
2018-05-19 16:25:11 +02:00
Anthony Lapenna 5d3f438288 fix(tasks): fix an issue when filtering tasks (#1913) 2018-05-19 10:47:58 +02:00
Anthony Lapenna e7e7d73f20 docs(api): update swagger.yml 2018-05-18 10:58:16 +02:00
Anthony Lapenna 0ea91f7185 chore(codefresh): remove develop pipeline 2018-05-18 10:15:56 +02:00
Anthony Lapenna 034fde6d1a chore(codefresh): add branch pipeline 2018-05-18 10:07:25 +02:00
Anthony Lapenna 45f52657cf fix(websocket): feat(websocket): remove Origin header before handling request (#1901) 2018-05-16 09:13:46 +02:00
Anthony Lapenna 32800a843a feat(sidebar): update endpoint selection UX (#1902)
* style(sidebar): update selected endpoint name color

* feat(sidebar): sort groups/endpoints alphabetically
2018-05-16 08:49:14 +02:00
Anthony Lapenna 5df09923b6 feat(api): add debug statements in response handling 2018-05-15 19:13:27 +02:00
Anthony Lapenna 79f4c20c25 fix(endpoints): set TLSSkipVerify to false when TLS is not enabled during update (#1896) 2018-05-15 18:24:54 +02:00
Anthony Lapenna 2c0595f5ed feat(exec): relocate config.json to data folder and re-use existing content (#1898) 2018-05-15 14:12:49 +02:00
Anthony Lapenna a09af01e17 chore(build-system): update gruntfile 2018-05-14 21:41:24 +02:00
Anthony Lapenna be236f9d09 fix(api): fix default group for endpoint declared via -H 2018-05-14 21:40:50 +02:00
Anthony Lapenna 87fdd43afc Merge tag '1.17.0' into develop
Release 1.17.0
2018-05-10 17:22:26 +02:00
Anthony Lapenna 19bb83ba2a Merge branch 'release/1.17.0' 2018-05-10 17:22:20 +02:00
Anthony Lapenna f75c87315e chore(version): bump version number 2018-05-10 17:22:11 +02:00
Anthony Lapenna a0a667053e feat(tasks): change task name format in tasks datatable (#1884) 2018-05-10 17:17:53 +02:00
Miguel A. C b2b1c86067 fix(service-details): avoid sending unmodified service reservation, limits and update config (#1625) 2018-05-10 09:54:22 +02:00
Anthony Lapenna 74c92c4da8 Merge branch 'develop' of github.com:portainer/portainer into develop 2018-05-09 16:12:02 +02:00
Anthony Lapenna 7754933470 fix(api): fix a panic issue when retrieving Docker API response 2018-05-09 16:11:52 +02:00
Andrew Pearson 1c06bfd911 feat(container-details): update port mapping order (#1878)
Switching container port mapping around to match docker, correcting issue #1871
2018-05-09 10:26:47 +02:00
Anthony Lapenna 3b14e6b6b9 chore(codefresh): update codefresh pipelines (#1879) 2018-05-09 10:00:38 +02:00
Anthony Lapenna a83ea1554c chore(build-system): update docker binary version 2018-05-08 19:53:10 +02:00
Anthony Lapenna 4d79259748 feat(notifications): display image removal error 2018-05-08 08:20:27 +02:00
Anthony Lapenna cdb09a91a7 refactor(about): remove Swarm support 2018-05-08 08:20:04 +02:00
Konstantin Azizov 284f2b7752 feat(settings): allow hide container with label with no value (#1860) (#1872)
Also add ability to submit form by pressing "Enter" key

Fixes #1860
2018-05-08 07:46:07 +02:00
Konstantin Azizov 55a96767bb feat(security): add request rate limiter on authentication endpoint (#1866) 2018-05-07 20:01:39 +02:00
Anthony Lapenna 6360e6a20b fix(api): use the folder of the stackfile as working dir when deploying a stack (#1869) 2018-05-07 09:57:15 +02:00
Anthony Lapenna 2327d696e0 feat(agent): add agent support (#1828) 2018-05-06 09:15:57 +02:00
Anthony Lapenna 77a85bd385 fix(container-edit): fix an issue related to missing extra hosts in network config (#1862) 2018-05-04 09:59:51 +02:00
Anthony Lapenna e0cf088428 fix(log-viewer): strip headers in container logs when TTY is disabled (#1861) 2018-05-04 09:45:05 +02:00
Hans-Joachim Krauch 1e55ada6af feat(templates): allow to set hostname in container templates (#1833) 2018-05-02 20:41:46 +02:00
Anthony Lapenna e8744e8c0b chore(project): update issue templates 2018-05-02 17:01:05 +02:00
Anthony Lapenna 1162549209 feat(endpoint-groups): add endpoint-groups (#1837) 2018-04-26 18:08:46 +02:00
Anthony Lapenna 2ffcb946b1 fix(access-control): fix access control panel layout (#1844) 2018-04-25 22:13:06 +02:00
Anthony Lapenna 1d24a827de docs(api): update endpoint creation documentation (#1843) 2018-04-25 21:52:06 +02:00
Anthony Lapenna c705d27ac6 docs(api): update resource control creation docs (#1842) 2018-04-25 21:40:21 +02:00
Anthony Lapenna dea5038c93 chore(docker): upgrade Docker CLI version (#1841) 2018-04-25 21:29:23 +02:00
Herwono W. Wijaya f0317d6d87 fix(api): fix the ability to push images to private repositories 2018-04-25 16:58:08 +02:00
Guri afa3fd9a47 feat(app): remove charset from content-type of post/put/patch (#1791) 2018-04-25 16:00:29 +02:00
Anthony Lapenna fe74f36f62 fix(volume-creation): fix missing endpointProvider variable 2018-04-23 08:05:22 +02:00
Anthony Lapenna 05d6abf57b feat(api): ping the endpoint at creation time (#1817) 2018-04-16 13:19:24 +02:00
Hasnat 031b428e0c fix(external-endpoints): less verbose output (#1815) 2018-04-14 11:17:58 +02:00
Anthony Lapenna 23f4939ee7 docs(api): add missing supported resource control types (#1812) 2018-04-13 16:09:43 +02:00
Igor Karpovich 7690ef3c33 fix(api): add json content-type to all json API responses (#1809) 2018-04-13 16:01:02 +02:00
Anthony Lapenna 4f0e752d00 feat(api): remove any version api before proxying request (#1806) 2018-04-11 17:40:29 +02:00
Maximilian Pachl 2a9ba1f9a2 feat(swarm-visualizer): save settings to local storage (#1777) 2018-04-06 18:59:25 +10:00
Shahar Hadas 216d6c2b14 feat(container-console): add the ability to select ash (#1790)
Add /bin/ash as another dropbox option in addition to bash and sh
2018-04-06 18:43:08 +10:00
Rahul Ruikar dca1976252 feat(stack): Add the ability to scale services in stack-details (#1776) 2018-04-04 19:45:35 +10:00
Anthony Lapenna 1cfbec557c refactor(project): remove Swarm standalone support (#1720)
* refactor(project): remove Swarm standalone support

* fix(state): fix an issue with endpoint state not being registered
2018-04-04 10:31:04 +10:00
Lennart Nordgreen 517f983ec6 chore(disribution): update .spec files 2018-04-04 09:06:02 +10:00
Anthony Lapenna 0edcdbd612 Merge tag '1.16.5' into develop
Release 1.16.5
2018-04-02 07:44:33 +10:00
Anthony Lapenna a8ee774cf2 Merge branch 'release/1.16.5' 2018-04-02 07:44:28 +10:00
Anthony Lapenna 81ed0e4507 chore(version): bump version number 2018-04-02 07:44:19 +10:00
Anthony Lapenna 8d32703456 fix(service-details): prevent regular users from using bind mounts (#1778) 2018-03-29 18:41:47 +11:00
Anthony Lapenna eca39b11a8 chore(project): remove linting from contribution guidelines 2018-03-28 19:47:49 +11:00
Emanuele De Cupis b2b685ba6f style(datatables): prevent cell content to go to new line (#1770) 2018-03-28 08:11:17 +11:00
moncho 7e26d09881 feat(service-details): display stop grace period in a human-friendly format (#1773) 2018-03-28 08:05:01 +11:00
Rahul Ruikar 80a23b5351 feat(log-viewer): add the ability to display timestamps (#1697) 2018-03-25 10:36:13 +10:00
Anthony Lapenna 30dfd3d616 fix(api): manage registry authentication in the API (#1751) 2018-03-23 08:44:43 +10:00
Anthony Lapenna c267f8bf57 fix(stacks): fix an issue when deploying public stacks 2018-03-22 15:38:00 +10:00
Herwono W. Wijaya bca8936faa fix(templates): fix app templates stack deployment (#1747)
* fix(templates): fix app templates stack deployment

* fix(templates): stack deployment remove return statement and fix identation
2018-03-22 15:28:55 +10:00
Anthony Lapenna a72ffe4188 fix(extensions): use an empty object instead of a null value when registering extension (#1750) 2018-03-22 14:37:36 +10:00
Anthony Lapenna 27dcd708a6 fix(extensions): init endpoint extensions after admin user creation (#1733)
* fix(extensions): init endpoint extensions after admin user creation
2018-03-18 07:09:07 +10:00
Anthony Lapenna adf1ba7b47 feat(stack-creation): add the ability to specify git credentials (#1722)
* feat(stack-creation): add the ability to specify git credentials

* docs(api): update Swagger
2018-03-16 07:22:05 +10:00
Anthony Lapenna 50ece68f35 style(app): update icon style (#1727) 2018-03-14 15:32:14 +10:00
Paweł Kozioł 4e38e4ba33 feat(image-details): display image layer order and sort by it by default (#1715)
* feat(image-details): display image layer depth and sort by it by default (#1706)

* refactor(image-details): rename 'Depth' to 'Order' in image layers table

* refactor(image-details): sort image layers from the bottom to the top one
2018-03-14 10:27:06 +10:00
1138-4EB f0621cb09c chore(build-system): use regular vendor files, ignore (pre)minified (#1475) 2018-03-14 10:24:00 +10:00
Anthony Lapenna 9e47aedbe6 fix(api): ignore directory existence check and use os.MkdirAll (#1719) 2018-03-14 09:47:21 +10:00
Anthony Lapenna 706490db5e fix(api): use EntryPoint as a reference to overwrite stack Compose file (#1725) 2018-03-13 21:35:12 +10:00
Anthony Lapenna d34b1d5f9d fix(build-system): fix task order after fontawesome5 integration (#1724) 2018-03-13 21:09:02 +10:00
Herwono W. Wijaya 66f29dd103 style(app): upgrade to font awesome v5 2018-03-13 15:36:53 +10:00
Anthony Lapenna 96e77b3ada fix(api): fix a regression with the HTTP handler (#1718) 2018-03-13 09:06:38 +10:00
Anthony Lapenna 3d9a3f11e4 Merge tag '1.16.4' into develop
Release 1.16.4
2018-03-11 20:30:16 +10:00
862 changed files with 37543 additions and 12413 deletions
+40 -8
View File
@@ -1,5 +1,42 @@
---
engines:
version: "2"
checks:
argument-count:
enabled: true
config:
threshold: 4
complex-logic:
enabled: true
config:
threshold: 4
file-lines:
enabled: true
config:
threshold: 300
method-complexity:
enabled: false
method-count:
enabled: true
config:
threshold: 20
method-lines:
enabled: true
config:
threshold: 50
nested-control-flow:
enabled: true
config:
threshold: 4
return-statements:
enabled: false
similar-code:
enabled: true
config:
threshold: #language-specific defaults. overrides affect all languages.
identical-code:
enabled: true
config:
threshold: #language-specific defaults. overrides affect all languages.
plugins:
gofmt:
enabled: true
golint:
@@ -20,10 +57,5 @@ engines:
config: .eslintrc.yml
fixme:
enabled: true
ratings:
paths:
- "**.css"
- "**.js"
- "**.go"
exclude_paths:
exclude_patterns:
- test/
+4 -1
View File
@@ -141,7 +141,10 @@ rules:
no-undef-init: error
no-undef: off
no-undefined: off
no-unused-vars: off
no-unused-vars:
- warn
-
vars: local
no-use-before-define: off
# Node.js and CommonJS
+47
View File
@@ -0,0 +1,47 @@
---
name: Bug report
about: Create a bug report
---
<!--
Thanks for reporting a bug for Portainer !
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/ or gitter https://gitter.im/portainer/Lobby.
Before opening a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this
repository. If there is a duplicate, please close your issue and add a comment
to the existing issue instead.
Also, be sure to check our FAQ and documentation first: https://portainer.readthedocs.io
-->
**Bug description**
A clear and concise description of what the bug is.
**Expected behavior**
A clear and concise description of what you expected to happen.
Briefly describe what you were expecting.
**Steps to reproduce the issue:**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Technical details:**
* Portainer version:
* Docker version (managed by Portainer):
* Platform (windows/linux):
* Command used to start Portainer (`docker run -p 9000:9000 portainer/portainer`):
* Browser:
**Additional context**
Add any other context about the problem here.
+15
View File
@@ -0,0 +1,15 @@
---
name: Question
about: Ask us a question about Portainer usage or deployment
---
<!--
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/ or gitter https://gitter.im/portainer/Lobby.
Also, be sure to check our FAQ and documentation first: https://portainer.readthedocs.io
-->
**Question**:
How can I deploy Portainer on... ?
+31
View File
@@ -0,0 +1,31 @@
---
name: Feature request
about: Suggest a feature/enhancement that should be added in Portainer
---
<!--
Thanks for opening a feature request for Portainer !
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/ or gitter https://gitter.im/portainer/Lobby.
Before opening a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this
repository. If there is a duplicate, please close your issue and add a comment
to the existing issue instead.
Also, be sure to check our FAQ and documentation first: https://portainer.readthedocs.io
-->
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+1
View File
@@ -4,3 +4,4 @@ dist
portainer-checksum.txt
api/cmd/portainer/portainer*
.tmp
.vscode
+21 -10
View File
@@ -2,7 +2,7 @@
Some basic conventions for contributing to this project.
### General
## General
Please make sure that there aren't existing pull requests attempting to address the issue mentioned. Likewise, please check for issues related to update, as someone else may be working on the issue in a branch or fork.
@@ -13,7 +13,7 @@ When creating a new branch, prefix it with the *type* of the change (see section
For example, if you work on a bugfix for the issue #361, you could name the branch `fix361-template-selection`.
### Issues open to contribution
## Issues open to contribution
Want to contribute but don't know where to start?
@@ -24,17 +24,14 @@ Some of the open issues are labeled with prefix `exp/`, this is used to mark the
either AngularJS or Golang
* **advanced**: a task that require a deep understanding of the project codebase
You can have a use Github filters to list these issues:
You can use Github filters to list these issues:
* beginner labeled issues: https://github.com/portainer/portainer/labels/exp%2Fbeginner
* intermediate labeled issues: https://github.com/portainer/portainer/labels/exp%2Fintermediate
* advanced labeled issues: https://github.com/portainer/portainer/labels/exp%2Fadvanced
### Linting
Please check your code using `grunt lint` before submitting your pull requests.
### Commit Message Format
## Commit Message Format
Each commit message should include a **type**, a **scope** and a **subject**:
@@ -50,7 +47,7 @@ Lines should not exceed 100 characters. This allows the message to be easier to
#269 style(dashboard): update dashboard with new layout
```
#### Type
### Type
Must be one of the following:
@@ -64,16 +61,30 @@ Must be one of the following:
* **chore**: Changes to the build process or auxiliary tools and libraries such as documentation
generation
#### Scope
### Scope
The scope could be anything specifying place of the commit change. For example `networks`,
`containers`, `images` etc...
You can use the **area** label tag associated on the issue here (for `area/containers` use `containers` as a scope...)
#### Subject
### Subject
The subject contains succinct description of the change:
* use the imperative, present tense: "change" not "changed" nor "changes"
* don't capitalize first letter
* no dot (.) at the end
## Contribution process
Our contribution process is described below. Some of the steps can be visualized inside Github via specific `status/` labels, such as `status/1-functional-review` or `status/2-technical-review`.
### Bug report
![portainer_bugreport_workflow](https://user-images.githubusercontent.com/5485061/45727219-50190a00-bbf5-11e8-9fe8-3a563bb8d5d7.png)
### Feature request
The feature request process is similar to the bug report process but has an extra functional validation before the technical validation as well as a documentation validation before the testing phase.
![portainer_featurerequest_workflow](https://user-images.githubusercontent.com/5485061/45727229-5ad39f00-bbf5-11e8-9550-16ba66c50615.png)
+2 -44
View File
@@ -1,4 +1,4 @@
Portainer: Copyright (c) 2016 Portainer.io
Copyright (c) 2018 Portainer.io
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
@@ -14,46 +14,4 @@ freely, subject to the following restrictions:
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
Portainer contains code which was originally under this license:
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (portainer.io)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
rdash-angular: Copyright (c) [2014] [Elliot Hesp]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
3. This notice may not be removed or altered from any source distribution.
+13 -8
View File
@@ -1,15 +1,13 @@
<p align="center">
<img title="portainer" src='https://portainer.io/images/logo_alt.png' />
<img title="portainer" src='https://github.com/portainer/portainer/blob/develop/assets/images/logo_alt.png?raw=true' />
</p>
[![Docker Pulls](https://img.shields.io/docker/pulls/portainer/portainer.svg)](https://hub.docker.com/r/portainer/portainer/)
[![Microbadger](https://images.microbadger.com/badges/image/portainer/portainer.svg)](http://microbadger.com/images/portainer/portainer "Image size")
[![Documentation Status](https://readthedocs.org/projects/portainer/badge/?version=stable)](http://portainer.readthedocs.io/en/stable/?badge=stable)
[![Codefresh build status]( https://g.codefresh.io/api/badges/build?repoOwner=portainer&repoName=portainer&branch=develop&pipelineName=portainer-ci&accountName=deviantony&type=cf-1)]( https://g.codefresh.io/repositories/portainer/portainer/builds?filter=trigger:build;branch:develop;service:5922a08a3a1aab000116fcc6~portainer-ci)
[![Build Status](https://portainer.visualstudio.com/Portainer%20CI/_apis/build/status/Portainer%20CI?branchName=develop)](https://portainer.visualstudio.com/Portainer%20CI/_build/latest?definitionId=3&branchName=develop)
[![Code Climate](https://codeclimate.com/github/portainer/portainer/badges/gpa.svg)](https://codeclimate.com/github/portainer/portainer)
[![Slack](https://portainer.io/slack/badge.svg)](https://portainer.io/slack/)
[![Gitter](https://badges.gitter.im/portainer/Lobby.svg)](https://gitter.im/portainer/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6)
**_Portainer_** is a lightweight management UI which allows you to **easily** manage your different Docker environments (Docker hosts or Swarm clusters).
@@ -20,8 +18,6 @@
## Demo
<img src="https://portainer.io/images/screenshots/portainer.gif" width="77%"/>
You can try out the public demo instance: http://demo.portainer.io/ (login with the username **admin** and the password **tryportainer**).
Please note that the public demo cluster is **reset every 15min**.
@@ -44,7 +40,6 @@ Unlike the public demo, the playground sessions are deleted after 4 hours. Apart
* Issues: https://github.com/portainer/portainer/issues
* FAQ: https://portainer.readthedocs.io/en/latest/faq.html
* Slack (chat): https://portainer.io/slack/
* Gitter (chat): https://gitter.im/portainer/Lobby
## Reporting bugs and contributing
@@ -56,8 +51,18 @@ Unlike the public demo, the playground sessions are deleted after 4 hours. Apart
**_Portainer_** has full support for the following Docker versions:
* Docker 1.10 to the latest version
* Docker Swarm >= 1.2.3
* Standalone Docker Swarm >= 1.2.3 _(**NOTE:** Use of Standalone Docker Swarm is being discouraged since the introduction of built-in Swarm Mode in Docker. While older versions of Portainer had support for Standalone Docker Swarm, Portainer 1.17.0 and newer **do not** support it. However, the built-in Swarm Mode of Docker is fully supported.)_
Partial support for the following Docker versions (some features may not be available):
* Docker 1.9
## Licensing
Portainer is licensed under the zlib license. See [LICENSE](./LICENSE) for reference.
Portainer also contains the following code, which is licensed under the [MIT license](https://opensource.org/licenses/MIT):
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (portainer.io)
rdash-angular: Copyright (c) [2014] [Elliot Hesp]
+2 -2
View File
@@ -7,13 +7,13 @@ import (
// TarFileInBuffer will create a tar archive containing a single file named via fileName and using the content
// specified in fileContent. Returns the archive as a byte array.
func TarFileInBuffer(fileContent []byte, fileName string) ([]byte, error) {
func TarFileInBuffer(fileContent []byte, fileName string, mode int64) ([]byte, error) {
var buffer bytes.Buffer
tarWriter := tar.NewWriter(&buffer)
header := &tar.Header{
Name: fileName,
Mode: 0600,
Mode: mode,
Size: int64(len(fileContent)),
}
+48
View File
@@ -0,0 +1,48 @@
package archive
import (
"archive/zip"
"bytes"
"io"
"io/ioutil"
"os"
"path/filepath"
)
// UnzipArchive will unzip an archive from bytes into the dest destination folder on disk
func UnzipArchive(archiveData []byte, dest string) error {
zipReader, err := zip.NewReader(bytes.NewReader(archiveData), int64(len(archiveData)))
if err != nil {
return err
}
for _, zipFile := range zipReader.File {
f, err := zipFile.Open()
if err != nil {
return err
}
defer f.Close()
data, err := ioutil.ReadAll(f)
if err != nil {
return err
}
fpath := filepath.Join(dest, zipFile.Name)
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, zipFile.Mode())
if err != nil {
return err
}
_, err = io.Copy(outFile, bytes.NewReader(data))
if err != nil {
return err
}
outFile.Close()
}
return nil
}
+193 -79
View File
@@ -2,80 +2,74 @@ package bolt
import (
"log"
"os"
"path"
"time"
"github.com/boltdb/bolt"
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/dockerhub"
"github.com/portainer/portainer/bolt/endpoint"
"github.com/portainer/portainer/bolt/endpointgroup"
"github.com/portainer/portainer/bolt/extension"
"github.com/portainer/portainer/bolt/migrator"
"github.com/portainer/portainer/bolt/registry"
"github.com/portainer/portainer/bolt/resourcecontrol"
"github.com/portainer/portainer/bolt/schedule"
"github.com/portainer/portainer/bolt/settings"
"github.com/portainer/portainer/bolt/stack"
"github.com/portainer/portainer/bolt/tag"
"github.com/portainer/portainer/bolt/team"
"github.com/portainer/portainer/bolt/teammembership"
"github.com/portainer/portainer/bolt/template"
"github.com/portainer/portainer/bolt/user"
"github.com/portainer/portainer/bolt/version"
"github.com/portainer/portainer/bolt/webhook"
)
const (
databaseFileName = "portainer.db"
)
// Store defines the implementation of portainer.DataStore using
// BoltDB as the storage system.
type Store struct {
// Path where is stored the BoltDB database.
Path string
// Services
UserService *UserService
TeamService *TeamService
TeamMembershipService *TeamMembershipService
EndpointService *EndpointService
ResourceControlService *ResourceControlService
VersionService *VersionService
SettingsService *SettingsService
RegistryService *RegistryService
DockerHubService *DockerHubService
StackService *StackService
db *bolt.DB
checkForDataMigration bool
path string
db *bolt.DB
checkForDataMigration bool
fileService portainer.FileService
DockerHubService *dockerhub.Service
EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service
ExtensionService *extension.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
SettingsService *settings.Service
StackService *stack.Service
TagService *tag.Service
TeamMembershipService *teammembership.Service
TeamService *team.Service
TemplateService *template.Service
UserService *user.Service
VersionService *version.Service
WebhookService *webhook.Service
ScheduleService *schedule.Service
}
const (
databaseFileName = "portainer.db"
versionBucketName = "version"
userBucketName = "users"
teamBucketName = "teams"
teamMembershipBucketName = "team_membership"
endpointBucketName = "endpoints"
resourceControlBucketName = "resource_control"
settingsBucketName = "settings"
registryBucketName = "registries"
dockerhubBucketName = "dockerhub"
stackBucketName = "stacks"
)
// NewStore initializes a new Store and the associated services
func NewStore(storePath string) (*Store, error) {
func NewStore(storePath string, fileService portainer.FileService) (*Store, error) {
store := &Store{
Path: storePath,
UserService: &UserService{},
TeamService: &TeamService{},
TeamMembershipService: &TeamMembershipService{},
EndpointService: &EndpointService{},
ResourceControlService: &ResourceControlService{},
VersionService: &VersionService{},
SettingsService: &SettingsService{},
RegistryService: &RegistryService{},
DockerHubService: &DockerHubService{},
StackService: &StackService{},
path: storePath,
fileService: fileService,
}
store.UserService.store = store
store.TeamService.store = store
store.TeamMembershipService.store = store
store.EndpointService.store = store
store.ResourceControlService.store = store
store.VersionService.store = store
store.SettingsService.store = store
store.RegistryService.store = store
store.DockerHubService.store = store
store.StackService.store = store
_, err := os.Stat(storePath + "/" + databaseFileName)
if err != nil && os.IsNotExist(err) {
store.checkForDataMigration = false
} else if err != nil {
databasePath := path.Join(storePath, databaseFileName)
databaseFileExists, err := fileService.FileExists(databasePath)
if err != nil {
return nil, err
}
if !databaseFileExists {
store.checkForDataMigration = false
} else {
store.checkForDataMigration = true
}
@@ -85,29 +79,37 @@ func NewStore(storePath string) (*Store, error) {
// Open opens and initializes the BoltDB database.
func (store *Store) Open() error {
path := store.Path + "/" + databaseFileName
db, err := bolt.Open(path, 0600, &bolt.Options{Timeout: 1 * time.Second})
databasePath := path.Join(store.path, databaseFileName)
db, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second})
if err != nil {
return err
}
store.db = db
bucketsToCreate := []string{versionBucketName, userBucketName, teamBucketName, endpointBucketName,
resourceControlBucketName, teamMembershipBucketName, settingsBucketName,
registryBucketName, dockerhubBucketName, stackBucketName}
return store.initServices()
}
return db.Update(func(tx *bolt.Tx) error {
// Init creates the default data set.
func (store *Store) Init() error {
groups, err := store.EndpointGroupService.EndpointGroups()
if err != nil {
return err
}
for _, bucket := range bucketsToCreate {
_, err := tx.CreateBucketIfNotExists([]byte(bucket))
if err != nil {
return err
}
if len(groups) == 0 {
unassignedGroup := &portainer.EndpointGroup{
Name: "Unassigned",
Description: "Unassigned endpoints",
Labels: []portainer.Pair{},
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Tags: []string{},
}
return nil
})
return store.EndpointGroupService.CreateEndpointGroup(unassignedGroup)
}
return nil
}
// Close closes the BoltDB database.
@@ -121,28 +123,140 @@ func (store *Store) Close() error {
// MigrateData automatically migrate the data based on the DBVersion.
func (store *Store) MigrateData() error {
if !store.checkForDataMigration {
err := store.VersionService.StoreDBVersion(portainer.DBVersion)
if err != nil {
return err
}
return nil
return store.VersionService.StoreDBVersion(portainer.DBVersion)
}
version, err := store.VersionService.DBVersion()
if err == portainer.ErrDBVersionNotFound {
if err == portainer.ErrObjectNotFound {
version = 0
} else if err != nil {
return err
}
if version < portainer.DBVersion {
migratorParams := &migrator.Parameters{
DB: store.db,
DatabaseVersion: version,
EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService,
ExtensionService: store.ExtensionService,
ResourceControlService: store.ResourceControlService,
SettingsService: store.SettingsService,
StackService: store.StackService,
TemplateService: store.TemplateService,
UserService: store.UserService,
VersionService: store.VersionService,
FileService: store.fileService,
}
migrator := migrator.NewMigrator(migratorParams)
log.Printf("Migrating database from version %v to %v.\n", version, portainer.DBVersion)
migrator := NewMigrator(store, version)
err = migrator.Migrate()
if err != nil {
log.Printf("An error occurred during database migration: %s\n", err)
return err
}
}
return nil
}
func (store *Store) initServices() error {
dockerhubService, err := dockerhub.NewService(store.db)
if err != nil {
return err
}
store.DockerHubService = dockerhubService
endpointgroupService, err := endpointgroup.NewService(store.db)
if err != nil {
return err
}
store.EndpointGroupService = endpointgroupService
endpointService, err := endpoint.NewService(store.db)
if err != nil {
return err
}
store.EndpointService = endpointService
extensionService, err := extension.NewService(store.db)
if err != nil {
return err
}
store.ExtensionService = extensionService
registryService, err := registry.NewService(store.db)
if err != nil {
return err
}
store.RegistryService = registryService
resourcecontrolService, err := resourcecontrol.NewService(store.db)
if err != nil {
return err
}
store.ResourceControlService = resourcecontrolService
settingsService, err := settings.NewService(store.db)
if err != nil {
return err
}
store.SettingsService = settingsService
stackService, err := stack.NewService(store.db)
if err != nil {
return err
}
store.StackService = stackService
tagService, err := tag.NewService(store.db)
if err != nil {
return err
}
store.TagService = tagService
teammembershipService, err := teammembership.NewService(store.db)
if err != nil {
return err
}
store.TeamMembershipService = teammembershipService
teamService, err := team.NewService(store.db)
if err != nil {
return err
}
store.TeamService = teamService
templateService, err := template.NewService(store.db)
if err != nil {
return err
}
store.TemplateService = templateService
userService, err := user.NewService(store.db)
if err != nil {
return err
}
store.UserService = userService
versionService, err := version.NewService(store.db)
if err != nil {
return err
}
store.VersionService = versionService
webhookService, err := webhook.NewService(store.db)
if err != nil {
return err
}
store.WebhookService = webhookService
scheduleService, err := schedule.NewService(store.db)
if err != nil {
return err
}
store.ScheduleService = scheduleService
return nil
}
+48
View File
@@ -0,0 +1,48 @@
package dockerhub
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "dockerhub"
dockerHubKey = "DOCKERHUB"
)
// Service represents a service for managing Dockerhub data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// DockerHub returns the DockerHub object.
func (service *Service) DockerHub() (*portainer.DockerHub, error) {
var dockerhub portainer.DockerHub
err := internal.GetObject(service.db, BucketName, []byte(dockerHubKey), &dockerhub)
if err != nil {
return nil, err
}
return &dockerhub, nil
}
// UpdateDockerHub updates a DockerHub object.
func (service *Service) UpdateDockerHub(dockerhub *portainer.DockerHub) error {
return internal.UpdateObject(service.db, BucketName, []byte(dockerHubKey), dockerhub)
}
-61
View File
@@ -1,61 +0,0 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// DockerHubService represents a service for managing registries.
type DockerHubService struct {
store *Store
}
const (
dbDockerHubKey = "DOCKERHUB"
)
// DockerHub returns the DockerHub object.
func (service *DockerHubService) DockerHub() (*portainer.DockerHub, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(dockerhubBucketName))
value := bucket.Get([]byte(dbDockerHubKey))
if value == nil {
return portainer.ErrDockerHubNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var dockerhub portainer.DockerHub
err = internal.UnmarshalDockerHub(data, &dockerhub)
if err != nil {
return nil, err
}
return &dockerhub, nil
}
// StoreDockerHub persists a DockerHub object.
func (service *DockerHubService) StoreDockerHub(dockerhub *portainer.DockerHub) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(dockerhubBucketName))
data, err := internal.MarshalDockerHub(dockerhub)
if err != nil {
return err
}
err = bucket.Put([]byte(dbDockerHubKey), data)
if err != nil {
return err
}
return nil
})
}
+146
View File
@@ -0,0 +1,146 @@
package endpoint
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "endpoints"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// Endpoint returns an endpoint by ID.
func (service *Service) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
var endpoint portainer.Endpoint
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &endpoint)
if err != nil {
return nil, err
}
return &endpoint, nil
}
// UpdateEndpoint updates an endpoint.
func (service *Service) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, endpoint)
}
// DeleteEndpoint deletes an endpoint.
func (service *Service) DeleteEndpoint(ID portainer.EndpointID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
// Endpoints return an array containing all the endpoints.
func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
var endpoints = make([]portainer.Endpoint, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var endpoint portainer.Endpoint
err := internal.UnmarshalObject(v, &endpoint)
if err != nil {
return err
}
endpoints = append(endpoints, endpoint)
}
return nil
})
return endpoints, err
}
// CreateEndpoint assign an ID to a new endpoint and saves it.
func (service *Service) CreateEndpoint(endpoint *portainer.Endpoint) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
// We manually manage sequences for endpoints
err := bucket.SetSequence(uint64(endpoint.ID))
if err != nil {
return err
}
data, err := internal.MarshalObject(endpoint)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(endpoint.ID)), data)
})
}
// GetNextIdentifier returns the next identifier for an endpoint.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
}
// Synchronize creates, updates and deletes endpoints inside a single transaction.
func (service *Service) Synchronize(toCreate, toUpdate, toDelete []*portainer.Endpoint) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
for _, endpoint := range toCreate {
id, _ := bucket.NextSequence()
endpoint.ID = portainer.EndpointID(id)
data, err := internal.MarshalObject(endpoint)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(endpoint.ID)), data)
if err != nil {
return err
}
}
for _, endpoint := range toUpdate {
data, err := internal.MarshalObject(endpoint)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(endpoint.ID)), data)
if err != nil {
return err
}
}
for _, endpoint := range toDelete {
err := bucket.Delete(internal.Itob(int(endpoint.ID)))
if err != nil {
return err
}
}
return nil
})
}
-154
View File
@@ -1,154 +0,0 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// EndpointService represents a service for managing endpoints.
type EndpointService struct {
store *Store
}
// Endpoint returns an endpoint by ID.
func (service *EndpointService) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
value := bucket.Get(internal.Itob(int(ID)))
if value == nil {
return portainer.ErrEndpointNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var endpoint portainer.Endpoint
err = internal.UnmarshalEndpoint(data, &endpoint)
if err != nil {
return nil, err
}
return &endpoint, nil
}
// Endpoints return an array containing all the endpoints.
func (service *EndpointService) Endpoints() ([]portainer.Endpoint, error) {
var endpoints = make([]portainer.Endpoint, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var endpoint portainer.Endpoint
err := internal.UnmarshalEndpoint(v, &endpoint)
if err != nil {
return err
}
endpoints = append(endpoints, endpoint)
}
return nil
})
if err != nil {
return nil, err
}
return endpoints, nil
}
// Synchronize creates, updates and deletes endpoints inside a single transaction.
func (service *EndpointService) Synchronize(toCreate, toUpdate, toDelete []*portainer.Endpoint) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
for _, endpoint := range toCreate {
err := storeNewEndpoint(endpoint, bucket)
if err != nil {
return err
}
}
for _, endpoint := range toUpdate {
err := marshalAndStoreEndpoint(endpoint, bucket)
if err != nil {
return err
}
}
for _, endpoint := range toDelete {
err := bucket.Delete(internal.Itob(int(endpoint.ID)))
if err != nil {
return err
}
}
return nil
})
}
// CreateEndpoint assign an ID to a new endpoint and saves it.
func (service *EndpointService) CreateEndpoint(endpoint *portainer.Endpoint) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
err := storeNewEndpoint(endpoint, bucket)
if err != nil {
return err
}
return nil
})
}
// UpdateEndpoint updates an endpoint.
func (service *EndpointService) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
data, err := internal.MarshalEndpoint(endpoint)
if err != nil {
return err
}
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
err = bucket.Put(internal.Itob(int(ID)), data)
if err != nil {
return err
}
return nil
})
}
// DeleteEndpoint deletes an endpoint.
func (service *EndpointService) DeleteEndpoint(ID portainer.EndpointID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
err := bucket.Delete(internal.Itob(int(ID)))
if err != nil {
return err
}
return nil
})
}
func marshalAndStoreEndpoint(endpoint *portainer.Endpoint, bucket *bolt.Bucket) error {
data, err := internal.MarshalEndpoint(endpoint)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(endpoint.ID)), data)
if err != nil {
return err
}
return nil
}
func storeNewEndpoint(endpoint *portainer.Endpoint, bucket *bolt.Bucket) error {
id, _ := bucket.NextSequence()
endpoint.ID = portainer.EndpointID(id)
return marshalAndStoreEndpoint(endpoint, bucket)
}
+95
View File
@@ -0,0 +1,95 @@
package endpointgroup
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "endpoint_groups"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// EndpointGroup returns an endpoint group by ID.
func (service *Service) EndpointGroup(ID portainer.EndpointGroupID) (*portainer.EndpointGroup, error) {
var endpointGroup portainer.EndpointGroup
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &endpointGroup)
if err != nil {
return nil, err
}
return &endpointGroup, nil
}
// UpdateEndpointGroup updates an endpoint group.
func (service *Service) UpdateEndpointGroup(ID portainer.EndpointGroupID, endpointGroup *portainer.EndpointGroup) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, endpointGroup)
}
// DeleteEndpointGroup deletes an endpoint group.
func (service *Service) DeleteEndpointGroup(ID portainer.EndpointGroupID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
// EndpointGroups return an array containing all the endpoint groups.
func (service *Service) EndpointGroups() ([]portainer.EndpointGroup, error) {
var endpointGroups = make([]portainer.EndpointGroup, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var endpointGroup portainer.EndpointGroup
err := internal.UnmarshalObject(v, &endpointGroup)
if err != nil {
return err
}
endpointGroups = append(endpointGroups, endpointGroup)
}
return nil
})
return endpointGroups, err
}
// CreateEndpointGroup assign an ID to a new endpoint group and saves it.
func (service *Service) CreateEndpointGroup(endpointGroup *portainer.EndpointGroup) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
endpointGroup.ID = portainer.EndpointGroupID(id)
data, err := internal.MarshalObject(endpointGroup)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(endpointGroup.ID)), data)
})
}
+86
View File
@@ -0,0 +1,86 @@
package extension
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "extension"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// Extension returns a extension by ID
func (service *Service) Extension(ID portainer.ExtensionID) (*portainer.Extension, error) {
var extension portainer.Extension
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &extension)
if err != nil {
return nil, err
}
return &extension, nil
}
// Extensions return an array containing all the extensions.
func (service *Service) Extensions() ([]portainer.Extension, error) {
var extensions = make([]portainer.Extension, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var extension portainer.Extension
err := internal.UnmarshalObject(v, &extension)
if err != nil {
return err
}
extensions = append(extensions, extension)
}
return nil
})
return extensions, err
}
// Persist persists a extension inside the database.
func (service *Service) Persist(extension *portainer.Extension) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data, err := internal.MarshalObject(extension)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(extension.ID)), data)
})
}
// DeleteExtension deletes a Extension.
func (service *Service) DeleteExtension(ID portainer.ExtensionID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
+94
View File
@@ -0,0 +1,94 @@
package internal
import (
"encoding/binary"
"github.com/boltdb/bolt"
"github.com/portainer/portainer"
)
// Itob returns an 8-byte big endian representation of v.
// This function is typically used for encoding integer IDs to byte slices
// so that they can be used as BoltDB keys.
func Itob(v int) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, uint64(v))
return b
}
// CreateBucket is a generic function used to create a bucket inside a bolt database.
func CreateBucket(db *bolt.DB, bucketName string) error {
return db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(bucketName))
if err != nil {
return err
}
return nil
})
}
// GetObject is a generic function used to retrieve an unmarshalled object from a bolt database.
func GetObject(db *bolt.DB, bucketName string, key []byte, object interface{}) error {
var data []byte
err := db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
value := bucket.Get(key)
if value == nil {
return portainer.ErrObjectNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return err
}
return UnmarshalObject(data, object)
}
// UpdateObject is a generic function used to update an object inside a bolt database.
func UpdateObject(db *bolt.DB, bucketName string, key []byte, object interface{}) error {
return db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
data, err := MarshalObject(object)
if err != nil {
return err
}
err = bucket.Put(key, data)
if err != nil {
return err
}
return nil
})
}
// DeleteObject is a generic function used to delete an object inside a bolt database.
func DeleteObject(db *bolt.DB, bucketName string, key []byte) error {
return db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
return bucket.Delete(key)
})
}
// GetNextIdentifier is a generic function that returns the specified bucket identifier incremented by 1.
func GetNextIdentifier(db *bolt.DB, bucketName string) int {
var identifier int
db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
id := bucket.Sequence()
identifier = int(id)
return nil
})
identifier++
return identifier
}
-107
View File
@@ -1,107 +0,0 @@
package internal
import (
"github.com/portainer/portainer"
"encoding/binary"
"encoding/json"
)
// MarshalUser encodes a user to binary format.
func MarshalUser(user *portainer.User) ([]byte, error) {
return json.Marshal(user)
}
// UnmarshalUser decodes a user from a binary data.
func UnmarshalUser(data []byte, user *portainer.User) error {
return json.Unmarshal(data, user)
}
// MarshalTeam encodes a team to binary format.
func MarshalTeam(team *portainer.Team) ([]byte, error) {
return json.Marshal(team)
}
// UnmarshalTeam decodes a team from a binary data.
func UnmarshalTeam(data []byte, team *portainer.Team) error {
return json.Unmarshal(data, team)
}
// MarshalTeamMembership encodes a team membership to binary format.
func MarshalTeamMembership(membership *portainer.TeamMembership) ([]byte, error) {
return json.Marshal(membership)
}
// UnmarshalTeamMembership decodes a team membership from a binary data.
func UnmarshalTeamMembership(data []byte, membership *portainer.TeamMembership) error {
return json.Unmarshal(data, membership)
}
// MarshalEndpoint encodes an endpoint to binary format.
func MarshalEndpoint(endpoint *portainer.Endpoint) ([]byte, error) {
return json.Marshal(endpoint)
}
// UnmarshalEndpoint decodes an endpoint from a binary data.
func UnmarshalEndpoint(data []byte, endpoint *portainer.Endpoint) error {
return json.Unmarshal(data, endpoint)
}
// MarshalStack encodes a stack to binary format.
func MarshalStack(stack *portainer.Stack) ([]byte, error) {
return json.Marshal(stack)
}
// UnmarshalStack decodes a stack from a binary data.
func UnmarshalStack(data []byte, stack *portainer.Stack) error {
return json.Unmarshal(data, stack)
}
// MarshalRegistry encodes a registry to binary format.
func MarshalRegistry(registry *portainer.Registry) ([]byte, error) {
return json.Marshal(registry)
}
// UnmarshalRegistry decodes a registry from a binary data.
func UnmarshalRegistry(data []byte, registry *portainer.Registry) error {
return json.Unmarshal(data, registry)
}
// MarshalResourceControl encodes a resource control object to binary format.
func MarshalResourceControl(rc *portainer.ResourceControl) ([]byte, error) {
return json.Marshal(rc)
}
// UnmarshalResourceControl decodes a resource control object from a binary data.
func UnmarshalResourceControl(data []byte, rc *portainer.ResourceControl) error {
return json.Unmarshal(data, rc)
}
// MarshalSettings encodes a settings object to binary format.
func MarshalSettings(settings *portainer.Settings) ([]byte, error) {
return json.Marshal(settings)
}
// UnmarshalSettings decodes a settings object from a binary data.
func UnmarshalSettings(data []byte, settings *portainer.Settings) error {
return json.Unmarshal(data, settings)
}
// MarshalDockerHub encodes a Dockerhub object to binary format.
func MarshalDockerHub(settings *portainer.DockerHub) ([]byte, error) {
return json.Marshal(settings)
}
// UnmarshalDockerHub decodes a Dockerhub object from a binary data.
func UnmarshalDockerHub(data []byte, settings *portainer.DockerHub) error {
return json.Unmarshal(data, settings)
}
// Itob returns an 8-byte big endian representation of v.
// This function is typically used for encoding integer IDs to byte slices
// so that they can be used as BoltDB keys.
func Itob(v int) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, uint64(v))
return b
}
+15
View File
@@ -0,0 +1,15 @@
package internal
import (
"encoding/json"
)
// MarshalObject encodes an object to binary format
func MarshalObject(object interface{}) ([]byte, error) {
return json.Marshal(object)
}
// UnmarshalObject decodes an object from binary data
func UnmarshalObject(data []byte, object interface{}) error {
return json.Unmarshal(data, object)
}
-16
View File
@@ -1,16 +0,0 @@
package bolt
func (m *Migrator) updateSettingsToVersion5() error {
legacySettings, err := m.SettingsService.Settings()
if err != nil {
return err
}
legacySettings.AllowBindMountsForRegularUsers = true
err = m.SettingsService.StoreSettings(legacySettings)
if err != nil {
return err
}
return nil
}
-16
View File
@@ -1,16 +0,0 @@
package bolt
func (m *Migrator) updateSettingsToVersion6() error {
legacySettings, err := m.SettingsService.Settings()
if err != nil {
return err
}
legacySettings.AllowPrivilegedModeForRegularUsers = true
err = m.SettingsService.StoreSettings(legacySettings)
if err != nil {
return err
}
return nil
}
-16
View File
@@ -1,16 +0,0 @@
package bolt
func (m *Migrator) updateSettingsToVersion7() error {
legacySettings, err := m.SettingsService.Settings()
if err != nil {
return err
}
legacySettings.DisplayDonationHeader = true
err = m.SettingsService.StoreSettings(legacySettings)
if err != nil {
return err
}
return nil
}
-104
View File
@@ -1,104 +0,0 @@
package bolt
import "github.com/portainer/portainer"
// Migrator defines a service to migrate data after a Portainer version update.
type Migrator struct {
UserService *UserService
EndpointService *EndpointService
ResourceControlService *ResourceControlService
SettingsService *SettingsService
VersionService *VersionService
CurrentDBVersion int
store *Store
}
// NewMigrator creates a new Migrator.
func NewMigrator(store *Store, version int) *Migrator {
return &Migrator{
UserService: store.UserService,
EndpointService: store.EndpointService,
ResourceControlService: store.ResourceControlService,
SettingsService: store.SettingsService,
VersionService: store.VersionService,
CurrentDBVersion: version,
store: store,
}
}
// Migrate checks the database version and migrate the existing data to the most recent data model.
func (m *Migrator) Migrate() error {
// Portainer < 1.12
if m.CurrentDBVersion < 1 {
err := m.updateAdminUserToDBVersion1()
if err != nil {
return err
}
}
// Portainer 1.12.x
if m.CurrentDBVersion < 2 {
err := m.updateResourceControlsToDBVersion2()
if err != nil {
return err
}
err = m.updateEndpointsToDBVersion2()
if err != nil {
return err
}
}
// Portainer 1.13.x
if m.CurrentDBVersion < 3 {
err := m.updateSettingsToDBVersion3()
if err != nil {
return err
}
}
// Portainer 1.14.0
if m.CurrentDBVersion < 4 {
err := m.updateEndpointsToDBVersion4()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1235
if m.CurrentDBVersion < 5 {
err := m.updateSettingsToVersion5()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1236
if m.CurrentDBVersion < 6 {
err := m.updateSettingsToVersion6()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1449
if m.CurrentDBVersion < 7 {
err := m.updateSettingsToVersion7()
if err != nil {
return err
}
}
if m.CurrentDBVersion < 8 {
err := m.updateEndpointsToVersion8()
if err != nil {
return err
}
}
err := m.VersionService.StoreDBVersion(portainer.DBVersion)
if err != nil {
return err
}
return nil
}
@@ -1,19 +1,20 @@
package bolt
package migrator
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/user"
)
func (m *Migrator) updateAdminUserToDBVersion1() error {
u, err := m.UserService.UserByUsername("admin")
u, err := m.userService.UserByUsername("admin")
if err == nil {
admin := &portainer.User{
Username: "admin",
Password: u.Password,
Role: portainer.AdministratorRole,
}
err = m.UserService.CreateUser(admin)
err = m.userService.CreateUser(admin)
if err != nil {
return err
}
@@ -21,19 +22,15 @@ func (m *Migrator) updateAdminUserToDBVersion1() error {
if err != nil {
return err
}
} else if err != nil && err != portainer.ErrUserNotFound {
} else if err != nil && err != portainer.ErrObjectNotFound {
return err
}
return nil
}
func (m *Migrator) removeLegacyAdminUser() error {
return m.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
err := bucket.Delete([]byte("admin"))
if err != nil {
return err
}
return nil
return m.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(user.BucketName))
return bucket.Delete([]byte("admin"))
})
}
@@ -1,4 +1,4 @@
package bolt
package migrator
import (
"github.com/boltdb/bolt"
@@ -16,7 +16,7 @@ func (m *Migrator) updateResourceControlsToDBVersion2() error {
resourceControl.SubResourceIDs = []string{}
resourceControl.TeamAccesses = []portainer.TeamResourceAccess{}
owner, err := m.UserService.User(resourceControl.OwnerID)
owner, err := m.userService.User(resourceControl.OwnerID)
if err != nil {
return err
}
@@ -33,7 +33,7 @@ func (m *Migrator) updateResourceControlsToDBVersion2() error {
resourceControl.UserAccesses = []portainer.UserResourceAccess{userAccess}
}
err = m.ResourceControlService.CreateResourceControl(&resourceControl)
err = m.resourceControlService.CreateResourceControl(&resourceControl)
if err != nil {
return err
}
@@ -43,14 +43,14 @@ func (m *Migrator) updateResourceControlsToDBVersion2() error {
}
func (m *Migrator) updateEndpointsToDBVersion2() error {
legacyEndpoints, err := m.EndpointService.Endpoints()
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
endpoint.AuthorizedTeams = []portainer.TeamID{}
err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
@@ -61,12 +61,12 @@ func (m *Migrator) updateEndpointsToDBVersion2() error {
func (m *Migrator) retrieveLegacyResourceControls() ([]portainer.ResourceControl, error) {
legacyResourceControls := make([]portainer.ResourceControl, 0)
err := m.store.db.View(func(tx *bolt.Tx) error {
err := m.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte("containerResourceControl"))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var resourceControl portainer.ResourceControl
err := internal.UnmarshalResourceControl(v, &resourceControl)
err := internal.UnmarshalObject(v, &resourceControl)
if err != nil {
return err
}
@@ -78,7 +78,7 @@ func (m *Migrator) retrieveLegacyResourceControls() ([]portainer.ResourceControl
cursor = bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var resourceControl portainer.ResourceControl
err := internal.UnmarshalResourceControl(v, &resourceControl)
err := internal.UnmarshalObject(v, &resourceControl)
if err != nil {
return err
}
@@ -90,7 +90,7 @@ func (m *Migrator) retrieveLegacyResourceControls() ([]portainer.ResourceControl
cursor = bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var resourceControl portainer.ResourceControl
err := internal.UnmarshalResourceControl(v, &resourceControl)
err := internal.UnmarshalObject(v, &resourceControl)
if err != nil {
return err
}
+28
View File
@@ -0,0 +1,28 @@
package migrator
import "github.com/portainer/portainer"
func (m *Migrator) updateEndpointsToVersion11() error {
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
if endpoint.Type == portainer.AgentOnDockerEnvironment {
endpoint.TLSConfig.TLS = true
endpoint.TLSConfig.TLSSkipVerify = true
} else {
if endpoint.TLSConfig.TLSSkipVerify && !endpoint.TLSConfig.TLS {
endpoint.TLSConfig.TLSSkipVerify = false
}
}
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}
+127
View File
@@ -0,0 +1,127 @@
package migrator
import (
"strconv"
"strings"
"github.com/boltdb/bolt"
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/portainer/portainer/bolt/stack"
)
func (m *Migrator) updateEndpointsToVersion12() error {
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
endpoint.Tags = []string{}
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) updateEndpointGroupsToVersion12() error {
legacyEndpointGroups, err := m.endpointGroupService.EndpointGroups()
if err != nil {
return err
}
for _, group := range legacyEndpointGroups {
group.Tags = []string{}
err = m.endpointGroupService.UpdateEndpointGroup(group.ID, &group)
if err != nil {
return err
}
}
return nil
}
type legacyStack struct {
ID string `json:"Id"`
Name string `json:"Name"`
EndpointID portainer.EndpointID `json:"EndpointId"`
SwarmID string `json:"SwarmId"`
EntryPoint string `json:"EntryPoint"`
Env []portainer.Pair `json:"Env"`
ProjectPath string
}
func (m *Migrator) updateStacksToVersion12() error {
legacyStacks, err := m.retrieveLegacyStacks()
if err != nil {
return err
}
for _, legacyStack := range legacyStacks {
err := m.convertLegacyStack(&legacyStack)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) convertLegacyStack(s *legacyStack) error {
stackID := m.stackService.GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Name: s.Name,
Type: portainer.DockerSwarmStack,
SwarmID: s.SwarmID,
EndpointID: 0,
EntryPoint: s.EntryPoint,
Env: s.Env,
}
stack.ProjectPath = strings.Replace(s.ProjectPath, s.ID, strconv.Itoa(stackID), 1)
err := m.fileService.Rename(s.ProjectPath, stack.ProjectPath)
if err != nil {
return err
}
err = m.deleteLegacyStack(s.ID)
if err != nil {
return err
}
return m.stackService.CreateStack(stack)
}
func (m *Migrator) deleteLegacyStack(legacyID string) error {
return m.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(stack.BucketName))
return bucket.Delete([]byte(legacyID))
})
}
func (m *Migrator) retrieveLegacyStacks() ([]legacyStack, error) {
var legacyStacks = make([]legacyStack, 0)
err := m.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(stack.BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var stack legacyStack
err := internal.UnmarshalObject(v, &stack)
if err != nil {
return err
}
legacyStacks = append(legacyStacks, stack)
}
return nil
})
return legacyStacks, err
}
+17
View File
@@ -0,0 +1,17 @@
package migrator
import "github.com/portainer/portainer"
func (m *Migrator) updateSettingsToVersion13() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.LDAPSettings.AutoCreateUsers = false
legacySettings.LDAPSettings.GroupSearchSettings = []portainer.LDAPGroupSearchSettings{
portainer.LDAPGroupSearchSettings{},
}
return m.settingsService.UpdateSettings(legacySettings)
}
+19
View File
@@ -0,0 +1,19 @@
package migrator
func (m *Migrator) updateResourceControlsToDBVersion14() error {
resourceControls, err := m.resourceControlService.ResourceControls()
if err != nil {
return err
}
for _, resourceControl := range resourceControls {
if resourceControl.AdministratorsOnly == true {
err = m.resourceControlService.DeleteResourceControl(resourceControl.ID)
if err != nil {
return err
}
}
}
return nil
}
+35
View File
@@ -0,0 +1,35 @@
package migrator
import (
"strings"
"github.com/portainer/portainer"
)
func (m *Migrator) updateSettingsToDBVersion15() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.EnableHostManagementFeatures = false
return m.settingsService.UpdateSettings(legacySettings)
}
func (m *Migrator) updateTemplatesToVersion15() error {
legacyTemplates, err := m.templateService.Templates()
if err != nil {
return err
}
for _, template := range legacyTemplates {
template.Logo = strings.Replace(template.Logo, "https://portainer.io/images", portainer.AssetsServerURL, -1)
err = m.templateService.UpdateTemplate(template.ID, &template)
if err != nil {
return err
}
}
return nil
}
+14
View File
@@ -0,0 +1,14 @@
package migrator
func (m *Migrator) updateSettingsToDBVersion16() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
if legacySettings.SnapshotInterval == "" {
legacySettings.SnapshotInterval = "5m"
}
return m.settingsService.UpdateSettings(legacySettings)
}
+19
View File
@@ -0,0 +1,19 @@
package migrator
func (m *Migrator) updateExtensionsToDBVersion17() error {
legacyExtensions, err := m.extensionService.Extensions()
if err != nil {
return err
}
for _, extension := range legacyExtensions {
extension.License.Valid = true
err = m.extensionService.Persist(&extension)
if err != nil {
return err
}
}
return nil
}
@@ -1,9 +1,9 @@
package bolt
package migrator
import "github.com/portainer/portainer"
func (m *Migrator) updateSettingsToDBVersion3() error {
legacySettings, err := m.SettingsService.Settings()
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
@@ -16,10 +16,5 @@ func (m *Migrator) updateSettingsToDBVersion3() error {
},
}
err = m.SettingsService.StoreSettings(legacySettings)
if err != nil {
return err
}
return nil
return m.settingsService.UpdateSettings(legacySettings)
}
@@ -1,9 +1,9 @@
package bolt
package migrator
import "github.com/portainer/portainer"
func (m *Migrator) updateEndpointsToDBVersion4() error {
legacyEndpoints, err := m.EndpointService.Endpoints()
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
@@ -17,7 +17,8 @@ func (m *Migrator) updateEndpointsToDBVersion4() error {
endpoint.TLSConfig.TLSCertPath = endpoint.TLSCertPath
endpoint.TLSConfig.TLSKeyPath = endpoint.TLSKeyPath
}
err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
+11
View File
@@ -0,0 +1,11 @@
package migrator
func (m *Migrator) updateSettingsToVersion5() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.AllowBindMountsForRegularUsers = true
return m.settingsService.UpdateSettings(legacySettings)
}
+11
View File
@@ -0,0 +1,11 @@
package migrator
func (m *Migrator) updateSettingsToVersion6() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.AllowPrivilegedModeForRegularUsers = true
return m.settingsService.UpdateSettings(legacySettings)
}
+11
View File
@@ -0,0 +1,11 @@
package migrator
func (m *Migrator) updateSettingsToVersion7() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.DisplayDonationHeader = true
return m.settingsService.UpdateSettings(legacySettings)
}
@@ -1,16 +1,16 @@
package bolt
package migrator
import "github.com/portainer/portainer"
func (m *Migrator) updateEndpointsToVersion8() error {
legacyEndpoints, err := m.EndpointService.Endpoints()
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
endpoint.Extensions = []portainer.EndpointExtension{}
err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
+20
View File
@@ -0,0 +1,20 @@
package migrator
import "github.com/portainer/portainer"
func (m *Migrator) updateEndpointsToVersion9() error {
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
endpoint.GroupID = portainer.EndpointGroupID(1)
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}
+20
View File
@@ -0,0 +1,20 @@
package migrator
import "github.com/portainer/portainer"
func (m *Migrator) updateEndpointsToVersion10() error {
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
endpoint.Type = portainer.DockerEnvironment
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}
+226
View File
@@ -0,0 +1,226 @@
package migrator
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/endpoint"
"github.com/portainer/portainer/bolt/endpointgroup"
"github.com/portainer/portainer/bolt/extension"
"github.com/portainer/portainer/bolt/resourcecontrol"
"github.com/portainer/portainer/bolt/settings"
"github.com/portainer/portainer/bolt/stack"
"github.com/portainer/portainer/bolt/template"
"github.com/portainer/portainer/bolt/user"
"github.com/portainer/portainer/bolt/version"
)
type (
// Migrator defines a service to migrate data after a Portainer version update.
Migrator struct {
currentDBVersion int
db *bolt.DB
endpointGroupService *endpointgroup.Service
endpointService *endpoint.Service
extensionService *extension.Service
resourceControlService *resourcecontrol.Service
settingsService *settings.Service
stackService *stack.Service
templateService *template.Service
userService *user.Service
versionService *version.Service
fileService portainer.FileService
}
// Parameters represents the required parameters to create a new Migrator instance.
Parameters struct {
DB *bolt.DB
DatabaseVersion int
EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service
ExtensionService *extension.Service
ResourceControlService *resourcecontrol.Service
SettingsService *settings.Service
StackService *stack.Service
TemplateService *template.Service
UserService *user.Service
VersionService *version.Service
FileService portainer.FileService
}
)
// NewMigrator creates a new Migrator.
func NewMigrator(parameters *Parameters) *Migrator {
return &Migrator{
db: parameters.DB,
currentDBVersion: parameters.DatabaseVersion,
endpointGroupService: parameters.EndpointGroupService,
endpointService: parameters.EndpointService,
extensionService: parameters.ExtensionService,
resourceControlService: parameters.ResourceControlService,
settingsService: parameters.SettingsService,
templateService: parameters.TemplateService,
stackService: parameters.StackService,
userService: parameters.UserService,
versionService: parameters.VersionService,
fileService: parameters.FileService,
}
}
// Migrate checks the database version and migrate the existing data to the most recent data model.
func (m *Migrator) Migrate() error {
// Portainer < 1.12
if m.currentDBVersion < 1 {
err := m.updateAdminUserToDBVersion1()
if err != nil {
return err
}
}
// Portainer 1.12.x
if m.currentDBVersion < 2 {
err := m.updateResourceControlsToDBVersion2()
if err != nil {
return err
}
err = m.updateEndpointsToDBVersion2()
if err != nil {
return err
}
}
// Portainer 1.13.x
if m.currentDBVersion < 3 {
err := m.updateSettingsToDBVersion3()
if err != nil {
return err
}
}
// Portainer 1.14.0
if m.currentDBVersion < 4 {
err := m.updateEndpointsToDBVersion4()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1235
if m.currentDBVersion < 5 {
err := m.updateSettingsToVersion5()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1236
if m.currentDBVersion < 6 {
err := m.updateSettingsToVersion6()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1449
if m.currentDBVersion < 7 {
err := m.updateSettingsToVersion7()
if err != nil {
return err
}
}
if m.currentDBVersion < 8 {
err := m.updateEndpointsToVersion8()
if err != nil {
return err
}
}
// https: //github.com/portainer/portainer/issues/1396
if m.currentDBVersion < 9 {
err := m.updateEndpointsToVersion9()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/461
if m.currentDBVersion < 10 {
err := m.updateEndpointsToVersion10()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1906
if m.currentDBVersion < 11 {
err := m.updateEndpointsToVersion11()
if err != nil {
return err
}
}
// Portainer 1.18.0
if m.currentDBVersion < 12 {
err := m.updateEndpointsToVersion12()
if err != nil {
return err
}
err = m.updateEndpointGroupsToVersion12()
if err != nil {
return err
}
err = m.updateStacksToVersion12()
if err != nil {
return err
}
}
// Portainer 1.19.0
if m.currentDBVersion < 13 {
err := m.updateSettingsToVersion13()
if err != nil {
return err
}
}
// Portainer 1.19.2
if m.currentDBVersion < 14 {
err := m.updateResourceControlsToDBVersion14()
if err != nil {
return err
}
}
// Portainer 1.20.0
if m.currentDBVersion < 15 {
err := m.updateSettingsToDBVersion15()
if err != nil {
return err
}
err = m.updateTemplatesToVersion15()
if err != nil {
return err
}
}
if m.currentDBVersion < 16 {
err := m.updateSettingsToDBVersion16()
if err != nil {
return err
}
}
// Portainer 1.20.1
if m.currentDBVersion < 17 {
err := m.updateExtensionsToDBVersion17()
if err != nil {
return err
}
}
return m.versionService.StoreDBVersion(portainer.DBVersion)
}
+95
View File
@@ -0,0 +1,95 @@
package registry
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "registries"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// Registry returns an registry by ID.
func (service *Service) Registry(ID portainer.RegistryID) (*portainer.Registry, error) {
var registry portainer.Registry
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &registry)
if err != nil {
return nil, err
}
return &registry, nil
}
// Registries returns an array containing all the registries.
func (service *Service) Registries() ([]portainer.Registry, error) {
var registries = make([]portainer.Registry, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var registry portainer.Registry
err := internal.UnmarshalObject(v, &registry)
if err != nil {
return err
}
registries = append(registries, registry)
}
return nil
})
return registries, err
}
// CreateRegistry creates a new registry.
func (service *Service) CreateRegistry(registry *portainer.Registry) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
registry.ID = portainer.RegistryID(id)
data, err := internal.MarshalObject(registry)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(registry.ID)), data)
})
}
// UpdateRegistry updates an registry.
func (service *Service) UpdateRegistry(ID portainer.RegistryID, registry *portainer.Registry) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, registry)
}
// DeleteRegistry deletes an registry.
func (service *Service) DeleteRegistry(ID portainer.RegistryID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
-114
View File
@@ -1,114 +0,0 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// RegistryService represents a service for managing registries.
type RegistryService struct {
store *Store
}
// Registry returns an registry by ID.
func (service *RegistryService) Registry(ID portainer.RegistryID) (*portainer.Registry, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(registryBucketName))
value := bucket.Get(internal.Itob(int(ID)))
if value == nil {
return portainer.ErrRegistryNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var registry portainer.Registry
err = internal.UnmarshalRegistry(data, &registry)
if err != nil {
return nil, err
}
return &registry, nil
}
// Registries returns an array containing all the registries.
func (service *RegistryService) Registries() ([]portainer.Registry, error) {
var registries = make([]portainer.Registry, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(registryBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var registry portainer.Registry
err := internal.UnmarshalRegistry(v, &registry)
if err != nil {
return err
}
registries = append(registries, registry)
}
return nil
})
if err != nil {
return nil, err
}
return registries, nil
}
// CreateRegistry creates a new registry.
func (service *RegistryService) CreateRegistry(registry *portainer.Registry) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(registryBucketName))
id, _ := bucket.NextSequence()
registry.ID = portainer.RegistryID(id)
data, err := internal.MarshalRegistry(registry)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(registry.ID)), data)
if err != nil {
return err
}
return nil
})
}
// UpdateRegistry updates an registry.
func (service *RegistryService) UpdateRegistry(ID portainer.RegistryID, registry *portainer.Registry) error {
data, err := internal.MarshalRegistry(registry)
if err != nil {
return err
}
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(registryBucketName))
err = bucket.Put(internal.Itob(int(ID)), data)
if err != nil {
return err
}
return nil
})
}
// DeleteRegistry deletes an registry.
func (service *RegistryService) DeleteRegistry(ID portainer.RegistryID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(registryBucketName))
err := bucket.Delete(internal.Itob(int(ID)))
if err != nil {
return err
}
return nil
})
}
-148
View File
@@ -1,148 +0,0 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// ResourceControlService represents a service for managing resource controls.
type ResourceControlService struct {
store *Store
}
// ResourceControl returns a ResourceControl object by ID
func (service *ResourceControlService) ResourceControl(ID portainer.ResourceControlID) (*portainer.ResourceControl, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(resourceControlBucketName))
value := bucket.Get(internal.Itob(int(ID)))
if value == nil {
return portainer.ErrResourceControlNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var resourceControl portainer.ResourceControl
err = internal.UnmarshalResourceControl(data, &resourceControl)
if err != nil {
return nil, err
}
return &resourceControl, nil
}
// ResourceControlByResourceID returns a ResourceControl object by checking if the resourceID is equal
// to the main ResourceID or in SubResourceIDs
func (service *ResourceControlService) ResourceControlByResourceID(resourceID string) (*portainer.ResourceControl, error) {
var resourceControl *portainer.ResourceControl
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(resourceControlBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var rc portainer.ResourceControl
err := internal.UnmarshalResourceControl(v, &rc)
if err != nil {
return err
}
if rc.ResourceID == resourceID {
resourceControl = &rc
}
for _, subResourceID := range rc.SubResourceIDs {
if subResourceID == resourceID {
resourceControl = &rc
}
}
}
if resourceControl == nil {
return portainer.ErrResourceControlNotFound
}
return nil
})
if err != nil {
return nil, err
}
return resourceControl, nil
}
// ResourceControls returns all the ResourceControl objects
func (service *ResourceControlService) ResourceControls() ([]portainer.ResourceControl, error) {
var rcs = make([]portainer.ResourceControl, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(resourceControlBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var resourceControl portainer.ResourceControl
err := internal.UnmarshalResourceControl(v, &resourceControl)
if err != nil {
return err
}
rcs = append(rcs, resourceControl)
}
return nil
})
if err != nil {
return nil, err
}
return rcs, nil
}
// CreateResourceControl creates a new ResourceControl object
func (service *ResourceControlService) CreateResourceControl(resourceControl *portainer.ResourceControl) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(resourceControlBucketName))
id, _ := bucket.NextSequence()
resourceControl.ID = portainer.ResourceControlID(id)
data, err := internal.MarshalResourceControl(resourceControl)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(resourceControl.ID)), data)
if err != nil {
return err
}
return nil
})
}
// UpdateResourceControl saves a ResourceControl object.
func (service *ResourceControlService) UpdateResourceControl(ID portainer.ResourceControlID, resourceControl *portainer.ResourceControl) error {
data, err := internal.MarshalResourceControl(resourceControl)
if err != nil {
return err
}
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(resourceControlBucketName))
err = bucket.Put(internal.Itob(int(ID)), data)
if err != nil {
return err
}
return nil
})
}
// DeleteResourceControl deletes a ResourceControl object by ID
func (service *ResourceControlService) DeleteResourceControl(ID portainer.ResourceControlID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(resourceControlBucketName))
err := bucket.Delete(internal.Itob(int(ID)))
if err != nil {
return err
}
return nil
})
}
+134
View File
@@ -0,0 +1,134 @@
package resourcecontrol
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "resource_control"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// ResourceControl returns a ResourceControl object by ID
func (service *Service) ResourceControl(ID portainer.ResourceControlID) (*portainer.ResourceControl, error) {
var resourceControl portainer.ResourceControl
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &resourceControl)
if err != nil {
return nil, err
}
return &resourceControl, nil
}
// ResourceControlByResourceID returns a ResourceControl object by checking if the resourceID is equal
// to the main ResourceID or in SubResourceIDs
func (service *Service) ResourceControlByResourceID(resourceID string) (*portainer.ResourceControl, error) {
var resourceControl *portainer.ResourceControl
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var rc portainer.ResourceControl
err := internal.UnmarshalObject(v, &rc)
if err != nil {
return err
}
if rc.ResourceID == resourceID {
resourceControl = &rc
break
}
for _, subResourceID := range rc.SubResourceIDs {
if subResourceID == resourceID {
resourceControl = &rc
break
}
}
}
if resourceControl == nil {
return portainer.ErrObjectNotFound
}
return nil
})
return resourceControl, err
}
// ResourceControls returns all the ResourceControl objects
func (service *Service) ResourceControls() ([]portainer.ResourceControl, error) {
var rcs = make([]portainer.ResourceControl, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var resourceControl portainer.ResourceControl
err := internal.UnmarshalObject(v, &resourceControl)
if err != nil {
return err
}
rcs = append(rcs, resourceControl)
}
return nil
})
return rcs, err
}
// CreateResourceControl creates a new ResourceControl object
func (service *Service) CreateResourceControl(resourceControl *portainer.ResourceControl) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
resourceControl.ID = portainer.ResourceControlID(id)
data, err := internal.MarshalObject(resourceControl)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(resourceControl.ID)), data)
})
}
// UpdateResourceControl saves a ResourceControl object.
func (service *Service) UpdateResourceControl(ID portainer.ResourceControlID, resourceControl *portainer.ResourceControl) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, resourceControl)
}
// DeleteResourceControl deletes a ResourceControl object by ID
func (service *Service) DeleteResourceControl(ID portainer.ResourceControlID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
+129
View File
@@ -0,0 +1,129 @@
package schedule
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "schedules"
)
// Service represents a service for managing schedule data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// Schedule returns a schedule by ID.
func (service *Service) Schedule(ID portainer.ScheduleID) (*portainer.Schedule, error) {
var schedule portainer.Schedule
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &schedule)
if err != nil {
return nil, err
}
return &schedule, nil
}
// UpdateSchedule updates a schedule.
func (service *Service) UpdateSchedule(ID portainer.ScheduleID, schedule *portainer.Schedule) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, schedule)
}
// DeleteSchedule deletes a schedule.
func (service *Service) DeleteSchedule(ID portainer.ScheduleID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
// Schedules return a array containing all the schedules.
func (service *Service) Schedules() ([]portainer.Schedule, error) {
var schedules = make([]portainer.Schedule, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var schedule portainer.Schedule
err := internal.UnmarshalObject(v, &schedule)
if err != nil {
return err
}
schedules = append(schedules, schedule)
}
return nil
})
return schedules, err
}
// SchedulesByJobType return a array containing all the schedules
// with the specified JobType.
func (service *Service) SchedulesByJobType(jobType portainer.JobType) ([]portainer.Schedule, error) {
var schedules = make([]portainer.Schedule, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var schedule portainer.Schedule
err := internal.UnmarshalObject(v, &schedule)
if err != nil {
return err
}
if schedule.JobType == jobType {
schedules = append(schedules, schedule)
}
}
return nil
})
return schedules, err
}
// CreateSchedule assign an ID to a new schedule and saves it.
func (service *Service) CreateSchedule(schedule *portainer.Schedule) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
// We manually manage sequences for schedules
err := bucket.SetSequence(uint64(schedule.ID))
if err != nil {
return err
}
data, err := internal.MarshalObject(schedule)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(schedule.ID)), data)
})
}
// GetNextIdentifier returns the next identifier for a schedule.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
}
+48
View File
@@ -0,0 +1,48 @@
package settings
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "settings"
settingsKey = "SETTINGS"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// Settings retrieve the settings object.
func (service *Service) Settings() (*portainer.Settings, error) {
var settings portainer.Settings
err := internal.GetObject(service.db, BucketName, []byte(settingsKey), &settings)
if err != nil {
return nil, err
}
return &settings, nil
}
// UpdateSettings persists a Settings object.
func (service *Service) UpdateSettings(settings *portainer.Settings) error {
return internal.UpdateObject(service.db, BucketName, []byte(settingsKey), settings)
}
-61
View File
@@ -1,61 +0,0 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// SettingsService represents a service to manage application settings.
type SettingsService struct {
store *Store
}
const (
dbSettingsKey = "SETTINGS"
)
// Settings retrieve the settings object.
func (service *SettingsService) Settings() (*portainer.Settings, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(settingsBucketName))
value := bucket.Get([]byte(dbSettingsKey))
if value == nil {
return portainer.ErrSettingsNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var settings portainer.Settings
err = internal.UnmarshalSettings(data, &settings)
if err != nil {
return nil, err
}
return &settings, nil
}
// StoreSettings persists a Settings object.
func (service *SettingsService) StoreSettings(settings *portainer.Settings) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(settingsBucketName))
data, err := internal.MarshalSettings(settings)
if err != nil {
return err
}
err = bucket.Put([]byte(dbSettingsKey), data)
if err != nil {
return err
}
return nil
})
}
+134
View File
@@ -0,0 +1,134 @@
package stack
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "stacks"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// Stack returns a stack object by ID.
func (service *Service) Stack(ID portainer.StackID) (*portainer.Stack, error) {
var stack portainer.Stack
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &stack)
if err != nil {
return nil, err
}
return &stack, nil
}
// StackByName returns a stack object by name.
func (service *Service) StackByName(name string) (*portainer.Stack, error) {
var stack *portainer.Stack
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var t portainer.Stack
err := internal.UnmarshalObject(v, &t)
if err != nil {
return err
}
if t.Name == name {
stack = &t
break
}
}
if stack == nil {
return portainer.ErrObjectNotFound
}
return nil
})
return stack, err
}
// Stacks returns an array containing all the stacks.
func (service *Service) Stacks() ([]portainer.Stack, error) {
var stacks = make([]portainer.Stack, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var stack portainer.Stack
err := internal.UnmarshalObject(v, &stack)
if err != nil {
return err
}
stacks = append(stacks, stack)
}
return nil
})
return stacks, err
}
// GetNextIdentifier returns the next identifier for a stack.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
}
// CreateStack creates a new stack.
func (service *Service) CreateStack(stack *portainer.Stack) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
// We manually manage sequences for stacks
err := bucket.SetSequence(uint64(stack.ID))
if err != nil {
return err
}
data, err := internal.MarshalObject(stack)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(stack.ID)), data)
})
}
// UpdateStack updates a stack.
func (service *Service) UpdateStack(ID portainer.StackID, stack *portainer.Stack) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, stack)
}
// DeleteStack deletes a stack.
func (service *Service) DeleteStack(ID portainer.StackID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
-138
View File
@@ -1,138 +0,0 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// StackService represents a service for managing stacks.
type StackService struct {
store *Store
}
// Stack returns a stack object by ID.
func (service *StackService) Stack(ID portainer.StackID) (*portainer.Stack, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(stackBucketName))
value := bucket.Get([]byte(ID))
if value == nil {
return portainer.ErrStackNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var stack portainer.Stack
err = internal.UnmarshalStack(data, &stack)
if err != nil {
return nil, err
}
return &stack, nil
}
// Stacks returns an array containing all the stacks.
func (service *StackService) Stacks() ([]portainer.Stack, error) {
var stacks = make([]portainer.Stack, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(stackBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var stack portainer.Stack
err := internal.UnmarshalStack(v, &stack)
if err != nil {
return err
}
stacks = append(stacks, stack)
}
return nil
})
if err != nil {
return nil, err
}
return stacks, nil
}
// StacksBySwarmID return an array containing all the stacks related to the specified Swarm ID.
func (service *StackService) StacksBySwarmID(id string) ([]portainer.Stack, error) {
var stacks = make([]portainer.Stack, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(stackBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var stack portainer.Stack
err := internal.UnmarshalStack(v, &stack)
if err != nil {
return err
}
if stack.SwarmID == id {
stacks = append(stacks, stack)
}
}
return nil
})
if err != nil {
return nil, err
}
return stacks, nil
}
// CreateStack creates a new stack.
func (service *StackService) CreateStack(stack *portainer.Stack) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(stackBucketName))
data, err := internal.MarshalStack(stack)
if err != nil {
return err
}
err = bucket.Put([]byte(stack.ID), data)
if err != nil {
return err
}
return nil
})
}
// UpdateStack updates an stack.
func (service *StackService) UpdateStack(ID portainer.StackID, stack *portainer.Stack) error {
data, err := internal.MarshalStack(stack)
if err != nil {
return err
}
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(stackBucketName))
err = bucket.Put([]byte(ID), data)
if err != nil {
return err
}
return nil
})
}
// DeleteStack deletes an stack.
func (service *StackService) DeleteStack(ID portainer.StackID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(stackBucketName))
err := bucket.Delete([]byte(ID))
if err != nil {
return err
}
return nil
})
}
+76
View File
@@ -0,0 +1,76 @@
package tag
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "tags"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// Tags return an array containing all the tags.
func (service *Service) Tags() ([]portainer.Tag, error) {
var tags = make([]portainer.Tag, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var tag portainer.Tag
err := internal.UnmarshalObject(v, &tag)
if err != nil {
return err
}
tags = append(tags, tag)
}
return nil
})
return tags, err
}
// CreateTag creates a new tag.
func (service *Service) CreateTag(tag *portainer.Tag) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
tag.ID = portainer.TagID(id)
data, err := internal.MarshalObject(tag)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(tag.ID)), data)
})
}
// DeleteTag deletes a tag.
func (service *Service) DeleteTag(ID portainer.TagID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
+126
View File
@@ -0,0 +1,126 @@
package team
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "teams"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// Team returns a Team by ID
func (service *Service) Team(ID portainer.TeamID) (*portainer.Team, error) {
var team portainer.Team
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &team)
if err != nil {
return nil, err
}
return &team, nil
}
// TeamByName returns a team by name.
func (service *Service) TeamByName(name string) (*portainer.Team, error) {
var team *portainer.Team
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var t portainer.Team
err := internal.UnmarshalObject(v, &t)
if err != nil {
return err
}
if t.Name == name {
team = &t
break
}
}
if team == nil {
return portainer.ErrObjectNotFound
}
return nil
})
return team, err
}
// Teams return an array containing all the teams.
func (service *Service) Teams() ([]portainer.Team, error) {
var teams = make([]portainer.Team, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var team portainer.Team
err := internal.UnmarshalObject(v, &team)
if err != nil {
return err
}
teams = append(teams, team)
}
return nil
})
return teams, err
}
// UpdateTeam saves a Team.
func (service *Service) UpdateTeam(ID portainer.TeamID, team *portainer.Team) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, team)
}
// CreateTeam creates a new Team.
func (service *Service) CreateTeam(team *portainer.Team) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
team.ID = portainer.TeamID(id)
data, err := internal.MarshalObject(team)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(team.ID)), data)
})
}
// DeleteTeam deletes a Team.
func (service *Service) DeleteTeam(ID portainer.TeamID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
-217
View File
@@ -1,217 +0,0 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// TeamMembershipService represents a service for managing TeamMembership objects.
type TeamMembershipService struct {
store *Store
}
// TeamMembership returns a TeamMembership object by ID
func (service *TeamMembershipService) TeamMembership(ID portainer.TeamMembershipID) (*portainer.TeamMembership, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
value := bucket.Get(internal.Itob(int(ID)))
if value == nil {
return portainer.ErrTeamMembershipNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var membership portainer.TeamMembership
err = internal.UnmarshalTeamMembership(data, &membership)
if err != nil {
return nil, err
}
return &membership, nil
}
// TeamMemberships return an array containing all the TeamMembership objects.
func (service *TeamMembershipService) TeamMemberships() ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalTeamMembership(v, &membership)
if err != nil {
return err
}
memberships = append(memberships, membership)
}
return nil
})
if err != nil {
return nil, err
}
return memberships, nil
}
// TeamMembershipsByUserID return an array containing all the TeamMembership objects where the specified userID is present.
func (service *TeamMembershipService) TeamMembershipsByUserID(userID portainer.UserID) ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalTeamMembership(v, &membership)
if err != nil {
return err
}
if membership.UserID == userID {
memberships = append(memberships, membership)
}
}
return nil
})
if err != nil {
return nil, err
}
return memberships, nil
}
// TeamMembershipsByTeamID return an array containing all the TeamMembership objects where the specified teamID is present.
func (service *TeamMembershipService) TeamMembershipsByTeamID(teamID portainer.TeamID) ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalTeamMembership(v, &membership)
if err != nil {
return err
}
if membership.TeamID == teamID {
memberships = append(memberships, membership)
}
}
return nil
})
if err != nil {
return nil, err
}
return memberships, nil
}
// UpdateTeamMembership saves a TeamMembership object.
func (service *TeamMembershipService) UpdateTeamMembership(ID portainer.TeamMembershipID, membership *portainer.TeamMembership) error {
data, err := internal.MarshalTeamMembership(membership)
if err != nil {
return err
}
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
err = bucket.Put(internal.Itob(int(ID)), data)
if err != nil {
return err
}
return nil
})
}
// CreateTeamMembership creates a new TeamMembership object.
func (service *TeamMembershipService) CreateTeamMembership(membership *portainer.TeamMembership) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
id, _ := bucket.NextSequence()
membership.ID = portainer.TeamMembershipID(id)
data, err := internal.MarshalTeamMembership(membership)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(membership.ID)), data)
if err != nil {
return err
}
return nil
})
}
// DeleteTeamMembership deletes a TeamMembership object.
func (service *TeamMembershipService) DeleteTeamMembership(ID portainer.TeamMembershipID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
err := bucket.Delete(internal.Itob(int(ID)))
if err != nil {
return err
}
return nil
})
}
// DeleteTeamMembershipByUserID deletes all the TeamMembership object associated to a UserID.
func (service *TeamMembershipService) DeleteTeamMembershipByUserID(userID portainer.UserID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalTeamMembership(v, &membership)
if err != nil {
return err
}
if membership.UserID == userID {
err := bucket.Delete(internal.Itob(int(membership.ID)))
if err != nil {
return err
}
}
}
return nil
})
}
// DeleteTeamMembershipByTeamID deletes all the TeamMembership object associated to a TeamID.
func (service *TeamMembershipService) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalTeamMembership(v, &membership)
if err != nil {
return err
}
if membership.TeamID == teamID {
err := bucket.Delete(internal.Itob(int(membership.ID)))
if err != nil {
return err
}
}
}
return nil
})
}
-144
View File
@@ -1,144 +0,0 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// TeamService represents a service for managing teams.
type TeamService struct {
store *Store
}
// Team returns a Team by ID
func (service *TeamService) Team(ID portainer.TeamID) (*portainer.Team, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamBucketName))
value := bucket.Get(internal.Itob(int(ID)))
if value == nil {
return portainer.ErrTeamNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var team portainer.Team
err = internal.UnmarshalTeam(data, &team)
if err != nil {
return nil, err
}
return &team, nil
}
// TeamByName returns a team by name.
func (service *TeamService) TeamByName(name string) (*portainer.Team, error) {
var team *portainer.Team
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var t portainer.Team
err := internal.UnmarshalTeam(v, &t)
if err != nil {
return err
}
if t.Name == name {
team = &t
}
}
if team == nil {
return portainer.ErrTeamNotFound
}
return nil
})
if err != nil {
return nil, err
}
return team, nil
}
// Teams return an array containing all the teams.
func (service *TeamService) Teams() ([]portainer.Team, error) {
var teams = make([]portainer.Team, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var team portainer.Team
err := internal.UnmarshalTeam(v, &team)
if err != nil {
return err
}
teams = append(teams, team)
}
return nil
})
if err != nil {
return nil, err
}
return teams, nil
}
// UpdateTeam saves a Team.
func (service *TeamService) UpdateTeam(ID portainer.TeamID, team *portainer.Team) error {
data, err := internal.MarshalTeam(team)
if err != nil {
return err
}
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamBucketName))
err = bucket.Put(internal.Itob(int(ID)), data)
if err != nil {
return err
}
return nil
})
}
// CreateTeam creates a new Team.
func (service *TeamService) CreateTeam(team *portainer.Team) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamBucketName))
id, _ := bucket.NextSequence()
team.ID = portainer.TeamID(id)
data, err := internal.MarshalTeam(team)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(team.ID)), data)
if err != nil {
return err
}
return nil
})
}
// DeleteTeam deletes a Team.
func (service *TeamService) DeleteTeam(ID portainer.TeamID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamBucketName))
err := bucket.Delete(internal.Itob(int(ID)))
if err != nil {
return err
}
return nil
})
}
+197
View File
@@ -0,0 +1,197 @@
package teammembership
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "team_membership"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// TeamMembership returns a TeamMembership object by ID
func (service *Service) TeamMembership(ID portainer.TeamMembershipID) (*portainer.TeamMembership, error) {
var membership portainer.TeamMembership
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &membership)
if err != nil {
return nil, err
}
return &membership, nil
}
// TeamMemberships return an array containing all the TeamMembership objects.
func (service *Service) TeamMemberships() ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalObject(v, &membership)
if err != nil {
return err
}
memberships = append(memberships, membership)
}
return nil
})
return memberships, err
}
// TeamMembershipsByUserID return an array containing all the TeamMembership objects where the specified userID is present.
func (service *Service) TeamMembershipsByUserID(userID portainer.UserID) ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalObject(v, &membership)
if err != nil {
return err
}
if membership.UserID == userID {
memberships = append(memberships, membership)
}
}
return nil
})
return memberships, err
}
// TeamMembershipsByTeamID return an array containing all the TeamMembership objects where the specified teamID is present.
func (service *Service) TeamMembershipsByTeamID(teamID portainer.TeamID) ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalObject(v, &membership)
if err != nil {
return err
}
if membership.TeamID == teamID {
memberships = append(memberships, membership)
}
}
return nil
})
return memberships, err
}
// UpdateTeamMembership saves a TeamMembership object.
func (service *Service) UpdateTeamMembership(ID portainer.TeamMembershipID, membership *portainer.TeamMembership) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, membership)
}
// CreateTeamMembership creates a new TeamMembership object.
func (service *Service) CreateTeamMembership(membership *portainer.TeamMembership) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
membership.ID = portainer.TeamMembershipID(id)
data, err := internal.MarshalObject(membership)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(membership.ID)), data)
})
}
// DeleteTeamMembership deletes a TeamMembership object.
func (service *Service) DeleteTeamMembership(ID portainer.TeamMembershipID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
// DeleteTeamMembershipByUserID deletes all the TeamMembership object associated to a UserID.
func (service *Service) DeleteTeamMembershipByUserID(userID portainer.UserID) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalObject(v, &membership)
if err != nil {
return err
}
if membership.UserID == userID {
err := bucket.Delete(internal.Itob(int(membership.ID)))
if err != nil {
return err
}
}
}
return nil
})
}
// DeleteTeamMembershipByTeamID deletes all the TeamMembership object associated to a TeamID.
func (service *Service) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalObject(v, &membership)
if err != nil {
return err
}
if membership.TeamID == teamID {
err := bucket.Delete(internal.Itob(int(membership.ID)))
if err != nil {
return err
}
}
}
return nil
})
}
+95
View File
@@ -0,0 +1,95 @@
package template
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "templates"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// Templates return an array containing all the templates.
func (service *Service) Templates() ([]portainer.Template, error) {
var templates = make([]portainer.Template, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var template portainer.Template
err := internal.UnmarshalObject(v, &template)
if err != nil {
return err
}
templates = append(templates, template)
}
return nil
})
return templates, err
}
// Template returns a template by ID.
func (service *Service) Template(ID portainer.TemplateID) (*portainer.Template, error) {
var template portainer.Template
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &template)
if err != nil {
return nil, err
}
return &template, nil
}
// CreateTemplate creates a new template.
func (service *Service) CreateTemplate(template *portainer.Template) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
template.ID = portainer.TemplateID(id)
data, err := internal.MarshalObject(template)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(template.ID)), data)
})
}
// UpdateTemplate saves a template.
func (service *Service) UpdateTemplate(ID portainer.TemplateID, template *portainer.Template) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, template)
}
// DeleteTemplate deletes a template.
func (service *Service) DeleteTemplate(ID portainer.TemplateID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
+149
View File
@@ -0,0 +1,149 @@
package user
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "users"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// User returns a user by ID
func (service *Service) User(ID portainer.UserID) (*portainer.User, error) {
var user portainer.User
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &user)
if err != nil {
return nil, err
}
return &user, nil
}
// UserByUsername returns a user by username.
func (service *Service) UserByUsername(username string) (*portainer.User, error) {
var user *portainer.User
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var u portainer.User
err := internal.UnmarshalObject(v, &u)
if err != nil {
return err
}
if u.Username == username {
user = &u
break
}
}
if user == nil {
return portainer.ErrObjectNotFound
}
return nil
})
return user, err
}
// Users return an array containing all the users.
func (service *Service) Users() ([]portainer.User, error) {
var users = make([]portainer.User, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var user portainer.User
err := internal.UnmarshalObject(v, &user)
if err != nil {
return err
}
users = append(users, user)
}
return nil
})
return users, err
}
// UsersByRole return an array containing all the users with the specified role.
func (service *Service) UsersByRole(role portainer.UserRole) ([]portainer.User, error) {
var users = make([]portainer.User, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var user portainer.User
err := internal.UnmarshalObject(v, &user)
if err != nil {
return err
}
if user.Role == role {
users = append(users, user)
}
}
return nil
})
return users, err
}
// UpdateUser saves a user.
func (service *Service) UpdateUser(ID portainer.UserID, user *portainer.User) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, user)
}
// CreateUser creates a new user.
func (service *Service) CreateUser(user *portainer.User) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
user.ID = portainer.UserID(id)
data, err := internal.MarshalObject(user)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(user.ID)), data)
})
}
// DeleteUser deletes a user.
func (service *Service) DeleteUser(ID portainer.UserID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
-170
View File
@@ -1,170 +0,0 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// UserService represents a service for managing users.
type UserService struct {
store *Store
}
// User returns a user by ID
func (service *UserService) User(ID portainer.UserID) (*portainer.User, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
value := bucket.Get(internal.Itob(int(ID)))
if value == nil {
return portainer.ErrUserNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var user portainer.User
err = internal.UnmarshalUser(data, &user)
if err != nil {
return nil, err
}
return &user, nil
}
// UserByUsername returns a user by username.
func (service *UserService) UserByUsername(username string) (*portainer.User, error) {
var user *portainer.User
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var u portainer.User
err := internal.UnmarshalUser(v, &u)
if err != nil {
return err
}
if u.Username == username {
user = &u
}
}
if user == nil {
return portainer.ErrUserNotFound
}
return nil
})
if err != nil {
return nil, err
}
return user, nil
}
// Users return an array containing all the users.
func (service *UserService) Users() ([]portainer.User, error) {
var users = make([]portainer.User, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var user portainer.User
err := internal.UnmarshalUser(v, &user)
if err != nil {
return err
}
users = append(users, user)
}
return nil
})
if err != nil {
return nil, err
}
return users, nil
}
// UsersByRole return an array containing all the users with the specified role.
func (service *UserService) UsersByRole(role portainer.UserRole) ([]portainer.User, error) {
var users = make([]portainer.User, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var user portainer.User
err := internal.UnmarshalUser(v, &user)
if err != nil {
return err
}
if user.Role == role {
users = append(users, user)
}
}
return nil
})
if err != nil {
return nil, err
}
return users, nil
}
// UpdateUser saves a user.
func (service *UserService) UpdateUser(ID portainer.UserID, user *portainer.User) error {
data, err := internal.MarshalUser(user)
if err != nil {
return err
}
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
err = bucket.Put(internal.Itob(int(ID)), data)
if err != nil {
return err
}
return nil
})
}
// CreateUser creates a new user.
func (service *UserService) CreateUser(user *portainer.User) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
id, _ := bucket.NextSequence()
user.ID = portainer.UserID(id)
data, err := internal.MarshalUser(user)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(user.ID)), data)
if err != nil {
return err
}
return nil
})
}
// DeleteUser deletes a user.
func (service *UserService) DeleteUser(ID portainer.UserID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
err := bucket.Delete(internal.Itob(int(ID)))
if err != nil {
return err
}
return nil
})
}
+66
View File
@@ -0,0 +1,66 @@
package version
import (
"strconv"
"github.com/boltdb/bolt"
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "version"
versionKey = "DB_VERSION"
)
// Service represents a service to manage stored versions.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// DBVersion retrieves the stored database version.
func (service *Service) DBVersion() (int, error) {
var data []byte
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
value := bucket.Get([]byte(versionKey))
if value == nil {
return portainer.ErrObjectNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return 0, err
}
return strconv.Atoi(string(data))
}
// StoreDBVersion store the database version.
func (service *Service) StoreDBVersion(version int) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data := []byte(strconv.Itoa(version))
return bucket.Put([]byte(versionKey), data)
})
}
-58
View File
@@ -1,58 +0,0 @@
package bolt
import (
"strconv"
"github.com/portainer/portainer"
"github.com/boltdb/bolt"
)
// VersionService represents a service to manage stored versions.
type VersionService struct {
store *Store
}
const (
dBVersionKey = "DB_VERSION"
)
// DBVersion retrieves the stored database version.
func (service *VersionService) DBVersion() (int, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(versionBucketName))
value := bucket.Get([]byte(dBVersionKey))
if value == nil {
return portainer.ErrDBVersionNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return 0, err
}
dbVersion, err := strconv.Atoi(string(data))
if err != nil {
return 0, err
}
return dbVersion, nil
}
// StoreDBVersion store the database version.
func (service *VersionService) StoreDBVersion(version int) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(versionBucketName))
data := []byte(strconv.Itoa(version))
err := bucket.Put([]byte(dBVersionKey), data)
if err != nil {
return err
}
return nil
})
}
+151
View File
@@ -0,0 +1,151 @@
package webhook
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "webhooks"
)
// Service represents a service for managing webhook data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
//Webhooks returns an array of all webhooks
func (service *Service) Webhooks() ([]portainer.Webhook, error) {
var webhooks = make([]portainer.Webhook, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var webhook portainer.Webhook
err := internal.UnmarshalObject(v, &webhook)
if err != nil {
return err
}
webhooks = append(webhooks, webhook)
}
return nil
})
return webhooks, err
}
// Webhook returns a webhook by ID.
func (service *Service) Webhook(ID portainer.WebhookID) (*portainer.Webhook, error) {
var webhook portainer.Webhook
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &webhook)
if err != nil {
return nil, err
}
return &webhook, nil
}
// WebhookByResourceID returns a webhook by the ResourceID it is associated with.
func (service *Service) WebhookByResourceID(ID string) (*portainer.Webhook, error) {
var webhook *portainer.Webhook
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var w portainer.Webhook
err := internal.UnmarshalObject(v, &w)
if err != nil {
return err
}
if w.ResourceID == ID {
webhook = &w
break
}
}
if webhook == nil {
return portainer.ErrObjectNotFound
}
return nil
})
return webhook, err
}
// WebhookByToken returns a webhook by the random token it is associated with.
func (service *Service) WebhookByToken(token string) (*portainer.Webhook, error) {
var webhook *portainer.Webhook
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var w portainer.Webhook
err := internal.UnmarshalObject(v, &w)
if err != nil {
return err
}
if w.Token == token {
webhook = &w
break
}
}
if webhook == nil {
return portainer.ErrObjectNotFound
}
return nil
})
return webhook, err
}
// DeleteWebhook deletes a webhook.
func (service *Service) DeleteWebhook(ID portainer.WebhookID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
// CreateWebhook assign an ID to a new webhook and saves it.
func (service *Service) CreateWebhook(webhook *portainer.Webhook) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
webhook.ID = portainer.WebhookID(id)
data, err := internal.MarshalObject(webhook)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(webhook.ID)), data)
})
}
+50 -13
View File
@@ -16,10 +16,12 @@ import (
type Service struct{}
const (
errInvalidEndpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix:// or tcp://")
errSocketNotFound = portainer.Error("Unable to locate Unix socket")
errInvalidEndpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix://, npipe:// or tcp://")
errSocketOrNamedPipeNotFound = portainer.Error("Unable to locate Unix socket or named pipe")
errEndpointsFileNotFound = portainer.Error("Unable to locate external endpoints file")
errTemplateFileNotFound = portainer.Error("Unable to locate template file on disk")
errInvalidSyncInterval = portainer.Error("Invalid synchronization interval")
errInvalidSnapshotInterval = portainer.Error("Invalid snapshot interval")
errEndpointExcludeExternal = portainer.Error("Cannot use the -H flag mutually with --external-endpoints")
errNoAuthExcludeAdminPassword = portainer.Error("Cannot use --no-auth with --admin-password or --admin-password-file")
errAdminPassExcludeAdminPassFile = portainer.Error("Cannot use --admin-password with --admin-password-file")
@@ -33,11 +35,12 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
Endpoint: kingpin.Flag("host", "Dockerd endpoint").Short('H').String(),
EndpointURL: kingpin.Flag("host", "Endpoint URL").Short('H').String(),
ExternalEndpoints: kingpin.Flag("external-endpoints", "Path to a file defining available endpoints").String(),
NoAuth: kingpin.Flag("no-auth", "Disable authentication").Default(defaultNoAuth).Bool(),
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAnalytics).Bool(),
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLSVerify).Bool(),
TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(),
TLSSkipVerify: kingpin.Flag("tlsskipverify", "Disable TLS server verification").Default(defaultTLSSkipVerify).Bool(),
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(),
TLSKey: kingpin.Flag("tlskey", "Path to the TLS key").Default(defaultTLSKeyPath).String(),
@@ -45,11 +48,14 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").Default(defaultSSLCertPath).String(),
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").Default(defaultSSLKeyPath).String(),
SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source").Default(defaultSyncInterval).String(),
Snapshot: kingpin.Flag("snapshot", "Start a background job to create endpoint snapshots").Default(defaultSnapshot).Bool(),
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each endpoint 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 (apps) definitions").Short('t').String(),
Templates: kingpin.Flag("templates", "URL to the templates definitions.").Short('t').String(),
TemplateFile: kingpin.Flag("template-file", "Path to the templates (app) definitions on the filesystem").Default(defaultTemplateFile).String(),
}
kingpin.Parse()
@@ -68,11 +74,16 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
// ValidateFlags validates the values of the flags.
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
if *flags.Endpoint != "" && *flags.ExternalEndpoints != "" {
if *flags.EndpointURL != "" && *flags.ExternalEndpoints != "" {
return errEndpointExcludeExternal
}
err := validateEndpoint(*flags.Endpoint)
err := validateTemplateFile(*flags.TemplateFile)
if err != nil {
return err
}
err = validateEndpointURL(*flags.EndpointURL)
if err != nil {
return err
}
@@ -87,6 +98,11 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
return err
}
err = validateSnapshotInterval(*flags.SnapshotInterval)
if err != nil {
return err
}
if *flags.NoAuth && (*flags.AdminPassword != "" || *flags.AdminPasswordFile != "") {
return errNoAuthExcludeAdminPassword
}
@@ -98,17 +114,18 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
return nil
}
func validateEndpoint(endpoint string) error {
if endpoint != "" {
if !strings.HasPrefix(endpoint, "unix://") && !strings.HasPrefix(endpoint, "tcp://") {
func validateEndpointURL(endpointURL string) error {
if endpointURL != "" {
if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") && !strings.HasPrefix(endpointURL, "npipe://") {
return errInvalidEndpointProtocol
}
if strings.HasPrefix(endpoint, "unix://") {
socketPath := strings.TrimPrefix(endpoint, "unix://")
if strings.HasPrefix(endpointURL, "unix://") || strings.HasPrefix(endpointURL, "npipe://") {
socketPath := strings.TrimPrefix(endpointURL, "unix://")
socketPath = strings.TrimPrefix(socketPath, "npipe://")
if _, err := os.Stat(socketPath); err != nil {
if os.IsNotExist(err) {
return errSocketNotFound
return errSocketOrNamedPipeNotFound
}
return err
}
@@ -129,6 +146,16 @@ func validateExternalEndpoints(externalEndpoints string) error {
return nil
}
func validateTemplateFile(templateFile string) error {
if _, err := os.Stat(templateFile); err != nil {
if os.IsNotExist(err) {
return errTemplateFileNotFound
}
return err
}
return nil
}
func validateSyncInterval(syncInterval string) error {
if syncInterval != defaultSyncInterval {
_, err := time.ParseDuration(syncInterval)
@@ -138,3 +165,13 @@ func validateSyncInterval(syncInterval string) error {
}
return nil
}
func validateSnapshotInterval(snapshotInterval string) error {
if snapshotInterval != defaultSnapshotInterval {
_, err := time.ParseDuration(snapshotInterval)
if err != nil {
return errInvalidSnapshotInterval
}
}
return nil
}
+17 -13
View File
@@ -3,17 +3,21 @@
package cli
const (
defaultBindAddress = ":9000"
defaultDataDirectory = "/data"
defaultAssetsDirectory = "./"
defaultNoAuth = "false"
defaultNoAnalytics = "false"
defaultTLSVerify = "false"
defaultTLSCACertPath = "/certs/ca.pem"
defaultTLSCertPath = "/certs/cert.pem"
defaultTLSKeyPath = "/certs/key.pem"
defaultSSL = "false"
defaultSSLCertPath = "/certs/portainer.crt"
defaultSSLKeyPath = "/certs/portainer.key"
defaultSyncInterval = "60s"
defaultBindAddress = ":9000"
defaultDataDirectory = "/data"
defaultAssetsDirectory = "./"
defaultNoAuth = "false"
defaultNoAnalytics = "false"
defaultTLS = "false"
defaultTLSSkipVerify = "false"
defaultTLSCACertPath = "/certs/ca.pem"
defaultTLSCertPath = "/certs/cert.pem"
defaultTLSKeyPath = "/certs/key.pem"
defaultSSL = "false"
defaultSSLCertPath = "/certs/portainer.crt"
defaultSSLKeyPath = "/certs/portainer.key"
defaultSyncInterval = "60s"
defaultSnapshot = "true"
defaultSnapshotInterval = "5m"
defaultTemplateFile = "/templates.json"
)
+17 -13
View File
@@ -1,17 +1,21 @@
package cli
const (
defaultBindAddress = ":9000"
defaultDataDirectory = "C:\\data"
defaultAssetsDirectory = "./"
defaultNoAuth = "false"
defaultNoAnalytics = "false"
defaultTLSVerify = "false"
defaultTLSCACertPath = "C:\\certs\\ca.pem"
defaultTLSCertPath = "C:\\certs\\cert.pem"
defaultTLSKeyPath = "C:\\certs\\key.pem"
defaultSSL = "false"
defaultSSLCertPath = "C:\\certs\\portainer.crt"
defaultSSLKeyPath = "C:\\certs\\portainer.key"
defaultSyncInterval = "60s"
defaultBindAddress = ":9000"
defaultDataDirectory = "C:\\data"
defaultAssetsDirectory = "./"
defaultNoAuth = "false"
defaultNoAnalytics = "false"
defaultTLS = "false"
defaultTLSSkipVerify = "false"
defaultTLSCACertPath = "C:\\certs\\ca.pem"
defaultTLSCertPath = "C:\\certs\\cert.pem"
defaultTLSKeyPath = "C:\\certs\\key.pem"
defaultSSL = "false"
defaultSSLCertPath = "C:\\certs\\portainer.crt"
defaultSSLKeyPath = "C:\\certs\\portainer.key"
defaultSyncInterval = "60s"
defaultSnapshot = "true"
defaultSnapshotInterval = "5m"
defaultTemplateFile = "/templates.json"
)
+456 -67
View File
@@ -1,17 +1,25 @@
package main // import "github.com/portainer/portainer"
import (
"encoding/json"
"os"
"strings"
"time"
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt"
"github.com/portainer/portainer/cli"
"github.com/portainer/portainer/cron"
"github.com/portainer/portainer/crypto"
"github.com/portainer/portainer/docker"
"github.com/portainer/portainer/exec"
"github.com/portainer/portainer/filesystem"
"github.com/portainer/portainer/git"
"github.com/portainer/portainer/http"
"github.com/portainer/portainer/http/client"
"github.com/portainer/portainer/jwt"
"github.com/portainer/portainer/ldap"
"github.com/portainer/portainer/libcompose"
"log"
)
@@ -38,8 +46,8 @@ func initFileService(dataStorePath string) portainer.FileService {
return fileService
}
func initStore(dataStorePath string) *bolt.Store {
store, err := bolt.NewStore(dataStorePath)
func initStore(dataStorePath string, fileService portainer.FileService) *bolt.Store {
store, err := bolt.NewStore(dataStorePath, fileService)
if err != nil {
log.Fatal(err)
}
@@ -49,6 +57,11 @@ func initStore(dataStorePath string) *bolt.Store {
log.Fatal(err)
}
err = store.Init()
if err != nil {
log.Fatal(err)
}
err = store.MigrateData()
if err != nil {
log.Fatal(err)
@@ -56,8 +69,12 @@ func initStore(dataStorePath string) *bolt.Store {
return store
}
func initStackManager(assetsPath string) portainer.StackManager {
return exec.NewStackManager(assetsPath)
func initComposeStackManager(dataStorePath string) portainer.ComposeStackManager {
return libcompose.NewComposeStackManager(dataStorePath)
}
func initSwarmStackManager(assetsPath string, dataStorePath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (portainer.SwarmStackManager, error) {
return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService)
}
func initJWTService(authenticationEnabled bool) portainer.JWTService {
@@ -71,6 +88,10 @@ func initJWTService(authenticationEnabled bool) portainer.JWTService {
return nil
}
func initDigitalSignatureService() portainer.DigitalSignatureService {
return crypto.NewECDSAService(os.Getenv("AGENT_SECRET"))
}
func initCryptoService() portainer.CryptoService {
return &crypto.Service{}
}
@@ -83,38 +104,139 @@ func initGitService() portainer.GitService {
return &git.Service{}
}
func initEndpointWatcher(endpointService portainer.EndpointService, externalEnpointFile string, syncInterval string) bool {
authorizeEndpointMgmt := true
if externalEnpointFile != "" {
authorizeEndpointMgmt = false
log.Println("Using external endpoint definition. Endpoint management via the API will be disabled.")
endpointWatcher := cron.NewWatcher(endpointService, syncInterval)
err := endpointWatcher.WatchEndpointFile(externalEnpointFile)
if err != nil {
log.Fatal(err)
}
}
return authorizeEndpointMgmt
func initClientFactory(signatureService portainer.DigitalSignatureService) *docker.ClientFactory {
return docker.NewClientFactory(signatureService)
}
func initStatus(authorizeEndpointMgmt bool, flags *portainer.CLIFlags) *portainer.Status {
func initSnapshotter(clientFactory *docker.ClientFactory) portainer.Snapshotter {
return docker.NewSnapshotter(clientFactory)
}
func initJobScheduler() portainer.JobScheduler {
return cron.NewJobScheduler()
}
func loadSnapshotSystemSchedule(jobScheduler portainer.JobScheduler, snapshotter portainer.Snapshotter, scheduleService portainer.ScheduleService, endpointService portainer.EndpointService, settingsService portainer.SettingsService) error {
settings, err := settingsService.Settings()
if err != nil {
return err
}
schedules, err := scheduleService.SchedulesByJobType(portainer.SnapshotJobType)
if err != nil {
return err
}
var snapshotSchedule *portainer.Schedule
if len(schedules) == 0 {
snapshotJob := &portainer.SnapshotJob{}
snapshotSchedule = &portainer.Schedule{
ID: portainer.ScheduleID(scheduleService.GetNextIdentifier()),
Name: "system_snapshot",
CronExpression: "@every " + settings.SnapshotInterval,
Recurring: true,
JobType: portainer.SnapshotJobType,
SnapshotJob: snapshotJob,
Created: time.Now().Unix(),
}
} else {
snapshotSchedule = &schedules[0]
}
snapshotJobContext := cron.NewSnapshotJobContext(endpointService, snapshotter)
snapshotJobRunner := cron.NewSnapshotJobRunner(snapshotSchedule, snapshotJobContext)
err = jobScheduler.ScheduleJob(snapshotJobRunner)
if err != nil {
return err
}
if len(schedules) == 0 {
return scheduleService.CreateSchedule(snapshotSchedule)
}
return nil
}
func loadEndpointSyncSystemSchedule(jobScheduler portainer.JobScheduler, scheduleService portainer.ScheduleService, endpointService portainer.EndpointService, flags *portainer.CLIFlags) error {
if *flags.ExternalEndpoints == "" {
return nil
}
log.Println("Using external endpoint definition. Endpoint management via the API will be disabled.")
schedules, err := scheduleService.SchedulesByJobType(portainer.EndpointSyncJobType)
if err != nil {
return err
}
if len(schedules) != 0 {
return nil
}
endpointSyncJob := &portainer.EndpointSyncJob{}
endpointSyncSchedule := &portainer.Schedule{
ID: portainer.ScheduleID(scheduleService.GetNextIdentifier()),
Name: "system_endpointsync",
CronExpression: "@every " + *flags.SyncInterval,
Recurring: true,
JobType: portainer.EndpointSyncJobType,
EndpointSyncJob: endpointSyncJob,
Created: time.Now().Unix(),
}
endpointSyncJobContext := cron.NewEndpointSyncJobContext(endpointService, *flags.ExternalEndpoints)
endpointSyncJobRunner := cron.NewEndpointSyncJobRunner(endpointSyncSchedule, endpointSyncJobContext)
err = jobScheduler.ScheduleJob(endpointSyncJobRunner)
if err != nil {
return err
}
return scheduleService.CreateSchedule(endpointSyncSchedule)
}
func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService portainer.JobService, scheduleService portainer.ScheduleService, endpointService portainer.EndpointService, fileService portainer.FileService) error {
schedules, err := scheduleService.Schedules()
if err != nil {
return err
}
for _, schedule := range schedules {
if schedule.JobType == portainer.ScriptExecutionJobType {
jobContext := cron.NewScriptExecutionJobContext(jobService, endpointService, fileService)
jobRunner := cron.NewScriptExecutionJobRunner(&schedule, jobContext)
err = jobScheduler.ScheduleJob(jobRunner)
if err != nil {
return err
}
}
}
return nil
}
func initStatus(endpointManagement, snapshot bool, flags *portainer.CLIFlags) *portainer.Status {
return &portainer.Status{
Analytics: !*flags.NoAnalytics,
Authentication: !*flags.NoAuth,
EndpointManagement: authorizeEndpointMgmt,
EndpointManagement: endpointManagement,
Snapshot: snapshot,
Version: portainer.APIVersion,
}
}
func initDockerHub(dockerHubService portainer.DockerHubService) error {
_, err := dockerHubService.DockerHub()
if err == portainer.ErrDockerHubNotFound {
if err == portainer.ErrObjectNotFound {
dockerhub := &portainer.DockerHub{
Authentication: false,
Username: "",
Password: "",
}
return dockerHubService.StoreDockerHub(dockerhub)
return dockerHubService.UpdateDockerHub(dockerhub)
} else if err != nil {
return err
}
@@ -124,26 +246,29 @@ func initDockerHub(dockerHubService portainer.DockerHubService) error {
func initSettings(settingsService portainer.SettingsService, flags *portainer.CLIFlags) error {
_, err := settingsService.Settings()
if err == portainer.ErrSettingsNotFound {
if err == portainer.ErrObjectNotFound {
settings := &portainer.Settings{
LogoURL: *flags.Logo,
DisplayDonationHeader: true,
DisplayExternalContributors: false,
AuthenticationMethod: portainer.AuthenticationInternal,
LogoURL: *flags.Logo,
AuthenticationMethod: portainer.AuthenticationInternal,
LDAPSettings: portainer.LDAPSettings{
TLSConfig: portainer.TLSConfiguration{},
AutoCreateUsers: true,
TLSConfig: portainer.TLSConfiguration{},
SearchSettings: []portainer.LDAPSearchSettings{
portainer.LDAPSearchSettings{},
},
GroupSearchSettings: []portainer.LDAPGroupSearchSettings{
portainer.LDAPGroupSearchSettings{},
},
},
OAuthSettings: portainer.OAuthSettings{},
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
EnableHostManagementFeatures: false,
SnapshotInterval: *flags.SnapshotInterval,
}
if *flags.Templates != "" {
settings.TemplatesURL = *flags.Templates
} else {
settings.TemplatesURL = portainer.DefaultTemplatesURL
}
if *flags.Labels != nil {
@@ -152,7 +277,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
settings.BlackListedLabels = make([]portainer.Pair, 0)
}
return settingsService.StoreSettings(settings)
return settingsService.UpdateSettings(settings)
} else if err != nil {
return err
}
@@ -160,6 +285,45 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
return nil
}
func initTemplates(templateService portainer.TemplateService, fileService portainer.FileService, templateURL, templateFile string) error {
if templateURL != "" {
log.Printf("Portainer started with the --templates flag. Using external templates, template management will be disabled.")
return nil
}
existingTemplates, err := templateService.Templates()
if err != nil {
return err
}
if len(existingTemplates) != 0 {
log.Printf("Templates already registered inside the database. Skipping template import.")
return nil
}
templatesJSON, err := fileService.GetFileContent(templateFile)
if err != nil {
log.Println("Unable to retrieve template definitions via filesystem")
return err
}
var templates []portainer.Template
err = json.Unmarshal(templatesJSON, &templates)
if err != nil {
log.Println("Unable to parse templates file. Please review your template definition file.")
return err
}
for _, template := range templates {
err := templateService.CreateTemplate(&template)
if err != nil {
return err
}
}
return nil
}
func retrieveFirstEndpointFromDatabase(endpointService portainer.EndpointService) *portainer.Endpoint {
endpoints, err := endpointService.Endpoints()
if err != nil {
@@ -168,65 +332,273 @@ func retrieveFirstEndpointFromDatabase(endpointService portainer.EndpointService
return &endpoints[0]
}
func loadAndParseKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
private, public, err := fileService.LoadKeyPair()
if err != nil {
return err
}
return signatureService.ParseKeyPair(private, public)
}
func generateAndStoreKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
private, public, err := signatureService.GenerateKeyPair()
if err != nil {
return err
}
privateHeader, publicHeader := signatureService.PEMHeaders()
return fileService.StoreKeyPair(private, public, privateHeader, publicHeader)
}
func initKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
existingKeyPair, err := fileService.KeyPairFilesExist()
if err != nil {
log.Fatal(err)
}
if existingKeyPair {
return loadAndParseKeyPair(fileService, signatureService)
}
return generateAndStoreKeyPair(fileService, signatureService)
}
func createTLSSecuredEndpoint(flags *portainer.CLIFlags, endpointService portainer.EndpointService, snapshotter portainer.Snapshotter) error {
tlsConfiguration := portainer.TLSConfiguration{
TLS: *flags.TLS,
TLSSkipVerify: *flags.TLSSkipVerify,
}
if *flags.TLS {
tlsConfiguration.TLSCACertPath = *flags.TLSCacert
tlsConfiguration.TLSCertPath = *flags.TLSCert
tlsConfiguration.TLSKeyPath = *flags.TLSKey
} else if !*flags.TLS && *flags.TLSSkipVerify {
tlsConfiguration.TLS = true
}
endpointID := endpointService.GetNextIdentifier()
endpoint := &portainer.Endpoint{
ID: portainer.EndpointID(endpointID),
Name: "primary",
URL: *flags.EndpointURL,
GroupID: portainer.EndpointGroupID(1),
Type: portainer.DockerEnvironment,
TLSConfig: tlsConfiguration,
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
Tags: []string{},
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
}
if strings.HasPrefix(endpoint.URL, "tcp://") {
tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(tlsConfiguration.TLSCACertPath, tlsConfiguration.TLSCertPath, tlsConfiguration.TLSKeyPath, tlsConfiguration.TLSSkipVerify)
if err != nil {
return err
}
agentOnDockerEnvironment, err := client.ExecutePingOperation(endpoint.URL, tlsConfig)
if err != nil {
return err
}
if agentOnDockerEnvironment {
endpoint.Type = portainer.AgentOnDockerEnvironment
}
}
return snapshotAndPersistEndpoint(endpoint, endpointService, snapshotter)
}
func createUnsecuredEndpoint(endpointURL string, endpointService portainer.EndpointService, snapshotter portainer.Snapshotter) error {
if strings.HasPrefix(endpointURL, "tcp://") {
_, err := client.ExecutePingOperation(endpointURL, nil)
if err != nil {
return err
}
}
endpointID := endpointService.GetNextIdentifier()
endpoint := &portainer.Endpoint{
ID: portainer.EndpointID(endpointID),
Name: "primary",
URL: endpointURL,
GroupID: portainer.EndpointGroupID(1),
Type: portainer.DockerEnvironment,
TLSConfig: portainer.TLSConfiguration{},
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
Tags: []string{},
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
}
return snapshotAndPersistEndpoint(endpoint, endpointService, snapshotter)
}
func snapshotAndPersistEndpoint(endpoint *portainer.Endpoint, endpointService portainer.EndpointService, snapshotter portainer.Snapshotter) error {
snapshot, err := snapshotter.CreateSnapshot(endpoint)
endpoint.Status = portainer.EndpointStatusUp
if err != nil {
log.Printf("http error: endpoint snapshot error (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
}
if snapshot != nil {
endpoint.Snapshots = []portainer.Snapshot{*snapshot}
}
return endpointService.CreateEndpoint(endpoint)
}
func initEndpoint(flags *portainer.CLIFlags, endpointService portainer.EndpointService, snapshotter portainer.Snapshotter) error {
if *flags.EndpointURL == "" {
return nil
}
endpoints, err := endpointService.Endpoints()
if err != nil {
return err
}
if len(endpoints) > 0 {
log.Println("Instance already has defined endpoints. Skipping the endpoint defined via CLI.")
return nil
}
if *flags.TLS || *flags.TLSSkipVerify {
return createTLSSecuredEndpoint(flags, endpointService, snapshotter)
}
return createUnsecuredEndpoint(*flags.EndpointURL, endpointService, snapshotter)
}
func initJobService(dockerClientFactory *docker.ClientFactory) portainer.JobService {
return docker.NewJobService(dockerClientFactory)
}
func initExtensionManager(fileService portainer.FileService, extensionService portainer.ExtensionService) (portainer.ExtensionManager, error) {
extensionManager := exec.NewExtensionManager(fileService, extensionService)
extensions, err := extensionService.Extensions()
if err != nil {
return nil, err
}
for _, extension := range extensions {
err := extensionManager.EnableExtension(&extension, extension.License.LicenseKey)
if err != nil {
log.Printf("Unable to enable extension: %s [extension: %s]", err.Error(), extension.Name)
extension.Enabled = false
extension.License.Valid = false
extensionService.Persist(&extension)
}
}
return extensionManager, nil
}
func terminateIfNoAdminCreated(userService portainer.UserService) {
timer1 := time.NewTimer(5 * time.Minute)
<-timer1.C
users, err := userService.UsersByRole(portainer.AdministratorRole)
if err != nil {
log.Fatal(err)
}
if len(users) == 0 {
log.Fatal("No administrator account was created after 5 min. Shutting down the Portainer instance for security reasons.")
return
}
}
func main() {
flags := initCLI()
fileService := initFileService(*flags.Data)
store := initStore(*flags.Data)
store := initStore(*flags.Data, fileService)
defer store.Close()
stackManager := initStackManager(*flags.Assets)
jwtService := initJWTService(!*flags.NoAuth)
cryptoService := initCryptoService()
ldapService := initLDAPService()
gitService := initGitService()
authorizeEndpointMgmt := initEndpointWatcher(store.EndpointService, *flags.ExternalEndpoints, *flags.SyncInterval)
cryptoService := initCryptoService()
err := initSettings(store.SettingsService, flags)
digitalSignatureService := initDigitalSignatureService()
err := initKeyPair(fileService, digitalSignatureService)
if err != nil {
log.Fatal(err)
}
extensionManager, err := initExtensionManager(fileService, store.ExtensionService)
if err != nil {
log.Fatal(err)
}
clientFactory := initClientFactory(digitalSignatureService)
jobService := initJobService(clientFactory)
snapshotter := initSnapshotter(clientFactory)
endpointManagement := true
if *flags.ExternalEndpoints != "" {
endpointManagement = false
}
swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService)
if err != nil {
log.Fatal(err)
}
composeStackManager := initComposeStackManager(*flags.Data)
err = initTemplates(store.TemplateService, fileService, *flags.Templates, *flags.TemplateFile)
if err != nil {
log.Fatal(err)
}
err = initSettings(store.SettingsService, flags)
if err != nil {
log.Fatal(err)
}
jobScheduler := initJobScheduler()
err = loadSchedulesFromDatabase(jobScheduler, jobService, store.ScheduleService, store.EndpointService, fileService)
if err != nil {
log.Fatal(err)
}
err = loadEndpointSyncSystemSchedule(jobScheduler, store.ScheduleService, store.EndpointService, flags)
if err != nil {
log.Fatal(err)
}
if *flags.Snapshot {
err = loadSnapshotSystemSchedule(jobScheduler, snapshotter, store.ScheduleService, store.EndpointService, store.SettingsService)
if err != nil {
log.Fatal(err)
}
}
jobScheduler.Start()
err = initDockerHub(store.DockerHubService)
if err != nil {
log.Fatal(err)
}
applicationStatus := initStatus(authorizeEndpointMgmt, flags)
applicationStatus := initStatus(endpointManagement, *flags.Snapshot, flags)
if *flags.Endpoint != "" {
endpoints, err := store.EndpointService.Endpoints()
if err != nil {
log.Fatal(err)
}
if len(endpoints) == 0 {
endpoint := &portainer.Endpoint{
Name: "primary",
URL: *flags.Endpoint,
TLSConfig: portainer.TLSConfiguration{
TLS: *flags.TLSVerify,
TLSSkipVerify: false,
TLSCACertPath: *flags.TLSCacert,
TLSCertPath: *flags.TLSCert,
TLSKeyPath: *flags.TLSKey,
},
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
}
err = store.EndpointService.CreateEndpoint(endpoint)
if err != nil {
log.Fatal(err)
}
} else {
log.Println("Instance already has defined endpoints. Skipping the endpoint defined via CLI.")
}
err = initEndpoint(flags, store.EndpointService, snapshotter)
if err != nil {
log.Fatal(err)
}
adminPasswordHash := ""
@@ -235,7 +607,7 @@ func main() {
if err != nil {
log.Fatal(err)
}
adminPasswordHash, err = cryptoService.Hash(content)
adminPasswordHash, err = cryptoService.Hash(string(content))
if err != nil {
log.Fatal(err)
}
@@ -265,30 +637,47 @@ func main() {
}
}
if !*flags.NoAuth {
go terminateIfNoAdminCreated(store.UserService)
}
var server portainer.Server = &http.Server{
Status: applicationStatus,
BindAddress: *flags.Addr,
AssetsPath: *flags.Assets,
AuthDisabled: *flags.NoAuth,
EndpointManagement: authorizeEndpointMgmt,
EndpointManagement: endpointManagement,
UserService: store.UserService,
TeamService: store.TeamService,
TeamMembershipService: store.TeamMembershipService,
EndpointService: store.EndpointService,
EndpointGroupService: store.EndpointGroupService,
ExtensionService: store.ExtensionService,
ResourceControlService: store.ResourceControlService,
SettingsService: store.SettingsService,
RegistryService: store.RegistryService,
DockerHubService: store.DockerHubService,
StackService: store.StackService,
StackManager: stackManager,
ScheduleService: store.ScheduleService,
TagService: store.TagService,
TemplateService: store.TemplateService,
WebhookService: store.WebhookService,
SwarmStackManager: swarmStackManager,
ComposeStackManager: composeStackManager,
ExtensionManager: extensionManager,
CryptoService: cryptoService,
JWTService: jwtService,
FileService: fileService,
LDAPService: ldapService,
GitService: gitService,
SignatureService: digitalSignatureService,
JobScheduler: jobScheduler,
Snapshotter: snapshotter,
SSL: *flags.SSL,
SSLCert: *flags.SSLCert,
SSLKey: *flags.SSLKey,
DockerClientFactory: clientFactory,
JobService: jobService,
}
log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr)
@@ -4,52 +4,98 @@ import (
"encoding/json"
"io/ioutil"
"log"
"os"
"strings"
"github.com/portainer/portainer"
)
type (
endpointSyncJob struct {
logger *log.Logger
endpointService portainer.EndpointService
endpointFilePath string
}
// EndpointSyncJobRunner is used to run a EndpointSyncJob
type EndpointSyncJobRunner struct {
schedule *portainer.Schedule
context *EndpointSyncJobContext
}
synchronization struct {
endpointsToCreate []*portainer.Endpoint
endpointsToUpdate []*portainer.Endpoint
endpointsToDelete []*portainer.Endpoint
}
// EndpointSyncJobContext represents the context of execution of a EndpointSyncJob
type EndpointSyncJobContext struct {
endpointService portainer.EndpointService
endpointFilePath string
}
fileEndpoint struct {
Name string `json:"Name"`
URL string `json:"URL"`
TLS bool `json:"TLS,omitempty"`
TLSSkipVerify bool `json:"TLSSkipVerify,omitempty"`
TLSCACert string `json:"TLSCACert,omitempty"`
TLSCert string `json:"TLSCert,omitempty"`
TLSKey string `json:"TLSKey,omitempty"`
}
)
const (
// ErrEmptyEndpointArray is an error raised when the external endpoint source array is empty.
ErrEmptyEndpointArray = portainer.Error("External endpoint source is empty")
)
func newEndpointSyncJob(endpointFilePath string, endpointService portainer.EndpointService) endpointSyncJob {
return endpointSyncJob{
logger: log.New(os.Stderr, "", log.LstdFlags),
// NewEndpointSyncJobContext returns a new context that can be used to execute a EndpointSyncJob
func NewEndpointSyncJobContext(endpointService portainer.EndpointService, endpointFilePath string) *EndpointSyncJobContext {
return &EndpointSyncJobContext{
endpointService: endpointService,
endpointFilePath: endpointFilePath,
}
}
func endpointSyncError(err error, logger *log.Logger) bool {
// NewEndpointSyncJobRunner returns a new runner that can be scheduled
func NewEndpointSyncJobRunner(schedule *portainer.Schedule, context *EndpointSyncJobContext) *EndpointSyncJobRunner {
return &EndpointSyncJobRunner{
schedule: schedule,
context: context,
}
}
type synchronization struct {
endpointsToCreate []*portainer.Endpoint
endpointsToUpdate []*portainer.Endpoint
endpointsToDelete []*portainer.Endpoint
}
type fileEndpoint struct {
Name string `json:"Name"`
URL string `json:"URL"`
TLS bool `json:"TLS,omitempty"`
TLSSkipVerify bool `json:"TLSSkipVerify,omitempty"`
TLSCACert string `json:"TLSCACert,omitempty"`
TLSCert string `json:"TLSCert,omitempty"`
TLSKey string `json:"TLSKey,omitempty"`
}
// GetSchedule returns the schedule associated to the runner
func (runner *EndpointSyncJobRunner) GetSchedule() *portainer.Schedule {
return runner.schedule
}
// Run triggers the execution of the endpoint synchronization process.
func (runner *EndpointSyncJobRunner) Run() {
data, err := ioutil.ReadFile(runner.context.endpointFilePath)
if endpointSyncError(err) {
return
}
var fileEndpoints []fileEndpoint
err = json.Unmarshal(data, &fileEndpoints)
if endpointSyncError(err) {
return
}
if len(fileEndpoints) == 0 {
log.Println("background job error (endpoint synchronization). External endpoint source is empty")
return
}
storedEndpoints, err := runner.context.endpointService.Endpoints()
if endpointSyncError(err) {
return
}
convertedFileEndpoints := convertFileEndpoints(fileEndpoints)
sync := prepareSyncData(storedEndpoints, convertedFileEndpoints)
if sync.requireSync() {
err = runner.context.endpointService.Synchronize(sync.endpointsToCreate, sync.endpointsToUpdate, sync.endpointsToDelete)
if endpointSyncError(err) {
return
}
log.Printf("Endpoint synchronization ended. [created: %v] [updated: %v] [deleted: %v]", len(sync.endpointsToCreate), len(sync.endpointsToUpdate), len(sync.endpointsToDelete))
}
}
func endpointSyncError(err error) bool {
if err != nil {
logger.Printf("Endpoint synchronization error: %s", err)
log.Printf("background job error (endpoint synchronization). Unable to synchronize endpoints (err=%s)\n", err)
return true
}
return false
@@ -129,8 +175,7 @@ func (sync synchronization) requireSync() bool {
return false
}
// TMP: endpointSyncJob method to access logger, should be generic
func (job endpointSyncJob) prepareSyncData(storedEndpoints, fileEndpoints []portainer.Endpoint) *synchronization {
func prepareSyncData(storedEndpoints, fileEndpoints []portainer.Endpoint) *synchronization {
endpointsToCreate := make([]*portainer.Endpoint, 0)
endpointsToUpdate := make([]*portainer.Endpoint, 0)
endpointsToDelete := make([]*portainer.Endpoint, 0)
@@ -140,25 +185,23 @@ func (job endpointSyncJob) prepareSyncData(storedEndpoints, fileEndpoints []port
if fidx != -1 {
endpoint := mergeEndpointIfRequired(&storedEndpoints[idx], &fileEndpoints[fidx])
if endpoint != nil {
job.logger.Printf("New definition for a stored endpoint found in file, updating database. [name: %v] [url: %v]\n", endpoint.Name, endpoint.URL)
log.Printf("New definition for a stored endpoint found in file, updating database. [name: %v] [url: %v]\n", endpoint.Name, endpoint.URL)
endpointsToUpdate = append(endpointsToUpdate, endpoint)
} else {
job.logger.Printf("No change detected for a stored endpoint. [name: %v] [url: %v]\n", storedEndpoints[idx].Name, storedEndpoints[idx].URL)
}
} else {
job.logger.Printf("Stored endpoint not found in file (definition might be invalid), removing from database. [name: %v] [url: %v]", storedEndpoints[idx].Name, storedEndpoints[idx].URL)
log.Printf("Stored endpoint not found in file (definition might be invalid), removing from database. [name: %v] [url: %v]", storedEndpoints[idx].Name, storedEndpoints[idx].URL)
endpointsToDelete = append(endpointsToDelete, &storedEndpoints[idx])
}
}
for idx, endpoint := range fileEndpoints {
if !isValidEndpoint(&endpoint) {
job.logger.Printf("Invalid file endpoint definition, skipping. [name: %v] [url: %v]", endpoint.Name, endpoint.URL)
log.Printf("Invalid file endpoint definition, skipping. [name: %v] [url: %v]", endpoint.Name, endpoint.URL)
continue
}
sidx := endpointExists(&fileEndpoints[idx], storedEndpoints)
if sidx == -1 {
job.logger.Printf("File endpoint not found in database, adding to database. [name: %v] [url: %v]", fileEndpoints[idx].Name, fileEndpoints[idx].URL)
log.Printf("File endpoint not found in database, adding to database. [name: %v] [url: %v]", fileEndpoints[idx].Name, fileEndpoints[idx].URL)
endpointsToCreate = append(endpointsToCreate, &fileEndpoints[idx])
}
}
@@ -169,43 +212,3 @@ func (job endpointSyncJob) prepareSyncData(storedEndpoints, fileEndpoints []port
endpointsToDelete: endpointsToDelete,
}
}
func (job endpointSyncJob) Sync() error {
data, err := ioutil.ReadFile(job.endpointFilePath)
if endpointSyncError(err, job.logger) {
return err
}
var fileEndpoints []fileEndpoint
err = json.Unmarshal(data, &fileEndpoints)
if endpointSyncError(err, job.logger) {
return err
}
if len(fileEndpoints) == 0 {
return ErrEmptyEndpointArray
}
storedEndpoints, err := job.endpointService.Endpoints()
if endpointSyncError(err, job.logger) {
return err
}
convertedFileEndpoints := convertFileEndpoints(fileEndpoints)
sync := job.prepareSyncData(storedEndpoints, convertedFileEndpoints)
if sync.requireSync() {
err = job.endpointService.Synchronize(sync.endpointsToCreate, sync.endpointsToUpdate, sync.endpointsToDelete)
if endpointSyncError(err, job.logger) {
return err
}
job.logger.Printf("Endpoint synchronization ended. [created: %v] [updated: %v] [deleted: %v]", len(sync.endpointsToCreate), len(sync.endpointsToUpdate), len(sync.endpointsToDelete))
}
return nil
}
func (job endpointSyncJob) Run() {
job.logger.Println("Endpoint synchronization job started.")
err := job.Sync()
endpointSyncError(err, job.logger)
}
+96
View File
@@ -0,0 +1,96 @@
package cron
import (
"log"
"time"
"github.com/portainer/portainer"
)
// ScriptExecutionJobRunner is used to run a ScriptExecutionJob
type ScriptExecutionJobRunner struct {
schedule *portainer.Schedule
context *ScriptExecutionJobContext
executedOnce bool
}
// ScriptExecutionJobContext represents the context of execution of a ScriptExecutionJob
type ScriptExecutionJobContext struct {
jobService portainer.JobService
endpointService portainer.EndpointService
fileService portainer.FileService
}
// NewScriptExecutionJobContext returns a new context that can be used to execute a ScriptExecutionJob
func NewScriptExecutionJobContext(jobService portainer.JobService, endpointService portainer.EndpointService, fileService portainer.FileService) *ScriptExecutionJobContext {
return &ScriptExecutionJobContext{
jobService: jobService,
endpointService: endpointService,
fileService: fileService,
}
}
// NewScriptExecutionJobRunner returns a new runner that can be scheduled
func NewScriptExecutionJobRunner(schedule *portainer.Schedule, context *ScriptExecutionJobContext) *ScriptExecutionJobRunner {
return &ScriptExecutionJobRunner{
schedule: schedule,
context: context,
executedOnce: false,
}
}
// Run triggers the execution of the job.
// It will iterate through all the endpoints specified in the context to
// execute the script associated to the job.
func (runner *ScriptExecutionJobRunner) Run() {
if !runner.schedule.Recurring && runner.executedOnce {
return
}
runner.executedOnce = true
scriptFile, err := runner.context.fileService.GetFileContent(runner.schedule.ScriptExecutionJob.ScriptPath)
if err != nil {
log.Printf("scheduled job error (script execution). Unable to retrieve script file (err=%s)\n", err)
return
}
targets := make([]*portainer.Endpoint, 0)
for _, endpointID := range runner.schedule.ScriptExecutionJob.Endpoints {
endpoint, err := runner.context.endpointService.Endpoint(endpointID)
if err != nil {
log.Printf("scheduled job error (script execution). Unable to retrieve information about endpoint (id=%d) (err=%s)\n", endpointID, err)
return
}
targets = append(targets, endpoint)
}
runner.executeAndRetry(targets, scriptFile, 0)
}
func (runner *ScriptExecutionJobRunner) executeAndRetry(endpoints []*portainer.Endpoint, script []byte, retryCount int) {
retryTargets := make([]*portainer.Endpoint, 0)
for _, endpoint := range endpoints {
err := runner.context.jobService.ExecuteScript(endpoint, "", runner.schedule.ScriptExecutionJob.Image, script, runner.schedule)
if err == portainer.ErrUnableToPingEndpoint {
retryTargets = append(retryTargets, endpoint)
} else if err != nil {
log.Printf("scheduled job error (script execution). Unable to execute script (endpoint=%s) (err=%s)\n", endpoint.Name, err)
}
}
retryCount++
if retryCount >= runner.schedule.ScriptExecutionJob.RetryCount {
return
}
time.Sleep(time.Duration(runner.schedule.ScriptExecutionJob.RetryInterval) * time.Second)
runner.executeAndRetry(retryTargets, script, retryCount)
}
// GetSchedule returns the schedule associated to the runner
func (runner *ScriptExecutionJobRunner) GetSchedule() *portainer.Schedule {
return runner.schedule
}
+85
View File
@@ -0,0 +1,85 @@
package cron
import (
"log"
"github.com/portainer/portainer"
)
// SnapshotJobRunner is used to run a SnapshotJob
type SnapshotJobRunner struct {
schedule *portainer.Schedule
context *SnapshotJobContext
}
// SnapshotJobContext represents the context of execution of a SnapshotJob
type SnapshotJobContext struct {
endpointService portainer.EndpointService
snapshotter portainer.Snapshotter
}
// NewSnapshotJobContext returns a new context that can be used to execute a SnapshotJob
func NewSnapshotJobContext(endpointService portainer.EndpointService, snapshotter portainer.Snapshotter) *SnapshotJobContext {
return &SnapshotJobContext{
endpointService: endpointService,
snapshotter: snapshotter,
}
}
// NewSnapshotJobRunner returns a new runner that can be scheduled
func NewSnapshotJobRunner(schedule *portainer.Schedule, context *SnapshotJobContext) *SnapshotJobRunner {
return &SnapshotJobRunner{
schedule: schedule,
context: context,
}
}
// GetSchedule returns the schedule associated to the runner
func (runner *SnapshotJobRunner) GetSchedule() *portainer.Schedule {
return runner.schedule
}
// Run triggers the execution of the schedule.
// It will iterate through all the endpoints available in the database to
// create a snapshot of each one of them.
// As a snapshot can be a long process, to avoid any concurrency issue we
// retrieve the latest version of the endpoint right after a snapshot.
func (runner *SnapshotJobRunner) Run() {
go func() {
endpoints, err := runner.context.endpointService.Endpoints()
if err != nil {
log.Printf("background schedule error (endpoint snapshot). Unable to retrieve endpoint list (err=%s)\n", err)
return
}
for _, endpoint := range endpoints {
if endpoint.Type == portainer.AzureEnvironment {
continue
}
snapshot, snapshotError := runner.context.snapshotter.CreateSnapshot(&endpoint)
latestEndpointReference, err := runner.context.endpointService.Endpoint(endpoint.ID)
if latestEndpointReference == nil {
log.Printf("background schedule error (endpoint snapshot). Endpoint not found inside the database anymore (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
continue
}
latestEndpointReference.Status = portainer.EndpointStatusUp
if snapshotError != nil {
log.Printf("background schedule error (endpoint snapshot). Unable to create snapshot (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, snapshotError)
latestEndpointReference.Status = portainer.EndpointStatusDown
}
if snapshot != nil {
latestEndpointReference.Snapshots = []portainer.Snapshot{*snapshot}
}
err = runner.context.endpointService.UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference)
if err != nil {
log.Printf("background schedule error (endpoint snapshot). Unable to update endpoint (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
return
}
}
}()
}
+115
View File
@@ -0,0 +1,115 @@
package cron
import (
"github.com/portainer/portainer"
"github.com/robfig/cron"
)
// JobScheduler represents a service for managing crons
type JobScheduler struct {
cron *cron.Cron
}
// NewJobScheduler initializes a new service
func NewJobScheduler() *JobScheduler {
return &JobScheduler{
cron: cron.New(),
}
}
// ScheduleJob schedules the execution of a job via a runner
func (scheduler *JobScheduler) ScheduleJob(runner portainer.JobRunner) error {
return scheduler.cron.AddJob(runner.GetSchedule().CronExpression, runner)
}
// UpdateSystemJobSchedule updates the first occurence of the specified
// scheduled job based on the specified job type.
// It does so by re-creating a new cron
// and adding all the existing jobs. It will then re-schedule the new job
// with the update cron expression passed in parameter.
// NOTE: the cron library do not support updating schedules directly
// hence the work-around
func (scheduler *JobScheduler) UpdateSystemJobSchedule(jobType portainer.JobType, newCronExpression string) error {
cronEntries := scheduler.cron.Entries()
newCron := cron.New()
for _, entry := range cronEntries {
if entry.Job.(portainer.JobRunner).GetSchedule().JobType == jobType {
err := newCron.AddJob(newCronExpression, entry.Job)
if err != nil {
return err
}
continue
}
newCron.Schedule(entry.Schedule, entry.Job)
}
scheduler.cron.Stop()
scheduler.cron = newCron
scheduler.cron.Start()
return nil
}
// UpdateJobSchedule updates a specific scheduled job by re-creating a new cron
// and adding all the existing jobs. It will then re-schedule the new job
// via the specified JobRunner parameter.
// NOTE: the cron library do not support updating schedules directly
// hence the work-around
func (scheduler *JobScheduler) UpdateJobSchedule(runner portainer.JobRunner) error {
cronEntries := scheduler.cron.Entries()
newCron := cron.New()
for _, entry := range cronEntries {
if entry.Job.(portainer.JobRunner).GetSchedule().ID == runner.GetSchedule().ID {
var jobRunner cron.Job = runner
if entry.Job.(portainer.JobRunner).GetSchedule().JobType == portainer.SnapshotJobType {
jobRunner = entry.Job
}
err := newCron.AddJob(runner.GetSchedule().CronExpression, jobRunner)
if err != nil {
return err
}
continue
}
newCron.Schedule(entry.Schedule, entry.Job)
}
scheduler.cron.Stop()
scheduler.cron = newCron
scheduler.cron.Start()
return nil
}
// UnscheduleJob remove a scheduled job by re-creating a new cron
// and adding all the existing jobs except for the one specified via scheduleID.
// NOTE: the cron library do not support removing schedules directly
// hence the work-around
func (scheduler *JobScheduler) UnscheduleJob(scheduleID portainer.ScheduleID) {
cronEntries := scheduler.cron.Entries()
newCron := cron.New()
for _, entry := range cronEntries {
if entry.Job.(portainer.JobRunner).GetSchedule().ID == scheduleID {
continue
}
newCron.Schedule(entry.Schedule, entry.Job)
}
scheduler.cron.Stop()
scheduler.cron = newCron
scheduler.cron.Start()
}
// Start starts the scheduled jobs
func (scheduler *JobScheduler) Start() {
if len(scheduler.cron.Entries()) > 0 {
scheduler.cron.Start()
}
}
-40
View File
@@ -1,40 +0,0 @@
package cron
import (
"github.com/portainer/portainer"
"github.com/robfig/cron"
)
// Watcher represents a service for managing crons.
type Watcher struct {
Cron *cron.Cron
EndpointService portainer.EndpointService
syncInterval string
}
// NewWatcher initializes a new service.
func NewWatcher(endpointService portainer.EndpointService, syncInterval string) *Watcher {
return &Watcher{
Cron: cron.New(),
EndpointService: endpointService,
syncInterval: syncInterval,
}
}
// WatchEndpointFile starts a cron job to synchronize the endpoints from a file
func (watcher *Watcher) WatchEndpointFile(endpointFilePath string) error {
job := newEndpointSyncJob(endpointFilePath, watcher.EndpointService)
err := job.Sync()
if err != nil {
return err
}
err = watcher.Cron.AddJob("@every "+watcher.syncInterval, job)
if err != nil {
return err
}
watcher.Cron.Start()
return nil
}
+137
View File
@@ -0,0 +1,137 @@
package crypto
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"math/big"
)
const (
// PrivateKeyPemHeader represents the header that is appended to the PEM file when
// storing the private key.
PrivateKeyPemHeader = "EC PRIVATE KEY"
// PublicKeyPemHeader represents the header that is appended to the PEM file when
// storing the public key.
PublicKeyPemHeader = "ECDSA PUBLIC KEY"
)
// ECDSAService is a service used to create digital signatures when communicating with
// an agent based environment. It will automatically generates a key pair using ECDSA or
// can also reuse an existing ECDSA key pair.
type ECDSAService struct {
privateKey *ecdsa.PrivateKey
publicKey *ecdsa.PublicKey
encodedPubKey string
secret string
}
// NewECDSAService returns a pointer to a ECDSAService.
// An optional secret can be specified
func NewECDSAService(secret string) *ECDSAService {
return &ECDSAService{
secret: secret,
}
}
// EncodedPublicKey returns the encoded version of the public that can be used
// to be shared with other services. It's the hexadecimal encoding of the public key
// content.
func (service *ECDSAService) EncodedPublicKey() string {
return service.encodedPubKey
}
// PEMHeaders returns the ECDSA PEM headers.
func (service *ECDSAService) PEMHeaders() (string, string) {
return PrivateKeyPemHeader, PublicKeyPemHeader
}
// ParseKeyPair parses existing private/public key pair content and associate
// the parsed keys to the service.
func (service *ECDSAService) ParseKeyPair(private, public []byte) error {
privateKey, err := x509.ParseECPrivateKey(private)
if err != nil {
return err
}
service.privateKey = privateKey
encodedKey := hex.EncodeToString(public)
service.encodedPubKey = encodedKey
publicKey, err := x509.ParsePKIXPublicKey(public)
if err != nil {
return err
}
service.publicKey = publicKey.(*ecdsa.PublicKey)
return nil
}
// GenerateKeyPair will create a new key pair using ECDSA.
func (service *ECDSAService) GenerateKeyPair() ([]byte, []byte, error) {
pubkeyCurve := elliptic.P256()
privatekey, err := ecdsa.GenerateKey(pubkeyCurve, rand.Reader)
if err != nil {
return nil, nil, err
}
service.privateKey = privatekey
service.publicKey = &privatekey.PublicKey
private, err := x509.MarshalECPrivateKey(service.privateKey)
if err != nil {
return nil, nil, err
}
public, err := x509.MarshalPKIXPublicKey(service.publicKey)
if err != nil {
return nil, nil, err
}
encodedKey := hex.EncodeToString(public)
service.encodedPubKey = encodedKey
return private, public, nil
}
// CreateSignature creates a digital signature.
// It automatically hash a specific message using MD5 and creates a signature from
// that hash.
// If a secret is associated to the service, it will be used instead of the specified
// message.
// It then encodes the generated signature in base64.
func (service *ECDSAService) CreateSignature(message string) (string, error) {
if service.secret != "" {
message = service.secret
}
hash := HashFromBytes([]byte(message))
r := big.NewInt(0)
s := big.NewInt(0)
r, s, err := ecdsa.Sign(rand.Reader, service.privateKey, hash)
if err != nil {
return "", err
}
keyBytes := service.privateKey.Params().BitSize / 8
rBytes := r.Bytes()
rBytesPadded := make([]byte, keyBytes)
copy(rBytesPadded[keyBytes-len(rBytes):], rBytes)
sBytes := s.Bytes()
sBytesPadded := make([]byte, keyBytes)
copy(sBytesPadded[keyBytes-len(sBytes):], sBytes)
signature := append(rBytesPadded, sBytesPadded...)
return base64.RawStdEncoding.EncodeToString(signature), nil
}
+10
View File
@@ -0,0 +1,10 @@
package crypto
import "crypto/md5"
// HashFromBytes returns the hash of the specified data
func HashFromBytes(data []byte) []byte {
digest := md5.New()
digest.Write(data)
return digest.Sum(nil)
}
+35 -15
View File
@@ -4,36 +4,56 @@ import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"github.com/portainer/portainer"
)
// CreateTLSConfiguration initializes a tls.Config using a CA certificate, a certificate and a key
func CreateTLSConfiguration(config *portainer.TLSConfiguration) (*tls.Config, error) {
TLSConfig := &tls.Config{}
// CreateTLSConfigurationFromBytes initializes a tls.Config using a CA certificate, a certificate and a key
// loaded from memory.
func CreateTLSConfigurationFromBytes(caCert, cert, key []byte, skipClientVerification, skipServerVerification bool) (*tls.Config, error) {
config := &tls.Config{}
config.InsecureSkipVerify = skipServerVerification
if config.TLSCertPath != "" && config.TLSKeyPath != "" {
cert, err := tls.LoadX509KeyPair(config.TLSCertPath, config.TLSKeyPath)
if !skipClientVerification {
certificate, err := tls.X509KeyPair(cert, key)
if err != nil {
return nil, err
}
config.Certificates = []tls.Certificate{certificate}
}
if !skipServerVerification {
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
config.RootCAs = caCertPool
}
return config, nil
}
// CreateTLSConfigurationFromDisk initializes a tls.Config using a CA certificate, a certificate and a key
// loaded from disk.
func CreateTLSConfigurationFromDisk(caCertPath, certPath, keyPath string, skipServerVerification bool) (*tls.Config, error) {
config := &tls.Config{}
config.InsecureSkipVerify = skipServerVerification
if certPath != "" && keyPath != "" {
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return nil, err
}
TLSConfig.Certificates = []tls.Certificate{cert}
config.Certificates = []tls.Certificate{cert}
}
if !config.TLSSkipVerify {
caCert, err := ioutil.ReadFile(config.TLSCACertPath)
if !skipServerVerification && caCertPath != "" {
caCert, err := ioutil.ReadFile(caCertPath)
if err != nil {
return nil, err
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
TLSConfig.RootCAs = caCertPool
config.RootCAs = caCertPool
}
TLSConfig.InsecureSkipVerify = config.TLSSkipVerify
return TLSConfig, nil
return config, nil
}
+108
View File
@@ -0,0 +1,108 @@
package docker
import (
"net/http"
"strings"
"time"
"github.com/docker/docker/client"
"github.com/portainer/portainer"
"github.com/portainer/portainer/crypto"
)
const (
unsupportedEnvironmentType = portainer.Error("Environment not supported")
)
// ClientFactory is used to create Docker clients
type ClientFactory struct {
signatureService portainer.DigitalSignatureService
}
// NewClientFactory returns a new instance of a ClientFactory
func NewClientFactory(signatureService portainer.DigitalSignatureService) *ClientFactory {
return &ClientFactory{
signatureService: signatureService,
}
}
// CreateClient is a generic function to create a Docker client based on
// a specific endpoint configuration. The nodeName parameter can be used
// with an agent enabled endpoint to target a specific node in an agent cluster.
func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeName string) (*client.Client, error) {
if endpoint.Type == portainer.AzureEnvironment {
return nil, unsupportedEnvironmentType
} else if endpoint.Type == portainer.AgentOnDockerEnvironment {
return createAgentClient(endpoint, factory.signatureService, nodeName)
}
if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") {
return createLocalClient(endpoint)
}
return createTCPClient(endpoint)
}
func createLocalClient(endpoint *portainer.Endpoint) (*client.Client, error) {
return client.NewClientWithOpts(
client.WithHost(endpoint.URL),
client.WithVersion(portainer.SupportedDockerAPIVersion),
)
}
func createTCPClient(endpoint *portainer.Endpoint) (*client.Client, error) {
httpCli, err := httpClient(endpoint)
if err != nil {
return nil, err
}
return client.NewClientWithOpts(
client.WithHost(endpoint.URL),
client.WithVersion(portainer.SupportedDockerAPIVersion),
client.WithHTTPClient(httpCli),
)
}
func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.DigitalSignatureService, nodeName string) (*client.Client, error) {
httpCli, err := httpClient(endpoint)
if err != nil {
return nil, err
}
signature, err := signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
if err != nil {
return nil, err
}
headers := map[string]string{
portainer.PortainerAgentPublicKeyHeader: signatureService.EncodedPublicKey(),
portainer.PortainerAgentSignatureHeader: signature,
}
if nodeName != "" {
headers[portainer.PortainerAgentTargetHeader] = nodeName
}
return client.NewClientWithOpts(
client.WithHost(endpoint.URL),
client.WithVersion(portainer.SupportedDockerAPIVersion),
client.WithHTTPClient(httpCli),
client.WithHTTPHeaders(headers),
)
}
func httpClient(endpoint *portainer.Endpoint) (*http.Client, error) {
transport := &http.Transport{}
if endpoint.TLSConfig.TLS {
tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
if err != nil {
return nil, err
}
transport.TLSClientConfig = tlsConfig
}
return &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}, nil
}
+115
View File
@@ -0,0 +1,115 @@
package docker
import (
"bytes"
"context"
"io"
"io/ioutil"
"strconv"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/strslice"
"github.com/docker/docker/client"
"github.com/portainer/portainer"
"github.com/portainer/portainer/archive"
)
// JobService represents a service that handles the execution of jobs
type JobService struct {
dockerClientFactory *ClientFactory
}
// NewJobService returns a pointer to a new job service
func NewJobService(dockerClientFactory *ClientFactory) *JobService {
return &JobService{
dockerClientFactory: dockerClientFactory,
}
}
// ExecuteScript will leverage a privileged container to execute a script against the specified endpoint/nodename.
// It will copy the script content specified as a parameter inside a container based on the specified image and execute it.
func (service *JobService) ExecuteScript(endpoint *portainer.Endpoint, nodeName, image string, script []byte, schedule *portainer.Schedule) error {
buffer, err := archive.TarFileInBuffer(script, "script.sh", 0700)
if err != nil {
return err
}
cli, err := service.dockerClientFactory.CreateClient(endpoint, nodeName)
if err != nil {
return err
}
defer cli.Close()
_, err = cli.Ping(context.Background())
if err != nil {
return portainer.ErrUnableToPingEndpoint
}
err = pullImage(cli, image)
if err != nil {
return err
}
containerConfig := &container.Config{
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
Tty: true,
WorkingDir: "/tmp",
Image: image,
Labels: map[string]string{
"io.portainer.job.endpoint": strconv.Itoa(int(endpoint.ID)),
},
Cmd: strslice.StrSlice([]string{"sh", "/tmp/script.sh"}),
}
if schedule != nil {
containerConfig.Labels["io.portainer.schedule.id"] = strconv.Itoa(int(schedule.ID))
}
hostConfig := &container.HostConfig{
Binds: []string{"/:/host", "/etc:/etc:ro", "/usr:/usr:ro", "/run:/run:ro", "/sbin:/sbin:ro", "/var:/var:ro"},
NetworkMode: "host",
Privileged: true,
}
networkConfig := &network.NetworkingConfig{}
body, err := cli.ContainerCreate(context.Background(), containerConfig, hostConfig, networkConfig, "")
if err != nil {
return err
}
if schedule != nil {
err = cli.ContainerRename(context.Background(), body.ID, schedule.Name+"_"+body.ID)
if err != nil {
return err
}
}
copyOptions := types.CopyToContainerOptions{}
err = cli.CopyToContainer(context.Background(), body.ID, "/tmp", bytes.NewReader(buffer), copyOptions)
if err != nil {
return err
}
startOptions := types.ContainerStartOptions{}
return cli.ContainerStart(context.Background(), body.ID, startOptions)
}
func pullImage(cli *client.Client, image string) error {
imageReadCloser, err := cli.ImagePull(context.Background(), image, types.ImagePullOptions{})
if err != nil {
return err
}
defer imageReadCloser.Close()
_, err = io.Copy(ioutil.Discard, imageReadCloser)
if err != nil {
return err
}
return nil
}
+188
View File
@@ -0,0 +1,188 @@
package docker
import (
"context"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"github.com/portainer/portainer"
)
func snapshot(cli *client.Client) (*portainer.Snapshot, error) {
_, err := cli.Ping(context.Background())
if err != nil {
return nil, err
}
snapshot := &portainer.Snapshot{
StackCount: 0,
}
err = snapshotInfo(snapshot, cli)
if err != nil {
return nil, err
}
if snapshot.Swarm {
err = snapshotSwarmServices(snapshot, cli)
if err != nil {
return nil, err
}
err = snapshotNodes(snapshot, cli)
if err != nil {
return nil, err
}
}
err = snapshotContainers(snapshot, cli)
if err != nil {
return nil, err
}
err = snapshotImages(snapshot, cli)
if err != nil {
return nil, err
}
err = snapshotVolumes(snapshot, cli)
if err != nil {
return nil, err
}
err = snapshotNetworks(snapshot, cli)
if err != nil {
return nil, err
}
err = snapshotVersion(snapshot, cli)
if err != nil {
return nil, err
}
snapshot.Time = time.Now().Unix()
return snapshot, nil
}
func snapshotInfo(snapshot *portainer.Snapshot, cli *client.Client) error {
info, err := cli.Info(context.Background())
if err != nil {
return err
}
snapshot.Swarm = info.Swarm.ControlAvailable
snapshot.DockerVersion = info.ServerVersion
snapshot.TotalCPU = info.NCPU
snapshot.TotalMemory = info.MemTotal
snapshot.SnapshotRaw.Info = info
return nil
}
func snapshotNodes(snapshot *portainer.Snapshot, cli *client.Client) error {
nodes, err := cli.NodeList(context.Background(), types.NodeListOptions{})
if err != nil {
return err
}
var nanoCpus int64
var totalMem int64
for _, node := range nodes {
nanoCpus += node.Description.Resources.NanoCPUs
totalMem += node.Description.Resources.MemoryBytes
}
snapshot.TotalCPU = int(nanoCpus / 1e9)
snapshot.TotalMemory = totalMem
return nil
}
func snapshotSwarmServices(snapshot *portainer.Snapshot, cli *client.Client) error {
stacks := make(map[string]struct{})
services, err := cli.ServiceList(context.Background(), types.ServiceListOptions{})
if err != nil {
return err
}
for _, service := range services {
for k, v := range service.Spec.Labels {
if k == "com.docker.stack.namespace" {
stacks[v] = struct{}{}
}
}
}
snapshot.ServiceCount = len(services)
snapshot.StackCount += len(stacks)
return nil
}
func snapshotContainers(snapshot *portainer.Snapshot, cli *client.Client) error {
containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{All: true})
if err != nil {
return err
}
runningContainers := 0
stoppedContainers := 0
stacks := make(map[string]struct{})
for _, container := range containers {
if container.State == "exited" {
stoppedContainers++
} else if container.State == "running" {
runningContainers++
}
for k, v := range container.Labels {
if k == "com.docker.compose.project" {
stacks[v] = struct{}{}
}
}
}
snapshot.RunningContainerCount = runningContainers
snapshot.StoppedContainerCount = stoppedContainers
snapshot.StackCount += len(stacks)
snapshot.SnapshotRaw.Containers = containers
return nil
}
func snapshotImages(snapshot *portainer.Snapshot, cli *client.Client) error {
images, err := cli.ImageList(context.Background(), types.ImageListOptions{})
if err != nil {
return err
}
snapshot.ImageCount = len(images)
snapshot.SnapshotRaw.Images = images
return nil
}
func snapshotVolumes(snapshot *portainer.Snapshot, cli *client.Client) error {
volumes, err := cli.VolumeList(context.Background(), filters.Args{})
if err != nil {
return err
}
snapshot.VolumeCount = len(volumes.Volumes)
snapshot.SnapshotRaw.Volumes = volumes
return nil
}
func snapshotNetworks(snapshot *portainer.Snapshot, cli *client.Client) error {
networks, err := cli.NetworkList(context.Background(), types.NetworkListOptions{})
if err != nil {
return err
}
snapshot.SnapshotRaw.Networks = networks
return nil
}
func snapshotVersion(snapshot *portainer.Snapshot, cli *client.Client) error {
version, err := cli.ServerVersion(context.Background())
if err != nil {
return err
}
snapshot.SnapshotRaw.Version = version
return nil
}
+28
View File
@@ -0,0 +1,28 @@
package docker
import (
"github.com/portainer/portainer"
)
// Snapshotter represents a service used to create endpoint snapshots
type Snapshotter struct {
clientFactory *ClientFactory
}
// NewSnapshotter returns a new Snapshotter instance
func NewSnapshotter(clientFactory *ClientFactory) *Snapshotter {
return &Snapshotter{
clientFactory: clientFactory,
}
}
// CreateSnapshot creates a snapshot of a specific endpoint
func (snapshotter *Snapshotter) CreateSnapshot(endpoint *portainer.Endpoint) (*portainer.Snapshot, error) {
cli, err := snapshotter.clientFactory.CreateClient(endpoint, "")
if err != nil {
return nil, err
}
defer cli.Close()
return snapshot(cli)
}
+44 -30
View File
@@ -4,57 +4,65 @@ package portainer
const (
ErrUnauthorized = Error("Unauthorized")
ErrResourceAccessDenied = Error("Access denied to resource")
ErrResourceNotFound = Error("Unable to find resource")
ErrUnsupportedDockerAPI = Error("Unsupported Docker API response")
ErrObjectNotFound = Error("Object not found inside the database")
ErrMissingSecurityContext = Error("Unable to find security details in request context")
)
// User errors.
const (
ErrUserNotFound = Error("User not found")
ErrUserAlreadyExists = Error("User already exists")
ErrInvalidUsername = Error("Invalid username. White spaces are not allowed")
ErrAdminAlreadyInitialized = Error("An administrator user already exists")
ErrCannotRemoveAdmin = Error("Cannot remove the default administrator account")
ErrAdminCannotRemoveSelf = Error("Cannot remove your own user account. Contact another administrator")
ErrUserAlreadyExists = Error("User already exists")
ErrInvalidUsername = Error("Invalid username. White spaces are not allowed")
ErrAdminAlreadyInitialized = Error("An administrator user already exists")
ErrAdminCannotRemoveSelf = Error("Cannot remove your own user account. Contact another administrator")
ErrCannotRemoveLastLocalAdmin = Error("Cannot remove the last local administrator account")
)
// Team errors.
const (
ErrTeamNotFound = Error("Team not found")
ErrTeamAlreadyExists = Error("Team already exists")
)
// TeamMembership errors.
const (
ErrTeamMembershipNotFound = Error("Team membership not found")
ErrTeamMembershipAlreadyExists = Error("Team membership already exists for this user and team.")
ErrTeamMembershipAlreadyExists = Error("Team membership already exists for this user and team")
)
// ResourceControl errors.
const (
ErrResourceControlNotFound = Error("Resource control not found")
ErrResourceControlAlreadyExists = Error("A resource control is already applied on this resource")
ErrInvalidResourceControlType = Error("Unsupported resource control type")
)
// Endpoint errors.
const (
ErrEndpointNotFound = Error("Endpoint not found")
ErrEndpointAccessDenied = Error("Access denied to endpoint")
)
// Azure environment errors
const (
ErrAzureInvalidCredentials = Error("Invalid Azure credentials")
)
// Endpoint group errors.
const (
ErrCannotRemoveDefaultGroup = Error("Cannot remove the default endpoint group")
)
// Registry errors.
const (
ErrRegistryNotFound = Error("Registry not found")
ErrRegistryAlreadyExists = Error("A registry is already defined for this URL")
)
// Stack errors
const (
ErrStackNotFound = Error("Stack not found")
ErrStackAlreadyExists = Error("A stack already exists with this name")
ErrComposeFileNotFoundInRepository = Error("Unable to find a Compose file in the repository")
ErrStackNotExternal = Error("Not an external stack")
)
// Tag errors
const (
ErrTagAlreadyExists = Error("A tag already exists with this name")
)
// Endpoint extensions error
@@ -63,21 +71,6 @@ const (
ErrEndpointExtensionAlreadyAssociated = Error("This extension is already associated to the endpoint")
)
// Version errors.
const (
ErrDBVersionNotFound = Error("DB version not found")
)
// Settings errors.
const (
ErrSettingsNotFound = Error("Settings not found")
)
// DockerHub errors.
const (
ErrDockerHubNotFound = Error("Dockerhub not found")
)
// Crypto errors.
const (
ErrCryptoHashFailure = Error("Unable to hash data")
@@ -95,8 +88,29 @@ const (
ErrUndefinedTLSFileType = Error("Undefined TLS file type")
)
// Extension errors.
const (
ErrExtensionAlreadyEnabled = Error("This extension is already enabled")
)
// Docker errors.
const (
ErrUnableToPingEndpoint = Error("Unable to communicate with the endpoint")
)
// Schedule errors.
const (
ErrHostManagementFeaturesDisabled = Error("Host management features are disabled")
)
// Error represents an application error.
type Error string
// Error returns the error message.
func (e Error) Error() string { return string(e) }
// Webhook errors
const (
ErrWebhookAlreadyExists = Error("A webhook for this resource already exists")
ErrUnsupportedWebhookType = Error("Webhooks for this resource are not currently supported")
)
+211
View File
@@ -0,0 +1,211 @@
package exec
import (
"bytes"
"encoding/json"
"errors"
"os/exec"
"path"
"runtime"
"strconv"
"strings"
"github.com/orcaman/concurrent-map"
"github.com/portainer/portainer"
"github.com/portainer/portainer/http/client"
)
var extensionDownloadBaseURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com/extensions/"
var extensionBinaryMap = map[portainer.ExtensionID]string{
portainer.RegistryManagementExtension: "extension-registry-management",
portainer.OAuthAuthenticationExtension: "extension-oauth-authentication",
}
// ExtensionManager represents a service used to
// manage extension processes.
type ExtensionManager struct {
processes cmap.ConcurrentMap
fileService portainer.FileService
extensionService portainer.ExtensionService
}
// NewExtensionManager returns a pointer to an ExtensionManager
func NewExtensionManager(fileService portainer.FileService, extensionService portainer.ExtensionService) *ExtensionManager {
return &ExtensionManager{
processes: cmap.New(),
fileService: fileService,
extensionService: extensionService,
}
}
func processKey(ID portainer.ExtensionID) string {
return strconv.Itoa(int(ID))
}
func buildExtensionURL(extension *portainer.Extension) string {
extensionURL := extensionDownloadBaseURL
extensionURL += extensionBinaryMap[extension.ID]
extensionURL += "-" + runtime.GOOS + "-" + runtime.GOARCH
extensionURL += "-" + extension.Version
extensionURL += ".zip"
return extensionURL
}
func buildExtensionPath(binaryPath string, extension *portainer.Extension) string {
extensionFilename := extensionBinaryMap[extension.ID]
extensionFilename += "-" + runtime.GOOS + "-" + runtime.GOARCH
extensionFilename += "-" + extension.Version
if runtime.GOOS == "windows" {
extensionFilename += ".exe"
}
extensionPath := path.Join(
binaryPath,
extensionFilename)
return extensionPath
}
// FetchExtensionDefinitions will fetch the list of available
// extension definitions from the official Portainer assets server
func (manager *ExtensionManager) FetchExtensionDefinitions() ([]portainer.Extension, error) {
extensionData, err := client.Get(portainer.ExtensionDefinitionsURL, 30)
if err != nil {
return nil, err
}
var extensions []portainer.Extension
err = json.Unmarshal(extensionData, &extensions)
if err != nil {
return nil, err
}
return extensions, nil
}
// EnableExtension will check for the existence of the extension binary on the filesystem
// first. If it does not exist, it will download it from the official Portainer assets server.
// After installing the binary on the filesystem, it will execute the binary in license check
// mode to validate the extension license. If the license is valid, it will then start
// the extension process and register it in the processes map.
func (manager *ExtensionManager) EnableExtension(extension *portainer.Extension, licenseKey string) error {
extensionBinaryPath := buildExtensionPath(manager.fileService.GetBinaryFolder(), extension)
extensionBinaryExists, err := manager.fileService.FileExists(extensionBinaryPath)
if err != nil {
return err
}
if !extensionBinaryExists {
err := manager.downloadExtension(extension)
if err != nil {
return err
}
}
licenseDetails, err := validateLicense(extensionBinaryPath, licenseKey)
if err != nil {
return err
}
extension.License = portainer.LicenseInformation{
LicenseKey: licenseKey,
Company: licenseDetails[0],
Expiration: licenseDetails[1],
Valid: true,
}
extension.Version = licenseDetails[2]
return manager.startExtensionProcess(extension, extensionBinaryPath)
}
// DisableExtension will retrieve the process associated to the extension
// from the processes map and kill the process. It will then remove the process
// from the processes map and remove the binary associated to the extension
// from the filesystem
func (manager *ExtensionManager) DisableExtension(extension *portainer.Extension) error {
process, ok := manager.processes.Get(processKey(extension.ID))
if !ok {
return nil
}
err := process.(*exec.Cmd).Process.Kill()
if err != nil {
return err
}
manager.processes.Remove(processKey(extension.ID))
extensionBinaryPath := buildExtensionPath(manager.fileService.GetBinaryFolder(), extension)
return manager.fileService.RemoveDirectory(extensionBinaryPath)
}
// UpdateExtension will download the new extension binary from the official Portainer assets
// server, disable the previous extension via DisableExtension, trigger a license check
// and then start the extension process and add it to the processes map
func (manager *ExtensionManager) UpdateExtension(extension *portainer.Extension, version string) error {
oldVersion := extension.Version
extension.Version = version
err := manager.downloadExtension(extension)
if err != nil {
return err
}
extension.Version = oldVersion
err = manager.DisableExtension(extension)
if err != nil {
return err
}
extension.Version = version
extensionBinaryPath := buildExtensionPath(manager.fileService.GetBinaryFolder(), extension)
licenseDetails, err := validateLicense(extensionBinaryPath, extension.License.LicenseKey)
if err != nil {
return err
}
extension.Version = licenseDetails[2]
return manager.startExtensionProcess(extension, extensionBinaryPath)
}
func (manager *ExtensionManager) downloadExtension(extension *portainer.Extension) error {
extensionURL := buildExtensionURL(extension)
data, err := client.Get(extensionURL, 30)
if err != nil {
return err
}
return manager.fileService.ExtractExtensionArchive(data)
}
func validateLicense(binaryPath, licenseKey string) ([]string, error) {
licenseCheckProcess := exec.Command(binaryPath, "-license", licenseKey, "-check")
cmdOutput := &bytes.Buffer{}
licenseCheckProcess.Stdout = cmdOutput
err := licenseCheckProcess.Run()
if err != nil {
return nil, errors.New("Invalid extension license key")
}
output := string(cmdOutput.Bytes())
return strings.Split(output, "|"), nil
}
func (manager *ExtensionManager) startExtensionProcess(extension *portainer.Extension, binaryPath string) error {
extensionProcess := exec.Command(binaryPath, "-license", extension.License.LicenseKey)
err := extensionProcess.Start()
if err != nil {
return err
}
manager.processes.Set(processKey(extension.ID), extensionProcess)
return nil
}
-117
View File
@@ -1,117 +0,0 @@
package exec
import (
"bytes"
"os"
"os/exec"
"path"
"runtime"
"github.com/portainer/portainer"
)
// StackManager represents a service for managing stacks.
type StackManager struct {
binaryPath string
}
// NewStackManager initializes a new StackManager service.
func NewStackManager(binaryPath string) *StackManager {
return &StackManager{
binaryPath: binaryPath,
}
}
// Login executes the docker login command against a list of registries (including DockerHub).
func (manager *StackManager) Login(dockerhub *portainer.DockerHub, registries []portainer.Registry, endpoint *portainer.Endpoint) {
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
for _, registry := range registries {
if registry.Authentication {
registryArgs := append(args, "login", "--username", registry.Username, "--password", registry.Password, registry.URL)
runCommandAndCaptureStdErr(command, registryArgs, nil, "")
}
}
if dockerhub.Authentication {
dockerhubArgs := append(args, "login", "--username", dockerhub.Username, "--password", dockerhub.Password)
runCommandAndCaptureStdErr(command, dockerhubArgs, nil, "")
}
}
// Logout executes the docker logout command.
func (manager *StackManager) Logout(endpoint *portainer.Endpoint) error {
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
args = append(args, "logout")
return runCommandAndCaptureStdErr(command, args, nil, "")
}
// Deploy executes the docker stack deploy command.
func (manager *StackManager) Deploy(stack *portainer.Stack, prune bool, endpoint *portainer.Endpoint) error {
stackFilePath := path.Join(stack.ProjectPath, stack.EntryPoint)
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
if prune {
args = append(args, "stack", "deploy", "--prune", "--with-registry-auth", "--compose-file", stackFilePath, stack.Name)
} else {
args = append(args, "stack", "deploy", "--with-registry-auth", "--compose-file", stackFilePath, stack.Name)
}
env := make([]string, 0)
for _, envvar := range stack.Env {
env = append(env, envvar.Name+"="+envvar.Value)
}
return runCommandAndCaptureStdErr(command, args, env, stack.ProjectPath)
}
// Remove executes the docker stack rm command.
func (manager *StackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
args = append(args, "stack", "rm", stack.Name)
return runCommandAndCaptureStdErr(command, args, nil, "")
}
func runCommandAndCaptureStdErr(command string, args []string, env []string, workingDir string) error {
var stderr bytes.Buffer
cmd := exec.Command(command, args...)
cmd.Stderr = &stderr
cmd.Dir = workingDir
if env != nil {
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, env...)
}
err := cmd.Run()
if err != nil {
return portainer.Error(stderr.String())
}
return nil
}
func prepareDockerCommandAndArgs(binaryPath string, endpoint *portainer.Endpoint) (string, []string) {
// Assume Linux as a default
command := path.Join(binaryPath, "docker")
if runtime.GOOS == "windows" {
command = path.Join(binaryPath, "docker.exe")
}
args := make([]string, 0)
args = append(args, "-H", endpoint.URL)
if endpoint.TLSConfig.TLS {
args = append(args, "--tls")
if !endpoint.TLSConfig.TLSSkipVerify {
args = append(args, "--tlsverify", "--tlscacert", endpoint.TLSConfig.TLSCACertPath)
}
if endpoint.TLSConfig.TLSCertPath != "" && endpoint.TLSConfig.TLSKeyPath != "" {
args = append(args, "--tlscert", endpoint.TLSConfig.TLSCertPath, "--tlskey", endpoint.TLSConfig.TLSKeyPath)
}
}
return command, args
}
+178
View File
@@ -0,0 +1,178 @@
package exec
import (
"bytes"
"encoding/json"
"os"
"os/exec"
"path"
"runtime"
"github.com/portainer/portainer"
)
// SwarmStackManager represents a service for managing stacks.
type SwarmStackManager struct {
binaryPath string
dataPath string
signatureService portainer.DigitalSignatureService
fileService portainer.FileService
}
// NewSwarmStackManager initializes a new SwarmStackManager service.
// It also updates the configuration of the Docker CLI binary.
func NewSwarmStackManager(binaryPath, dataPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (*SwarmStackManager, error) {
manager := &SwarmStackManager{
binaryPath: binaryPath,
dataPath: dataPath,
signatureService: signatureService,
fileService: fileService,
}
err := manager.updateDockerCLIConfiguration(dataPath)
if err != nil {
return nil, err
}
return manager, nil
}
// Login executes the docker login command against a list of registries (including DockerHub).
func (manager *SwarmStackManager) Login(dockerhub *portainer.DockerHub, registries []portainer.Registry, endpoint *portainer.Endpoint) {
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
for _, registry := range registries {
if registry.Authentication {
registryArgs := append(args, "login", "--username", registry.Username, "--password", registry.Password, registry.URL)
runCommandAndCaptureStdErr(command, registryArgs, nil, "")
}
}
if dockerhub.Authentication {
dockerhubArgs := append(args, "login", "--username", dockerhub.Username, "--password", dockerhub.Password)
runCommandAndCaptureStdErr(command, dockerhubArgs, nil, "")
}
}
// Logout executes the docker logout command.
func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
args = append(args, "logout")
return runCommandAndCaptureStdErr(command, args, nil, "")
}
// Deploy executes the docker stack deploy command.
func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, endpoint *portainer.Endpoint) error {
stackFilePath := path.Join(stack.ProjectPath, stack.EntryPoint)
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
if prune {
args = append(args, "stack", "deploy", "--prune", "--with-registry-auth", "--compose-file", stackFilePath, stack.Name)
} else {
args = append(args, "stack", "deploy", "--with-registry-auth", "--compose-file", stackFilePath, stack.Name)
}
env := make([]string, 0)
for _, envvar := range stack.Env {
env = append(env, envvar.Name+"="+envvar.Value)
}
stackFolder := path.Dir(stackFilePath)
return runCommandAndCaptureStdErr(command, args, env, stackFolder)
}
// Remove executes the docker stack rm command.
func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
args = append(args, "stack", "rm", stack.Name)
return runCommandAndCaptureStdErr(command, args, nil, "")
}
func runCommandAndCaptureStdErr(command string, args []string, env []string, workingDir string) error {
var stderr bytes.Buffer
cmd := exec.Command(command, args...)
cmd.Stderr = &stderr
cmd.Dir = workingDir
if env != nil {
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, env...)
}
err := cmd.Run()
if err != nil {
return portainer.Error(stderr.String())
}
return nil
}
func prepareDockerCommandAndArgs(binaryPath, dataPath string, endpoint *portainer.Endpoint) (string, []string) {
// Assume Linux as a default
command := path.Join(binaryPath, "docker")
if runtime.GOOS == "windows" {
command = path.Join(binaryPath, "docker.exe")
}
args := make([]string, 0)
args = append(args, "--config", dataPath)
args = append(args, "-H", endpoint.URL)
if endpoint.TLSConfig.TLS {
args = append(args, "--tls")
if !endpoint.TLSConfig.TLSSkipVerify {
args = append(args, "--tlsverify", "--tlscacert", endpoint.TLSConfig.TLSCACertPath)
}
if endpoint.TLSConfig.TLSCertPath != "" && endpoint.TLSConfig.TLSKeyPath != "" {
args = append(args, "--tlscert", endpoint.TLSConfig.TLSCertPath, "--tlskey", endpoint.TLSConfig.TLSKeyPath)
}
}
return command, args
}
func (manager *SwarmStackManager) updateDockerCLIConfiguration(dataPath string) error {
configFilePath := path.Join(dataPath, "config.json")
config, err := manager.retrieveConfigurationFromDisk(configFilePath)
if err != nil {
return err
}
signature, err := manager.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
if err != nil {
return err
}
if config["HttpHeaders"] == nil {
config["HttpHeaders"] = make(map[string]interface{})
}
headersObject := config["HttpHeaders"].(map[string]interface{})
headersObject["X-PortainerAgent-ManagerOperation"] = "1"
headersObject["X-PortainerAgent-Signature"] = signature
headersObject["X-PortainerAgent-PublicKey"] = manager.signatureService.EncodedPublicKey()
err = manager.fileService.WriteJSONToFile(configFilePath, config)
if err != nil {
return err
}
return nil
}
func (manager *SwarmStackManager) retrieveConfigurationFromDisk(path string) (map[string]interface{}, error) {
var config map[string]interface{}
raw, err := manager.fileService.GetFileContent(path)
if err != nil {
return make(map[string]interface{}), nil
}
err = json.Unmarshal(raw, &config)
if err != nil {
return nil, err
}
return config, nil
}
+205 -46
View File
@@ -2,9 +2,12 @@ package filesystem
import (
"bytes"
"encoding/json"
"encoding/pem"
"io/ioutil"
"github.com/portainer/portainer"
"github.com/portainer/portainer/archive"
"io"
"os"
@@ -26,6 +29,17 @@ const (
ComposeStorePath = "compose"
// ComposeFileDefaultName represents the default name of a compose file.
ComposeFileDefaultName = "docker-compose.yml"
// 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.
PublicKeyFile = "portainer.pub"
// BinaryStorePath represents the subfolder where binaries are stored in the file store folder.
BinaryStorePath = "bin"
// ScheduleStorePath represents the subfolder where schedule files are stored.
ScheduleStorePath = "schedules"
// ExtensionRegistryManagementStorePath represents the subfolder where files related to the
// registry management extension are stored.
ExtensionRegistryManagementStorePath = "extensions"
)
// Service represents a service for managing files and directories.
@@ -42,20 +56,22 @@ func NewService(dataStorePath, fileStorePath string) (*Service, error) {
fileStorePath: path.Join(dataStorePath, fileStorePath),
}
// Checking if a mount directory exists is broken with Go on Windows.
// This will need to be reviewed after the issue has been fixed in Go.
// See: https://github.com/portainer/portainer/issues/474
// err := createDirectoryIfNotExist(dataStorePath, 0755)
// if err != nil {
// return nil, err
// }
err := service.createDirectoryInStoreIfNotExist(TLSStorePath)
err := os.MkdirAll(dataStorePath, 0755)
if err != nil {
return nil, err
}
err = service.createDirectoryInStoreIfNotExist(ComposeStorePath)
err = service.createDirectoryInStore(TLSStorePath)
if err != nil {
return nil, err
}
err = service.createDirectoryInStore(ComposeStorePath)
if err != nil {
return nil, err
}
err = service.createDirectoryInStore(BinaryStorePath)
if err != nil {
return nil, err
}
@@ -63,6 +79,22 @@ func NewService(dataStorePath, fileStorePath string) (*Service, error) {
return service, nil
}
// GetBinaryFolder returns the full path to the binary store on the filesystem
func (service *Service) GetBinaryFolder() string {
return path.Join(service.fileStorePath, BinaryStorePath)
}
// ExtractExtensionArchive extracts the content of an extension archive
// specified as raw data into the binary store on the filesystem
func (service *Service) ExtractExtensionArchive(data []byte) error {
err := archive.UnzipArchive(data, path.Join(service.fileStorePath, BinaryStorePath))
if err != nil {
return err
}
return nil
}
// RemoveDirectory removes a directory on the filesystem.
func (service *Service) RemoveDirectory(directoryPath string) error {
return os.RemoveAll(directoryPath)
@@ -74,17 +106,16 @@ func (service *Service) GetStackProjectPath(stackIdentifier string) string {
return path.Join(service.fileStorePath, ComposeStorePath, stackIdentifier)
}
// StoreStackFileFromString creates a subfolder in the ComposeStorePath and stores a new file using the content from a string.
// StoreStackFileFromBytes creates a subfolder in the ComposeStorePath and stores a new file from bytes.
// It returns the path to the folder where the file is stored.
func (service *Service) StoreStackFileFromString(stackIdentifier, stackFileContent string) (string, error) {
func (service *Service) StoreStackFileFromBytes(stackIdentifier, fileName string, data []byte) (string, error) {
stackStorePath := path.Join(ComposeStorePath, stackIdentifier)
err := service.createDirectoryInStoreIfNotExist(stackStorePath)
err := service.createDirectoryInStore(stackStorePath)
if err != nil {
return "", err
}
composeFilePath := path.Join(stackStorePath, ComposeFileDefaultName)
data := []byte(stackFileContent)
composeFilePath := path.Join(stackStorePath, fileName)
r := bytes.NewReader(data)
err = service.createFileInStore(composeFilePath, r)
@@ -95,31 +126,34 @@ func (service *Service) StoreStackFileFromString(stackIdentifier, stackFileConte
return path.Join(service.fileStorePath, stackStorePath), nil
}
// StoreStackFileFromReader creates a subfolder in the ComposeStorePath and stores a new file using the content from an io.Reader.
// StoreRegistryManagementFileFromBytes creates a subfolder in the
// ExtensionRegistryManagementStorePath and stores a new file from bytes.
// It returns the path to the folder where the file is stored.
func (service *Service) StoreStackFileFromReader(stackIdentifier string, r io.Reader) (string, error) {
stackStorePath := path.Join(ComposeStorePath, stackIdentifier)
err := service.createDirectoryInStoreIfNotExist(stackStorePath)
func (service *Service) StoreRegistryManagementFileFromBytes(folder, fileName string, data []byte) (string, error) {
extensionStorePath := path.Join(ExtensionRegistryManagementStorePath, folder)
err := service.createDirectoryInStore(extensionStorePath)
if err != nil {
return "", err
}
composeFilePath := path.Join(stackStorePath, ComposeFileDefaultName)
file := path.Join(extensionStorePath, fileName)
r := bytes.NewReader(data)
err = service.createFileInStore(composeFilePath, r)
err = service.createFileInStore(file, r)
if err != nil {
return "", err
}
return path.Join(service.fileStorePath, stackStorePath), nil
return path.Join(service.fileStorePath, file), nil
}
// StoreTLSFile creates a folder in the TLSStorePath and stores a new file with the content from r.
func (service *Service) StoreTLSFile(folder string, fileType portainer.TLSFileType, r io.Reader) error {
// StoreTLSFileFromBytes creates a folder in the TLSStorePath and stores a new file from bytes.
// It returns the path to the newly created file.
func (service *Service) StoreTLSFileFromBytes(folder string, fileType portainer.TLSFileType, data []byte) (string, error) {
storePath := path.Join(TLSStorePath, folder)
err := service.createDirectoryInStoreIfNotExist(storePath)
err := service.createDirectoryInStore(storePath)
if err != nil {
return err
return "", err
}
var fileName string
@@ -131,15 +165,16 @@ func (service *Service) StoreTLSFile(folder string, fileType portainer.TLSFileTy
case portainer.TLSFileKey:
fileName = TLSKeyFile
default:
return portainer.ErrUndefinedTLSFileType
return "", portainer.ErrUndefinedTLSFileType
}
tlsFilePath := path.Join(storePath, fileName)
r := bytes.NewReader(data)
err = service.createFileInStore(tlsFilePath, r)
if err != nil {
return err
return "", err
}
return nil
return path.Join(service.fileStorePath, tlsFilePath), nil
}
// GetPathForTLSFile returns the absolute path to a specific TLS file for an endpoint.
@@ -191,36 +226,101 @@ func (service *Service) DeleteTLSFile(folder string, fileType portainer.TLSFileT
return nil
}
// GetFileContent returns a string content from file.
func (service *Service) GetFileContent(filePath string) (string, error) {
// GetFileContent returns the content of a file as bytes.
func (service *Service) GetFileContent(filePath string) ([]byte, error) {
content, err := ioutil.ReadFile(filePath)
if err != nil {
return "", err
return nil, err
}
return string(content), nil
return content, nil
}
// createDirectoryInStoreIfNotExist creates a new directory in the file store if it doesn't exists on the file system.
func (service *Service) createDirectoryInStoreIfNotExist(name string) error {
path := path.Join(service.fileStorePath, name)
return createDirectoryIfNotExist(path, 0700)
// Rename renames a file or directory
func (service *Service) Rename(oldPath, newPath string) error {
return os.Rename(oldPath, newPath)
}
// createDirectoryIfNotExist creates a directory if it doesn't exists on the file system.
func createDirectoryIfNotExist(path string, mode uint32) error {
_, err := os.Stat(path)
if os.IsNotExist(err) {
err = os.Mkdir(path, os.FileMode(mode))
if err != nil {
return err
}
} else if err != nil {
// WriteJSONToFile writes JSON to the specified file.
func (service *Service) WriteJSONToFile(path string, content interface{}) error {
jsonContent, err := json.Marshal(content)
if err != nil {
return err
}
return ioutil.WriteFile(path, jsonContent, 0644)
}
// FileExists checks for the existence of the specified file.
func (service *Service) FileExists(filePath string) (bool, error) {
if _, err := os.Stat(filePath); err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
}
// KeyPairFilesExist checks for the existence of the key files.
func (service *Service) KeyPairFilesExist() (bool, error) {
privateKeyPath := path.Join(service.dataStorePath, PrivateKeyFile)
exists, err := service.FileExists(privateKeyPath)
if err != nil {
return false, err
}
if !exists {
return false, nil
}
publicKeyPath := path.Join(service.dataStorePath, PublicKeyFile)
exists, err = service.FileExists(publicKeyPath)
if err != nil {
return false, err
}
if !exists {
return false, nil
}
return true, nil
}
// StoreKeyPair store the specified keys content as PEM files on disk.
func (service *Service) StoreKeyPair(private, public []byte, privatePEMHeader, publicPEMHeader string) error {
err := service.createPEMFileInStore(private, privatePEMHeader, PrivateKeyFile)
if err != nil {
return err
}
err = service.createPEMFileInStore(public, publicPEMHeader, PublicKeyFile)
if err != nil {
return err
}
return nil
}
// LoadKeyPair retrieve the content of both key files on disk.
func (service *Service) LoadKeyPair() ([]byte, []byte, error) {
privateKey, err := service.getContentFromPEMFile(PrivateKeyFile)
if err != nil {
return nil, nil, err
}
publicKey, err := service.getContentFromPEMFile(PublicKeyFile)
if err != nil {
return nil, nil, err
}
return privateKey, publicKey, nil
}
// createDirectoryInStore creates a new directory in the file store
func (service *Service) createDirectoryInStore(name string) error {
path := path.Join(service.fileStorePath, name)
return os.MkdirAll(path, 0700)
}
// createFile creates a new file in the file store with the content from r.
func (service *Service) createFileInStore(filePath string, r io.Reader) error {
path := path.Join(service.fileStorePath, filePath)
@@ -238,3 +338,62 @@ func (service *Service) createFileInStore(filePath string, r io.Reader) error {
return nil
}
func (service *Service) createPEMFileInStore(content []byte, fileType, filePath string) error {
path := path.Join(service.fileStorePath, filePath)
block := &pem.Block{Type: fileType, Bytes: content}
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer out.Close()
err = pem.Encode(out, block)
if err != nil {
return err
}
return nil
}
func (service *Service) getContentFromPEMFile(filePath string) ([]byte, error) {
path := path.Join(service.fileStorePath, filePath)
fileContent, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
block, _ := pem.Decode(fileContent)
return block.Bytes, nil
}
// GetScheduleFolder returns the absolute path on the filesystem for a schedule based
// on its identifier.
func (service *Service) GetScheduleFolder(identifier string) string {
return path.Join(service.fileStorePath, ScheduleStorePath, identifier)
}
// StoreScheduledJobFileFromBytes creates a subfolder in the ScheduleStorePath and stores a new file from bytes.
// It returns the path to the folder where the file is stored.
func (service *Service) StoreScheduledJobFileFromBytes(identifier string, data []byte) (string, error) {
scheduleStorePath := path.Join(ScheduleStorePath, identifier)
err := service.createDirectoryInStore(scheduleStorePath)
if err != nil {
return "", err
}
filePath := path.Join(scheduleStorePath, createScheduledJobFileName(identifier))
r := bytes.NewReader(data)
err = service.createFileInStore(filePath, r)
if err != nil {
return "", err
}
return path.Join(service.fileStorePath, filePath), nil
}
func createScheduledJobFileName(identifier string) string {
return "job_" + identifier + ".sh"
}
+26 -5
View File
@@ -1,7 +1,11 @@
package git
import (
"net/url"
"strings"
"gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/plumbing"
)
// Service represents a service for managing Git.
@@ -14,12 +18,29 @@ func NewService(dataStorePath string) (*Service, error) {
return service, nil
}
// CloneRepository clones a git repository using the specified URL in the specified
// ClonePublicRepository clones a public git repository using the specified URL in the specified
// destination folder.
func (service *Service) CloneRepository(url, destination string) error {
_, err := git.PlainClone(destination, false, &git.CloneOptions{
URL: url,
})
func (service *Service) ClonePublicRepository(repositoryURL, referenceName string, destination string) error {
return cloneRepository(repositoryURL, referenceName, destination)
}
// ClonePrivateRepositoryWithBasicAuth clones a private git repository using the specified URL in the specified
// destination folder. It will use the specified username and password for basic HTTP authentication.
func (service *Service) ClonePrivateRepositoryWithBasicAuth(repositoryURL, referenceName string, destination, username, password string) error {
credentials := username + ":" + url.PathEscape(password)
repositoryURL = strings.Replace(repositoryURL, "://", "://"+credentials+"@", 1)
return cloneRepository(repositoryURL, referenceName, destination)
}
func cloneRepository(repositoryURL, referenceName string, destination string) error {
options := &git.CloneOptions{
URL: repositoryURL,
}
if referenceName != "" {
options.ReferenceName = plumbing.ReferenceName(referenceName)
}
_, err := git.PlainClone(destination, false, options)
return err
}
+136
View File
@@ -0,0 +1,136 @@
package client
import (
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
"github.com/portainer/portainer"
)
const (
errInvalidResponseStatus = portainer.Error("Invalid response status (expecting 200)")
defaultHTTPTimeout = 5
)
// HTTPClient represents a client to send HTTP requests.
type HTTPClient struct {
*http.Client
}
// NewHTTPClient is used to build a new HTTPClient.
func NewHTTPClient() *HTTPClient {
return &HTTPClient{
&http.Client{
Timeout: time.Second * time.Duration(defaultHTTPTimeout),
},
}
}
// AzureAuthenticationResponse represents an Azure API authentication response.
type AzureAuthenticationResponse struct {
AccessToken string `json:"access_token"`
ExpiresOn string `json:"expires_on"`
}
// ExecuteAzureAuthenticationRequest is used to execute an authentication request
// against the Azure API. It re-uses the same http.Client.
func (client *HTTPClient) ExecuteAzureAuthenticationRequest(credentials *portainer.AzureCredentials) (*AzureAuthenticationResponse, error) {
loginURL := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/token", credentials.TenantID)
params := url.Values{
"grant_type": {"client_credentials"},
"client_id": {credentials.ApplicationID},
"client_secret": {credentials.AuthenticationKey},
"resource": {"https://management.azure.com/"},
}
response, err := client.PostForm(loginURL, params)
if err != nil {
return nil, err
}
if response.StatusCode != http.StatusOK {
return nil, portainer.ErrAzureInvalidCredentials
}
var token AzureAuthenticationResponse
err = json.NewDecoder(response.Body).Decode(&token)
if err != nil {
return nil, err
}
return &token, nil
}
// Get executes a simple HTTP GET to the specified URL and returns
// the content of the response body. Timeout can be specified via the timeout parameter,
// will default to defaultHTTPTimeout if set to 0.
func Get(url string, timeout int) ([]byte, error) {
if timeout == 0 {
timeout = defaultHTTPTimeout
}
client := &http.Client{
Timeout: time.Second * time.Duration(timeout),
}
response, err := client.Get(url)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, errInvalidResponseStatus
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
return body, nil
}
// ExecutePingOperation will send a SystemPing operation HTTP request to a Docker environment
// using the specified host and optional TLS configuration.
// It uses a new Http.Client for each operation.
func ExecutePingOperation(host string, tlsConfig *tls.Config) (bool, error) {
transport := &http.Transport{}
scheme := "http"
if tlsConfig != nil {
transport.TLSClientConfig = tlsConfig
scheme = "https"
}
client := &http.Client{
Timeout: time.Second * 3,
Transport: transport,
}
target := strings.Replace(host, "tcp://", scheme+"://", 1)
return pingOperation(client, target)
}
func pingOperation(client *http.Client, target string) (bool, error) {
pingOperationURL := target + "/_ping"
response, err := client.Get(pingOperationURL)
if err != nil {
return false, err
}
agentOnDockerEnvironment := false
if response.Header.Get(portainer.PortainerAgentHeader) != "" {
agentOnDockerEnvironment = true
}
return agentOnDockerEnvironment, nil
}
-22
View File
@@ -1,22 +0,0 @@
package error
import (
"encoding/json"
"log"
"net/http"
)
// errorResponse is a generic response for sending a error.
type errorResponse struct {
Err string `json:"err,omitempty"`
}
// WriteErrorResponse writes an error message to the response and logger.
func WriteErrorResponse(w http.ResponseWriter, err error, code int, logger *log.Logger) {
if logger != nil {
logger.Printf("http error: %s (code=%d)", err, code)
}
w.WriteHeader(code)
json.NewEncoder(w).Encode(&errorResponse{Err: err.Error()})
}
-126
View File
@@ -1,126 +0,0 @@
package handler
import (
"github.com/portainer/portainer"
"encoding/json"
"log"
"net/http"
"os"
"github.com/asaskevich/govalidator"
"github.com/gorilla/mux"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security"
)
// AuthHandler represents an HTTP API handler for managing authentication.
type AuthHandler struct {
*mux.Router
Logger *log.Logger
authDisabled bool
UserService portainer.UserService
CryptoService portainer.CryptoService
JWTService portainer.JWTService
LDAPService portainer.LDAPService
SettingsService portainer.SettingsService
}
const (
// ErrInvalidCredentialsFormat is an error raised when credentials format is not valid
ErrInvalidCredentialsFormat = portainer.Error("Invalid credentials format")
// ErrInvalidCredentials is an error raised when credentials for a user are invalid
ErrInvalidCredentials = portainer.Error("Invalid credentials")
// ErrAuthDisabled is an error raised when trying to access the authentication endpoints
// when the server has been started with the --no-auth flag
ErrAuthDisabled = portainer.Error("Authentication is disabled")
)
// NewAuthHandler returns a new instance of AuthHandler.
func NewAuthHandler(bouncer *security.RequestBouncer, authDisabled bool) *AuthHandler {
h := &AuthHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
authDisabled: authDisabled,
}
h.Handle("/auth",
bouncer.PublicAccess(http.HandlerFunc(h.handlePostAuth))).Methods(http.MethodPost)
return h
}
type (
postAuthRequest struct {
Username string `valid:"required"`
Password string `valid:"required"`
}
postAuthResponse struct {
JWT string `json:"jwt"`
}
)
func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Request) {
if handler.authDisabled {
httperror.WriteErrorResponse(w, ErrAuthDisabled, http.StatusServiceUnavailable, handler.Logger)
return
}
var req postAuthRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err := govalidator.ValidateStruct(req)
if err != nil {
httperror.WriteErrorResponse(w, ErrInvalidCredentialsFormat, http.StatusBadRequest, handler.Logger)
return
}
var username = req.Username
var password = req.Password
u, err := handler.UserService.UserByUsername(username)
if err == portainer.ErrUserNotFound {
httperror.WriteErrorResponse(w, ErrInvalidCredentials, http.StatusBadRequest, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
settings, err := handler.SettingsService.Settings()
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if settings.AuthenticationMethod == portainer.AuthenticationLDAP && u.ID != 1 {
err = handler.LDAPService.AuthenticateUser(username, password, &settings.LDAPSettings)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
} else {
err = handler.CryptoService.CompareHashAndData(u.Password, password)
if err != nil {
httperror.WriteErrorResponse(w, ErrInvalidCredentials, http.StatusUnprocessableEntity, handler.Logger)
return
}
}
tokenData := &portainer.TokenData{
ID: u.ID,
Username: u.Username,
Role: u.Role,
}
token, err := handler.JWTService.GenerateToken(tokenData)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, &postAuthResponse{JWT: token}, handler.Logger)
}
+187
View File
@@ -0,0 +1,187 @@
package auth
import (
"log"
"net/http"
"strings"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer"
)
type authenticatePayload struct {
Username string
Password string
}
type authenticateResponse struct {
JWT string `json:"jwt"`
}
func (payload *authenticatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Username) {
return portainer.Error("Invalid username")
}
if govalidator.IsNull(payload.Password) {
return portainer.Error("Invalid password")
}
return nil
}
func (handler *Handler) authenticate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
if handler.authDisabled {
return &httperror.HandlerError{http.StatusServiceUnavailable, "Cannot authenticate user. Portainer was started with the --no-auth flag", ErrAuthDisabled}
}
var payload authenticatePayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
settings, err := handler.SettingsService.Settings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
}
u, err := handler.UserService.UserByUsername(payload.Username)
if err != nil && err != portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err}
}
if err == portainer.ErrObjectNotFound && settings.AuthenticationMethod == portainer.AuthenticationInternal {
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", portainer.ErrUnauthorized}
}
if settings.AuthenticationMethod == portainer.AuthenticationLDAP {
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", portainer.ErrUnauthorized}
}
return handler.authenticateLDAP(w, u, payload.Password, &settings.LDAPSettings)
}
return handler.authenticateInternal(w, u, payload.Password)
}
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 {
err := handler.CryptoService.CompareHashAndData(user.Password, password)
if err != nil {
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", portainer.ErrUnauthorized}
}
return handler.writeToken(w, user)
}
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{http.StatusUnprocessableEntity, "Invalid credentials", err}
}
user := &portainer.User{
Username: username,
Role: portainer.StandardUserRole,
}
err = handler.UserService.CreateUser(user)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err}
}
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) writeToken(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError {
tokenData := &portainer.TokenData{
ID: user.ID,
Username: user.Username,
Role: user.Role,
}
token, err := handler.JWTService.GenerateToken(tokenData)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to generate JWT token", err}
}
return response.JSON(w, &authenticateResponse{JWT: token})
}
func (handler *Handler) addUserIntoTeams(user *portainer.User, settings *portainer.LDAPSettings) error {
teams, err := handler.TeamService.Teams()
if err != nil {
return err
}
userGroups, err := handler.LDAPService.GetUserGroups(user.Username, settings)
if err != nil {
return err
}
userMemberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(user.ID)
if err != nil {
return err
}
for _, team := range teams {
if teamExists(team.Name, userGroups) {
if teamMembershipExists(team.ID, userMemberships) {
continue
}
membership := &portainer.TeamMembership{
UserID: user.ID,
TeamID: team.ID,
Role: portainer.TeamMember,
}
err := handler.TeamMembershipService.CreateTeamMembership(membership)
if err != nil {
return err
}
}
}
return nil
}
func teamExists(teamName string, ldapGroups []string) bool {
for _, group := range ldapGroups {
if strings.ToLower(group) == strings.ToLower(teamName) {
return true
}
}
return false
}
func teamMembershipExists(teamID portainer.TeamID, memberships []portainer.TeamMembership) bool {
for _, membership := range memberships {
if membership.TeamID == teamID {
return true
}
}
return false
}
+138
View File
@@ -0,0 +1,138 @@
package auth
import (
"encoding/json"
"io/ioutil"
"net/http"
"log"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/portainer"
)
type oauthPayload struct {
Code string
}
func (payload *oauthPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Code) {
return portainer.Error("Invalid OAuth authorization code")
}
return nil
}
func (handler *Handler) authenticateThroughExtension(code, licenseKey string, settings *portainer.OAuthSettings) (string, error) {
extensionURL := handler.ProxyManager.GetExtensionURL(portainer.OAuthAuthenticationExtension)
encodedConfiguration, err := json.Marshal(settings)
if err != nil {
return "", nil
}
req, err := http.NewRequest("GET", extensionURL+"/validate", nil)
if err != nil {
return "", err
}
client := &http.Client{}
req.Header.Set("X-OAuth-Config", string(encodedConfiguration))
req.Header.Set("X-OAuth-Code", code)
req.Header.Set("X-PortainerExtension-License", licenseKey)
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
type extensionResponse struct {
Username string `json:"Username,omitempty"`
Err string `json:"err,omitempty"`
Details string `json:"details,omitempty"`
}
var extResp extensionResponse
err = json.Unmarshal(body, &extResp)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", portainer.Error(extResp.Err + ":" + extResp.Details)
}
return extResp.Username, nil
}
func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload oauthPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
settings, err := handler.SettingsService.Settings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
}
if settings.AuthenticationMethod != 3 {
return &httperror.HandlerError{http.StatusForbidden, "OAuth authentication is not enabled", portainer.Error("OAuth authentication is not enabled")}
}
extension, err := handler.ExtensionService.Extension(portainer.OAuthAuthenticationExtension)
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Oauth authentication extension is not enabled", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
}
username, err := handler.authenticateThroughExtension(payload.Code, extension.License.LicenseKey, &settings.OAuthSettings)
if err != nil {
log.Printf("[DEBUG] - OAuth authentication error: %s", err)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate through OAuth", portainer.ErrUnauthorized}
}
user, err := handler.UserService.UserByUsername(username)
if err != nil && err != portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err}
}
if user == nil && !settings.OAuthSettings.OAuthAutoCreateUsers {
return &httperror.HandlerError{http.StatusForbidden, "Account not created beforehand in Portainer and automatic user provisioning not enabled", portainer.ErrUnauthorized}
}
if user == nil {
user = &portainer.User{
Username: username,
Role: portainer.StandardUserRole,
}
err = handler.UserService.CreateUser(user)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err}
}
if settings.OAuthSettings.DefaultTeamID != 0 {
membership := &portainer.TeamMembership{
UserID: user.ID,
TeamID: settings.OAuthSettings.DefaultTeamID,
Role: portainer.TeamMember,
}
err = handler.TeamMembershipService.CreateTeamMembership(membership)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist team membership inside the database", err}
}
}
}
return handler.writeToken(w, user)
}
+49
View File
@@ -0,0 +1,49 @@
package auth
import (
"net/http"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer"
"github.com/portainer/portainer/http/proxy"
"github.com/portainer/portainer/http/security"
)
const (
// ErrInvalidCredentials is an error raised when credentials for a user are invalid
ErrInvalidCredentials = portainer.Error("Invalid credentials")
// ErrAuthDisabled is an error raised when trying to access the authentication endpoints
// when the server has been started with the --no-auth flag
ErrAuthDisabled = portainer.Error("Authentication is disabled")
)
// Handler is the HTTP handler used to handle authentication operations.
type Handler struct {
*mux.Router
authDisabled bool
UserService portainer.UserService
CryptoService portainer.CryptoService
JWTService portainer.JWTService
LDAPService portainer.LDAPService
SettingsService portainer.SettingsService
TeamService portainer.TeamService
TeamMembershipService portainer.TeamMembershipService
ExtensionService portainer.ExtensionService
ProxyManager *proxy.Manager
}
// NewHandler creates a handler to manage authentication operations.
func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimiter, authDisabled bool) *Handler {
h := &Handler{
Router: mux.NewRouter(),
authDisabled: authDisabled,
}
h.Handle("/auth/oauth/validate",
rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.validateOAuth)))).Methods(http.MethodPost)
h.Handle("/auth",
rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.authenticate)))).Methods(http.MethodPost)
return h
}
-83
View File
@@ -1,83 +0,0 @@
package handler
import (
"strconv"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/proxy"
"github.com/portainer/portainer/http/security"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
)
// DockerHandler represents an HTTP API handler for proxying requests to the Docker API.
type DockerHandler struct {
*mux.Router
Logger *log.Logger
EndpointService portainer.EndpointService
TeamMembershipService portainer.TeamMembershipService
ProxyManager *proxy.Manager
}
// NewDockerHandler returns a new instance of DockerHandler.
func NewDockerHandler(bouncer *security.RequestBouncer) *DockerHandler {
h := &DockerHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.PathPrefix("/{id}/docker").Handler(
bouncer.AuthenticatedAccess(http.HandlerFunc(h.proxyRequestsToDockerAPI)))
return h
}
func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
parsedID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
endpointID := portainer.EndpointID(parsedID)
endpoint, err := handler.EndpointService.Endpoint(endpointID)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if tokenData.Role != portainer.AdministratorRole && !security.AuthorizedEndpointAccess(endpoint, tokenData.ID, memberships) {
httperror.WriteErrorResponse(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger)
return
}
var proxy http.Handler
proxy = handler.ProxyManager.GetProxy(string(endpointID))
if proxy == nil {
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
http.StripPrefix("/"+id+"/docker", proxy).ServeHTTP(w, r)
}
-89
View File
@@ -1,89 +0,0 @@
package handler
import (
"encoding/json"
"github.com/asaskevich/govalidator"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
)
// DockerHubHandler represents an HTTP API handler for managing DockerHub.
type DockerHubHandler struct {
*mux.Router
Logger *log.Logger
DockerHubService portainer.DockerHubService
}
// NewDockerHubHandler returns a new instance of DockerHubHandler.
func NewDockerHubHandler(bouncer *security.RequestBouncer) *DockerHubHandler {
h := &DockerHubHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.Handle("/dockerhub",
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleGetDockerHub))).Methods(http.MethodGet)
h.Handle("/dockerhub",
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutDockerHub))).Methods(http.MethodPut)
return h
}
type (
putDockerHubRequest struct {
Authentication bool `valid:""`
Username string `valid:""`
Password string `valid:""`
}
)
// handleGetDockerHub handles GET requests on /dockerhub
func (handler *DockerHubHandler) handleGetDockerHub(w http.ResponseWriter, r *http.Request) {
dockerhub, err := handler.DockerHubService.DockerHub()
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, dockerhub, handler.Logger)
return
}
// handlePutDockerHub handles PUT requests on /dockerhub
func (handler *DockerHubHandler) handlePutDockerHub(w http.ResponseWriter, r *http.Request) {
var req putDockerHubRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err := govalidator.ValidateStruct(req)
if err != nil {
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
dockerhub := &portainer.DockerHub{
Authentication: false,
Username: "",
Password: "",
}
if req.Authentication {
dockerhub.Authentication = true
dockerhub.Username = req.Username
dockerhub.Password = req.Password
}
err = handler.DockerHubService.StoreDockerHub(dockerhub)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
}
}
@@ -0,0 +1,19 @@
package dockerhub
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
)
// GET request on /api/dockerhub
func (handler *Handler) dockerhubInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
dockerhub, err := handler.DockerHubService.DockerHub()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve DockerHub details from the database", err}
}
hideFields(dockerhub)
return response.JSON(w, dockerhub)
}
@@ -0,0 +1,52 @@
package dockerhub
import (
"net/http"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer"
)
type dockerhubUpdatePayload struct {
Authentication bool
Username string
Password string
}
func (payload *dockerhubUpdatePayload) Validate(r *http.Request) error {
if payload.Authentication && (govalidator.IsNull(payload.Username) || govalidator.IsNull(payload.Password)) {
return portainer.Error("Invalid credentials. Username and password must be specified when authentication is enabled")
}
return nil
}
// PUT request on /api/dockerhub
func (handler *Handler) dockerhubUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload dockerhubUpdatePayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
dockerhub := &portainer.DockerHub{
Authentication: false,
Username: "",
Password: "",
}
if payload.Authentication {
dockerhub.Authentication = true
dockerhub.Username = payload.Username
dockerhub.Password = payload.Password
}
err = handler.DockerHubService.UpdateDockerHub(dockerhub)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the Dockerhub changes inside the database", err}
}
return response.Empty(w)
}
+33
View File
@@ -0,0 +1,33 @@
package dockerhub
import (
"net/http"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer"
"github.com/portainer/portainer/http/security"
)
func hideFields(dockerHub *portainer.DockerHub) {
dockerHub.Password = ""
}
// Handler is the HTTP handler used to handle DockerHub operations.
type Handler struct {
*mux.Router
DockerHubService portainer.DockerHubService
}
// NewHandler creates a handler to manage Dockerhub operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
}
h.Handle("/dockerhub",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.dockerhubInspect))).Methods(http.MethodGet)
h.Handle("/dockerhub",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.dockerhubUpdate))).Methods(http.MethodPut)
return h
}
-391
View File
@@ -1,391 +0,0 @@
package handler
import (
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/proxy"
"github.com/portainer/portainer/http/security"
"encoding/json"
"log"
"net/http"
"os"
"strconv"
"github.com/asaskevich/govalidator"
"github.com/gorilla/mux"
)
// EndpointHandler represents an HTTP API handler for managing Docker endpoints.
type EndpointHandler struct {
*mux.Router
Logger *log.Logger
authorizeEndpointManagement bool
EndpointService portainer.EndpointService
FileService portainer.FileService
ProxyManager *proxy.Manager
}
const (
// ErrEndpointManagementDisabled is an error raised when trying to access the endpoints management endpoints
// when the server has been started with the --external-endpoints flag
ErrEndpointManagementDisabled = portainer.Error("Endpoint management is disabled")
)
// NewEndpointHandler returns a new instance of EndpointHandler.
func NewEndpointHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bool) *EndpointHandler {
h := &EndpointHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
authorizeEndpointManagement: authorizeEndpointManagement,
}
h.Handle("/endpoints",
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePostEndpoints))).Methods(http.MethodPost)
h.Handle("/endpoints",
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetEndpoints))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}",
bouncer.AdministratorAccess(http.HandlerFunc(h.handleGetEndpoint))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}",
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutEndpoint))).Methods(http.MethodPut)
h.Handle("/endpoints/{id}/access",
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutEndpointAccess))).Methods(http.MethodPut)
h.Handle("/endpoints/{id}",
bouncer.AdministratorAccess(http.HandlerFunc(h.handleDeleteEndpoint))).Methods(http.MethodDelete)
return h
}
type (
postEndpointsRequest struct {
Name string `valid:"required"`
URL string `valid:"required"`
PublicURL string `valid:"-"`
TLS bool
TLSSkipVerify bool
TLSSkipClientVerify bool
}
postEndpointsResponse struct {
ID int `json:"Id"`
}
putEndpointAccessRequest struct {
AuthorizedUsers []int `valid:"-"`
AuthorizedTeams []int `valid:"-"`
}
putEndpointsRequest struct {
Name string `valid:"-"`
URL string `valid:"-"`
PublicURL string `valid:"-"`
TLS bool `valid:"-"`
TLSSkipVerify bool `valid:"-"`
TLSSkipClientVerify bool `valid:"-"`
}
)
// handleGetEndpoints handles GET requests on /endpoints
func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *http.Request) {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
filteredEndpoints, err := security.FilterEndpoints(endpoints, securityContext)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, filteredEndpoints, handler.Logger)
}
// handlePostEndpoints handles POST requests on /endpoints
func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *http.Request) {
if !handler.authorizeEndpointManagement {
httperror.WriteErrorResponse(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
return
}
var req postEndpointsRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err := govalidator.ValidateStruct(req)
if err != nil {
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
endpoint := &portainer.Endpoint{
Name: req.Name,
URL: req.URL,
PublicURL: req.PublicURL,
TLSConfig: portainer.TLSConfiguration{
TLS: req.TLS,
TLSSkipVerify: req.TLSSkipVerify,
},
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
}
err = handler.EndpointService.CreateEndpoint(endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if req.TLS {
folder := strconv.Itoa(int(endpoint.ID))
if !req.TLSSkipVerify {
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
endpoint.TLSConfig.TLSCACertPath = caCertPath
}
if !req.TLSSkipClientVerify {
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
endpoint.TLSConfig.TLSCertPath = certPath
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
endpoint.TLSConfig.TLSKeyPath = keyPath
}
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
encodeJSON(w, &postEndpointsResponse{ID: int(endpoint.ID)}, handler.Logger)
}
// handleGetEndpoint handles GET requests on /endpoints/:id
func (handler *EndpointHandler) handleGetEndpoint(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
endpointID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrEndpointNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, endpoint, handler.Logger)
}
// handlePutEndpointAccess handles PUT requests on /endpoints/:id/access
func (handler *EndpointHandler) handlePutEndpointAccess(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
endpointID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
var req putEndpointAccessRequest
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err = govalidator.ValidateStruct(req)
if err != nil {
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrEndpointNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if req.AuthorizedUsers != nil {
authorizedUserIDs := []portainer.UserID{}
for _, value := range req.AuthorizedUsers {
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
}
endpoint.AuthorizedUsers = authorizedUserIDs
}
if req.AuthorizedTeams != nil {
authorizedTeamIDs := []portainer.TeamID{}
for _, value := range req.AuthorizedTeams {
authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value))
}
endpoint.AuthorizedTeams = authorizedTeamIDs
}
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
// handlePutEndpoint handles PUT requests on /endpoints/:id
func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http.Request) {
if !handler.authorizeEndpointManagement {
httperror.WriteErrorResponse(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
return
}
vars := mux.Vars(r)
id := vars["id"]
endpointID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
var req putEndpointsRequest
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err = govalidator.ValidateStruct(req)
if err != nil {
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrEndpointNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if req.Name != "" {
endpoint.Name = req.Name
}
if req.URL != "" {
endpoint.URL = req.URL
}
if req.PublicURL != "" {
endpoint.PublicURL = req.PublicURL
}
folder := strconv.Itoa(int(endpoint.ID))
if req.TLS {
endpoint.TLSConfig.TLS = true
endpoint.TLSConfig.TLSSkipVerify = req.TLSSkipVerify
if !req.TLSSkipVerify {
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
endpoint.TLSConfig.TLSCACertPath = caCertPath
} else {
endpoint.TLSConfig.TLSCACertPath = ""
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCA)
}
if !req.TLSSkipClientVerify {
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
endpoint.TLSConfig.TLSCertPath = certPath
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
endpoint.TLSConfig.TLSKeyPath = keyPath
} else {
endpoint.TLSConfig.TLSCertPath = ""
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCert)
endpoint.TLSConfig.TLSKeyPath = ""
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileKey)
}
} else {
endpoint.TLSConfig.TLS = false
endpoint.TLSConfig.TLSSkipVerify = true
endpoint.TLSConfig.TLSCACertPath = ""
endpoint.TLSConfig.TLSCertPath = ""
endpoint.TLSConfig.TLSKeyPath = ""
err = handler.FileService.DeleteTLSFiles(folder)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
_, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
// handleDeleteEndpoint handles DELETE requests on /endpoints/:id
func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *http.Request) {
if !handler.authorizeEndpointManagement {
httperror.WriteErrorResponse(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
return
}
vars := mux.Vars(r)
id := vars["id"]
endpointID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrEndpointNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
handler.ProxyManager.DeleteProxy(string(endpointID))
handler.ProxyManager.DeleteExtensionProxies(string(endpointID))
err = handler.EndpointService.DeleteEndpoint(portainer.EndpointID(endpointID))
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if endpoint.TLSConfig.TLS {
err = handler.FileService.DeleteTLSFiles(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
}
@@ -0,0 +1,66 @@
package endpointgroups
import (
"net/http"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer"
)
type endpointGroupCreatePayload struct {
Name string
Description string
AssociatedEndpoints []portainer.EndpointID
Tags []string
}
func (payload *endpointGroupCreatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
return portainer.Error("Invalid endpoint group name")
}
if payload.Tags == nil {
payload.Tags = []string{}
}
return nil
}
// POST request on /api/endpoint_groups
func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload endpointGroupCreatePayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
endpointGroup := &portainer.EndpointGroup{
Name: payload.Name,
Description: payload.Description,
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Tags: payload.Tags,
}
err = handler.EndpointGroupService.CreateEndpointGroup(endpointGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the endpoint group inside the database", err}
}
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
}
for _, endpoint := range endpoints {
if endpoint.GroupID == portainer.EndpointGroupID(1) {
err = handler.checkForGroupAssignment(endpoint, endpointGroup.ID, payload.AssociatedEndpoints)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
}
}
}
return response.JSON(w, endpointGroup)
}
@@ -0,0 +1,51 @@
package endpointgroups
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer"
)
// DELETE request on /api/endpoint_groups/:id
func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err}
}
if endpointGroupID == 1 {
return &httperror.HandlerError{http.StatusForbidden, "Unable to remove the default 'Unassigned' group", portainer.ErrCannotRemoveDefaultGroup}
}
_, err = handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err}
}
err = handler.EndpointGroupService.DeleteEndpointGroup(portainer.EndpointGroupID(endpointGroupID))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the endpoint group from the database", err}
}
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
}
for _, endpoint := range endpoints {
if endpoint.GroupID == portainer.EndpointGroupID(endpointGroupID) {
endpoint.GroupID = portainer.EndpointGroupID(1)
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
}
}
}
return response.Empty(w)
}
@@ -0,0 +1,27 @@
package endpointgroups
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer"
)
// GET request on /api/endpoint_groups/:id
func (handler *Handler) endpointGroupInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err}
}
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err}
}
return response.JSON(w, endpointGroup)
}

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