Compare commits

..

1164 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
Anthony Lapenna 9c277733d5 Merge branch 'release/1.16.4' 2018-03-11 20:30:12 +10:00
Anthony Lapenna ec2a9e149b chore(version): bump version number 2018-03-11 20:30:07 +10:00
Anthony Lapenna aa41fd02ef feat(log-viewer): use only one switch to manage collection/autoscroll (#1713)
* feat(log-viewer): use only one switch to manage collection/autoscroll

* feat(log-viewer): add the ability to clear selection

* style(log-viewer): update unselect button design
2018-03-11 20:29:13 +10:00
Anthony Lapenna 28c73323bf refactor(extensions): review bouncer settings for extensions endpoint (#1711) 2018-03-10 08:18:59 +10:00
Herwono W. Wijaya b389e3c65a fix(service-logs): fix services log view breadcrumb link (#1709) 2018-03-10 08:09:03 +10:00
Anthony Lapenna 02b3d54a75 fix(extensions): fix invalid storidge API URL (#1707) 2018-03-09 19:50:48 +10:00
Anthony Lapenna f1a21c07bd feat(storidge): add extension check on endpoint switch (#1693)
* feat(storidge): add extension check on endpoint switch

* feat(storidge): add extension check post login
2018-03-09 08:49:43 +10:00
Anthony Lapenna 403de0d319 chore(momentjs): upgrade momentjs version (#1701) 2018-03-08 11:42:50 +10:00
Anthony Lapenna a76ccff7c9 refactor(xterm): update xtermjs to latest version (#1692) 2018-03-06 17:40:02 +10:00
Anthony Lapenna 1ae9832980 Merge tag '1.16.3' into develop
Release 1.16.3
2018-03-03 09:20:05 +10:00
Anthony Lapenna 8a9619c7e8 Merge branch 'release/1.16.3' 2018-03-03 09:19:59 +10:00
Anthony Lapenna 9634cf1563 chore(version): bump version number 2018-03-03 09:19:54 +10:00
Mauro Cortellazzi 716cd033b2 feat(events): add missing events support (#1682) 2018-03-02 18:21:26 +10:00
Anthony Lapenna 28bca85e01 feat(registries): remove actual password from registry password input (#1687) 2018-03-02 18:16:33 +10:00
Anthony Lapenna 73e6498d2f refactor(swarm-visualizer): move task border logic to a filter (#1686) 2018-03-02 09:00:34 +10:00
Mauro Cortellazzi 1b8d5e89d1 feat(swarm-visualizer): swarm visualizer color by service (#1683) 2018-03-02 08:10:14 +10:00
Anthony Lapenna 76aeee7237 feat(templates): add support for the name property (#1680) 2018-02-28 08:59:31 +01:00
Anthony Lapenna b9a1c68ea0 feat(security): check user existence for each protected requests (#1679) 2018-02-28 08:09:51 +01:00
Anthony Lapenna b8f8df5f48 fix(endpoints-creation): remove endpoint if an error is raised during creation (#1678) 2018-02-28 07:52:40 +01:00
Anthony Lapenna 0c5152fb5f feat(log-viewer): introduce the log viewer component (#1666) 2018-02-28 07:19:28 +01:00
Anthony Lapenna 81de2a5afb feat(image-build): add the ability to build images (#1672) 2018-02-28 07:19:06 +01:00
Anthony Lapenna e065bd4a47 style(containers): update label color for unhealthy containers (#1677) 2018-02-28 05:54:13 +01:00
Anthony Lapenna 9b80b6adb2 refactor(code-editor): introduce code-editor component (#1674)
* refactor(code-editor): introduce code-editor component

* refactor(code-editor): add some extra validation
2018-02-27 08:19:21 +01:00
Anthony Lapenna eb43579378 feat(storidge): introduce endpoint extensions and proxy Storidge API (#1661) 2018-02-23 03:10:26 +01:00
Anthony Lapenna b5e256c967 fix(services): use the Public URL instead of a manager IP (#1665) 2018-02-21 10:55:51 +01:00
Boissier Florian ae5416583e style(containers): update quick actions tooltips messages (#1659) 2018-02-17 09:44:29 +01:00
Anthony Lapenna 5b9cb1a883 feat(api): use the stack ProjectPath as the working directory during deployment (#1648) 2018-02-09 10:55:51 +01:00
Anthony Lapenna b040b3ff8c Merge tag '1.16.2' into develop
Release 1.16.2
2018-02-08 09:27:27 +01:00
Anthony Lapenna 3ff49542f3 Merge branch 'release/1.16.2' 2018-02-08 09:27:20 +01:00
Anthony Lapenna 27dcfd043b chore(version): bump version number 2018-02-08 09:27:13 +01:00
Anthony Lapenna 1de0619fd5 fix(api): ignore Docker login errors during stack deployment (#1635) 2018-02-07 08:37:01 +01:00
Anthony Lapenna 1c67db0c70 feat(ux): enable auto-focus on search field (#1636) 2018-02-06 16:58:05 +01:00
Anthony Lapenna 7365e69c59 fix(config-creation): fix an issue setting config editor as read-only (#1634) 2018-02-06 14:23:08 +01:00
Anthony Lapenna 23a565243a Merge branch 'develop' of github.com:portainer/portainer into develop 2018-02-01 13:29:43 +01:00
Anthony Lapenna 27dceadba1 refactor(app): introduce new project structure for the frontend (#1623) 2018-02-01 13:27:52 +01:00
Anthony Lapenna 6f471cef34 Merge branch 'master' into develop 2018-01-31 21:35:20 +01:00
Ben Yanke e6422a6d75 style(container-details): fix a typo in container status 2018-01-31 20:28:36 +01:00
Anthony Lapenna 56cab429de Revert "feat(container-details): fix typo in container status" (#1619)
This reverts commit 5f742c2163.
2018-01-31 19:11:20 +01:00
Ben Yanke 5f742c2163 feat(container-details): fix typo in container status 2018-01-31 19:09:10 +01:00
Anthony Lapenna f31f29fa2f feat(volumes): check if volumes are used in service definitions (#1601) 2018-01-25 08:13:56 +01:00
Anthony Lapenna 672819f3af refactor(api): remove CLI deprecation related code (#1602) 2018-01-24 21:58:58 +01:00
Anthony Lapenna 0ff0c3ed0d Merge tag '1.16.1' into develop
Release 1.16.1
2018-01-23 16:53:03 +01:00
Anthony Lapenna 54750f002a Merge branch 'release/1.16.1' 2018-01-23 16:52:59 +01:00
Anthony Lapenna 4c2dfb3346 chore(version): bump version number 2018-01-23 16:52:54 +01:00
Miguel A. C 8ae3abf29e fix(service-details): avoid sending unmodified restart policy settings when updating a service (#1576) 2018-01-23 10:06:58 +01:00
Anthony Lapenna 362f036a68 fix(state): ensure API version >= 1.25 before extension check (#1594)
* fix(state): ensure API version >= 1.25 before extension check
2018-01-23 09:50:14 +01:00
Anthony Lapenna 0d0072a50e extension(storidge): support cluster shutdown (#1589) 2018-01-23 09:49:29 +01:00
Anthony Lapenna 173ea372c2 fix(extension): bypass the error returned by plugin service during ex… (#1586)
* fix(extension): bypass the error returned by plugin service during extension check

* feat(plugins): bypass the error returned by plugin service during plugin retrieval
2018-01-23 09:47:36 +01:00
Anthony Lapenna 8c75f705e2 chore(dependency): upgrade jquery version to latest (#1592) 2018-01-22 17:44:49 +01:00
Anthony Lapenna b1863430df revert: revert PR 1366 (#1588) 2018-01-22 10:06:47 +01:00
Anthony Lapenna c51db23c32 Merge tag '1.16.0' into develop
Release 1.16.0
2018-01-21 17:30:18 +01:00
Anthony Lapenna c40f120da2 Merge branch 'release/1.16.0' 2018-01-21 17:30:13 +01:00
Anthony Lapenna a7cb0ca823 chore(version): bump version number 2018-01-21 17:30:06 +01:00
Anthony Lapenna 7817d4bd0b extension(storidge): add Storidge extension (#1581) 2018-01-21 17:26:24 +01:00
Miguel A. C edadce359c feat(stack-details): add stack deploy prune option (#1567)
* feat(stack-details): add stack deploy prune option

* fix go fmt issues

* add changes proposed by reviewer

* refactor deployStack as suggested by codeclimate
2018-01-20 18:05:01 +01:00
Anthony Lapenna e1bf9599ef fix(stack-details): fix broken link for services published ports (#1578) 2018-01-20 11:31:26 +01:00
RobbyVoid c3ba9e6a53 feat(networks): Show untruncated network name as link title (#1574)
If the network name was truncated (40 characters) it should be visible as a mouse over title
2018-01-19 12:41:18 +01:00
Vincent Besançon 10174b98b9 refactor(api): Fixed typo in check health cli flag (#1570) 2018-01-17 16:34:15 +01:00
1138-4EB 6acfb580dc feat(cli): Add CLI flag for health-check (#1366) 2018-01-15 19:34:07 +01:00
Miguel A. C 340ec841fe feat(swarm-visualizer): add auto-refresh to the cluster visualizer (#1561) 2018-01-12 16:10:02 +01:00
Anthony Lapenna a515b96a46 fix(app): fix a Javascript error related to missing $state parameter (#1562) 2018-01-09 20:06:19 +01:00
Anthony Lapenna 46da85c8cf feat(services): bind enter key when scaling a service (#1560) 2018-01-09 10:59:33 +01:00
Anthony Lapenna f52ac8fb12 feat(UX): improve UX for service update (#1558) 2018-01-09 10:40:30 +01:00
Miguel A. C 0e28aebd65 feat(service): add force update in service list/detail (#1536) 2018-01-08 22:06:56 +01:00
Anthony Lapenna 35892525ff docs(api): document the stack management endpoint (#1557) 2018-01-08 18:27:45 +01:00
Anthony Lapenna d2f3309842 refactor(api): rename file package to filesystem (#1555) 2018-01-06 18:53:12 +01:00
Rahul Ruikar 03f6cc0acf feat(templates): add labels to container template (#1538) 2018-01-06 18:24:51 +01:00
cbrherms f8c7ee7ae6 feat(container-creation): add support for mac assignments (#1546)
* feat(container-creation): add support for mac assignments (#1524)

* refactor(container-creation): code relocation to relevant function

* style(container-creation): fix typo in environment variables function
2018-01-06 11:53:03 +01:00
Thomas Krzero 00daedca30 fix(service): check endpoint spec existence before update 2018-01-05 14:49:41 +01:00
Anthony Lapenna e2b8633aac fix(stack-details): fix an issue related to env vars (#1512) 2018-01-05 14:32:23 +01:00
Anthony Lapenna 50dbb572b1 fix(containers): update the persisted filters after refresh (#1553) 2018-01-05 14:31:20 +01:00
Anthony Lapenna 95b595d2a9 fix(UAC): fix an issue with network/volume ownership update (#1552) 2018-01-05 13:43:25 +01:00
Anthony Lapenna f57ce8b327 feat(containers): trim the @sha256 suffix in the image name (#1551) 2018-01-05 12:32:53 +01:00
Anthony Lapenna 5787df5599 refactor(stack): replace $stateParams usage with $transition$.params() 2018-01-04 21:53:10 +01:00
Anthony Lapenna 52ac9504c1 chore(codefresh): fix the build_frontend step (#1547) 2018-01-02 13:14:26 +01:00
Yassir Hannoun 1da64f2e75 * fix(containers): display a subset of the sha images name in the containers datatable
* Removed unnecessary filter

* refactor(common): improve trimshasum  filter

* refactor(common): improve trimshasum filter
2017-12-22 19:39:06 +01:00
Miguel A. C 8bf3f669d0 feat(service): add logging driver config in service create/update (#1516) 2017-12-22 10:05:31 +01:00
Anthony Lapenna eec10541b3 fix(users): fix invalid Authentication value (#1528) 2017-12-21 19:56:54 +01:00
Anthony Lapenna e0b09f20b0 fix(cache): add a cache validity mechanism (#1527) 2017-12-21 19:49:39 +01:00
Miguel A. C 8e40eb1844 feat(service): add hosts file entries in service create/update (#1511) 2017-12-21 09:53:34 +01:00
Anthony Lapenna c9e060d574 fix(container-logs): add missing dependency to Notifications (#1514) 2017-12-18 21:24:51 +01:00
Anthony Lapenna 9c9e16b2b2 fix(containers): fix the ability to stop/pause a healthy container (#1507) 2017-12-14 10:31:16 +01:00
Anthony Lapenna 35f7ce5f3d Merge tag '1.15.5' into develop
Release 1.15.5
2017-12-11 16:04:03 +01:00
Anthony Lapenna 45e7938c5c Merge branch 'release/1.15.5' 2017-12-11 16:03:58 +01:00
Anthony Lapenna fbd9139928 chore(version): bump version number 2017-12-11 16:03:53 +01:00
Anthony Lapenna d0da9860af style(datatables): use normal font weight for table headers (#1496) 2017-12-11 16:03:00 +01:00
Duvel 46d8dba137 style(networks): change the label of the add button (#1495) 2017-12-11 15:50:59 +01:00
Anthony Lapenna 3660f6eeb5 Merge tag '1.15.4' into develop
Release 1.15.4
2017-12-10 10:10:01 +01:00
Anthony Lapenna 39236ae84e Merge branch 'release/1.15.4' 2017-12-10 10:09:56 +01:00
Anthony Lapenna 7dcf5c2d0b chore(version): bump version number 2017-12-10 10:09:11 +01:00
Miguel A. C d0e147137d feat(service): add restart policy options in service create/details (#1479) 2017-12-07 21:05:45 +01:00
Anthony Lapenna bdb23a8dd2 feat(UX): replace tables with datatables (#1460) 2017-12-06 12:04:02 +01:00
1138-4EB 7922ecc4a1 chore(build-system): refactor gruntfile (#1447) 2017-12-05 21:26:45 +01:00
Miguel A. C 728ef35cc1 feat(service): change update delay format to a time string in service… (#1470) 2017-12-05 20:12:54 +01:00
Anthony Lapenna f3a23c7dd1 feat(container-details): display loading when using recreate (#1471) 2017-12-05 17:46:11 +01:00
Anthony Lapenna 283faca4f7 feat(dashboard): add a link to the visualizer (#1469) 2017-12-05 17:34:29 +01:00
Anthony Lapenna 2b2850d17a fix(stacks): fix an issue with stacks using docker in their name (#1468) 2017-12-05 14:56:40 +01:00
1138-4EB 997af882c4 chore(build-system): drop bower, use npm|yarn for frontend dependencies (#1416)
* chore(build-system): drop bower, use npm|yarn for frontend dependencies

* chore(build-sytem): for github dependencies, use semver format instead of tag/commit

* add yarn.lock
2017-12-05 09:52:38 +01:00
1138-4EB 75b3a78e2b refactor(services): Refactor chartService and pluginService (#1340) 2017-12-05 09:49:04 +01:00
Miguel A. C d8f6b14726 feat(authentication-settings): add default port when not set in url (#1456) 2017-12-04 19:41:59 +01:00
Miguel A. C 406757d751 feat(swarm-visualizer): add ram and cpu info to nodes & limits to tasks (#1458) 2017-12-04 18:01:07 +01:00
Miguel A. C f3b5f803f5 feat(tasks): add missing task states, set new default state color (#1459) 2017-12-04 17:58:46 +01:00
1138-4EB f1d9b72a06 docs(README): add ref to portainer-demo/play-with-docker (#1455) 2017-12-02 09:39:10 +01:00
doncicuto 9513da80f6 feat(node): add engine labels info in the swarm nodes view (#1451) 2017-12-01 09:26:03 +01:00
Anthony Lapenna ca036b56c1 feat(database-migration): enable donation header when upgrading Portainer (#1450) 2017-11-28 13:40:33 +01:00
Anthony Lapenna 27a388a030 Merge tag '1.15.3' into develop
Release 1.15.3
2017-11-26 10:08:08 +01:00
Anthony Lapenna 65cde27334 Merge branch 'release/1.15.3' 2017-11-26 10:08:04 +01:00
Anthony Lapenna 2275467bdc chore(version): bump version number 2017-11-26 10:07:59 +01:00
1138-4EB 688b15fb4b feat(about): add a new about view as well as a support header 2017-11-26 10:05:03 +01:00
Anthony Lapenna 3362ba0c8c fix(services): do not display exposed ports when published port is missing (#1440) 2017-11-25 10:30:40 +01:00
Anthony Lapenna 39cf4d75ff fix(container-creation): reset NetworkConfig when changing the network during container edition (#1431) 2017-11-23 16:02:40 +01:00
Duvel 13d8d38bf9 fix(service-details): fix an issue with invalid service restart policy (#1415) 2017-11-23 10:47:39 +01:00
1138-4EB e51246ee78 style(sidebar): prevent icon of active item moving on hover (#1422) 2017-11-23 09:59:29 +01:00
Anthony Lapenna 4ab580923f fix(templates): fix an issue preventing linuxserver.io templates to be displayed (#1426) 2017-11-22 22:16:53 +01:00
1138-4EB 547511c8aa feat(UX): change background color for selected items (#1414) 2017-11-20 17:46:01 +01:00
1138-4EB 8a101f67f6 style(container-details): change the grouping of buttons
* style(containers) make add container button responsive

* style(container) make action buttons responsive, group as in containers
2017-11-20 14:48:42 +01:00
Thomas Krzero 3ee2e20f8e feat(services): add the ability to specify a target for secrets (#1365) 2017-11-20 14:44:23 +01:00
Yassir Hannoun 6b9f3dad7a feat(UX): add an image autocomplete feature for services and containers (#1389) 2017-11-20 14:34:14 +01:00
1138-4EB a2d41e5316 feat(build-system): check that files listed in vendor.yml exist (#1398)
* chore(build-system) check that files listed in vendor.yml exist (#1410)

* fix(build-system) Chart.min.js duplicated in vendor.yml (#1410)
2017-11-20 10:09:11 +01:00
Thomas Kooi 3548f0db6f refactor(webapp): simplify isAdmin statement (#1388) 2017-11-14 08:54:35 +01:00
Anthony Lapenna 521cc3d6ab Merge tag '1.15.2' into develop
Release 1.15.2
2017-11-13 10:11:27 +01:00
Anthony Lapenna b044aa9a84 Merge branch 'release/1.15.2' 2017-11-13 10:11:14 +01:00
Anthony Lapenna d9262d4b7f chore(version): bump version number 2017-11-13 10:11:11 +01:00
Anthony Lapenna efc3154617 refactor(ux): rename deploymentInProgress variable (#1385) 2017-11-12 22:39:12 +01:00
Anthony Lapenna d68708add7 feat(ux): replace spinners (#1383) 2017-11-12 20:27:28 +01:00
Anthony Lapenna 9bef7cd69f Merge tag '1.15.1' into develop
Release 1.15.1
2017-11-08 08:29:09 +01:00
Anthony Lapenna ff82d4320f Merge branch 'release/1.15.1' 2017-11-08 08:29:05 +01:00
Anthony Lapenna 7ee16d1e51 chore(version): bump version number 2017-11-08 08:28:37 +01:00
Anthony Lapenna 6c6171c1f4 revert(images): revert image autocompletion (#1367) 2017-11-08 08:18:52 +01:00
Anthony Lapenna d06667218f feat(container-edit): container edit/duplicate feature not experimental anymore (#1363) 2017-11-07 09:20:59 +01:00
Anthony Lapenna 4a291247ac feat(service-creation): pass volume driver and options when mapping a… (#1360)
* feat(service-creation): pass volume driver and options when mapping an existing volume

* refactor(service-creation): remove commented code
2017-11-07 08:32:09 +01:00
Anthony Lapenna 9ceb3a8051 feat(templates): add support for stack templates (#1346) 2017-11-07 08:18:23 +01:00
Yassir Hannoun 1b6b4733bd feat(images): enable auto completion for image names when creating a container or a service (#1355) 2017-11-07 08:05:13 +01:00
Thomas Krzero b9e535d7a5 fix(services): Fix invalid replica count for global services (#1353) 2017-11-06 15:50:59 +01:00
Thomas Kooi 407f0f5807 feat(configs): add support for docker configs (#996) 2017-11-06 09:47:31 +01:00
Fish2 ade66414a4 chore(assets): lossless image compression 2017-11-05 14:51:07 +01:00
Anthony Lapenna 693f1319a4 feat(stacks): add the ability to specify env vars when deploying stacks (#1345) 2017-11-01 10:30:02 +01:00
1138-4EB 42347d714f style(sidebar): automatically adjust title form-control size based on height (#1338) 2017-10-30 09:29:22 +01:00
1138-4EB a028413496 feat(assets): make URLs for favicons relative (#1343) 2017-10-30 08:56:21 +01:00
Anthony Lapenna 86e5ca57e9 style(sidebar): automatically adjust sidebar font-size based on height (#1336) 2017-10-28 19:42:55 +02:00
Riccardo Capuani 1d150414d9 feat(templates): add /etc/hosts entries support (#1307) 2017-10-27 10:48:11 +02:00
1138-4EB f8451e944a style(sidebar): make sidebar-header fixed, use flex instead of absolute to position footer (#1315) 2017-10-27 09:35:35 +02:00
Anthony Lapenna b5629c5b1a feat(stacks): allow to use images from private registries in stacks (#1327) 2017-10-26 14:22:09 +02:00
1138-4EB 34d40e4876 chore(build-system): make assets default relative, serve assets from assets/public (#1309) 2017-10-26 11:17:45 +02:00
Philippe Leblond c4e75fc858 fix(swarm): display node links when authentication is disabled (#1332) 2017-10-26 08:15:08 +02:00
Anthony Lapenna 77503b448e fix(container-details): use container.Mounts instead of container.HostConfig.Binds (#1329) 2017-10-25 17:03:40 +02:00
Anthony Lapenna 25f325bbaa fix(network-details): fix an issue caused by stopped containers (#1328) 2017-10-25 13:37:52 +02:00
utzb 711128284e chore(build-system): use system architecture instead of hardcoded amd64 value 2017-10-25 08:56:57 +02:00
Anthony Lapenna 514da445a4 Revert "fix(swarm): display node links when authentication is disabled #1320" (#1326)
This reverts commit 089d2cf0fe.
2017-10-25 08:42:19 +02:00
Philippe Leblond 089d2cf0fe fix(swarm): display node links when authentication is disabled #1320 2017-10-25 08:40:48 +02:00
Anthony Lapenna aa32213f7c fix(dashboard): do not display stack and service info when connected to Swarm worker (#1319) 2017-10-24 19:17:07 +02:00
utzb 11feae19b7 chore(build-system): add support for linux s390x platform (#1316)
s390x works fine (like other Linux architectures).
2017-10-24 10:26:35 +02:00
1138-4EB ddd804ee2e feat(container-inspect): display content in tree view by default (#1310) 2017-10-24 09:32:21 +02:00
1138-4EB c97f1d24cd style(images): prevent unused label breaking to multiple lines (#1314) 2017-10-23 20:19:13 +02:00
spezzino 4a49942ae5 feat(endpoints): automatically strip URL's protocol when creating a new endpoint (#1294) 2017-10-18 19:50:20 +02:00
Boris Manojlovic c9ccdaaea4 chore(distribution): add rpm based packaging and system unit file (#1292) 2017-10-18 18:08:09 +02:00
G07cha f9218768c1 chore(build-system): replace individual package load with pattern (#1298) 2017-10-18 17:46:56 +02:00
spezzino 0af3c44e9a style(area/settings): replace LDAP URL label (#1288) 2017-10-18 17:45:17 +02:00
Anthony Lapenna 730925b286 fix(containers): fix an issue with filters 2017-10-17 10:12:16 +02:00
G07cha 7eaaf9a2a7 feat(container-inspect): add the ability to inspect containers 2017-10-17 08:56:40 +02:00
G07cha 925326e8aa feat(volume-details): show a list of containers using the volume 2017-10-17 08:45:19 +02:00
Anthony Lapenna dc05ad4c8c fix(templates): add missing NetworkSettings field (#1287) 2017-10-16 18:54:48 +02:00
Anthony Lapenna 8ec7b4fcf5 chore(codefresh): add a step to download docker binary (#1283) 2017-10-16 10:32:51 +02:00
Anthony Lapenna dc48fa685f fix(cli): fix default asset directory value 2017-10-15 20:47:37 +02:00
Anthony Lapenna 7727fc6dcb Merge tag '1.15.0' into develop
Release 1.15.0
2017-10-15 19:27:39 +02:00
Anthony Lapenna 5785ba5f4a Merge branch 'release/1.15.0' 2017-10-15 19:27:34 +02:00
Anthony Lapenna e110986728 chore(version): bump version number 2017-10-15 19:27:23 +02:00
Anthony Lapenna 587e2fa673 feat(stacks): add support for stack deploy (#1280) 2017-10-15 19:24:40 +02:00
G07cha 80827935da chore(build-system): fix 'gruntify-eslint' usage (#1276)
`eslint` is task from `gruntify-eslint` package and therefore package
should be loaded as well
2017-10-14 07:04:32 +01:00
Thomas Krzero f3a1250b27 feat(container-creation) - Add container resource management (#1224) 2017-10-04 07:39:59 +01:00
Anthony Lapenna 79121f9977 docs(swagger): add missing Username field in UserAdminInitRequest 2017-10-04 08:38:55 +02:00
pc f678d05088 feat(tasks): add a filter for tasks in service-details view 2017-10-03 10:38:30 +01:00
Anthony Lapenna c6341eead0 docs(swagger): update swagger docs 2017-10-02 18:21:42 +02:00
Anthony Lapenna 3e99fae070 style(sidebar): add a small logo in the sidebar (#1255) 2017-10-01 09:44:02 +01:00
Anthony Lapenna 249bcf5bac fix(api): prevent the creation of multiple admin users (#1251) 2017-09-29 18:44:30 +02:00
Anthony Lapenna 9c10a1def2 Merge tag '1.14.3' into develop
Release 1.14.3
2017-09-27 19:43:11 +02:00
Anthony Lapenna 93120d23c6 Merge branch 'hotfix/1.14.3' 2017-09-27 19:43:06 +02:00
Anthony Lapenna b59dd03b43 chore(version): bump version number 2017-09-27 19:43:01 +02:00
Anthony Lapenna 1263866548 fix(container-stats): adapt stats view when networks stats unavailable (#1244) 2017-09-27 09:47:11 +02:00
Anthony Lapenna 0bdcff09f8 feat(settings): add a setting to disable privileged mode for non-admins (#1239) 2017-09-27 09:26:04 +02:00
Anthony Lapenna ca9d9b9a77 feat(settings): add a setting to disable bind mounts for non-admins (#1237)
* feat(settings): add a setting to disable bind mounts for non-admins

* refactor(gruntfile): remove temporary setting
2017-09-26 05:36:51 +02:00
Nenad Ilic 6cfffb38f9 feat(cli): Allow adding admin password using docker secrets aka file (#1199) (#1214) 2017-09-25 18:13:56 +02:00
Anthony Lapenna e2979a631a style(swarm-visualizer): update font-size (#1228) 2017-09-22 08:53:08 +02:00
Anthony Lapenna 7b924bde83 fix(userSettings): allow to change admin password when using LDAP auth (#1227) 2017-09-22 08:00:13 +02:00
Anthony Lapenna 6bf7c90634 refactor(vendor): relocate angular libraries 2017-09-22 07:45:43 +02:00
Anthony Lapenna f5749f82d8 fix(endpoint-details): fix an issue when updating the local endpoint (#1226) 2017-09-22 07:34:17 +02:00
Anthony Lapenna 8413b79fa9 Merge tag '1.14.2' into develop
Release 1.14.2
2017-09-21 17:22:18 +02:00
Anthony Lapenna dffcdcc148 Merge branch 'hotfix/1.14.2' 2017-09-21 17:22:08 +02:00
Anthony Lapenna 4b53c3422f chore(version): bump version number 2017-09-21 17:22:01 +02:00
Anthony Lapenna 3fb668474d fix(tls): fix an issue with TLSConfig ignored when using LDAP StartTLS 2017-09-21 17:19:43 +02:00
Anthony Lapenna ff628bb438 refactor(app): upgrade to the latest version of ui-router (#1219)
* refactor(app): upgrade to the latest version of ui-router

* fix(app): define optional from parameter in action.create.container state

* refactor(app): replace $uiRouterGlobals with $transition$
2017-09-21 16:00:53 +02:00
Anthony Lapenna 819d0f6a16 refactor(app): split app.js in multiple files (#1217) 2017-09-21 10:23:51 +02:00
Anthony Lapenna 601ae9daf2 fix(ldap): prevent panic if search error arise (#1216) 2017-09-20 20:58:09 +02:00
Anthony Lapenna 09409804af Merge tag '1.14.1' into develop
Release 1.14.1
2017-09-20 15:41:12 +02:00
Anthony Lapenna 1bccd521f8 Merge branch 'release/1.14.1' 2017-09-20 15:41:06 +02:00
Anthony Lapenna 5e2b3c1d07 chore(version): bump version number 2017-09-20 15:41:01 +02:00
Anthony Lapenna 210bdc8022 refactor(vendor): fix path to min CSS file for rzslider 2017-09-20 14:38:16 +02:00
Thomas Krzero 3cb96235b7 #516 feat(services) - add the ability to manage cpu/mem limits 2017-09-20 08:32:19 +02:00
Anthony Lapenna d695657711 feat(sidebar): rename Docker to Engine (#1212) 2017-09-20 08:23:36 +02:00
Anthony Lapenna 5131c4c10b feat(notifications): do not display invalid JWT token notifications (#1209) 2017-09-19 20:59:28 +02:00
Anthony Lapenna 912ebf4672 feat(api): filter tasks based on service UAC (#1207) 2017-09-19 20:23:48 +02:00
Anthony Lapenna dd0fc6fab8 feat(swarm): restrict access to the node details view to administrators only (#1204) 2017-09-19 18:41:03 +02:00
Anthony Lapenna 910136ee9b feat(containers): store show all filter value in a cookie (#1203) 2017-09-19 18:24:41 +02:00
Anthony Lapenna 61f652da04 feat(secrets): add UAC (#1200) 2017-09-19 17:10:15 +02:00
Anthony Lapenna a2b4cd8050 feat(networks): add UAC (#1196) 2017-09-19 16:58:30 +02:00
Anthony Lapenna 774738110b feat(auth): add an auto-focus directive and remove username placeholder 2017-09-17 17:07:19 +02:00
Anthony Lapenna 851a1ac64c feat(sidebar): restrict access to Events for administrators only (#1193) 2017-09-15 09:57:04 +02:00
Anthony Lapenna d653391cdd feat(api): write Docker response code when using local proxy (#1192) 2017-09-14 11:09:36 +02:00
Anthony Lapenna f96b70841f feat(swarm-visualizer): add a platform icon next to node name (#1191) 2017-09-14 10:22:27 +02:00
Anthony Lapenna 8d4807c9e7 feat(api): TLS endpoint creation and init overhaul (#1173) 2017-09-14 08:08:37 +02:00
Anthony Lapenna 87825f7ebb feat(swarm-visualizer): add the swarm-visualizer view (#1190) 2017-09-14 08:04:59 +02:00
Anthony Lapenna be4f3ec81d fix(admin-init): do not redirect to endpoint-init if at least one endpoint is defined 2017-09-11 10:36:18 +02:00
Adrian Kirchner 56604a5445 fix(cli): fix wrong default value for --no-analytics (#1185) 2017-09-10 10:00:48 +02:00
Anthony Lapenna c0d282e85b feat(container-stats): overhaul (#1183) 2017-09-09 18:49:21 +02:00
Liam Cottam b9b32f0526 feat(network-creation): network dropdown for drivers (#1016) (#1062) 2017-09-06 15:11:38 +02:00
Anthony Lapenna be4beacdf7 feat(container-creation): display a warning message when editing a container with an unknow registry (#1143) 2017-09-05 16:42:20 +02:00
Sylvain MOUQUET bf6b398a27 feat(containers): add a button to display the full name of containers (#1164) 2017-09-05 10:10:16 +02:00
Anthony Lapenna 9a0f0a9701 feat(favicon): fix favicon display (#1177) 2017-09-05 09:57:49 +02:00
Anthony Lapenna ef8edfb67b feat(api): display version in startup logs (#1175) 2017-09-04 19:04:30 +02:00
Anthony Lapenna 0e8da2db18 docs(swagger): update UserAdminInitRequest definition 2017-08-29 09:11:19 +02:00
Anthony Lapenna e65d132b3d feat(init-admin): allow to specify a username for the initial admin account (#1160) 2017-08-28 20:59:13 +02:00
Anthony Lapenna 13b2fcffd2 docs(templates): add deprecation notice for old volume format 2017-08-28 20:57:41 +02:00
Adam Snodgrass c1e486bf43 feat(templates): add support for bind mounts in volumes
* #777 feat(templates): add support for binding to host path

* #777 feat(templates): add link to templates documentation

* refactor(templates): update warning style to match theme

* fix(templates): remove trailing comma

* refactor(templates): use bind instead of self declaration

* feat(templates): support readonly property in template volumes

* #777 refactor(templates): remove deprecation notice

* #777 refactor(templates): remove deprecated condition from template
2017-08-28 20:53:36 +02:00
Anthony Lapenna 8c68e92e74 feat(images): use containers instead of /system/df to check unused images (#1150) 2017-08-24 07:53:34 +02:00
Anthony Lapenna a6ef27164c feat(container-details): prevent re-creation, edition & duplication for service task (#1149) 2017-08-23 10:06:18 +02:00
Anthony Lapenna d50a650686 feat(dashboard): remove driver information in volumes (#1148) 2017-08-23 09:51:42 +02:00
Anthony Lapenna 35dd3916dd fix(authentication): do not use $sanitize with LDAP authentication (#1136) 2017-08-22 16:36:12 +02:00
Anthony Lapenna 1a28e1091c docs(api): update swagger.yml (#1130) 2017-08-16 10:15:58 +02:00
Anthony Lapenna 124458c3d6 Merge tag '1.14.0' into develop
Release 1.14.0
2017-08-13 20:17:35 +02:00
Anthony Lapenna 8e2dbd1775 Merge branch 'release/1.14.0' 2017-08-13 20:17:30 +02:00
Anthony Lapenna 27188f4dff chore(version): bump version number 2017-08-13 20:17:23 +02:00
Anthony Lapenna ef13f6fb3b feat(sidebar): do not display services and secrets when managing a worker node (#1114) 2017-08-13 16:55:02 +02:00
Anthony Lapenna 92391254bc feat(api): introduces swagger.yml (#1112) 2017-08-13 16:45:55 +02:00
Anthony Lapenna d3e87b2435 style(settings): fix typo 2017-08-13 15:04:24 +02:00
Anthony Lapenna e5666dfdf2 feat(vic): fix multiple issues when managing a VIC engine (#1069) 2017-08-13 13:31:50 +02:00
Anthony Lapenna e96e615761 feat(container-details): add the ability to specify if image should be pulled when re-creating a container 2017-08-13 12:55:52 +02:00
Thomas Krzero c85aa0739d feat(container-details): add the ability to re-create, duplicate and edit a container (#855) 2017-08-13 12:17:41 +02:00
Anthony Lapenna d814f3aaa4 fix(networks): review how networks are loaded for usage in multiple views (#1104) 2017-08-11 09:46:55 +02:00
Anthony Lapenna 3d5f9a76e4 fix(team-details): fix an issue when sorting columns (#1106) 2017-08-10 15:25:53 +02:00
Anthony Lapenna d27528a771 feat(authentication): add LDAP authentication support (#1093) 2017-08-10 10:35:23 +02:00
Anthony Lapenna 04ea81e7cd feat(service): support the Order field for Update Configuration (#1101) 2017-08-09 15:30:50 +02:00
Anthony Lapenna d7769dec33 fix(images): fix the way the registry and image name are extracted fr… (#1099)
* fix(images): fix the way the registry and image name are extracted from a repository
2017-08-09 10:40:46 +02:00
Liam Cottam 12adeadc94 fix(container-details): connected network section disappearing (#1092) 2017-08-06 10:42:38 +02:00
Anthony Lapenna b5429f7504 docs(README): add code climate badge 2017-08-04 08:09:29 +02:00
Liam Cottam cf5c3ee536 fix(container-console): fix an issue with scrollbar (#932) (#1086) 2017-08-04 08:02:26 +02:00
tfenster 86c450bd91 feat(templates): Use container name as hostname (#1084) 2017-08-04 07:54:03 +02:00
Anthony Lapenna 0d6ab099ac feat(templates): update LinuxServer.io templates feed URL (#1089) 2017-08-01 11:24:44 +02:00
Anthony Lapenna 5110f83fae fix(rest): fix an issue with rest factories using $http (#1077) 2017-07-27 10:46:29 +02:00
Anthony Lapenna 252e05e963 fix(container-details): add missing Created field from ContainerDetailsViewModel (#1075) 2017-07-26 17:12:02 +02:00
Dan Hlavenka 635ecdef72 style(sidebar): crop logo.png to fit in sidebar without scaling (#1072) 2017-07-26 07:52:44 +02:00
Anthony Lapenna b08d2b07bc feat(volume-creation): add plugin support (#1044)
* feat(volume-creation): add plugin support

* feat(plugins): only use systemInfo to retrieve plugins when API version < 1.25

* refactor(createVolume): remove unused dependencies
2017-07-25 16:21:32 +02:00
Anthony Lapenna 3919ad3ccf fix(images): show image usage only if endpoint API version >= 1.25 (#1067) 2017-07-24 19:11:12 +02:00
Konstantin Azizov aca4f5c286 fix(containers): Fix available buttons for created container (#1065) 2017-07-24 16:39:04 +02:00
Anthony Lapenna 387b4c66d9 fix(containers): fix an issue when only containers without ports are running (#1068) 2017-07-24 16:29:28 +02:00
Anthony Lapenna 7c40d2caa9 fix(services): use secrets with services only if endpoint API version >= 1.25 2017-07-24 11:59:09 +02:00
Anthony Lapenna 02203e7ce5 refactor(api): relocate /docker API endpoint under /endpoints (#1053) 2017-07-20 16:22:27 +02:00
Anthony Lapenna 53583741ba fix(UAC): fix the ability to update the ownership of a resource from public to another type (#1054) 2017-07-20 15:48:05 +02:00
1138-4EB 12eb9671de style(volumes): replace label 'Dangling' with 'Unused' (#1052) 2017-07-20 08:47:11 +02:00
Anthony Lapenna 29d66bfd97 fix(containers): add support for the 'dead' status (#1048) 2017-07-19 16:34:11 +02:00
Anthony Lapenna 57fde5ae7c feat(Dockerfile): use portainer/base image (#1045) 2017-07-18 12:17:31 +02:00
Anthony Lapenna 471f902171 Merge tag '1.13.6' into develop
Release 1.13.6
2017-07-17 16:00:47 +02:00
Anthony Lapenna 2e2aba1bbb Merge branch 'release/1.13.6' 2017-07-17 16:00:40 +02:00
Anthony Lapenna f2347b2f77 chore(version): bump version number 2017-07-17 15:59:43 +02:00
Anthony Lapenna a39645a297 fix(images): fix the system/df call to display unused images (#1037) 2017-07-17 15:58:53 +02:00
Anthony Lapenna 806a0b92a0 Merge tag '1.13.5' into develop
Release 1.13.5
2017-07-13 18:08:50 +02:00
Anthony Lapenna a438357b45 Merge branch 'release/1.13.5' 2017-07-13 18:08:46 +02:00
Anthony Lapenna 206eb0513d chore(version): bump version number 2017-07-13 18:08:39 +02:00
Anthony Lapenna 5ad6837547 feat(container-console): improve container console UX (#1031) 2017-07-13 18:04:58 +02:00
Anthony Lapenna 272a040c91 feat(volumes): add a label in front of dangling volumes (#1025) 2017-07-13 13:50:59 +02:00
Anthony Lapenna c04b9e5340 feat(volumes): new truncate method for volume paths (#1028) 2017-07-13 13:50:42 +02:00
Anthony Lapenna 3f085a977c fix(UAC): allow a team member to delete a resource control (#1030) 2017-07-13 09:12:06 +02:00
Anthony Lapenna a1dd12a947 feat(sidebar): sort available endpoints alphabetically (#1027) 2017-07-12 20:52:07 +02:00
Anthony Lapenna a7df43bd45 feat(container-details): show container ID (#1026) 2017-07-12 19:37:34 +02:00
Anthony Lapenna 5d749c2ebf feat(auth): use the same error message on invalid authentication (#1024) 2017-07-12 17:22:14 +02:00
Anthony Lapenna 536ca15e90 fix(swarm): fix multiple Swarm related issues (#1022)
* fix(containers): fix an issue where the containers would not be displayed

* fix(images): image usage filtering is not compliant with docker/swarm

* fix(volume-creation): do not load volume driver with docker/swarm
2017-07-12 16:11:11 +02:00
Anthony Lapenna 703e423e04 fix(external-endpoints): prevent the creation of an invalid file endpoint (#1021) 2017-07-12 15:15:42 +02:00
Anthony Lapenna 780fec8e36 fix(access): fix an issue where an access would disappear (#1018) 2017-07-12 14:13:51 +02:00
1138-4EB 0a436600f4 feat(build-system): dynamic vendoring (#994) 2017-07-12 11:28:51 +02:00
Anthony Lapenna 32c2ce90e2 feat(build-system): automatically remove binary build container 2017-07-12 10:13:00 +02:00
Anthony Lapenna a864641692 refactor(UAC): refactor common views to components (#1013) 2017-07-12 09:51:51 +02:00
Anthony Lapenna 344eee098d chore(deps): update xtermjs version (#1012) 2017-07-11 16:52:39 +02:00
Konstantin Azizov bc4b0a0b35 feat(images): display unused images tags (#1009) 2017-07-11 09:56:28 +02:00
1138-4EB b23943e30b refactor(build-system): reduce gruntfile verbosity, drop grunt-if, allow custom build (#939) 2017-07-11 09:30:25 +02:00
Glowbal 25ed6a71fb feat(services): add support for placement preferences (#1003) 2017-07-10 09:33:09 +02:00
Konstantin Azizov 8dc6d05ed6 feat(console): allow the user to specify a command in the console section (#259) (#1007) 2017-07-10 09:10:10 +02:00
Konstantin Azizov fe5a993fc9 feat(volumes): view dangling volumes (#993) 2017-07-09 18:49:36 +02:00
Thomas Krzero 6df5eb3787 feat(service-details) - add service logs (#671) 2017-07-08 11:34:21 +02:00
Konstantin Azizov bc3d5e97ea chore(build-system): update run-dev to mount assets (#997) 2017-07-08 10:42:41 +02:00
Glowbal 9909b6d481 feat(backend): make swarm api endpoint admin user protected (#991) 2017-07-08 10:34:04 +02:00
Glowbal 90a32d1b67 refactor(html): fix html tags and escape special characters (#987) 2017-07-08 10:23:00 +02:00
Konstantin Azizov 472834ac42 feat(containers): add buttons disabling based on cluster selection (#985) 2017-07-08 10:07:08 +02:00
Anthony Lapenna b3f4c6f751 refactor(image-details): place imageLayer model under models/docker 2017-07-08 09:22:39 +02:00
Anthony Lapenna 317303fc43 feat(image-details): image layer enhancements 2017-07-08 09:21:30 +02:00
Gábor Kovács b6b579d55d feat(image-details): simple image history (#425) 2017-07-08 08:59:32 +02:00
Anthony Lapenna 6d6f4f092d fix(secrets): fix an issue when removing a secret that is in use (#984) 2017-07-07 15:45:31 +02:00
Anthony Lapenna 7473681c5b fix(container-details): fix the ability to commit a container (#983) 2017-07-05 19:06:28 +02:00
Konstantin Azizov 54c8872d25 feat(container-console): add ability to specify the user (#976) 2017-07-05 07:16:57 +02:00
Konstantin Azizov c5ce45f588 chore(build-system): replace Recess with PostCSS (#975) 2017-07-04 14:30:22 +02:00
Anthony Lapenna 07a0c4dfe3 feat(endpoints): update information message (#974) 2017-07-03 08:36:18 +02:00
Anthony Lapenna 80bb94e745 docs(README): update README 2017-06-30 14:52:04 +02:00
Anthony Lapenna 6c89412f39 Merge tag '1.13.4' into develop
Release 1.13.4
2017-06-29 16:37:32 +02:00
Anthony Lapenna 034e29cd74 Merge branch 'release/1.13.4' 2017-06-29 16:37:28 +02:00
Anthony Lapenna 0e0764eff8 chore(version): bump version number 2017-06-29 16:37:22 +02:00
Anthony Lapenna e47db0b8c9 feat(volumes): display mount point for each volume (#967) 2017-06-29 16:14:17 +02:00
Anthony Lapenna 6d401dcd59 fix(templates): fix the ability to pull an image within an offline environment (#961) 2017-06-29 16:05:39 +02:00
Anthony Lapenna 6609c2e928 style(container-details): review responsiveness for the join network section 2017-06-29 16:04:49 +02:00
Adam Snodgrass a161d25d48 feat(container-details): add section to join networks (#927) 2017-06-29 15:49:35 +02:00
Anthony Lapenna 4adedf9436 fix(service-details): fix an issue where secret target would be overwritten (#964) 2017-06-29 08:37:05 +02:00
Anthony Lapenna 1168e94534 fix(service-creation): fix an issue when selecting a volume from available volumes (#963) 2017-06-29 07:41:37 +02:00
Anthony Lapenna b57bfe3eee Create CODE_OF_CONDUCT.md (#946) 2017-06-22 05:11:40 +02:00
Anthony Lapenna 3592e88e4f Merge tag '1.13.3' into develop
Release 1.13.3
2017-06-20 13:21:16 +02:00
Anthony Lapenna 219cde4733 Merge branch 'release/1.13.3' 2017-06-20 13:21:12 +02:00
Anthony Lapenna c82cd50d87 chore(version): bump version number 2017-06-20 13:21:06 +02:00
Anthony Lapenna dae4893fe1 feat(endpoint): remove the active endpoint edition restriction (#941) 2017-06-20 13:18:08 +02:00
Anthony Lapenna 1e686f0428 feat(state): persist application state in localstorage instead of ses… (#940) 2017-06-20 13:07:24 +02:00
Anthony Lapenna 08c5a5a4f6 feat(registries): add registry management (#930) 2017-06-20 13:00:32 +02:00
eliat123 9360f24d89 feat(service-details): add quick navigation menu anchors (#875) 2017-06-20 12:54:27 +02:00
Anthony Lapenna d0477b216f Merge branch 'develop' of github.com:portainer/portainer into develop 2017-06-17 17:05:52 +02:00
Anthony Lapenna a812f4729c docs(README): update links to portainer.io 2017-06-17 17:05:34 +02:00
Anthony Lapenna db324998e3 fix(templates): display templates without platform (#937) 2017-06-17 16:50:35 +02:00
Gabriel Lewertowski 4ec65a80df fix(user-creation): sanitize username and password (#934) 2017-06-17 15:25:23 +02:00
Anthony Lapenna f2b9700345 chore(codeclimate): update mass_threshold for the duplication engine 2017-06-17 15:20:19 +02:00
Anthony Lapenna d8f8ab785c fix(service-details): fix the ability to sort tasks (#931) 2017-06-15 22:52:49 +02:00
Anthony Lapenna b316efe80b Merge tag '1.13.2' into develop
Release 1.13.2
2017-06-05 08:42:20 +02:00
Anthony Lapenna 14a4587f5e Merge branch 'release/1.13.2' 2017-06-05 08:42:15 +02:00
Anthony Lapenna afd99d2d68 chore(version): bump version number 2017-06-05 08:42:08 +02:00
Anthony Lapenna 7bba1c9c5e style(settings): fix a small display issue in the hidden containers table 2017-06-05 08:40:42 +02:00
Anthony Lapenna fd79afb429 style(sidebar): moved Secrets section under the Volumes section 2017-06-05 08:17:56 +02:00
Anthony Lapenna d5f00597a5 fix(container-creation): ignore error when pulling an image (#914) 2017-06-05 07:55:18 +02:00
Fish2 1c4ccfe294 feat(assets): lossless compression of images saved 14KB (#915) 2017-06-05 07:47:55 +02:00
Anthony Lapenna f48423d5aa docs(README): update documentation badge 2017-06-03 16:52:33 +02:00
Anthony Lapenna 5d98d9b54b feat(settings): prevent the creation of empty filters 2017-06-01 10:30:22 +02:00
Anthony Lapenna 132dd4acc4 fix(container-details): fix an issue when renaming a container (#908) 2017-06-01 10:23:59 +02:00
Anthony Lapenna c7e306841a feat(settings): add settings management (#906) 2017-06-01 10:14:55 +02:00
Anthony Lapenna 5e74a3993b fix(api): add restrictions for the files served by the API (#903) 2017-05-29 22:10:36 +02:00
Anthony Lapenna 5bf10b89b1 docs(README): add Slack badge 2017-05-28 18:08:52 +02:00
Anthony Lapenna bde9dd8b88 feat(templates): add support for a restart_policy field (#898) 2017-05-27 10:11:42 +02:00
Anthony Lapenna 42d28db47a feat(secrets): add secret management (#894) 2017-05-27 09:23:49 +02:00
Anthony Lapenna 128601bb58 Merge tag '1.13.1' into develop
Release 1.13.1
2017-05-25 12:20:56 +02:00
Anthony Lapenna 86addbdc9a Merge branch 'release/1.13.1' 2017-05-25 12:20:52 +02:00
Anthony Lapenna de9be4bbe0 chore(version): bump version number 2017-05-25 12:20:43 +02:00
Anthony Lapenna 49b79aadfd docs(README): add codefresh badge 2017-05-25 12:17:51 +02:00
Renno Reinurm 6dab3eddea feat(task-details): show state message 2017-05-25 12:16:14 +02:00
Thomas Krzero 949f14b119 fix(service-creation) - issue with bind mount (#882) 2017-05-25 11:13:29 +02:00
Anthony Lapenna de2818de4c chore(codefresh): add codefresh.yml (#887) 2017-05-25 11:08:26 +02:00
Anthony Lapenna 0f3fcb2917 fix(templates): fix an issue with the maximum number of templates displayed (#883) 2017-05-24 14:38:53 +02:00
Anthony Lapenna 3356fd9815 Merge tag '1.13.0' into develop
Release 1.13.0
2017-05-23 21:14:11 +02:00
Anthony Lapenna 7bef930d0c Merge branch 'release/1.13.0' 2017-05-23 21:14:03 +02:00
Anthony Lapenna db1a754b39 chore(version): bump version number 2017-05-23 21:13:55 +02:00
Anthony Lapenna 9b9b2731ba refactor(api): fix lint issues 2017-05-23 21:01:19 +02:00
Anthony Lapenna 5523fc9023 feat(global): introduce user teams and new UAC system (#868) 2017-05-23 20:56:10 +02:00
Anthony Lapenna a380fd9adc fix(image-details): fix invalid CMD with images using HEALTHCHECK (#879) 2017-05-23 20:43:58 +02:00
Anthony Lapenna d3ecf1d7a8 fix(image-details): fix the ability to pull an image from a tag (#878) 2017-05-23 20:25:56 +02:00
Anthony Lapenna 6834c20b5d docs(README): update README 2017-05-23 17:54:14 +02:00
Anthony Lapenna b9035659d2 chore(build-system): update Gruntfile tasks 2017-05-23 15:33:40 +02:00
Anthony Lapenna 5b47427484 fix(build-system): fix broken tasks 2017-05-20 11:25:47 +02:00
Anthony Lapenna 6e95e1279a chore(build-system): add support for linux 386 architecture (#871) 2017-05-20 10:27:55 +02:00
Anthony Lapenna a2e781fb3f chore(build-system): add support for ppc64le architecture (#870) 2017-05-20 10:02:18 +02:00
Anthony Lapenna 69c7f116b1 fix(app): fix missing '=' char in state definitions 2017-05-19 17:51:01 +02:00
Anthony Lapenna 2ef1c90248 feat(app): disable Angular debug information on release (#867) 2017-05-19 17:48:03 +02:00
Anthony Lapenna 782df54570 fix(service-details): add missing Arguments field (#864) 2017-05-18 23:32:04 +02:00
Anthony Lapenna 0ba6645df0 fix(container-details): fix an issue with duplicate env var (#863) 2017-05-18 23:17:39 +02:00
Anthony Lapenna 0579251c70 feat(templates): new templates capabilities (#862) 2017-05-18 23:00:08 +02:00
Alex Seymour c3363604ac feat(templates): Support interactive templates (#819) 2017-05-18 22:49:55 +02:00
Anthony Lapenna 09aa67ba61 chore(github): update ISSUE_TEMPLATE.md 2017-05-05 06:29:26 +02:00
Glowbal 4ff7ee4e60 fix(services): Empty environment variables are not maintained (#836) 2017-05-05 06:25:48 +02:00
Anthony Lapenna 5b81b35bf8 chore(gruntfile): use eslint instead of jshint 2017-05-04 10:17:55 +02:00
Glowbal df3a529f0a feat(services): ability to publish ports using host mode (#838) 2017-05-04 09:43:20 +02:00
Glowbal 43e1f25f89 feat(service-creation): add placement constraints (#837) 2017-05-04 08:57:08 +02:00
Thomas Krzero 7c6c9284f2 feat(endpoints) - Access exposed containers on endpoint public URL (#826) 2017-05-01 11:19:43 +01:00
Thomas Krzero 3d8eec2557 feat(containers) - clean non-persistent volumes when removing a container (#824) 2017-05-01 11:18:06 +01:00
Thomas Krzero 5a07638f4d fix(container) - correct since date for created containers (#822) 2017-04-27 19:40:37 +01:00
Anthony Lapenna 87250d13d7 chore(project): update codeclimate configuration 2017-04-27 18:11:48 +02:00
Anthony Lapenna 90d13684e5 chore(project): add eslint and codeclimate configuration files 2017-04-27 18:09:40 +02:00
GP8x 25206e71cf feat(container-creation): add support for ip assignments (#812) 2017-04-25 21:32:27 +01:00
030 6fa6dde637 feat(backend): native SSL support 2017-04-25 10:51:22 +01:00
Thomas Krzero e70817f776 feat(containers): show health status of containers (#622) 2017-04-25 10:09:06 +01:00
Thomas Krzero ca5c606dfc fix(services): replicas count misunderstanding (#806) 2017-04-25 09:37:38 +01:00
Thomas Krzero ac872b577a feat(containers) - Add the ability to force remove a container with confirmation (#814) 2017-04-25 09:20:57 +01:00
Anthony Lapenna 2761959f93 feat(templates): add support for the note field (#805) 2017-04-18 17:16:00 +01:00
Anthony Lapenna 7bf708faab Merge branch 'develop' of github.com:portainer/portainer into develop 2017-04-16 11:16:05 +02:00
Anthony Lapenna c526209925 chore(gruntfile): remove --templates flag in run-dev task 2017-04-16 11:15:56 +02:00
Hilscher 8215cf7857 feat(container-creation): add support for devices (#729) 2017-04-16 08:57:47 +01:00
dedalusj 5745606fe7 feat(cli): Allow setting admin password from CLI (#752) 2017-04-16 08:54:51 +01:00
Anthony Lapenna f15cf3e8be feat(notifications): replace gritter with toastr (#793) 2017-04-12 20:47:22 +01:00
Anthony Lapenna 8e8b0578b2 docs(README): add docker pulls badge 2017-04-10 19:01:15 +02:00
Anthony Lapenna abc929824c fix(endpoints): add the ability to update TLS for an existing endpoint (#784) 2017-04-09 19:38:41 +01:00
Anthony Lapenna 44e48423ed fix(endpoint-init): fix an issue when connecting to a remote TLS endpoint (#783) 2017-04-08 19:38:19 +01:00
Anthony Lapenna 3883cc8b67 Merge tag '1.12.4' into develop
Release 1.12.4
2017-04-06 10:37:37 +02:00
Anthony Lapenna 8e6272920b Merge branch 'release/1.12.4' 2017-04-06 10:37:32 +02:00
Anthony Lapenna 0cde215259 chore(version): bump version number 2017-04-06 10:37:26 +02:00
Anthony Lapenna 3fc54c095e fix(service-details): fix an update issue when no ports are defined (#765) 2017-04-06 09:35:01 +01:00
Anthony Lapenna 80a0a15490 fix(service-details): display spinner when updating the service (#764) 2017-04-06 09:34:49 +01:00
Anthony Lapenna af49c78498 Merge tag '1.12.3' into develop
Release 1.12.3
2017-04-05 10:15:14 +02:00
Anthony Lapenna 4839c5f313 Merge branch 'release/1.12.3' 2017-04-05 10:15:08 +02:00
Anthony Lapenna e9c6feb3c4 chore(version): bump version number 2017-04-05 10:15:03 +02:00
Anthony Lapenna b8803f380b feat(templates): LinuxServer.io templates integration (#761) 2017-04-05 10:13:32 +02:00
Anthony Lapenna 16166c3367 fix(network-creation): fix internal network switch (#760) 2017-04-05 10:04:29 +02:00
Anthony Lapenna db4b153ce1 fix(service-creation): fix invalid mount specs (#757) 2017-04-04 09:16:13 +02:00
Anthony Lapenna 50305e0eee feat(volume-creation): retrieve available drivers from the engine (#751) 2017-04-01 12:18:46 +02:00
Thomas Krzero 53f31ba3b8 feat(templates): add the ability to connect a template to swarm attachable networks (#642) 2017-03-31 22:12:58 +02:00
Anthony Lapenna ffca440135 fix(services): let Docker automatically assign port when PublishedPort is not defined (#747) 2017-03-30 12:00:16 +02:00
Thomas Krzero 9fda8f9c92 fix(services) - Fix exposed ports (#746) 2017-03-30 11:39:37 +02:00
Anthony Lapenna a48503d821 feat(services): add a confirmation modal before deleting one or multiple services (#742) 2017-03-30 11:22:59 +02:00
Anthony Lapenna f9c1941384 chore(api): update comment 2017-03-30 11:17:54 +02:00
Anthony Lapenna 9520380388 style(services): update empty service list text alignment (#744) 2017-03-29 18:54:27 +02:00
Anthony Lapenna a88d02b0b4 style(templates): update ownership buttons style 2017-03-29 18:47:43 +02:00
Adrian Dimitrov 0a8501fcbb fix(containers): fix an issue with hidden labels (#740) 2017-03-29 17:47:56 +02:00
Anthony Lapenna c9d50641c8 Merge tag '1.12.2' into develop
Release 1.12.2
2017-03-28 15:18:40 +02:00
Anthony Lapenna 9e06cfbdf0 Merge branch 'release/1.12.2' 2017-03-28 15:18:33 +02:00
Anthony Lapenna 135a92feb4 chore(version): bump version number 2017-03-28 15:18:29 +02:00
Anthony Lapenna cd4b5e0c80 docs(README): update supported versions 2017-03-28 15:17:49 +02:00
Anthony Lapenna 3cd0506810 feat(build): update build script 2017-03-28 15:16:42 +02:00
Thomas Krzero ffa2cf62f5 feat(services) - add exposed ports (#690) 2017-03-28 15:12:54 +02:00
Anthony Lapenna 0e439d7ae6 fix(Dockerfiles): use a volume to store data (#731) 2017-03-28 15:07:42 +02:00
Anthony Lapenna a99c6c4cbe fix(backend): use a thread-safe implementation of map for proxies (#728) 2017-03-28 14:28:17 +02:00
Anthony Lapenna 9e818c2882 fix(authentication): remove any user credentials if not allowed on any endpoint (#719) 2017-03-27 15:24:35 +02:00
Anthony Lapenna c243a02e7a feat(UX): UX/responsiveness enhancements 2017-03-27 14:44:39 +02:00
Anthony Lapenna 967286f45d docs(contributing): update contribution guidelines 2017-03-24 12:22:58 +01:00
dantheman0207 8e794be13f feat(containers): truncate long names & ids in the containers view (#699) 2017-03-22 08:13:59 +01:00
Glowbal a8f70d7f59 feat(service-details): add ability to edit service details (#453) 2017-03-20 21:28:09 +01:00
Anthony Lapenna ab91ffe12c style(containers): use the same action sequence for container-details and containers (#707) 2017-03-20 17:39:53 +01:00
Anthony Lapenna 24b51a7e87 refactor(image): refactor the code used in image and image details controller (#705) 2017-03-20 12:01:35 +01:00
Gábor Kovács c2e63070e6 feat(image-details): add the ability to pull/update a tag (#421) 2017-03-20 11:45:04 +01:00
AHumanPerson b6627098c2 docs(README): update demo username (#703) 2017-03-19 21:24:09 +01:00
Anthony Lapenna 097955e587 fix(templates): fix an issue where container links would fail (#701) 2017-03-19 19:07:22 +01:00
Anthony Lapenna 497a8392f6 fix(sidebar): fix a display issue on low resolution (#697) 2017-03-18 13:08:39 +01:00
Anthony Lapenna dcce211676 fix(api): allow empty array when removing accesses to an endpoint (#692) 2017-03-17 11:52:17 +01:00
Anthony Lapenna 631b29eddc fix(jshint): fix lint issues 2017-03-16 11:32:07 +01:00
Anthony Lapenna 9f12cbd43d fix(services): fix an issue with the sorting link for the ownership column (#682) 2017-03-16 11:24:47 +01:00
Anthony Lapenna b24825d453 feat(backend): check for the full database path to verify its existence (#681) 2017-03-16 11:23:01 +01:00
Anthony Lapenna 3861e964f4 fix(dockerfile): fix an issue with the data directory in Windows images 2017-03-14 18:28:21 +01:00
Anthony Lapenna ca4428cff2 feat(build): update build script 2017-03-13 10:23:49 +01:00
Anthony Lapenna 6b09c4f9b7 Merge tag '1.12.1' into develop
Release 1.12.1
2017-03-13 10:12:55 +01:00
Anthony Lapenna 5b2d5e17ab Merge branch 'release/1.12.1' 2017-03-13 10:12:49 +01:00
Anthony Lapenna be2acdbdfb chore(version): bump version number 2017-03-13 10:12:42 +01:00
Anthony Lapenna 723bf3874f fix(templates): fix an issue where the image would not be pulled correctly (#664) 2017-03-13 10:09:34 +01:00
Anthony Lapenna ebc378230f Merge tag '1.12.0' into develop
Release 1.12.0
2017-03-12 22:33:40 +01:00
Anthony Lapenna 7bef9c0708 Merge branch 'release/1.12.0' 2017-03-12 22:33:34 +01:00
Anthony Lapenna 1294ebaa8c chore(version): bump version number 2017-03-12 22:33:26 +01:00
Anthony Lapenna f40baa1287 feat(build): update build script 2017-03-12 22:30:50 +01:00
Richard Goater 35e2cecee1 feat(services): display clearer information about services 2017-03-12 18:24:41 +01:00
Anthony Lapenna 22c02a8fe9 fix(swarm): fix an issue when trying to access node view (#650) 2017-03-12 18:01:52 +01:00
Michael Friis 08868eb3e0 refactor(endpoint-init): update information warning for the local endpoint management 2017-03-12 17:43:33 +01:00
Damian 8a827950d8 Ability to select all endpoints via a checkbox (#607) 2017-03-12 17:39:27 +01:00
Anthony Lapenna d724f75016 fix(app): use lodash startsWith method instead of ECMAScript 2015 one (#648) 2017-03-12 17:36:24 +01:00
Anthony Lapenna 80d50378c5 feat(uac): add multi user management and UAC (#647) 2017-03-12 17:24:15 +01:00
WTFKr0 f28f223624 #643 feat(templates): add privileged flag to templates (#644) 2017-03-10 15:43:57 +01:00
Anthony Lapenna 082cf5772b merge remote branch 'develop' into develop 2017-03-03 13:07:16 +01:00
Anthony Lapenna 44ceae40b5 merge branch 'release-1.11.4' into develop 2017-03-03 12:54:22 +01:00
Anthony Lapenna b72cce810e Merge branch 'release/1.11.4' 2017-03-03 12:48:12 +01:00
Anthony Lapenna ccaabf3b6b chore(version): bump version number 2017-03-03 12:36:24 +01:00
Anthony Lapenna 2232adbd8b merge branch 'feat484-external-endpoints' into release-1.11.4 2017-03-03 12:35:54 +01:00
WTFKr0 cff999d7bb refactor(global): change file format (dos2unix) (#620) 2017-02-25 12:21:55 +01:00
Anthony Lapenna ec0cc84c7c refactor(lint): fix lint issue 2017-02-16 11:23:43 +13:00
Romain 64ef74321a feat(image): add the ability to force remove an image (#497) (#562) 2017-02-16 11:14:56 +13:00
Romain 6f53d1a35a feat (container): remember selection when refreshing a list view (#151) (#567) 2017-02-16 11:08:18 +13:00
Renato Silva f1c458b147 feat(container-creation): add the ability to add entries in the container host file 2017-02-16 10:48:40 +13:00
Anthony Lapenna 38244312c5 fix(stats): fix a small issue within statsController 2017-02-14 17:10:08 +13:00
Anthony Lapenna 52ab0bd50d feat(UX): automatically change the state to dashboard when switching endpoint (#602) 2017-02-14 16:22:24 +13:00
Anthony Lapenna 73082f1674 feat(cli): add a --no-analytics flag to disable google analytics (#601) 2017-02-14 12:37:37 +13:00
Anthony Lapenna 66c574f74d feat(project): add google analytics in app (#599) 2017-02-14 11:39:26 +13:00
Anthony Lapenna 85a07237b1 feat(swarm): display the IP address of each node when API Version >= … (#595) 2017-02-13 22:39:02 +13:00
Anthony Lapenna 781dad3e17 feat(templates): add the ability to update the volume configuration (#590) 2017-02-13 18:16:14 +13:00
Romain c5552d1b8e feat (container): add publish all ports option (#558) (#566) 2017-02-12 12:23:13 +13:00
Anthony Lapenna e0b94e4ff7 feat(templates): add support for the network field (#583) 2017-02-11 09:32:34 +13:00
Anthony Lapenna 3089268d88 fix(container-creation): split the container command to a token array (#586) 2017-02-10 18:21:07 +13:00
Anthony Lapenna d9624053d2 feat(templates): add support for the command field (#585) 2017-02-10 18:11:00 +13:00
Anthony Lapenna 9ebe2d96dd chore(jshint): update jshint library and configuration (#581) 2017-02-10 14:34:56 +13:00
Anthony Lapenna 2f3475b96a refactor(templates): refactor controller code and create required services (#580) 2017-02-10 14:11:36 +13:00
Samuel Tschiedel 06a484880b fix(index): fix a typo on the login page (#579) 2017-02-10 09:32:34 +13:00
Anthony Lapenna a78758123b style(cli): update error message 2017-02-07 16:27:40 +13:00
Anthony Lapenna f129bf3e97 refactor(api): refactor 2017-02-07 16:26:12 +13:00
Anthony Lapenna dc78ec5135 feat(endpoints): add the ability to define endpoints from an external source 2017-02-06 18:29:34 +13:00
Anthony Lapenna 10f7744a62 feat(authentication): add a --no-auth flag to disable authentication (#553) 2017-02-01 22:13:48 +13:00
Anthony Lapenna 0f81ad5654 feat(global): add a --no-auth flag to disable authentication 2017-02-01 22:10:07 +13:00
Anthony Lapenna 779fcf8e7f refactor(readme): remove useless version badge 2017-02-01 15:42:15 +13:00
Anthony Lapenna 7c2b186a61 refactor(assets): remove useless .jshintrc file 2017-02-01 15:40:49 +13:00
Anthony Lapenna fe0bf77bbb refactor(global): service separation #552 2017-02-01 12:26:29 +13:00
Anthony Lapenna 0abe8883d1 chore(dockerfiles): update data directory for windows Dockerfiles 2017-02-01 11:35:25 +13:00
Anthony Lapenna 84f2c2d735 Merge tag '1.11.3' into develop
Release 1.11.3
2017-02-01 11:02:15 +13:00
Anthony Lapenna 5d63c90203 Merge branch 'release/1.11.3' 2017-02-01 11:02:10 +13:00
Anthony Lapenna a97e7bbaae chore(version): bump version number 2017-02-01 11:02:05 +13:00
Anthony Lapenna f3cfb0a940 fix(cli): revert data/certs directories defaults to c:\data and c:\certs (#551) 2017-02-01 08:56:07 +13:00
Anthony Lapenna b1ca43934f Merge tag '1.11.2' into develop
Release 1.11.2
2017-01-26 17:44:00 +13:00
Anthony Lapenna 7afeb8a80d Merge branch 'release/1.11.2' 2017-01-26 17:43:53 +13:00
Anthony Lapenna f8ced03792 chore(version): bump version number 2017-01-26 17:43:47 +13:00
Jisu Park 1fdf56372b feat(containers): support container already pause message (#480) 2017-01-26 12:11:38 +13:00
Anthony Lapenna 835b273700 feat(api): force no-cache on HTML files 2017-01-26 11:45:03 +13:00
Anthony Lapenna fcc9203416 feat(node): add pagination to associated tasks 2017-01-26 10:35:05 +13:00
Anthony Lapenna e25c5a014c feat(swarm): set default sorting for Swarm nodes by role 2017-01-26 10:34:10 +13:00
Glowbal fa9ba303aa #414 feat(node-details): add ability to view and edit Swarm mode nodes (#417) 2017-01-26 10:12:04 +13:00
morph027 e6dee37af0 style(swarm): update node status filter for swarm mode nodes 2017-01-26 09:54:08 +13:00
Anthony Lapenna d03e992b4f feat(api): replace all calls to http.Error with custom Error writer 2017-01-24 16:35:48 +13:00
Anthony Lapenna 1a868be6ea fix(swarm): fix sorting issue with node table (#538) 2017-01-24 14:45:38 +13:00
Anthony Lapenna e2fc8af87a feat(ux): add the ability to change the number of paginated items on all entity tables (#537) 2017-01-24 14:28:40 +13:00
Anthony Lapenna 70933d1056 style(sidebar): add active class on Docker section (#534) 2017-01-24 09:39:13 +13:00
Anthony Lapenna 7e0b0a05de feat(authentication): clean the state and the browser local storage on logout 2017-01-23 17:04:34 +13:00
Anthony Lapenna 980f65a08a feat(api): initializes the endpoint with an empty slice instead of a pointer 2017-01-23 16:29:49 +13:00
Anthony Lapenna 8cf6d34362 style(container-creation): remove useless labels section (#532) 2017-01-23 16:10:12 +13:00
Anthony Lapenna 70f139514f fix(network-details): add a fallback for listing containers when APIV… (#531) 2017-01-23 16:06:51 +13:00
Anthony Lapenna fa4ec04c47 feat(state): introduce endpoint state (#529) 2017-01-23 12:14:34 +13:00
Anthony Lapenna 7ebe4af77d fix(images): fix an issue when deleting images with multiple tags (#526) 2017-01-22 14:42:12 +13:00
lpfeup 579241db92 #503 fix(container-stats): fix container stats timer not being properly canceled. (#504) 2017-01-21 18:04:28 +13:00
lpfeup 7d78871eee #446 fix(container-stats): fix issue in stats view with empty network data (#502) 2017-01-21 18:01:32 +13:00
Anthony Lapenna 3a6e9d2fbe fix(api): fix an issue introduced by the latest version of package gorilla/mux (#520) 2017-01-21 11:17:51 +13:00
Anthony Lapenna e4d98082dc fix(api): disable data directory creation (#495)
* fix(api): disable data directory creation

* feat(dockerhub): update volume instruction value for Windows Dockerfiles
2017-01-14 14:22:39 +13:00
Kilhog cd26051144 #476 fix(UX): Rename 'local' endpoint doesn't overwrite "unix://" (#477)
* #476 fix(UX): Rename 'local' endpoint doesn't overwrite "unix://"

* #477 fix(PR): Rename 'TYPE' in 'type'
2017-01-12 18:44:53 +13:00
Anthony Lapenna 27e584fc14 fix(api): check if admin user already exists when calling the /users/admin/init endpoint (#494) 2017-01-12 18:17:28 +13:00
Anthony Lapenna 2bdc9322de style(containers): update header text for published ports (#483) 2017-01-09 21:50:19 +13:00
Anthony Lapenna 35d5d75966 fix(api): update default value for data directory and TLS certs on Windows (#482) 2017-01-09 21:24:17 +13:00
Anthony Lapenna 2610e3d02a Merge tag '1.11.1' into develop
Release 1.11.1
2017-01-05 10:42:50 +13:00
Anthony Lapenna d579f62fa7 Merge branch 'release/1.11.1' 2017-01-05 10:42:46 +13:00
Anthony Lapenna d1b9820a29 chore(version): bump version number 2017-01-05 10:42:38 +13:00
Wouter Oet 13943c3d8b #372 feat(UX): Implement select all functionality (#437) 2017-01-05 09:15:41 +13:00
Anthony Lapenna d8b800ddbc feat(api): create platform dependant default values for CLI flags (#458) 2017-01-04 19:50:25 +13:00
Matthew Strickland 59f1a2f673 feat(templates): display container restart policy in container dashboard (#434) (#435) 2017-01-04 19:49:04 +13:00
Anthony Lapenna 9ee652c818 fix(api): creates the data directory if not exist (#452) 2017-01-03 08:32:53 +13:00
Anthony Lapenna 816c1ea448 chore(build-system): fix release tasks 2017-01-03 07:47:12 +13:00
Albert Domenech 0bacaef71a feat(images): initial aarch64/arm64 support (#447) 2017-01-03 07:42:21 +13:00
Anthony Lapenna 2ef821f118 style(service-details): update style for update failure action field (#443) 2016-12-31 13:32:20 +13:00
Anthony Lapenna 487cb4e755 Merge branch 'develop' of github.com:portainer/portainer into develop 2016-12-31 13:27:51 +13:00
Anthony Lapenna 06d3debf38 chore(build-system): fix grunt lint task 2016-12-31 13:27:35 +13:00
Anthony Lapenna 907f83aaff fix(global): remove automatic lowercase processing on image names (#442) 2016-12-31 13:25:42 +13:00
Gábor Kovács 4b747a78cd style(sidebar): Highlight active page in sidebar (#420)
* Issue #331

* New line
2016-12-31 13:12:51 +13:00
Anthony Lapenna d6f3dd8cda style(endpoint-initialization): update requirement message for local endpoint init (#424) 2016-12-31 13:00:30 +13:00
Anthony Lapenna 51632e367c fix(service-details): allow to specify the 0 value for replicas (#441) 2016-12-31 12:59:20 +13:00
Anthony Lapenna 6e98237419 feat(api): introduce cache busting mechanism (#439) 2016-12-31 12:20:38 +13:00
Anthony Lapenna ecc8857a32 fix(global): strip leading '/' in front of endpoints (#438) 2016-12-31 10:30:22 +13:00
Anthony Lapenna 7d05e81c37 chore(github): update ISSUE_TEMPLATE.md 2016-12-27 08:54:39 +13:00
Anthony Lapenna 6ce3fe7a9e Merge tag '1.11.0' into develop
Release 1.11.0
2016-12-26 13:30:20 +13:00
Anthony Lapenna 9443284f52 Merge branch 'release/1.11.0' 2016-12-26 13:30:15 +13:00
Anthony Lapenna 4d6dadd17c chore(version): bump version number 2016-12-26 13:30:06 +13:00
Anthony Lapenna d54d30a7be feat(global): multi endpoint management (#407) 2016-12-26 09:34:02 +13:00
Glowbal a08ea134fc feat(container-creation): add ability to specify labels in the container creation view (#412) 2016-12-26 09:33:14 +13:00
Glowbal c9ba16ef10 feat(network-creation): add labels on network create (#408) 2016-12-26 09:32:17 +13:00
Glowbal 986171ecfe feat(service): Add editable service update configuration (#346)
* #304 Add editable service update configuration

* fix unable to use 0 for update-delay

* apply margin top to center help text
2016-12-26 09:31:22 +13:00
Glowbal 712b4528c0 feat(network-details): add list of containers in network (#302)
- shows all containers currently connected to the network
- abillity to disconect a container from the network
- fix error when a container is not connected to any networks
2016-12-26 09:28:54 +13:00
Anthony Lapenna 03456ddcf8 feat(containers): add the ability to filter by state (#410) 2016-12-25 22:43:53 +13:00
Anthony Lapenna ce32ed5b98 fix(service-creation): fix the command specification and add the ability to specify an entrypoint (#409) 2016-12-25 22:14:26 +13:00
Paul Kling edeed41797 #186 feat(container): bind the enter key when renaming container (#385) 2016-12-25 08:53:57 +13:00
David Eisner 419727e1eb feat(api): Connect to docker behind a name based virtual host proxy (#379)
This involves copying and modifying go's httputil.NewSingleHostReverseProxy, which is documented to (perhaps surprisingly) leave the Host header untouched. Instead, we set the Host header to the target host for the connection for the benefit of name based virtual host proxies that make use of this. The value it would otherwise have in this app, typically 'localhost:8000', is strange and unlikely to be any use.

See golang/go#7618 and golang/go#10342
2016-12-24 17:49:29 +13:00
Anthony Lapenna 9165b5b215 fix(dashboard): add missing dependency to Messages service (#402) 2016-12-21 11:24:34 +13:00
Anthony Lapenna 0a38bba874 refactor(api): API overhaul (#392) 2016-12-18 18:21:29 +13:00
Anthony Lapenna d9f6124609 refactor(global): remove useless code related to CSRF (#387) 2016-12-16 14:00:57 +13:00
Anthony Lapenna 5b16deb73e fix(templates): fix an issue with template creation introduced with #384 2016-12-16 13:39:24 +13:00
Anthony Lapenna 4e77c72fa2 feat(global): add authentication support with single admin account 2016-12-15 16:33:47 +13:00
Anthony Lapenna 1e5207517d fix(container-creation): do not stop container creation if unable to pull image 2016-12-15 14:30:35 +13:00
Anthony Lapenna 2a28921984 docs(README): update readthedocs badge to point at stable version 2016-12-14 09:46:01 +13:00
Anthony Lapenna b5bf7cdead feat(templates): add support for the template registry field 2016-12-14 09:33:24 +13:00
Paul Kling 8869a2c79c feat(templates): automatically scroll up to the app template form after selecting a template 2016-12-14 09:25:23 +13:00
Anthony Lapenna 99d49a1f87 chore(project): update contribution guidelines 2016-12-02 19:19:24 +13:00
Anthony Lapenna a53c0f08a3 Merge tag '1.10.2' into develop
Release 1.10.2
2016-11-26 00:51:01 +13:00
Anthony Lapenna 0e40bb13fc Merge branch 'release/1.10.2' 2016-11-26 00:50:55 +13:00
Anthony Lapenna db46087799 chore(version): bump version number 2016-11-26 00:50:50 +13:00
Anthony Lapenna 367a275672 fix(service-details): fix an issue with the '=' separator in env variable values (#370) 2016-11-25 20:48:12 +09:00
Glowbal b3a641e15a feat(service-creation): add support for container labels (#365) 2016-11-25 15:21:06 +09:00
Glowbal 868b400af3 fix(volumes): fix loading text displayed when no volumes present
Volumes is undefined when no volumes are present. The loading text will remain until volumes is defined.
2016-11-25 15:16:28 +09:00
Rob McFadzean 8fcae6810e fix(templates): fixes an issue regarding template selection when paged 2016-11-22 09:21:36 +09:00
Anthony Lapenna 913c580340 feat(UX): add pagination for all object lists (#352) 2016-11-17 21:50:46 +09:00
Anthony Lapenna 13a8b11d3d Merge tag '1.10.1' into develop
Release 1.10.1
2016-11-16 23:17:51 +13:00
Anthony Lapenna 5af99c6fe3 Merge branch 'release/1.10.1' 2016-11-16 23:17:46 +13:00
Anthony Lapenna 2d35ac8f82 chore(version): bump version number 2016-11-16 23:17:39 +13:00
Anthony Lapenna 3db487f386 fix(service-details): fix a sorting issue when ordering by last update (#350) 2016-11-16 19:16:50 +09:00
Rob Brazier 643769d4a6 feat(container-creation): add the ability to use container as a network 2016-11-16 10:52:05 +09:00
Anthony Lapenna 2c49d3b5d9 docs(README): add a donate badge 2016-11-12 12:51:06 +13:00
Anthony Lapenna 714f515f0b chore(build-system): fix build script 2016-11-11 15:50:59 +13:00
Anthony Lapenna 672479bf4f Merge tag '1.10.0' into develop
Release 1.10.0
2016-11-11 15:29:25 +13:00
Anthony Lapenna 8c3f7b3ec2 Merge branch 'release/1.10.0' 2016-11-11 15:29:16 +13:00
Anthony Lapenna 3aa0f4d263 chore(version): bump version number 2016-11-11 15:29:02 +13:00
Anthony Lapenna 2f35f04207 fix(service-details): fix an issue when trying to update a global service (#343) 2016-11-11 11:26:19 +09:00
Anthony Lapenna 3b3b23142c chore(build-system): add a release for macos task (#342) 2016-11-11 11:17:38 +09:00
Anthony Lapenna 9bd88fd10d style(service-details): fix wrong display for some fields (#340) 2016-11-10 13:01:03 +09:00
Glowbal 3092d0b7eb chore(grunt): adda run local swarm grunt task 2016-11-10 11:42:07 +09:00
Glowbal d924d340d7 feat(service-details): add the ability to edit the labels associated to a service 2016-11-10 11:38:49 +09:00
Glowbal c1ffd02491 fix(container-details): fix an issue with the leave network action 2016-11-10 11:25:31 +09:00
Glowbal 8e9dd8c2df #304 feat(service-details): add the ability to update a service env vars and image 2016-11-09 13:23:56 +13:00
Glowbal 1bfd6bbe95 #280 feat(service-creation): add labels to service creation (#306) 2016-11-07 17:57:33 +13:00
Glowbal 715638e368 feat(container-details): show list of joined networks (#303)
- Add overview of joined networks in container view
- Add option ot leave a joined network
2016-11-07 17:36:00 +13:00
jjlorenzo 08c868bc1c Restore the ability to customize the logo image. (#327) 2016-11-07 17:14:58 +13:00
Anthony Lapenna 9f46b12625 fix(containers): fix an issue with container IP in overlay network (#324) 2016-11-07 17:13:57 +13:00
Anthony Lapenna 6fc25691bd feat(backend): add a simple log message to indicate portainer startup (#320) 2016-11-04 16:52:02 +13:00
Anthony Lapenna c1713e0d01 docs(readme): update Portainer description with Windows support 2016-11-04 10:48:36 +13:00
Glowbal 8187f17d33 fix(service-details): show labels in service view 2016-11-03 17:14:07 +13:00
Anthony Lapenna f0e194f63b Disable CSRF protection (#313) 2016-11-03 15:56:10 +13:00
Glowbal eabf1f10e4 feat(navigation): add clickable url in breadcrumbs 2016-11-02 18:14:52 +13:00
Stefan Scherer c913d858ee Add Linux ARM support (#299)
Signed-off-by: Stefan Scherer <scherer_stefan@icloud.com>
2016-11-01 09:07:32 +13:00
Anthony Lapenna 17f35ef705 fix(container-creation): fix default network on Windows platform (#298) 2016-10-29 17:49:21 +13:00
Anthony Lapenna 0bdbb4a75d feat(container-stats): make process list sortable 2016-10-29 17:39:15 +13:00
Stefan Scherer f9327b3337 Use microsoft base images (#296)
Signed-off-by: Stefan Scherer <scherer_stefan@icloud.com>
2016-10-29 16:38:32 +13:00
Anthony Lapenna bf6c9c8b3b refactor(style): refactor multiple similar css classes 2016-10-27 21:33:39 +13:00
Anthony Lapenna 45015a573b feat(container-creation): add the unless stopped container restart policy (#294) 2016-10-27 20:05:37 +13:00
Anthony Lapenna d4f0145161 feat(templates): allow to edit template port mapping (#293)
* feat(templates): allow to edit template port mapping

* refactor(templates): remove advanced template configuration feature
2016-10-27 19:55:44 +13:00
Anthony Lapenna fa53339fea feat(docker): new docker view (#292) 2016-10-27 17:13:53 +13:00
Anthony Lapenna e5396091a7 feat(console): automatically determine command presets based on container image OS (#284) 2016-10-26 16:29:29 +13:00
Anthony Lapenna 1ae18e1577 chore(grunt): fix an issue with the Docker image building process in grunt 2016-10-26 12:09:09 +13:00
Anthony Lapenna b953850a1f chore(grunt): fix issue with grunt run-* tasks 2016-10-26 12:05:29 +13:00
Anthony Lapenna d0954abe29 chore(docker): update build system with Docker for Windows support (#283) 2016-10-26 09:04:26 +13:00
Anthony Lapenna c3cf5b5f9d feat(templates): advanced template creation (#277) 2016-10-20 16:43:09 +13:00
Anthony Lapenna 6589730acc refactor(css): remove useless css classes (#274) 2016-10-19 17:57:38 +13:00
Anthony Lapenna 442dcff0f1 chore(license): relicense to zlib license (#271) 2016-10-16 14:39:38 +13:00
Anthony Lapenna 8bac1955a8 Merge tag '1.9.3' into develop
Release 1.9.3
2016-10-09 10:50:52 +13:00
Anthony Lapenna 09a5534499 Merge branch 'release/1.9.3' 2016-10-09 10:50:46 +13:00
Anthony Lapenna 65c126f6a1 chore(version): bump version number 2016-10-09 10:50:32 +13:00
Anthony Lapenna 6adec680a4 style(templates): new effect on hover (#268)
* style(templates): new effect on hover

* feat(templates): display a loading message
2016-10-09 10:49:24 +13:00
Anthony Lapenna b81d4fa7f2 feat(global): display a loading text in list views (#267) 2016-10-08 14:59:58 +13:00
Anthony Lapenna d8f2e3da86 docs(readme): update README 2016-10-08 10:10:12 +13:00
Anthony Lapenna b0c0512515 Merge tag '1.9.2' into develop
Release 1.9.2
2016-10-07 18:22:58 +13:00
Anthony Lapenna bb9e044e89 Merge branch 'release/1.9.2' 2016-10-07 18:22:53 +13:00
Anthony Lapenna 520532cb9a chore(version): bump version number 2016-10-07 18:22:44 +13:00
Anthony Lapenna 44e09ecadf feat(container-creation): let Docker assign a port when host port is not specified (#264) 2016-10-07 18:08:07 +13:00
Anthony Lapenna 35ced4901a feat(global): display a message when no item available in a list view (#263) 2016-10-07 17:55:09 +13:00
Anthony Lapenna 134416c9a3 fix(container-console): use xterm.js v2 (#262) 2016-10-07 17:19:25 +13:00
Anthony Lapenna 8f7f4acc0d chore(build): add a build script to archive binary 2016-10-05 11:33:32 +13:00
Anthony Lapenna fde0d3ea9f chore(github): add github issue template 2016-10-05 10:56:49 +13:00
Anthony Lapenna 477799af7e chore(project): update contribution guidelines 2016-10-05 10:44:29 +13:00
Anthony Lapenna 72570153a5 docs(badges): add the dockerhub version badge 2016-10-03 12:35:40 +13:00
Anthony Lapenna 9f335b692f Merge tag '1.9.1' into develop
Release 1.9.1
2016-10-02 16:26:25 +13:00
Anthony Lapenna e88b22bd45 Merge branch 'release/1.9.1' 2016-10-02 16:26:19 +13:00
Anthony Lapenna 833053a2e1 chore(version): bump version number 2016-10-02 16:26:11 +13:00
Anthony Lapenna 64c52348f3 fix(lint): fix linting issue 2016-10-02 16:25:37 +13:00
Anthony Lapenna c3b79e6cc2 chore(xterm): update xterm.js version to 1.1.3 (#254) 2016-10-02 16:19:11 +13:00
Anthony Lapenna 422a982d60 feat(templates): template configuration is now placed on top of template list (#253) 2016-10-02 16:11:20 +13:00
Anthony Lapenna 6e9fe26fde fix(templates): fix bad container display when swarm-mode enabled (#251) 2016-10-02 15:05:40 +13:00
Anthony Lapenna 6bfa3096dc feat(index): hide Events and Docker view when swarm-mode is enabled (#250) 2016-10-02 14:57:01 +13:00
Anthony Lapenna 7cd2da4c6e fix(console): fix issue with undefined socket (#248) 2016-10-01 21:44:23 +13:00
Anthony Lapenna 739a5ec299 fix(general): fix the size display using the filesize library (#246)
* fix(general): fix the size display using the filesize library

* refactor(humansize): use default value for filter
2016-10-01 21:38:20 +13:00
Anthony Lapenna 59e65222eb feat(swarm): support Swarm replica management (#245) 2016-10-01 17:50:46 +13:00
Anthony Lapenna 01d5d11c01 feat(events): support new events (#244) 2016-10-01 17:08:32 +13:00
Anthony Lapenna 29a59cab44 feat(containers): change exposed port format (#243) 2016-10-01 16:55:11 +13:00
Anthony Lapenna be184c11a6 style(favicon): update favicon (#242) 2016-10-01 16:51:45 +13:00
Anthony Lapenna d6ab97ad25 fix(containers): fix the ability to sort containers by status (#241) 2016-10-01 16:45:06 +13:00
Anthony Lapenna 6a0f76890e docs(README): update README 2016-09-30 18:51:55 +13:00
Anthony Lapenna 1946868248 docs(README): update links to readthedocs 2016-09-30 18:51:09 +13:00
Anthony Lapenna 84b02c711a docs(badge): add readthedocs badge 2016-09-30 18:47:36 +13:00
Anthony Lapenna 679a681749 Merge tag '1.9.0' into develop
Release 1.9.0
2016-09-24 22:33:37 +12:00
Anthony Lapenna c35d1b14ec Merge branch 'release/1.9.0' 2016-09-24 22:33:30 +12:00
Anthony Lapenna 87df297a56 chore(version): bump version number 2016-09-24 22:33:23 +12:00
Anthony Lapenna b8e420e0e8 docs(project): new documentation (#229) 2016-09-24 22:31:37 +12:00
Anthony Lapenna f8c8668863 docs(contribution): add contributions rules 2016-09-24 17:30:08 +12:00
Anthony Lapenna ced0746a81 Merge pull request #228 from portainer/chore218-portainer-org
chore(global): replace CloudInovasi with Portainer.io
2016-09-23 18:29:09 +12:00
Anthony Lapenna 39909d774f chore(global): replace CloudInovasi with Portainer.io 2016-09-23 18:28:20 +12:00
Anthony Lapenna 12e6e0557d Merge pull request #227 from cloud-inovasi/feat216-swarm-latest-support
feat(global): change the strategy used to determine if swarm mode is …
2016-09-23 18:02:48 +12:00
Anthony Lapenna e27282de3c feat(global): change the strategy used to determine if swarm mode is used 2016-09-23 18:02:03 +12:00
Anthony Lapenna fe63f9939a Merge pull request #226 from cloud-inovasi/style219-incoherent-container-icon
style(ui): use fa-server icon instead of fa-tasks for container entity
2016-09-23 17:20:52 +12:00
Anthony Lapenna b623a5d452 style(ui): use fa-server icon instead of fa-tasks for container entity 2016-09-23 17:19:57 +12:00
Anthony Lapenna d8113df979 Merge pull request #225 from cloud-inovasi/style220-github-icon
style(index): add a github icon next to the github link
2016-09-23 17:08:17 +12:00
Anthony Lapenna b3ba36c02a style(index): add a github icon next to the github link 2016-09-23 17:07:48 +12:00
Anthony Lapenna 37863e3f74 feat(global): swarm mode support (#213)
feat(global): swarm mode support
2016-09-23 16:54:58 +12:00
Anthony Lapenna da6f39b137 fix(lint): fix jshint issue 2016-09-14 17:50:54 +12:00
Anthony Lapenna 4fe63d7102 Merge pull request #211 from cloud-inovasi/feat200-network-view
feat(network): new network view
2016-09-14 17:49:24 +12:00
Anthony Lapenna 7c8881f37d feat(network): new network view 2016-09-14 17:48:20 +12:00
Anthony Lapenna c20069fce0 Merge pull request #210 from cloud-inovasi/feat195-quick-network-creation-form
feat(networks): add a quick network creation form
2016-09-14 16:31:02 +12:00
Anthony Lapenna 2eb1c9e857 feat(networks): add a quick network creation form 2016-09-14 16:28:38 +12:00
Anthony Lapenna 48e1fe769e Merge pull request #209 from cloud-inovasi/style199-action-icons
style(actions): add icons for every actions
2016-09-14 15:32:31 +12:00
Anthony Lapenna 2b8bc82d4e style(actions): add icons for every actions 2016-09-14 15:30:52 +12:00
Anthony Lapenna 8f33151647 Merge tag '1.8.1' into develop
Release 1.8.1
2016-09-07 18:31:48 +12:00
Anthony Lapenna 8e743a8d32 Merge branch 'release/1.8.1' 2016-09-07 18:31:44 +12:00
Anthony Lapenna 9f22e01d3b chore(version): bump version number 2016-09-07 18:31:32 +12:00
Anthony Lapenna 502c8718c5 Merge pull request #206 from cloud-inovasi/feat196-disable-create-button
feat(network-creation): disable create button while network name is e…
2016-09-07 18:28:57 +12:00
Anthony Lapenna 220faa52e7 feat(network-creation): disable create button while network name is empty 2016-09-07 18:28:14 +12:00
Anthony Lapenna 857c93bff9 Merge pull request #205 from cloud-inovasi/fix185-volume-deletion-error
fix(volumes): display an error message when trying to delete a bound …
2016-09-07 18:23:11 +12:00
Anthony Lapenna ca5cf33c8f fix(volumes): display an error message when trying to delete a bound volume 2016-09-07 18:21:46 +12:00
Anthony Lapenna 1cd620a45e Merge pull request #204 from cloud-inovasi/fix193-image-error-message
fix(image): support array in Messages.error
2016-09-07 18:05:15 +12:00
Anthony Lapenna 4eb9a9a0af fix(image): support array in Messages.error 2016-09-07 18:03:55 +12:00
Anthony Lapenna c82abae8e5 Merge pull request #203 from cloud-inovasi/bug198-hidden-containers
fix(containers): make hidden containers labels available in the $scope
2016-09-07 16:42:45 +12:00
Anthony Lapenna f56256f897 fix(containers): make hidden containers labels available in the $scope 2016-09-07 16:38:54 +12:00
Anthony Lapenna e31749e64d Merge pull request #202 from cloud-inovasi/style201-latest-logo
style(logo): use latest logo
2016-09-07 16:25:57 +12:00
Anthony Lapenna 89d666f365 style(logo): use latest logo 2016-09-07 16:25:21 +12:00
Anthony Lapenna b502852966 chore(badge): add the microbadger badge 2016-09-05 09:19:34 +12:00
Anthony Lapenna e101397a2c chore(gitter): add gitter badge 2016-09-04 15:33:59 +12:00
Anthony Lapenna ddcecc06d4 Merge tag '1.8.0' into develop
Release 1.8.0
2016-09-04 15:11:08 +12:00
Anthony Lapenna 4237f452df Merge branch 'release/1.8.0' 2016-09-04 15:11:03 +12:00
Anthony Lapenna 3f9276ee4c chore(version): bump version number 2016-09-04 15:10:47 +12:00
Anthony Lapenna 5a1f437cf9 fix(lint): fix linting issue 2016-09-04 15:06:33 +12:00
Anthony Lapenna bb9cebd759 Merge pull request #191 from cloud-inovasi/refactor153-rename-to-portainer
Refactor153 rename to portainer
2016-09-04 15:01:02 +12:00
Anthony Lapenna 62e313d13f style(docs): update dashboard picture 2016-09-04 14:53:01 +12:00
Anthony Lapenna 537ee24078 refactor(global): rename uifd to portainer 2016-09-04 14:50:37 +12:00
Anthony Lapenna 364756d9fa Merge pull request #190 from cloud-inovasi/docs95-docker112-support
docs(version): update Docker versions support section
2016-09-04 12:06:12 +12:00
Anthony Lapenna 6eb1cff8c5 docs(version): update Docker versions support section 2016-09-04 12:04:42 +12:00
Anthony Lapenna 44e02c0342 fix(network): add missing exception management 2016-09-02 17:59:32 +12:00
Anthony Lapenna b36767cdb7 Merge pull request #189 from cloud-inovasi/feat95-exception-mgmt
feat(ui): add missing exception management
2016-09-02 17:52:34 +12:00
Anthony Lapenna 67194109c6 feat(ui): add missing exception management 2016-09-02 17:51:41 +12:00
Anthony Lapenna 08032be2c4 Merge pull request #188 from cloud-inovasi/feat95-exception-mgmt
feat(ui): new exception management
2016-09-02 17:41:10 +12:00
Anthony Lapenna 74b97a0036 feat(ui): new exception management 2016-09-02 17:40:03 +12:00
Anthony Lapenna eac3239817 Merge pull request #187 from cloud-inovasi/feat95-remove-errormsgfilter
feat(ui): remove the errorMsg filter and replace it with proper error…
2016-09-02 15:26:26 +12:00
Anthony Lapenna 9698aa7ad5 feat(ui): remove the errorMsg filter and replace it with proper error management 2016-09-02 15:25:20 +12:00
Anthony Lapenna cbce2a70f5 Merge pull request #184 from cloud-inovasi/feat95-container-start-no-hostconfig
feat(container): do not pass HostConfig when starting a container
2016-09-02 13:52:32 +12:00
Anthony Lapenna a2d91ec2f9 feat(container): do not pass HostConfig when starting a container 2016-09-02 13:51:49 +12:00
Anthony Lapenna d93a69df95 Merge pull request #181 from cloud-inovasi/feat95-responsehandler-generic-handler
feat(container): add a deletion generic handler used for container/ne…
2016-09-01 15:08:12 +12:00
Anthony Lapenna fb982ca8f1 feat(container): add a deletion generic handler used for container/network deletion 2016-09-01 15:07:31 +12:00
Anthony Lapenna 4b979628b3 Merge pull request #180 from cloud-inovasi/feat95-responsehandler-image-delete
feat(image): define a new response handler for image deletion
2016-09-01 14:26:49 +12:00
Anthony Lapenna 789750cc86 feat(image): define a new response handler for image deletion 2016-09-01 14:24:47 +12:00
Anthony Lapenna 4125361fb5 Merge pull request #179 from cloud-inovasi/feat-responsehandler-delete-network
feat(network): define a response handler for image deletion
2016-09-01 12:21:44 +12:00
Anthony Lapenna 6b8b562e7c feat(network): define a response handler for image deletion 2016-09-01 12:20:19 +12:00
Anthony Lapenna 2e9a117255 Merge pull request #178 from cloud-inovasi/feat176-network-error-message
feat(network): display the correct error message when a network delet…
2016-09-01 11:33:15 +12:00
Anthony Lapenna 6d6a7e6923 feat(network): display the correct error message when a network deletion failure occurs 2016-09-01 11:31:25 +12:00
Anthony Lapenna 4edb4e014f refactor(ui): introduce helpers functions to centralize code (#174) 2016-08-31 18:06:10 +12:00
Anthony Lapenna f020e5a633 refactor(ui): introduce helpers functions to centralize code 2016-08-31 18:03:41 +12:00
Anthony Lapenna 5432424a40 fix(image): fix the deleteImageHandler so that messages are correctly displayed in the UI (#172) 2016-08-31 11:26:02 +12:00
Anthony Lapenna e81bfb6f37 fix(templates): hide hidden containers in templates (#165) 2016-08-24 20:26:51 +12:00
Anthony Lapenna 3c75c5fe25 refactor(templates): use the set field instead of default (#164) 2016-08-24 19:46:31 +12:00
Anthony Lapenna 7c5c693f17 feat(templates): support select for env fields with type 'container' (#163) 2016-08-24 18:32:54 +12:00
Anthony Lapenna 2d98e33e98 style(template): Update title and section name (#162) 2016-08-24 15:45:44 +12:00
Anthony Lapenna 4827d33ca1 feat(templates): support env variables with default value (#161) 2016-08-24 15:30:29 +12:00
Anthony Lapenna 71eb3feac9 feat(containers): update the containers view to add a column with exposed ports (#157) 2016-08-24 10:58:55 +12:00
Anthony Lapenna 5f290937d2 refactor(templates): rename field comment to description (#155) 2016-08-23 18:49:40 +12:00
Anthony Lapenna 1c8aa35479 feat(global): add templates support ('apps') (#154) 2016-08-23 18:09:14 +12:00
Anthony Lapenna faccf2a651 feat(container): container view overhaul (#150) 2016-08-19 18:41:45 +12:00
Anthony Lapenna 4d99c12215 fix(image): display a valid error message when deleting an image (#149)
fix(image): display a valid error message when deleting an image
2016-08-19 17:53:27 +12:00
Anthony Lapenna 7c2047cfbf feat(containers): rename the column header Host to Host IP (#145) 2016-08-18 16:55:19 +12:00
Anthony Lapenna 12d5cfe8e4 Merge tag '1.7.0' into develop
Release 1.7.0
2016-08-18 15:47:45 +12:00
994 changed files with 72594 additions and 6429 deletions
+61
View File
@@ -0,0 +1,61 @@
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:
enabled: true
govet:
enabled: true
csslint:
enabled: true
duplication:
enabled: true
config:
languages:
javascript:
mass_threshold: 80
eslint:
enabled: true
config:
config: .eslintrc.yml
fixme:
enabled: true
exclude_patterns:
- test/
+1
View File
@@ -1,2 +1,3 @@
*
!dist
!build
+287
View File
@@ -0,0 +1,287 @@
env:
browser: true
jquery: true
# globals:
# angular: true
# $: true
# _: true
# moment: true
# filesize: true
# splitargs: true
extends:
- 'eslint:recommended'
# http://eslint.org/docs/rules/
rules:
# Possible Errors
no-await-in-loop: off
no-cond-assign: error
no-console: off
no-constant-condition: error
no-control-regex: error
no-debugger: error
no-dupe-args: error
no-dupe-keys: error
no-duplicate-case: error
no-empty-character-class: error
no-empty: error
no-ex-assign: error
no-extra-boolean-cast: error
no-extra-parens: off
no-extra-semi: error
no-func-assign: error
no-inner-declarations:
- error
- functions
no-invalid-regexp: error
no-irregular-whitespace: error
no-negated-in-lhs: error
no-obj-calls: error
no-prototype-builtins: off
no-regex-spaces: error
no-sparse-arrays: error
no-template-curly-in-string: off
no-unexpected-multiline: error
no-unreachable: error
no-unsafe-finally: off
no-unsafe-negation: off
use-isnan: error
valid-jsdoc: off
valid-typeof: error
# Best Practices
accessor-pairs: error
array-callback-return: off
block-scoped-var: off
class-methods-use-this: off
complexity:
- error
- 6
consistent-return: off
curly: off
default-case: off
dot-location: off
dot-notation: off
eqeqeq: error
guard-for-in: error
no-alert: error
no-caller: error
no-case-declarations: error
no-div-regex: error
no-else-return: off
no-empty-function: off
no-empty-pattern: error
no-eq-null: error
no-eval: error
no-extend-native: error
no-extra-bind: error
no-extra-label: off
no-fallthrough: error
no-floating-decimal: off
no-global-assign: off
no-implicit-coercion: off
no-implied-eval: error
no-invalid-this: off
no-iterator: error
no-labels:
- error
- allowLoop: true
allowSwitch: true
no-lone-blocks: error
no-loop-func: error
no-magic-number: off
no-multi-spaces: off
no-multi-str: off
no-native-reassign: error
no-new-func: error
no-new-wrappers: error
no-new: error
no-octal-escape: error
no-octal: error
no-param-reassign: off
no-proto: error
no-redeclare: error
no-restricted-properties: off
no-return-assign: error
no-return-await: off
no-script-url: error
no-self-assign: off
no-self-compare: error
no-sequences: off
no-throw-literal: off
no-unmodified-loop-condition: off
no-unused-expressions: error
no-unused-labels: off
no-useless-call: error
no-useless-concat: error
no-useless-escape: off
no-useless-return: off
no-void: error
no-warning-comments: off
no-with: error
prefer-promise-reject-errors: off
radix: error
require-await: off
vars-on-top: off
wrap-iife: error
yoda: off
# Strict
strict: off
# Variables
init-declarations: off
no-catch-shadow: error
no-delete-var: error
no-label-var: error
no-restricted-globals: off
no-shadow-restricted-names: error
no-shadow: off
no-undef-init: error
no-undef: off
no-undefined: off
no-unused-vars:
- warn
-
vars: local
no-use-before-define: off
# Node.js and CommonJS
callback-return: error
global-require: error
handle-callback-err: error
no-mixed-requires: off
no-new-require: off
no-path-concat: error
no-process-env: off
no-process-exit: error
no-restricted-modules: off
no-sync: off
# Stylistic Issues
array-bracket-spacing: off
block-spacing: off
brace-style: off
camelcase: off
capitalized-comments: off
comma-dangle:
- error
- never
comma-spacing: off
comma-style: off
computed-property-spacing: off
consistent-this: off
eol-last: off
func-call-spacing: off
func-name-matching: off
func-names: off
func-style: off
id-length: off
id-match: off
indent: off
jsx-quotes: off
key-spacing: off
keyword-spacing: off
line-comment-position: off
linebreak-style:
- error
- unix
lines-around-comment: off
lines-around-directive: off
max-depth: off
max-len: off
max-nested-callbacks: off
max-params: off
max-statements-per-line: off
max-statements:
- error
- 30
multiline-ternary: off
new-cap: off
new-parens: off
newline-after-var: off
newline-before-return: off
newline-per-chained-call: off
no-array-constructor: off
no-bitwise: off
no-continue: off
no-inline-comments: off
no-lonely-if: off
no-mixed-operators: off
no-mixed-spaces-and-tabs: off
no-multi-assign: off
no-multiple-empty-lines: off
no-negated-condition: off
no-nested-ternary: off
no-new-object: off
no-plusplus: off
no-restricted-syntax: off
no-spaced-func: off
no-tabs: off
no-ternary: off
no-trailing-spaces: off
no-underscore-dangle: off
no-unneeded-ternary: off
object-curly-newline: off
object-curly-spacing: off
object-property-newline: off
one-var-declaration-per-line: off
one-var: off
operator-assignment: off
operator-linebreak: off
padded-blocks: off
quote-props: off
quotes:
- error
- single
require-jsdoc: off
semi-spacing: off
semi:
- error
- always
sort-keys: off
sort-vars: off
space-before-blocks: off
space-before-function-paren: off
space-in-parens: off
space-infix-ops: off
space-unary-ops: off
spaced-comment: off
template-tag-spacing: off
unicode-bom: off
wrap-regex: off
# ECMAScript 6
arrow-body-style: off
arrow-parens: off
arrow-spacing: off
constructor-super: off
generator-star-spacing: off
no-class-assign: off
no-confusing-arrow: off
no-const-assign: off
no-dupe-class-members: off
no-duplicate-imports: off
no-new-symbol: off
no-restricted-imports: off
no-this-before-super: off
no-useless-computed-key: off
no-useless-constructor: off
no-useless-rename: off
no-var: off
object-shorthand: off
prefer-arrow-callback: off
prefer-const: off
prefer-destructuring: off
prefer-numeric-literals: off
prefer-rest-params: off
prefer-reflect: off
prefer-spread: off
prefer-template: off
require-yield: off
rest-spread-spacing: off
sort-imports: off
symbol-description: off
template-curly-spacing: off
yield-star-spacing: off
+44
View File
@@ -0,0 +1,44 @@
<!--
Thanks for opening an issue on 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.
If you are reporting a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this
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
If you suspect your issue is a bug, please edit your issue description to
include the BUG REPORT INFORMATION shown below.
---------------------------------------------------
BUG REPORT INFORMATION
---------------------------------------------------
You do NOT have to include this information if this is a FEATURE REQUEST
-->
**Description**
<!--
Briefly describe the problem you are having in a few paragraphs.
-->
**Steps to reproduce the issue:**
1.
2.
3.
Any other info e.g. Why do you consider this to be a bug? What did you expect to happen instead?
**Technical details:**
* Portainer version:
* Target Docker version (the host/cluster you manage):
* Platform (windows/linux):
* Command used to start Portainer (`docker run -p 9000:9000 portainer/portainer`):
* Target Swarm version (if applicable):
* Browser:
+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.
+4 -7
View File
@@ -1,10 +1,7 @@
logs/*
!.gitkeep
*.esproj/*
node_modules
bower_components
.idea
*.iml
dist
dist/*
ui-for-docker-checksum.txt
portainer-checksum.txt
api/cmd/portainer/portainer*
.tmp
.vscode
+1 -1
View File
@@ -1 +1 @@
dockerui
portainer
+46
View File
@@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at anthony.lapenna@portainer.io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
+90
View File
@@ -0,0 +1,90 @@
# Contributing Guidelines
Some basic conventions for contributing to this project.
## 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.
* Please open a discussion in a new issue / existing issue to talk about the changes you'd like to bring
* Develop in a topic branch, not master/develop
When creating a new branch, prefix it with the *type* of the change (see section **Commit Message Format** below), the associated opened issue number, a dash and some text describing the issue (using dash as a separator).
For example, if you work on a bugfix for the issue #361, you could name the branch `fix361-template-selection`.
## Issues open to contribution
Want to contribute but don't know where to start?
Some of the open issues are labeled with prefix `exp/`, this is used to mark them as available for contributors to work on. All of these have an attributed difficulty level:
* **beginner**: a task that should be accessible with users not familiar with the codebase
* **intermediate**: a task that require some understanding of the project codebase or some experience in
either AngularJS or Golang
* **advanced**: a task that require a deep understanding of the project codebase
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
## Commit Message Format
Each commit message should include a **type**, a **scope** and a **subject**:
```
<type>(<scope>): <subject>
```
Lines should not exceed 100 characters. This allows the message to be easier to read on github as well as in various git tools and produces a nice, neat commit log ie:
```
#271 feat(containers): add exposed ports in the containers view
#270 fix(templates): fix a display issue in the templates view
#269 style(dashboard): update dashboard with new layout
```
### Type
Must be one of the following:
* **feat**: A new feature
* **fix**: A bug fix
* **docs**: Documentation only changes
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
semi-colons, etc)
* **refactor**: A code change that neither fixes a bug or adds a feature
* **test**: Adding missing tests
* **chore**: Changes to the build process or auxiliary tools and libraries such as documentation
generation
### 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
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)
-8
View File
@@ -1,8 +0,0 @@
FROM scratch
COPY dist /
VOLUME /data
EXPOSE 9000
ENTRYPOINT ["/ui-for-docker"]
+14 -56
View File
@@ -1,59 +1,17 @@
Cloudinovasi-ui: Copyright (c) 2016 Cloudinovasi
Copyright (c) 2018 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:
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
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.
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (anthonylapenna at cloudinovasi dot id)
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.
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
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.
-1
View File
@@ -1 +0,0 @@
web: dockerui -p ":$PORT" -e "$DOCKER_ENDPOINT"
+41 -132
View File
@@ -1,159 +1,68 @@
# Cloudinovasi UI for Docker
A fork of the amazing UI for Docker by Michael Crosby and Kevan Ahlquist (https://github.com/kevana/ui-for-docker) using the rdash-angular theme (https://github.com/rdash/rdash-angular).
<p align="center">
<img title="portainer" src='https://github.com/portainer/portainer/blob/develop/assets/images/logo_alt.png?raw=true' />
</p>
![Dashboard](/dashboard.png)
[![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)
[![Build Status](https://portainer.visualstudio.com/Portainer%20CI/_apis/build/status/Portainer%20CI?branchName=develop)](https://portainer.visualstudio.com/Portainer%20CI/_build/latest?definitionId=3&branchName=develop)
[![Code Climate](https://codeclimate.com/github/portainer/portainer/badges/gpa.svg)](https://codeclimate.com/github/portainer/portainer)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6)
UI For Docker is a web interface for the Docker Remote API. The goal is to provide a pure client side implementation so it is effortless to connect and manage docker.
**_Portainer_** is a lightweight management UI which allows you to **easily** manage your different Docker environments (Docker hosts or Swarm clusters).
## Goals
**_Portainer_** is meant to be as **simple** to deploy as it is to use. It consists of a single container that can run on any Docker engine (can be deployed as Linux container or a Windows native container).
* Minimal dependencies - I really want to keep this project a pure html/js app.
* Consistency - The web UI should be consistent with the commands found on the docker CLI.
**_Portainer_** allows you to manage your Docker containers, images, volumes, networks and more ! It is compatible with the *standalone Docker* engine and with *Docker Swarm mode*.
## Supported Docker versions
## Demo
The current Docker version support policy is the following: `N` to `N-2` included where `N` is the latest version.
You can try out the public demo instance: http://demo.portainer.io/ (login with the username **admin** and the password **tryportainer**).
At the moment, the following versions are supported: 1.9, 1.10 & 1.11.
Please note that the public demo cluster is **reset every 15min**.
## Run
Alternatively, you can deploy a copy of the demo stack inside a [play-with-docker (PWD)](https://labs.play-with-docker.com) playground:
### Quickstart
- Browse [PWD/?stack=portainer-demo/play-with-docker/docker-stack.yml](http://play-with-docker.com/?stack=https://raw.githubusercontent.com/portainer/portainer-demo/master/play-with-docker/docker-stack.yml)
- Sign in with your [Docker ID](https://docs.docker.com/docker-id)
- Follow [these](https://github.com/portainer/portainer-demo/blob/master/play-with-docker/docker-stack.yml#L5-L8) steps.
1. Run: `docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui`
Unlike the public demo, the playground sessions are deleted after 4 hours. Apart from that, all the settings are same, including default credentials.
2. Open your browser to `http://<dockerd host ip>:9000`
## Getting started
Bind mounting the Unix socket into the UI For Docker container is much more secure than exposing your docker daemon over TCP.
* [Deploy Portainer](https://portainer.readthedocs.io/en/latest/deployment.html)
* [Documentation](https://portainer.readthedocs.io)
The `--privileged` flag is required for hosts using SELinux.
## Getting help
### Specify socket to connect to Docker daemon
* Issues: https://github.com/portainer/portainer/issues
* FAQ: https://portainer.readthedocs.io/en/latest/faq.html
* Slack (chat): https://portainer.io/slack/
By default UI For Docker connects to the Docker daemon with`/var/run/docker.sock`. For this to work you need to bind mount the unix socket into the container with `-v /var/run/docker.sock:/var/run/docker.sock`.
## Reporting bugs and contributing
You can use the `--host`, `-H` flags to change this socket:
* Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new).
* Want to help us build **_portainer_**? Follow our [contribution guidelines](https://portainer.readthedocs.io/en/latest/contribute.html) to build it locally and make a pull request. We need all the help we can get!
```
# Connect to a tcp socket:
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H tcp://127.0.0.1:2375
```
## Limitations
```
# Connect to another unix socket:
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H unix:///path/to/docker.sock
```
**_Portainer_** has full support for the following Docker versions:
### Swarm support
* Docker 1.10 to the latest version
* 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.)_
**Supported Swarm version: 1.2.3**
Partial support for the following Docker versions (some features may not be available):
You can access a specific view for you Swarm cluster by defining the `--swarm` flag:
* Docker 1.9
```
# Connect to a tcp socket and enable Swarm:
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H tcp://<SWARM_HOST>:<SWARM_PORT> --swarm
```
## Licensing
*NOTE*: Due to Swarm not exposing information in a machine readable way, the app is bound to a specific version of Swarm at the moment.
Portainer is licensed under the zlib license. See [LICENSE](./LICENSE) for reference.
### Change address/port UI For Docker is served on
UI For Docker listens on port 9000 by default. If you run UI For Docker inside a container then you can bind the container's internal port to any external address and port:
Portainer also contains the following code, which is licensed under the [MIT license](https://opensource.org/licenses/MIT):
```
# Expose UI For Docker on 10.20.30.1:80
$ docker run -d -p 10.20.30.1:80:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui
```
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (portainer.io)
### Access a Docker engine protected via TLS
Ensure that you have access to the CA, the cert and the public key used to access your Docker engine.
These files will need to be named `ca.pem`, `cert.pem` and `key.pem` respectively. Store them somewhere on your disk and mount a volume containing these files inside the UI container:
```
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -v /path/to/certs:/certs -H https://my-docker-host.domain:2376 --tlsverify
```
You can also use the `--tlscacert`, `--tlscert` and `--tlskey` flags if you want to change the default path to the CA, certificate and key file respectively:
```
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -v /path/to/certs:/certs -H https://my-docker-host.domain:2376 --tlsverify --tlscacert /certs/myCa.pem --tlscert /certs/myCert.pem --tlskey /certs/myKey.pem
```
*Note*: Replace `/path/to/certs` to the path to the certificate files on your disk.
### Use your own logo
You can use the `--logo` flag to specify an URL to your own logo.
For example, using the Docker logo:
```
$ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui --logo "https://www.docker.com/sites/all/themes/docker/assets/images/brand-full.svg"
```
The custom logo will replace the CloudInovasi logo in the UI.
### Hide containers with specific labels
You can hide specific containers in the containers view by using the `--hide-label` or `-l` options and specifying a label.
For example, take a container started with the label `owner=acme`:
```
$ docker run -d --label owner=acme nginx
```
You can hide it in the view by starting the ui with:
```
$ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui -l owner=acme
```
### Reverse proxy configuration
Has been tested with Nginx 1.11.
Use the following configuration to host the UI at `myhost.mydomain.com/dockerui`:
```nginx
upstream cloudinovasi-ui {
server ADDRESS:PORT;
}
server {
listen 80;
location /dockerui/ {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://cloudinovasi-ui/;
}
location /dockerui/ws/ {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_pass http://cloudinovasi-ui/ws/;
}
}
```
Replace `ADDRESS:PORT` with the CloudInovasi UI container details.
### Available options
The following options are available for the `ui-for-docker` binary:
* `--host`, `-H`: Docker daemon endpoint (default: `"unix:///var/run/docker.sock"`)
* `--bind`, `-p`: Address and port to serve UI For Docker (default: `":9000"`)
* `--data`, `-d`: Path to the data folder (default: `"."`)
* `--assets`, `-a`: Path to the assets (default: `"."`)
* `--swarm`, `-s`: Swarm cluster support (default: `false`)
* `--tlsverify`: TLS support (default: `false`)
* `--tlscacert`: Path to the CA (default `/certs/ca.pem`)
* `--tlscert`: Path to the TLS certificate file (default `/certs/cert.pem`)
* `--tlskey`: Path to the TLS key (default `/certs/key.pem`)
* `--hide-label`, `-l`: Hide containers with a specific label in the UI
* `--logo`: URL to a picture to be displayed as a logo in the UI
rdash-angular: Copyright (c) [2014] [Elliot Hesp]
-57
View File
@@ -1,57 +0,0 @@
package main
import (
"crypto/tls"
"log"
"net/http"
"net/url"
)
type (
api struct {
endpoint *url.URL
bindAddress string
assetPath string
dataPath string
tlsConfig *tls.Config
}
apiConfig struct {
Endpoint string
BindAddress string
AssetPath string
DataPath string
SwarmSupport bool
TLSEnabled bool
TLSCACertPath string
TLSCertPath string
TLSKeyPath string
}
)
func (a *api) run(settings *Settings) {
handler := a.newHandler(settings)
if err := http.ListenAndServe(a.bindAddress, handler); err != nil {
log.Fatal(err)
}
}
func newAPI(apiConfig apiConfig) *api {
endpointURL, err := url.Parse(apiConfig.Endpoint)
if err != nil {
log.Fatal(err)
}
var tlsConfig *tls.Config
if apiConfig.TLSEnabled {
tlsConfig = newTLSConfig(apiConfig.TLSCACertPath, apiConfig.TLSCertPath, apiConfig.TLSKeyPath)
}
return &api{
endpoint: endpointURL,
bindAddress: apiConfig.BindAddress,
assetPath: apiConfig.AssetPath,
dataPath: apiConfig.DataPath,
tlsConfig: tlsConfig,
}
}
+36
View File
@@ -0,0 +1,36 @@
package archive
import (
"archive/tar"
"bytes"
)
// 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, mode int64) ([]byte, error) {
var buffer bytes.Buffer
tarWriter := tar.NewWriter(&buffer)
header := &tar.Header{
Name: fileName,
Mode: mode,
Size: int64(len(fileContent)),
}
err := tarWriter.WriteHeader(header)
if err != nil {
return nil, err
}
_, err = tarWriter.Write(fileContent)
if err != nil {
return nil, err
}
err = tarWriter.Close()
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
+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
}
+262
View File
@@ -0,0 +1,262 @@
package bolt
import (
"log"
"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 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
}
// NewStore initializes a new Store and the associated services
func NewStore(storePath string, fileService portainer.FileService) (*Store, error) {
store := &Store{
path: storePath,
fileService: fileService,
}
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
}
return store, nil
}
// Open opens and initializes the BoltDB database.
func (store *Store) Open() error {
databasePath := path.Join(store.path, databaseFileName)
db, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second})
if err != nil {
return err
}
store.db = db
return store.initServices()
}
// Init creates the default data set.
func (store *Store) Init() error {
groups, err := store.EndpointGroupService.EndpointGroups()
if err != nil {
return err
}
if len(groups) == 0 {
unassignedGroup := &portainer.EndpointGroup{
Name: "Unassigned",
Description: "Unassigned endpoints",
Labels: []portainer.Pair{},
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Tags: []string{},
}
return store.EndpointGroupService.CreateEndpointGroup(unassignedGroup)
}
return nil
}
// Close closes the BoltDB database.
func (store *Store) Close() error {
if store.db != nil {
return store.db.Close()
}
return nil
}
// MigrateData automatically migrate the data based on the DBVersion.
func (store *Store) MigrateData() error {
if !store.checkForDataMigration {
return store.VersionService.StoreDBVersion(portainer.DBVersion)
}
version, err := store.VersionService.DBVersion()
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)
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)
}
+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
})
}
+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
}
+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)
}
+36
View File
@@ -0,0 +1,36 @@
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")
if err == nil {
admin := &portainer.User{
Username: "admin",
Password: u.Password,
Role: portainer.AdministratorRole,
}
err = m.userService.CreateUser(admin)
if err != nil {
return err
}
err = m.removeLegacyAdminUser()
if err != nil {
return err
}
} else if err != nil && err != portainer.ErrObjectNotFound {
return err
}
return nil
}
func (m *Migrator) removeLegacyAdminUser() error {
return m.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(user.BucketName))
return bucket.Delete([]byte("admin"))
})
}
+103
View File
@@ -0,0 +1,103 @@
package migrator
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
)
func (m *Migrator) updateResourceControlsToDBVersion2() error {
legacyResourceControls, err := m.retrieveLegacyResourceControls()
if err != nil {
return err
}
for _, resourceControl := range legacyResourceControls {
resourceControl.SubResourceIDs = []string{}
resourceControl.TeamAccesses = []portainer.TeamResourceAccess{}
owner, err := m.userService.User(resourceControl.OwnerID)
if err != nil {
return err
}
if owner.Role == portainer.AdministratorRole {
resourceControl.AdministratorsOnly = true
resourceControl.UserAccesses = []portainer.UserResourceAccess{}
} else {
resourceControl.AdministratorsOnly = false
userAccess := portainer.UserResourceAccess{
UserID: resourceControl.OwnerID,
AccessLevel: portainer.ReadWriteAccessLevel,
}
resourceControl.UserAccesses = []portainer.UserResourceAccess{userAccess}
}
err = m.resourceControlService.CreateResourceControl(&resourceControl)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) updateEndpointsToDBVersion2() error {
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
endpoint.AuthorizedTeams = []portainer.TeamID{}
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) retrieveLegacyResourceControls() ([]portainer.ResourceControl, error) {
legacyResourceControls := make([]portainer.ResourceControl, 0)
err := m.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte("containerResourceControl"))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var resourceControl portainer.ResourceControl
err := internal.UnmarshalObject(v, &resourceControl)
if err != nil {
return err
}
resourceControl.Type = portainer.ContainerResourceControl
legacyResourceControls = append(legacyResourceControls, resourceControl)
}
bucket = tx.Bucket([]byte("serviceResourceControl"))
cursor = bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var resourceControl portainer.ResourceControl
err := internal.UnmarshalObject(v, &resourceControl)
if err != nil {
return err
}
resourceControl.Type = portainer.ServiceResourceControl
legacyResourceControls = append(legacyResourceControls, resourceControl)
}
bucket = tx.Bucket([]byte("volumeResourceControl"))
cursor = bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var resourceControl portainer.ResourceControl
err := internal.UnmarshalObject(v, &resourceControl)
if err != nil {
return err
}
resourceControl.Type = portainer.VolumeResourceControl
legacyResourceControls = append(legacyResourceControls, resourceControl)
}
return nil
})
return legacyResourceControls, err
}
+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
}
+20
View File
@@ -0,0 +1,20 @@
package migrator
import "github.com/portainer/portainer"
func (m *Migrator) updateSettingsToDBVersion3() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.AuthenticationMethod = portainer.AuthenticationInternal
legacySettings.LDAPSettings = portainer.LDAPSettings{
TLSConfig: portainer.TLSConfiguration{},
SearchSettings: []portainer.LDAPSearchSettings{
portainer.LDAPSearchSettings{},
},
}
return m.settingsService.UpdateSettings(legacySettings)
}
+28
View File
@@ -0,0 +1,28 @@
package migrator
import "github.com/portainer/portainer"
func (m *Migrator) updateEndpointsToDBVersion4() error {
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
endpoint.TLSConfig = portainer.TLSConfiguration{}
if endpoint.TLS {
endpoint.TLSConfig.TLS = true
endpoint.TLSConfig.TLSSkipVerify = false
endpoint.TLSConfig.TLSCACertPath = endpoint.TLSCACertPath
endpoint.TLSConfig.TLSCertPath = endpoint.TLSCertPath
endpoint.TLSConfig.TLSKeyPath = endpoint.TLSKeyPath
}
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}
+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)
}
+20
View File
@@ -0,0 +1,20 @@
package migrator
import "github.com/portainer/portainer"
func (m *Migrator) updateEndpointsToVersion8() error {
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)
if err != nil {
return err
}
}
return nil
}
+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)
}
+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)
}
+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)
}
+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)
}
+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)
}
+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)
})
}
+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)
})
}
+177
View File
@@ -0,0 +1,177 @@
package cli
import (
"time"
"github.com/portainer/portainer"
"os"
"path/filepath"
"strings"
"gopkg.in/alecthomas/kingpin.v2"
)
// Service implements the CLIService interface
type Service struct{}
const (
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")
)
// ParseFlags parse the CLI flags and return a portainer.Flags struct
func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
kingpin.Version(version)
flags := &portainer.CLIFlags{
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(),
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(),
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(),
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL").Default(defaultSSL).Bool(),
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").Default(defaultSSLCertPath).String(),
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").Default(defaultSSLKeyPath).String(),
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 definitions.").Short('t').String(),
TemplateFile: kingpin.Flag("template-file", "Path to the templates (app) definitions on the filesystem").Default(defaultTemplateFile).String(),
}
kingpin.Parse()
if !filepath.IsAbs(*flags.Assets) {
ex, err := os.Executable()
if err != nil {
panic(err)
}
*flags.Assets = filepath.Join(filepath.Dir(ex), *flags.Assets)
}
return flags, nil
}
// ValidateFlags validates the values of the flags.
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
if *flags.EndpointURL != "" && *flags.ExternalEndpoints != "" {
return errEndpointExcludeExternal
}
err := validateTemplateFile(*flags.TemplateFile)
if err != nil {
return err
}
err = validateEndpointURL(*flags.EndpointURL)
if err != nil {
return err
}
err = validateExternalEndpoints(*flags.ExternalEndpoints)
if err != nil {
return err
}
err = validateSyncInterval(*flags.SyncInterval)
if err != nil {
return err
}
err = validateSnapshotInterval(*flags.SnapshotInterval)
if err != nil {
return err
}
if *flags.NoAuth && (*flags.AdminPassword != "" || *flags.AdminPasswordFile != "") {
return errNoAuthExcludeAdminPassword
}
if *flags.AdminPassword != "" && *flags.AdminPasswordFile != "" {
return errAdminPassExcludeAdminPassFile
}
return nil
}
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(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 errSocketOrNamedPipeNotFound
}
return err
}
}
}
return nil
}
func validateExternalEndpoints(externalEndpoints string) error {
if externalEndpoints != "" {
if _, err := os.Stat(externalEndpoints); err != nil {
if os.IsNotExist(err) {
return errEndpointsFileNotFound
}
return err
}
}
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)
if err != nil {
return errInvalidSyncInterval
}
}
return nil
}
func validateSnapshotInterval(snapshotInterval string) error {
if snapshotInterval != defaultSnapshotInterval {
_, err := time.ParseDuration(snapshotInterval)
if err != nil {
return errInvalidSnapshotInterval
}
}
return nil
}
+23
View File
@@ -0,0 +1,23 @@
// +build !windows
package cli
const (
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"
)
+21
View File
@@ -0,0 +1,21 @@
package cli
const (
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"
)
+10 -16
View File
@@ -1,46 +1,40 @@
package main
package cli
import (
"github.com/portainer/portainer"
"fmt"
"gopkg.in/alecthomas/kingpin.v2"
"strings"
)
// pair defines a key/value pair
type pair struct {
Name string `json:"name"`
Value string `json:"value"`
}
type pairList []portainer.Pair
// pairList defines an array of Label
type pairList []pair
// Set implementation for Labels
// Set implementation for a list of portainer.Pair
func (l *pairList) Set(value string) error {
parts := strings.SplitN(value, "=", 2)
if len(parts) != 2 {
return fmt.Errorf("expected NAME=VALUE got '%s'", value)
}
p := new(pair)
p := new(portainer.Pair)
p.Name = parts[0]
p.Value = parts[1]
*l = append(*l, *p)
return nil
}
// String implementation for Labels
// String implementation for a list of pair
func (l *pairList) String() string {
return ""
}
// IsCumulative implementation for Labels
// IsCumulative implementation for a list of pair
func (l *pairList) IsCumulative() bool {
return true
}
// LabelParser defines a custom parser for Labels flags
func pairs(s kingpin.Settings) (target *[]pair) {
target = new([]pair)
func pairs(s kingpin.Settings) (target *[]portainer.Pair) {
target = new([]portainer.Pair)
s.SetValue((*pairList)(target))
return
}
+688
View File
@@ -0,0 +1,688 @@
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"
)
func initCLI() *portainer.CLIFlags {
var cli portainer.CLIService = &cli.Service{}
flags, err := cli.ParseFlags(portainer.APIVersion)
if err != nil {
log.Fatal(err)
}
err = cli.ValidateFlags(flags)
if err != nil {
log.Fatal(err)
}
return flags
}
func initFileService(dataStorePath string) portainer.FileService {
fileService, err := filesystem.NewService(dataStorePath, "")
if err != nil {
log.Fatal(err)
}
return fileService
}
func initStore(dataStorePath string, fileService portainer.FileService) *bolt.Store {
store, err := bolt.NewStore(dataStorePath, fileService)
if err != nil {
log.Fatal(err)
}
err = store.Open()
if err != nil {
log.Fatal(err)
}
err = store.Init()
if err != nil {
log.Fatal(err)
}
err = store.MigrateData()
if err != nil {
log.Fatal(err)
}
return store
}
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 {
if authenticationEnabled {
jwtService, err := jwt.NewService()
if err != nil {
log.Fatal(err)
}
return jwtService
}
return nil
}
func initDigitalSignatureService() portainer.DigitalSignatureService {
return crypto.NewECDSAService(os.Getenv("AGENT_SECRET"))
}
func initCryptoService() portainer.CryptoService {
return &crypto.Service{}
}
func initLDAPService() portainer.LDAPService {
return &ldap.Service{}
}
func initGitService() portainer.GitService {
return &git.Service{}
}
func initClientFactory(signatureService portainer.DigitalSignatureService) *docker.ClientFactory {
return docker.NewClientFactory(signatureService)
}
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: endpointManagement,
Snapshot: snapshot,
Version: portainer.APIVersion,
}
}
func initDockerHub(dockerHubService portainer.DockerHubService) error {
_, err := dockerHubService.DockerHub()
if err == portainer.ErrObjectNotFound {
dockerhub := &portainer.DockerHub{
Authentication: false,
Username: "",
Password: "",
}
return dockerHubService.UpdateDockerHub(dockerhub)
} else if err != nil {
return err
}
return nil
}
func initSettings(settingsService portainer.SettingsService, flags *portainer.CLIFlags) error {
_, err := settingsService.Settings()
if err == portainer.ErrObjectNotFound {
settings := &portainer.Settings{
LogoURL: *flags.Logo,
AuthenticationMethod: portainer.AuthenticationInternal,
LDAPSettings: portainer.LDAPSettings{
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
}
if *flags.Labels != nil {
settings.BlackListedLabels = *flags.Labels
} else {
settings.BlackListedLabels = make([]portainer.Pair, 0)
}
return settingsService.UpdateSettings(settings)
} else if err != nil {
return err
}
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 {
log.Fatal(err)
}
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, fileService)
defer store.Close()
jwtService := initJWTService(!*flags.NoAuth)
ldapService := initLDAPService()
gitService := initGitService()
cryptoService := initCryptoService()
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(endpointManagement, *flags.Snapshot, flags)
err = initEndpoint(flags, store.EndpointService, snapshotter)
if err != nil {
log.Fatal(err)
}
adminPasswordHash := ""
if *flags.AdminPasswordFile != "" {
content, err := fileService.GetFileContent(*flags.AdminPasswordFile)
if err != nil {
log.Fatal(err)
}
adminPasswordHash, err = cryptoService.Hash(string(content))
if err != nil {
log.Fatal(err)
}
} else if *flags.AdminPassword != "" {
adminPasswordHash = *flags.AdminPassword
}
if adminPasswordHash != "" {
users, err := store.UserService.UsersByRole(portainer.AdministratorRole)
if err != nil {
log.Fatal(err)
}
if len(users) == 0 {
log.Printf("Creating admin user with password hash %s", adminPasswordHash)
user := &portainer.User{
Username: "admin",
Role: portainer.AdministratorRole,
Password: adminPasswordHash,
}
err := store.UserService.CreateUser(user)
if err != nil {
log.Fatal(err)
}
} else {
log.Println("Instance already has an administrator user defined. Skipping admin password related flags.")
}
}
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: 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,
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)
err = server.Start()
if err != nil {
log.Fatal(err)
}
}
+214
View File
@@ -0,0 +1,214 @@
package cron
import (
"encoding/json"
"io/ioutil"
"log"
"strings"
"github.com/portainer/portainer"
)
// EndpointSyncJobRunner is used to run a EndpointSyncJob
type EndpointSyncJobRunner struct {
schedule *portainer.Schedule
context *EndpointSyncJobContext
}
// EndpointSyncJobContext represents the context of execution of a EndpointSyncJob
type EndpointSyncJobContext struct {
endpointService portainer.EndpointService
endpointFilePath string
}
// 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,
}
}
// 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 {
log.Printf("background job error (endpoint synchronization). Unable to synchronize endpoints (err=%s)\n", err)
return true
}
return false
}
func isValidEndpoint(endpoint *portainer.Endpoint) bool {
if endpoint.Name != "" && endpoint.URL != "" {
if !strings.HasPrefix(endpoint.URL, "unix://") && !strings.HasPrefix(endpoint.URL, "tcp://") {
return false
}
return true
}
return false
}
func convertFileEndpoints(fileEndpoints []fileEndpoint) []portainer.Endpoint {
convertedEndpoints := make([]portainer.Endpoint, 0)
for _, e := range fileEndpoints {
endpoint := portainer.Endpoint{
Name: e.Name,
URL: e.URL,
TLSConfig: portainer.TLSConfiguration{},
}
if e.TLS {
endpoint.TLSConfig.TLS = true
endpoint.TLSConfig.TLSSkipVerify = e.TLSSkipVerify
endpoint.TLSConfig.TLSCACertPath = e.TLSCACert
endpoint.TLSConfig.TLSCertPath = e.TLSCert
endpoint.TLSConfig.TLSKeyPath = e.TLSKey
}
convertedEndpoints = append(convertedEndpoints, endpoint)
}
return convertedEndpoints
}
func endpointExists(endpoint *portainer.Endpoint, endpoints []portainer.Endpoint) int {
for idx, v := range endpoints {
if endpoint.Name == v.Name && isValidEndpoint(&v) {
return idx
}
}
return -1
}
func mergeEndpointIfRequired(original, updated *portainer.Endpoint) *portainer.Endpoint {
var endpoint *portainer.Endpoint
if original.URL != updated.URL || original.TLSConfig.TLS != updated.TLSConfig.TLS ||
(updated.TLSConfig.TLS && original.TLSConfig.TLSSkipVerify != updated.TLSConfig.TLSSkipVerify) ||
(updated.TLSConfig.TLS && original.TLSConfig.TLSCACertPath != updated.TLSConfig.TLSCACertPath) ||
(updated.TLSConfig.TLS && original.TLSConfig.TLSCertPath != updated.TLSConfig.TLSCertPath) ||
(updated.TLSConfig.TLS && original.TLSConfig.TLSKeyPath != updated.TLSConfig.TLSKeyPath) {
endpoint = original
endpoint.URL = updated.URL
if updated.TLSConfig.TLS {
endpoint.TLSConfig.TLS = true
endpoint.TLSConfig.TLSSkipVerify = updated.TLSConfig.TLSSkipVerify
endpoint.TLSConfig.TLSCACertPath = updated.TLSConfig.TLSCACertPath
endpoint.TLSConfig.TLSCertPath = updated.TLSConfig.TLSCertPath
endpoint.TLSConfig.TLSKeyPath = updated.TLSConfig.TLSKeyPath
} else {
endpoint.TLSConfig.TLS = false
endpoint.TLSConfig.TLSSkipVerify = false
endpoint.TLSConfig.TLSCACertPath = ""
endpoint.TLSConfig.TLSCertPath = ""
endpoint.TLSConfig.TLSKeyPath = ""
}
}
return endpoint
}
func (sync synchronization) requireSync() bool {
if len(sync.endpointsToCreate) != 0 || len(sync.endpointsToUpdate) != 0 || len(sync.endpointsToDelete) != 0 {
return true
}
return false
}
func prepareSyncData(storedEndpoints, fileEndpoints []portainer.Endpoint) *synchronization {
endpointsToCreate := make([]*portainer.Endpoint, 0)
endpointsToUpdate := make([]*portainer.Endpoint, 0)
endpointsToDelete := make([]*portainer.Endpoint, 0)
for idx := range storedEndpoints {
fidx := endpointExists(&storedEndpoints[idx], fileEndpoints)
if fidx != -1 {
endpoint := mergeEndpointIfRequired(&storedEndpoints[idx], &fileEndpoints[fidx])
if endpoint != nil {
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 {
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) {
log.Printf("Invalid file endpoint definition, skipping. [name: %v] [url: %v]", endpoint.Name, endpoint.URL)
continue
}
sidx := endpointExists(&fileEndpoints[idx], storedEndpoints)
if sidx == -1 {
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])
}
}
return &synchronization{
endpointsToCreate: endpointsToCreate,
endpointsToUpdate: endpointsToUpdate,
endpointsToDelete: endpointsToDelete,
}
}
+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()
}
}
+22
View File
@@ -0,0 +1,22 @@
package crypto
import (
"golang.org/x/crypto/bcrypt"
)
// Service represents a service for encrypting/hashing data.
type Service struct{}
// Hash hashes a string using the bcrypt algorithm
func (*Service) Hash(data string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(data), bcrypt.DefaultCost)
if err != nil {
return "", nil
}
return string(hash), nil
}
// CompareHashAndData compares a hash to clear data and returns an error if the comparison fails.
func (*Service) CompareHashAndData(hash string, data string) error {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(data))
}
+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)
}
+59
View File
@@ -0,0 +1,59 @@
package crypto
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
)
// 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 !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
}
config.Certificates = []tls.Certificate{cert}
}
if !skipServerVerification && caCertPath != "" {
caCert, err := ioutil.ReadFile(caCertPath)
if err != nil {
return nil, err
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
config.RootCAs = caCertPool
}
return config, nil
}
-48
View File
@@ -1,48 +0,0 @@
package main
import (
"github.com/gorilla/csrf"
"github.com/gorilla/securecookie"
"io/ioutil"
"log"
"net/http"
)
const keyFile = "authKey.dat"
// newAuthKey reuses an existing CSRF authkey if present or generates a new one
func newAuthKey(path string) []byte {
var authKey []byte
authKeyPath := path + "/" + keyFile
data, err := ioutil.ReadFile(authKeyPath)
if err != nil {
log.Print("Unable to find an existing CSRF auth key. Generating a new key.")
authKey = securecookie.GenerateRandomKey(32)
err := ioutil.WriteFile(authKeyPath, authKey, 0644)
if err != nil {
log.Fatal("Unable to persist CSRF auth key.")
log.Fatal(err)
}
} else {
authKey = data
}
return authKey
}
// newCSRF initializes a new CSRF handler
func newCSRFHandler(keyPath string) func(h http.Handler) http.Handler {
authKey := newAuthKey(keyPath)
return csrf.Protect(
authKey,
csrf.HttpOnly(false),
csrf.Secure(false),
)
}
// newCSRFWrapper wraps a http.Handler to add the CSRF token
func newCSRFWrapper(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-CSRF-Token", csrf.Token(r))
h.ServeHTTP(w, r)
})
}
+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)
}
+116
View File
@@ -0,0 +1,116 @@
package portainer
// General errors.
const (
ErrUnauthorized = Error("Unauthorized")
ErrResourceAccessDenied = Error("Access denied to resource")
ErrObjectNotFound = Error("Object not found inside the database")
ErrMissingSecurityContext = Error("Unable to find security details in request context")
)
// User errors.
const (
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 (
ErrTeamAlreadyExists = Error("Team already exists")
)
// TeamMembership errors.
const (
ErrTeamMembershipAlreadyExists = Error("Team membership already exists for this user and team")
)
// ResourceControl errors.
const (
ErrResourceControlAlreadyExists = Error("A resource control is already applied on this resource")
ErrInvalidResourceControlType = Error("Unsupported resource control type")
)
// Endpoint errors.
const (
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 (
ErrRegistryAlreadyExists = Error("A registry is already defined for this URL")
)
// Stack errors
const (
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
const (
ErrEndpointExtensionNotSupported = Error("This extension is not supported")
ErrEndpointExtensionAlreadyAssociated = Error("This extension is already associated to the endpoint")
)
// Crypto errors.
const (
ErrCryptoHashFailure = Error("Unable to hash data")
)
// JWT errors.
const (
ErrSecretGeneration = Error("Unable to generate secret key")
ErrInvalidJWTToken = Error("Invalid JWT token")
ErrMissingContextData = Error("Unable to find JWT data in request context")
)
// File errors.
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")
)
-24
View File
@@ -1,24 +0,0 @@
package main
import (
"golang.org/x/net/websocket"
"log"
)
// execContainer is used to create a websocket communication with an exec instance
func (a *api) execContainer(ws *websocket.Conn) {
qry := ws.Request().URL.Query()
execID := qry.Get("id")
var host string
if a.endpoint.Scheme == "tcp" {
host = a.endpoint.Host
} else if a.endpoint.Scheme == "unix" {
host = a.endpoint.Path
}
if err := hijack(host, a.endpoint.Scheme, "POST", "/exec/"+execID+"/start", a.tlsConfig, true, ws, ws, ws, nil, nil); err != nil {
log.Fatalf("error during hijack: %s", err)
return
}
}
+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
}
+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
}
+399
View File
@@ -0,0 +1,399 @@
package filesystem
import (
"bytes"
"encoding/json"
"encoding/pem"
"io/ioutil"
"github.com/portainer/portainer"
"github.com/portainer/portainer/archive"
"io"
"os"
"path"
)
const (
// TLSStorePath represents the subfolder where TLS files are stored in the file store folder.
TLSStorePath = "tls"
// LDAPStorePath represents the subfolder where LDAP TLS files are stored in the TLSStorePath.
LDAPStorePath = "ldap"
// TLSCACertFile represents the name on disk for a TLS CA file.
TLSCACertFile = "ca.pem"
// TLSCertFile represents the name on disk for a TLS certificate file.
TLSCertFile = "cert.pem"
// TLSKeyFile represents the name on disk for a TLS key file.
TLSKeyFile = "key.pem"
// ComposeStorePath represents the subfolder where compose files are stored in the file store folder.
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.
type Service struct {
dataStorePath string
fileStorePath string
}
// NewService initializes a new service. It creates a data directory and a directory to store files
// inside this directory if they don't exist.
func NewService(dataStorePath, fileStorePath string) (*Service, error) {
service := &Service{
dataStorePath: dataStorePath,
fileStorePath: path.Join(dataStorePath, fileStorePath),
}
err := os.MkdirAll(dataStorePath, 0755)
if err != nil {
return nil, err
}
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
}
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)
}
// GetStackProjectPath returns the absolute path on the FS for a stack based
// on its identifier.
func (service *Service) GetStackProjectPath(stackIdentifier string) string {
return path.Join(service.fileStorePath, ComposeStorePath, stackIdentifier)
}
// StoreStackFileFromBytes creates a subfolder in the ComposeStorePath and stores a new file from bytes.
// It returns the path to the folder where the file is stored.
func (service *Service) StoreStackFileFromBytes(stackIdentifier, fileName string, data []byte) (string, error) {
stackStorePath := path.Join(ComposeStorePath, stackIdentifier)
err := service.createDirectoryInStore(stackStorePath)
if err != nil {
return "", err
}
composeFilePath := path.Join(stackStorePath, fileName)
r := bytes.NewReader(data)
err = service.createFileInStore(composeFilePath, r)
if err != nil {
return "", err
}
return path.Join(service.fileStorePath, stackStorePath), nil
}
// 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) StoreRegistryManagementFileFromBytes(folder, fileName string, data []byte) (string, error) {
extensionStorePath := path.Join(ExtensionRegistryManagementStorePath, folder)
err := service.createDirectoryInStore(extensionStorePath)
if err != nil {
return "", err
}
file := path.Join(extensionStorePath, fileName)
r := bytes.NewReader(data)
err = service.createFileInStore(file, r)
if err != nil {
return "", err
}
return path.Join(service.fileStorePath, file), nil
}
// 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.createDirectoryInStore(storePath)
if err != nil {
return "", err
}
var fileName string
switch fileType {
case portainer.TLSFileCA:
fileName = TLSCACertFile
case portainer.TLSFileCert:
fileName = TLSCertFile
case portainer.TLSFileKey:
fileName = TLSKeyFile
default:
return "", portainer.ErrUndefinedTLSFileType
}
tlsFilePath := path.Join(storePath, fileName)
r := bytes.NewReader(data)
err = service.createFileInStore(tlsFilePath, r)
if err != nil {
return "", err
}
return path.Join(service.fileStorePath, tlsFilePath), nil
}
// GetPathForTLSFile returns the absolute path to a specific TLS file for an endpoint.
func (service *Service) GetPathForTLSFile(folder string, fileType portainer.TLSFileType) (string, error) {
var fileName string
switch fileType {
case portainer.TLSFileCA:
fileName = TLSCACertFile
case portainer.TLSFileCert:
fileName = TLSCertFile
case portainer.TLSFileKey:
fileName = TLSKeyFile
default:
return "", portainer.ErrUndefinedTLSFileType
}
return path.Join(service.fileStorePath, TLSStorePath, folder, fileName), nil
}
// DeleteTLSFiles deletes a folder in the TLS store path.
func (service *Service) DeleteTLSFiles(folder string) error {
storePath := path.Join(service.fileStorePath, TLSStorePath, folder)
err := os.RemoveAll(storePath)
if err != nil {
return err
}
return nil
}
// DeleteTLSFile deletes a specific TLS file from a folder.
func (service *Service) DeleteTLSFile(folder string, fileType portainer.TLSFileType) error {
var fileName string
switch fileType {
case portainer.TLSFileCA:
fileName = TLSCACertFile
case portainer.TLSFileCert:
fileName = TLSCertFile
case portainer.TLSFileKey:
fileName = TLSKeyFile
default:
return portainer.ErrUndefinedTLSFileType
}
filePath := path.Join(service.fileStorePath, TLSStorePath, folder, fileName)
err := os.Remove(filePath)
if err != nil {
return err
}
return nil
}
// 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 nil, err
}
return content, nil
}
// Rename renames a file or directory
func (service *Service) Rename(oldPath, newPath string) error {
return os.Rename(oldPath, newPath)
}
// 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)
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, r)
if err != nil {
return err
}
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"
}
+46
View File
@@ -0,0 +1,46 @@
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.
type Service struct{}
// NewService initializes a new service.
func NewService(dataStorePath string) (*Service, error) {
service := &Service{}
return service, nil
}
// ClonePublicRepository clones a public git repository using the specified URL in the specified
// destination folder.
func (service *Service) ClonePublicRepository(repositoryURL, referenceName string, destination string) error {
return cloneRepository(repositoryURL, referenceName, destination)
}
// 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
}
-75
View File
@@ -1,75 +0,0 @@
package main
import (
"golang.org/x/net/websocket"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
)
// newHandler creates a new http.Handler with CSRF protection
func (a *api) newHandler(settings *Settings) http.Handler {
var (
mux = http.NewServeMux()
fileHandler = http.FileServer(http.Dir(a.assetPath))
)
handler := a.newAPIHandler()
CSRFHandler := newCSRFHandler(a.dataPath)
mux.Handle("/", fileHandler)
mux.Handle("/dockerapi/", http.StripPrefix("/dockerapi", handler))
mux.Handle("/ws/exec", websocket.Handler(a.execContainer))
mux.HandleFunc("/settings", func(w http.ResponseWriter, r *http.Request) {
settingsHandler(w, r, settings)
})
return CSRFHandler(newCSRFWrapper(mux))
}
// newAPIHandler initializes a new http.Handler based on the URL scheme
func (a *api) newAPIHandler() http.Handler {
var handler http.Handler
var endpoint = *a.endpoint
if endpoint.Scheme == "tcp" {
if a.tlsConfig != nil {
handler = a.newTCPHandlerWithTLS(&endpoint)
} else {
handler = a.newTCPHandler(&endpoint)
}
} else if endpoint.Scheme == "unix" {
socketPath := endpoint.Path
if _, err := os.Stat(socketPath); err != nil {
if os.IsNotExist(err) {
log.Fatalf("Unix socket %s does not exist", socketPath)
}
log.Fatal(err)
}
handler = a.newUnixHandler(socketPath)
} else {
log.Fatalf("Bad Docker enpoint: %v. Only unix:// and tcp:// are supported.", &endpoint)
}
return handler
}
// newUnixHandler initializes a new UnixHandler
func (a *api) newUnixHandler(e string) http.Handler {
return &unixHandler{e}
}
// newTCPHandler initializes a HTTP reverse proxy
func (a *api) newTCPHandler(u *url.URL) http.Handler {
u.Scheme = "http"
return httputil.NewSingleHostReverseProxy(u)
}
// newTCPHandlerWithL initializes a HTTPS reverse proxy with a TLS configuration
func (a *api) newTCPHandlerWithTLS(u *url.URL) http.Handler {
u.Scheme = "https"
proxy := httputil.NewSingleHostReverseProxy(u)
proxy.Transport = &http.Transport{
TLSClientConfig: a.tlsConfig,
}
return proxy
}
-123
View File
@@ -1,123 +0,0 @@
package main
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
"time"
)
type execConfig struct {
Tty bool
Detach bool
}
// hijack allows to upgrade an HTTP connection to a TCP connection
// It redirects IO streams for stdin, stdout and stderr to a websocket
func hijack(addr, scheme, method, path string, tlsConfig *tls.Config, setRawTerminal bool, in io.ReadCloser, stdout, stderr io.Writer, started chan io.Closer, data interface{}) error {
execConfig := &execConfig{
Tty: true,
Detach: false,
}
buf, err := json.Marshal(execConfig)
if err != nil {
return fmt.Errorf("error marshaling exec config: %s", err)
}
rdr := bytes.NewReader(buf)
req, err := http.NewRequest(method, path, rdr)
if err != nil {
return fmt.Errorf("error during hijack request: %s", err)
}
req.Header.Set("User-Agent", "Docker-Client")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Connection", "Upgrade")
req.Header.Set("Upgrade", "tcp")
req.Host = addr
var (
dial net.Conn
dialErr error
)
if tlsConfig == nil {
dial, dialErr = net.Dial(scheme, addr)
} else {
dial, dialErr = tls.Dial(scheme, addr, tlsConfig)
}
if dialErr != nil {
return dialErr
}
// When we set up a TCP connection for hijack, there could be long periods
// of inactivity (a long running command with no output) that in certain
// network setups may cause ECONNTIMEOUT, leaving the client in an unknown
// state. Setting TCP KeepAlive on the socket connection will prohibit
// ECONNTIMEOUT unless the socket connection truly is broken
if tcpConn, ok := dial.(*net.TCPConn); ok {
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(30 * time.Second)
}
if err != nil {
return err
}
clientconn := httputil.NewClientConn(dial, nil)
defer clientconn.Close()
// Server hijacks the connection, error 'connection closed' expected
clientconn.Do(req)
rwc, br := clientconn.Hijack()
defer rwc.Close()
if started != nil {
started <- rwc
}
var receiveStdout chan error
if stdout != nil || stderr != nil {
go func() (err error) {
if setRawTerminal && stdout != nil {
_, err = io.Copy(stdout, br)
}
return err
}()
}
go func() error {
if in != nil {
io.Copy(rwc, in)
}
if conn, ok := rwc.(interface {
CloseWrite() error
}); ok {
if err := conn.CloseWrite(); err != nil {
}
}
return nil
}()
if stdout != nil || stderr != nil {
if err := <-receiveStdout; err != nil {
return err
}
}
go func() {
for {
fmt.Println(br)
}
}()
return nil
}
+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
}
+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
}
@@ -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
}
@@ -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)
}
@@ -0,0 +1,25 @@
package endpointgroups
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/http/security"
)
// GET request on /api/endpoint_groups
func (handler *Handler) endpointGroupList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from the database", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
endpointGroups = security.FilterEndpointGroups(endpointGroups, securityContext)
return response.JSON(w, endpointGroups)
}
@@ -0,0 +1,73 @@
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"
)
type endpointGroupUpdatePayload struct {
Name string
Description string
AssociatedEndpoints []portainer.EndpointID
Tags []string
}
func (payload *endpointGroupUpdatePayload) Validate(r *http.Request) error {
return nil
}
// PUT request on /api/endpoint_groups/:id
func (handler *Handler) endpointGroupUpdate(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}
}
var payload endpointGroupUpdatePayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", 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}
}
if payload.Name != "" {
endpointGroup.Name = payload.Name
}
if payload.Description != "" {
endpointGroup.Description = payload.Description
}
if payload.Tags != nil {
endpointGroup.Tags = payload.Tags
}
err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint group changes 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 {
err = handler.updateEndpointGroup(endpoint, portainer.EndpointGroupID(endpointGroupID), payload.AssociatedEndpoints)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
}
}
return response.JSON(w, endpointGroup)
}
@@ -0,0 +1,63 @@
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"
)
type endpointGroupUpdateAccessPayload struct {
AuthorizedUsers []int
AuthorizedTeams []int
}
func (payload *endpointGroupUpdateAccessPayload) Validate(r *http.Request) error {
return nil
}
// PUT request on /api/endpoint_groups/:id/access
func (handler *Handler) endpointGroupUpdateAccess(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}
}
var payload endpointGroupUpdateAccessPayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", 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}
}
if payload.AuthorizedUsers != nil {
authorizedUserIDs := []portainer.UserID{}
for _, value := range payload.AuthorizedUsers {
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
}
endpointGroup.AuthorizedUsers = authorizedUserIDs
}
if payload.AuthorizedTeams != nil {
authorizedTeamIDs := []portainer.TeamID{}
for _, value := range payload.AuthorizedTeams {
authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value))
}
endpointGroup.AuthorizedTeams = authorizedTeamIDs
}
err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint group changes inside the database", err}
}
return response.JSON(w, endpointGroup)
}
@@ -0,0 +1,69 @@
package endpointgroups
import (
"net/http"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer"
"github.com/portainer/portainer/http/security"
)
// Handler is the HTTP handler used to handle endpoint group operations.
type Handler struct {
*mux.Router
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
}
// NewHandler creates a handler to manage endpoint group operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
}
h.Handle("/endpoint_groups",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupCreate))).Methods(http.MethodPost)
h.Handle("/endpoint_groups",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointGroupList))).Methods(http.MethodGet)
h.Handle("/endpoint_groups/{id}",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupInspect))).Methods(http.MethodGet)
h.Handle("/endpoint_groups/{id}",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupUpdate))).Methods(http.MethodPut)
h.Handle("/endpoint_groups/{id}/access",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupUpdateAccess))).Methods(http.MethodPut)
h.Handle("/endpoint_groups/{id}",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupDelete))).Methods(http.MethodDelete)
return h
}
func (handler *Handler) checkForGroupUnassignment(endpoint portainer.Endpoint, associatedEndpoints []portainer.EndpointID) error {
for _, id := range associatedEndpoints {
if id == endpoint.ID {
return nil
}
}
endpoint.GroupID = portainer.EndpointGroupID(1)
return handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
}
func (handler *Handler) checkForGroupAssignment(endpoint portainer.Endpoint, groupID portainer.EndpointGroupID, associatedEndpoints []portainer.EndpointID) error {
for _, id := range associatedEndpoints {
if id == endpoint.ID {
endpoint.GroupID = groupID
return handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
}
}
return nil
}
func (handler *Handler) updateEndpointGroup(endpoint portainer.Endpoint, groupID portainer.EndpointGroupID, associatedEndpoints []portainer.EndpointID) error {
if endpoint.GroupID == groupID {
return handler.checkForGroupUnassignment(endpoint, associatedEndpoints)
} else if endpoint.GroupID == portainer.EndpointGroupID(1) {
return handler.checkForGroupAssignment(endpoint, groupID, associatedEndpoints)
}
return nil
}
+32
View File
@@ -0,0 +1,32 @@
package endpointproxy
import (
"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"
)
// Handler is the HTTP handler used to proxy requests to external APIs.
type Handler struct {
*mux.Router
requestBouncer *security.RequestBouncer
EndpointService portainer.EndpointService
ProxyManager *proxy.Manager
}
// NewHandler creates a handler to proxy requests to external APIs.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
requestBouncer: bouncer,
}
h.PathPrefix("/{id}/azure").Handler(
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToAzureAPI)))
h.PathPrefix("/{id}/docker").Handler(
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI)))
h.PathPrefix("/{id}/extensions/storidge").Handler(
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToStoridgeAPI)))
return h
}
@@ -0,0 +1,43 @@
package endpointproxy
import (
"strconv"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/portainer"
"net/http"
)
func (handler *Handler) proxyRequestsToAzureAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
err = handler.requestBouncer.EndpointAccess(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
}
var proxy http.Handler
proxy = handler.ProxyManager.GetProxy(string(endpointID))
if proxy == nil {
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create proxy", err}
}
}
id := strconv.Itoa(endpointID)
http.StripPrefix("/"+id+"/azure", proxy).ServeHTTP(w, r)
return nil
}
@@ -0,0 +1,48 @@
package endpointproxy
import (
"errors"
"strconv"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/portainer"
"net/http"
)
func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
if endpoint.Status == portainer.EndpointStatusDown {
return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to query endpoint", errors.New("Endpoint is down")}
}
err = handler.requestBouncer.EndpointAccess(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
}
var proxy http.Handler
proxy = handler.ProxyManager.GetProxy(string(endpointID))
if proxy == nil {
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create proxy", err}
}
}
id := strconv.Itoa(endpointID)
http.StripPrefix("/"+id+"/docker", proxy).ServeHTTP(w, r)
return nil
}
@@ -0,0 +1,58 @@
package endpointproxy
// TODO: legacy extension management
import (
"strconv"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/portainer"
"net/http"
)
func (handler *Handler) proxyRequestsToStoridgeAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
err = handler.requestBouncer.EndpointAccess(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
}
var storidgeExtension *portainer.EndpointExtension
for _, extension := range endpoint.Extensions {
if extension.Type == portainer.StoridgeEndpointExtension {
storidgeExtension = &extension
}
}
if storidgeExtension == nil {
return &httperror.HandlerError{http.StatusServiceUnavailable, "Storidge extension not supported on this endpoint", portainer.ErrEndpointExtensionNotSupported}
}
proxyExtensionKey := string(endpoint.ID) + "_" + string(portainer.StoridgeEndpointExtension)
var proxy http.Handler
proxy = handler.ProxyManager.GetLegacyExtensionProxy(proxyExtensionKey)
if proxy == nil {
proxy, err = handler.ProxyManager.CreateLegacyExtensionProxy(proxyExtensionKey, storidgeExtension.URL)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create extension proxy", err}
}
}
id := strconv.Itoa(endpointID)
http.StripPrefix("/"+id+"/extensions/storidge", proxy).ServeHTTP(w, r)
return nil
}
@@ -0,0 +1,338 @@
package endpoints
import (
"log"
"net/http"
"runtime"
"strconv"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer"
"github.com/portainer/portainer/crypto"
"github.com/portainer/portainer/http/client"
)
type endpointCreatePayload struct {
Name string
URL string
EndpointType int
PublicURL string
GroupID int
TLS bool
TLSSkipVerify bool
TLSSkipClientVerify bool
TLSCACertFile []byte
TLSCertFile []byte
TLSKeyFile []byte
AzureApplicationID string
AzureTenantID string
AzureAuthenticationKey string
Tags []string
}
func (payload *endpointCreatePayload) Validate(r *http.Request) error {
name, err := request.RetrieveMultiPartFormValue(r, "Name", false)
if err != nil {
return portainer.Error("Invalid endpoint name")
}
payload.Name = name
endpointType, err := request.RetrieveNumericMultiPartFormValue(r, "EndpointType", false)
if err != nil || endpointType == 0 {
return portainer.Error("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment) or 3 (Azure environment)")
}
payload.EndpointType = endpointType
groupID, _ := request.RetrieveNumericMultiPartFormValue(r, "GroupID", true)
if groupID == 0 {
groupID = 1
}
payload.GroupID = groupID
var tags []string
err = request.RetrieveMultiPartFormJSONValue(r, "Tags", &tags, true)
if err != nil {
return portainer.Error("Invalid Tags parameter")
}
payload.Tags = tags
if payload.Tags == nil {
payload.Tags = make([]string, 0)
}
useTLS, _ := request.RetrieveBooleanMultiPartFormValue(r, "TLS", true)
payload.TLS = useTLS
if payload.TLS {
skipTLSServerVerification, _ := request.RetrieveBooleanMultiPartFormValue(r, "TLSSkipVerify", true)
payload.TLSSkipVerify = skipTLSServerVerification
skipTLSClientVerification, _ := request.RetrieveBooleanMultiPartFormValue(r, "TLSSkipClientVerify", true)
payload.TLSSkipClientVerify = skipTLSClientVerification
if !payload.TLSSkipVerify {
caCert, _, err := request.RetrieveMultiPartFormFile(r, "TLSCACertFile")
if err != nil {
return portainer.Error("Invalid CA certificate file. Ensure that the file is uploaded correctly")
}
payload.TLSCACertFile = caCert
}
if !payload.TLSSkipClientVerify {
cert, _, err := request.RetrieveMultiPartFormFile(r, "TLSCertFile")
if err != nil {
return portainer.Error("Invalid certificate file. Ensure that the file is uploaded correctly")
}
payload.TLSCertFile = cert
key, _, err := request.RetrieveMultiPartFormFile(r, "TLSKeyFile")
if err != nil {
return portainer.Error("Invalid key file. Ensure that the file is uploaded correctly")
}
payload.TLSKeyFile = key
}
}
switch portainer.EndpointType(payload.EndpointType) {
case portainer.AzureEnvironment:
azureApplicationID, err := request.RetrieveMultiPartFormValue(r, "AzureApplicationID", false)
if err != nil {
return portainer.Error("Invalid Azure application ID")
}
payload.AzureApplicationID = azureApplicationID
azureTenantID, err := request.RetrieveMultiPartFormValue(r, "AzureTenantID", false)
if err != nil {
return portainer.Error("Invalid Azure tenant ID")
}
payload.AzureTenantID = azureTenantID
azureAuthenticationKey, err := request.RetrieveMultiPartFormValue(r, "AzureAuthenticationKey", false)
if err != nil {
return portainer.Error("Invalid Azure authentication key")
}
payload.AzureAuthenticationKey = azureAuthenticationKey
default:
url, err := request.RetrieveMultiPartFormValue(r, "URL", true)
if err != nil {
return portainer.Error("Invalid endpoint URL")
}
payload.URL = url
publicURL, _ := request.RetrieveMultiPartFormValue(r, "PublicURL", true)
payload.PublicURL = publicURL
}
return nil
}
// POST request on /api/endpoints
func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
if !handler.authorizeEndpointManagement {
return &httperror.HandlerError{http.StatusServiceUnavailable, "Endpoint management is disabled", ErrEndpointManagementDisabled}
}
payload := &endpointCreatePayload{}
err := payload.Validate(r)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
endpoint, endpointCreationError := handler.createEndpoint(payload)
if endpointCreationError != nil {
return endpointCreationError
}
return response.JSON(w, endpoint)
}
func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
if portainer.EndpointType(payload.EndpointType) == portainer.AzureEnvironment {
return handler.createAzureEndpoint(payload)
}
if payload.TLS {
return handler.createTLSSecuredEndpoint(payload)
}
return handler.createUnsecuredEndpoint(payload)
}
func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
credentials := portainer.AzureCredentials{
ApplicationID: payload.AzureApplicationID,
TenantID: payload.AzureTenantID,
AuthenticationKey: payload.AzureAuthenticationKey,
}
httpClient := client.NewHTTPClient()
_, err := httpClient.ExecuteAzureAuthenticationRequest(&credentials)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate against Azure", err}
}
endpointID := handler.EndpointService.GetNextIdentifier()
endpoint := &portainer.Endpoint{
ID: portainer.EndpointID(endpointID),
Name: payload.Name,
URL: "https://management.azure.com",
Type: portainer.AzureEnvironment,
GroupID: portainer.EndpointGroupID(payload.GroupID),
PublicURL: payload.PublicURL,
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
AzureCredentials: credentials,
Tags: payload.Tags,
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
}
err = handler.EndpointService.CreateEndpoint(endpoint)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint inside the database", err}
}
return endpoint, nil
}
func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
endpointType := portainer.DockerEnvironment
if payload.URL == "" {
payload.URL = "unix:///var/run/docker.sock"
if runtime.GOOS == "windows" {
payload.URL = "npipe:////./pipe/docker_engine"
}
} else {
agentOnDockerEnvironment, err := client.ExecutePingOperation(payload.URL, nil)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to ping Docker environment", err}
}
if agentOnDockerEnvironment {
endpointType = portainer.AgentOnDockerEnvironment
}
}
endpointID := handler.EndpointService.GetNextIdentifier()
endpoint := &portainer.Endpoint{
ID: portainer.EndpointID(endpointID),
Name: payload.Name,
URL: payload.URL,
Type: endpointType,
GroupID: portainer.EndpointGroupID(payload.GroupID),
PublicURL: payload.PublicURL,
TLSConfig: portainer.TLSConfiguration{
TLS: false,
},
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
Tags: payload.Tags,
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
}
err := handler.snapshotAndPersistEndpoint(endpoint)
if err != nil {
return nil, err
}
return endpoint, nil
}
func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
tlsConfig, err := crypto.CreateTLSConfigurationFromBytes(payload.TLSCACertFile, payload.TLSCertFile, payload.TLSKeyFile, payload.TLSSkipClientVerify, payload.TLSSkipVerify)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to create TLS configuration", err}
}
agentOnDockerEnvironment, err := client.ExecutePingOperation(payload.URL, tlsConfig)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to ping Docker environment", err}
}
endpointType := portainer.DockerEnvironment
if agentOnDockerEnvironment {
endpointType = portainer.AgentOnDockerEnvironment
}
endpointID := handler.EndpointService.GetNextIdentifier()
endpoint := &portainer.Endpoint{
ID: portainer.EndpointID(endpointID),
Name: payload.Name,
URL: payload.URL,
Type: endpointType,
GroupID: portainer.EndpointGroupID(payload.GroupID),
PublicURL: payload.PublicURL,
TLSConfig: portainer.TLSConfiguration{
TLS: payload.TLS,
TLSSkipVerify: payload.TLSSkipVerify,
},
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
Tags: payload.Tags,
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
}
filesystemError := handler.storeTLSFiles(endpoint, payload)
if err != nil {
return nil, filesystemError
}
endpointCreationError := handler.snapshotAndPersistEndpoint(endpoint)
if endpointCreationError != nil {
return nil, endpointCreationError
}
return endpoint, nil
}
func (handler *Handler) snapshotAndPersistEndpoint(endpoint *portainer.Endpoint) *httperror.HandlerError {
snapshot, err := handler.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)
endpoint.Status = portainer.EndpointStatusDown
}
if snapshot != nil {
endpoint.Snapshots = []portainer.Snapshot{*snapshot}
}
err = handler.EndpointService.CreateEndpoint(endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint inside the database", err}
}
return nil
}
func (handler *Handler) storeTLSFiles(endpoint *portainer.Endpoint, payload *endpointCreatePayload) *httperror.HandlerError {
folder := strconv.Itoa(int(endpoint.ID))
if !payload.TLSSkipVerify {
caCertPath, err := handler.FileService.StoreTLSFileFromBytes(folder, portainer.TLSFileCA, payload.TLSCACertFile)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist TLS CA certificate file on disk", err}
}
endpoint.TLSConfig.TLSCACertPath = caCertPath
}
if !payload.TLSSkipClientVerify {
certPath, err := handler.FileService.StoreTLSFileFromBytes(folder, portainer.TLSFileCert, payload.TLSCertFile)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist TLS certificate file on disk", err}
}
endpoint.TLSConfig.TLSCertPath = certPath
keyPath, err := handler.FileService.StoreTLSFileFromBytes(folder, portainer.TLSFileKey, payload.TLSKeyFile)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist TLS key file on disk", err}
}
endpoint.TLSConfig.TLSKeyPath = keyPath
}
return nil
}

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