Compare commits

...

491 Commits

Author SHA1 Message Date
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
844 changed files with 52047 additions and 16238 deletions
+2 -1
View File
@@ -12,7 +12,8 @@ engines:
enabled: true
config:
languages:
- javascript
javascript:
mass_threshold: 80
eslint:
enabled: true
config:
@@ -13,14 +13,28 @@ steps:
image: portainer/angular-builder:latest
working_directory: ${{build_backend}}
commands:
- npm install -g bower grunt grunt-cli && npm install
- bower install --allow-root
- grunt build-webapp
- yarn
- yarn grunt build-webapp
- mv api/cmd/portainer/portainer dist/
get_docker_version:
image: alpine:3.7
working_directory: ${{build_frontend}}
commands:
- cf_export DOCKER_VERSION=`cat gruntfile.js | grep -m 1 'shippedDockerVersion' | cut -d\' -f2`
download_docker_binary:
image: busybox
working_directory: ${{build_frontend}}
commands:
- echo ${{DOCKER_VERSION}}
- wget -O /tmp/docker-binaries.tgz https://download.docker.com/linux/static/stable/x86_64/docker-${{DOCKER_VERSION}}.tgz
- tar -xf /tmp/docker-binaries.tgz -C /tmp
- mv /tmp/docker/docker dist/
build_image:
type: build
working_directory: ${{build_frontend}}
working_directory: ${{download_docker_binary}}
dockerfile: ./build/linux/Dockerfile
image_name: portainer/portainer
tag: ${{CF_BRANCH}}
@@ -30,7 +44,3 @@ steps:
candidate: '${{build_image}}'
tag: '${{CF_BRANCH}}'
registry: dockerhub
when:
branch:
only:
- develop
+46
View File
@@ -0,0 +1,46 @@
version: '1.0'
steps:
build_backend:
image: portainer/golang-builder:ci
working_directory: ${{main_clone}}
commands:
- mkdir -p /go/src/github.com/${{CF_REPO_OWNER}}
- ln -s /codefresh/volume/${{CF_REPO_NAME}}/api /go/src/github.com/${{CF_REPO_OWNER}}/${{CF_REPO_NAME}}
- /build.sh api/cmd/portainer
build_frontend:
image: portainer/angular-builder:latest
working_directory: ${{build_backend}}
commands:
- yarn
- yarn grunt build-webapp
- mv api/cmd/portainer/portainer dist/
get_docker_version:
image: alpine:3.7
working_directory: ${{build_frontend}}
commands:
- cf_export DOCKER_VERSION=`cat gruntfile.js | grep -m 1 'shippedDockerVersion' | cut -d\' -f2`
download_docker_binary:
image: busybox
working_directory: ${{build_frontend}}
commands:
- echo ${{DOCKER_VERSION}}
- wget -O /tmp/docker-binaries.tgz https://download.docker.com/linux/static/stable/x86_64/docker-${{DOCKER_VERSION}}.tgz
- tar -xf /tmp/docker-binaries.tgz -C /tmp
- mv /tmp/docker/docker dist/
build_image:
type: build
working_directory: ${{download_docker_binary}}
dockerfile: ./build/linux/Dockerfile
image_name: portainer/portainer
tag: ${{CF_BRANCH}}
push_image:
type: push
candidate: '${{build_image}}'
tag: 'pr${{CF_PULL_REQUEST_NUMBER}}'
registry: dockerhub
+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.
+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/
-3
View File
@@ -30,9 +30,6 @@ You can have a use Github filters to list these issues:
* intermediate labeled issues: https://github.com/portainer/portainer/labels/exp%2Fintermediate
* advanced labeled issues: https://github.com/portainer/portainer/labels/exp%2Fadvanced
### Linting
Please check your code using `grunt lint` before submitting your pull requests.
### Commit Message Format
+2 -44
View File
@@ -1,4 +1,4 @@
Portainer: Copyright (c) 2016 Portainer.io
Copyright (c) 2018 Portainer.io
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
@@ -14,46 +14,4 @@ freely, subject to the following restrictions:
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
Portainer contains code which was originally under this license:
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (portainer.io)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
rdash-angular: Copyright (c) [2014] [Elliot Hesp]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
3. This notice may not be removed or altered from any source distribution.
+27 -8
View File
@@ -1,30 +1,39 @@
<p align="center">
<img title="portainer" src='http://portainer.io/images/logo_alt.png' />
<img title="portainer" src='https://portainer.io/images/logo_alt.png' />
</p>
[![Docker Pulls](https://img.shields.io/docker/pulls/portainer/portainer.svg)](https://hub.docker.com/r/portainer/portainer/)
[![Microbadger](https://images.microbadger.com/badges/image/portainer/portainer.svg)](http://microbadger.com/images/portainer/portainer "Image size")
[![Documentation Status](https://readthedocs.org/projects/portainer/badge/?version=stable)](http://portainer.readthedocs.io/en/stable/?badge=stable)
[![Codefresh build status]( https://g.codefresh.io/api/badges/build?repoOwner=portainer&repoName=portainer&branch=develop&pipelineName=portainer-ci&accountName=deviantony&type=cf-1)]( https://g.codefresh.io/repositories/portainer/portainer/builds?filter=trigger:build;branch:develop;service:5922a08a3a1aab000116fcc6~portainer-ci)
[![Slack](http://portainer.io/slack/badge.svg)](http://portainer.io/slack/)
[![Code Climate](https://codeclimate.com/github/portainer/portainer/badges/gpa.svg)](https://codeclimate.com/github/portainer/portainer)
[![Slack](https://portainer.io/slack/badge.svg)](https://portainer.io/slack/)
[![Gitter](https://badges.gitter.im/portainer/Lobby.svg)](https://gitter.im/portainer/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6)
**_Portainer_** is a lightweight management UI which allows you to **easily** manage your Docker host or Swarm cluster.
**_Portainer_** is a lightweight management UI which allows you to **easily** manage your different Docker environments (Docker hosts or Swarm clusters).
**_Portainer_** is meant to be as **simple** to deploy as it is to use. It consists of a single container that can run on any Docker engine (Docker for Linux and Docker for Windows are supported).
**_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).
**_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*.
**_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*.
## Demo
<img src="http://portainer.io/images/screenshots/portainer.gif" width="77%"/>
<img src="https://portainer.io/images/screenshots/portainer.gif" width="77%"/>
You can try out the public demo instance: http://demo.portainer.io/ (login with the username **admin** and the password **tryportainer**).
Please note that the public demo cluster is **reset every 15min**.
Alternatively, you can deploy a copy of the demo stack inside a [play-with-docker (PWD)](https://labs.play-with-docker.com) playground:
- Browse [PWD/?stack=portainer-demo/play-with-docker/docker-stack.yml](http://play-with-docker.com/?stack=https://raw.githubusercontent.com/portainer/portainer-demo/master/play-with-docker/docker-stack.yml)
- Sign in with your [Docker ID](https://docs.docker.com/docker-id)
- Follow [these](https://github.com/portainer/portainer-demo/blob/master/play-with-docker/docker-stack.yml#L5-L8) steps.
Unlike the public demo, the playground sessions are deleted after 4 hours. Apart from that, all the settings are same, including default credentials.
## Getting started
* [Deploy Portainer](https://portainer.readthedocs.io/en/latest/deployment.html)
@@ -34,8 +43,8 @@ Please note that the public demo cluster is **reset every 15min**.
* Issues: https://github.com/portainer/portainer/issues
* FAQ: https://portainer.readthedocs.io/en/latest/faq.html
* Slack (chat): https://portainer.io/slack/
* Gitter (chat): https://gitter.im/portainer/Lobby
* Slack: http://portainer.io/slack/
## Reporting bugs and contributing
@@ -47,8 +56,18 @@ Please note that the public demo cluster is **reset every 15min**.
**_Portainer_** has full support for the following Docker versions:
* Docker 1.10 to the latest version
* Docker Swarm >= 1.2.3
* Standalone Docker Swarm >= 1.2.3 _(**NOTE:** Use of Standalone Docker Swarm is being discouraged since the introduction of built-in Swarm Mode in Docker. While older versions of Portainer had support for Standalone Docker Swarm, Portainer 1.17.0 and newer **do not** support it. However, the built-in Swarm Mode of Docker is fully supported.)_
Partial support for the following Docker versions (some features may not be available):
* Docker 1.9
## Licensing
Portainer is licensed under the zlib license. See [LICENSE](./LICENSE) for reference.
Portainer also contains the following code, which is licensed under the [MIT license](https://opensource.org/licenses/MIT):
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (portainer.io)
rdash-angular: Copyright (c) [2014] [Elliot Hesp]
+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) ([]byte, error) {
var buffer bytes.Buffer
tarWriter := tar.NewWriter(&buffer)
header := &tar.Header{
Name: fileName,
Mode: 0600,
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
}
+171 -85
View File
@@ -2,68 +2,68 @@ package bolt
import (
"log"
"os"
"path"
"time"
"github.com/boltdb/bolt"
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/dockerhub"
"github.com/portainer/portainer/bolt/endpoint"
"github.com/portainer/portainer/bolt/endpointgroup"
"github.com/portainer/portainer/bolt/migrator"
"github.com/portainer/portainer/bolt/registry"
"github.com/portainer/portainer/bolt/resourcecontrol"
"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"
)
const (
databaseFileName = "portainer.db"
)
// Store defines the implementation of portainer.DataStore using
// BoltDB as the storage system.
type Store struct {
// Path where is stored the BoltDB database.
Path string
// Services
UserService *UserService
TeamService *TeamService
TeamMembershipService *TeamMembershipService
EndpointService *EndpointService
ResourceControlService *ResourceControlService
VersionService *VersionService
SettingsService *SettingsService
db *bolt.DB
checkForDataMigration bool
path string
db *bolt.DB
checkForDataMigration bool
fileService portainer.FileService
DockerHubService *dockerhub.Service
EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.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
}
const (
databaseFileName = "portainer.db"
versionBucketName = "version"
userBucketName = "users"
teamBucketName = "teams"
teamMembershipBucketName = "team_membership"
endpointBucketName = "endpoints"
resourceControlBucketName = "resource_control"
settingsBucketName = "settings"
)
// NewStore initializes a new Store and the associated services
func NewStore(storePath string) (*Store, error) {
func NewStore(storePath string, fileService portainer.FileService) (*Store, error) {
store := &Store{
Path: storePath,
UserService: &UserService{},
TeamService: &TeamService{},
TeamMembershipService: &TeamMembershipService{},
EndpointService: &EndpointService{},
ResourceControlService: &ResourceControlService{},
VersionService: &VersionService{},
SettingsService: &SettingsService{},
path: storePath,
fileService: fileService,
}
store.UserService.store = store
store.TeamService.store = store
store.TeamMembershipService.store = store
store.EndpointService.store = store
store.ResourceControlService.store = store
store.VersionService.store = store
store.SettingsService.store = store
_, err := os.Stat(storePath + "/" + databaseFileName)
if err != nil && os.IsNotExist(err) {
store.checkForDataMigration = false
} else if err != nil {
databasePath := path.Join(storePath, databaseFileName)
databaseFileExists, err := fileService.FileExists(databasePath)
if err != nil {
return nil, err
}
if !databaseFileExists {
store.checkForDataMigration = false
} else {
store.checkForDataMigration = true
}
@@ -73,43 +73,37 @@ func NewStore(storePath string) (*Store, error) {
// Open opens and initializes the BoltDB database.
func (store *Store) Open() error {
path := store.Path + "/" + databaseFileName
db, err := bolt.Open(path, 0600, &bolt.Options{Timeout: 1 * time.Second})
databasePath := path.Join(store.path, databaseFileName)
db, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second})
if err != nil {
return err
}
store.db = db
return db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(versionBucketName))
if err != nil {
return err
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{},
}
_, err = tx.CreateBucketIfNotExists([]byte(userBucketName))
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists([]byte(teamBucketName))
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists([]byte(endpointBucketName))
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists([]byte(resourceControlBucketName))
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists([]byte(teamMembershipBucketName))
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists([]byte(settingsBucketName))
if err != nil {
return err
}
return nil
})
return store.EndpointGroupService.CreateEndpointGroup(unassignedGroup)
}
return nil
}
// Close closes the BoltDB database.
@@ -123,28 +117,120 @@ func (store *Store) Close() error {
// MigrateData automatically migrate the data based on the DBVersion.
func (store *Store) MigrateData() error {
if !store.checkForDataMigration {
err := store.VersionService.StoreDBVersion(portainer.DBVersion)
if err != nil {
return err
}
return nil
return store.VersionService.StoreDBVersion(portainer.DBVersion)
}
version, err := store.VersionService.DBVersion()
if err == portainer.ErrDBVersionNotFound {
if err == portainer.ErrObjectNotFound {
version = 0
} else if err != nil {
return err
}
if version < portainer.DBVersion {
migratorParams := &migrator.Parameters{
DB: store.db,
DatabaseVersion: version,
EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService,
ResourceControlService: store.ResourceControlService,
SettingsService: store.SettingsService,
StackService: store.StackService,
UserService: store.UserService,
VersionService: store.VersionService,
FileService: store.fileService,
}
migrator := migrator.NewMigrator(migratorParams)
log.Printf("Migrating database from version %v to %v.\n", version, portainer.DBVersion)
migrator := NewMigrator(store, version)
err = migrator.Migrate()
if err != nil {
log.Printf("An error occurred during database migration: %s\n", err)
return err
}
}
return nil
}
func (store *Store) initServices() error {
dockerhubService, err := dockerhub.NewService(store.db)
if err != nil {
return err
}
store.DockerHubService = dockerhubService
endpointgroupService, err := endpointgroup.NewService(store.db)
if err != nil {
return err
}
store.EndpointGroupService = endpointgroupService
endpointService, err := endpoint.NewService(store.db)
if err != nil {
return err
}
store.EndpointService = endpointService
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
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
})
}
-154
View File
@@ -1,154 +0,0 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// EndpointService represents a service for managing users.
type EndpointService struct {
store *Store
}
// Endpoint returns an endpoint by ID.
func (service *EndpointService) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
value := bucket.Get(internal.Itob(int(ID)))
if value == nil {
return portainer.ErrEndpointNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var endpoint portainer.Endpoint
err = internal.UnmarshalEndpoint(data, &endpoint)
if err != nil {
return nil, err
}
return &endpoint, nil
}
// Endpoints return an array containing all the endpoints.
func (service *EndpointService) Endpoints() ([]portainer.Endpoint, error) {
var endpoints = make([]portainer.Endpoint, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var endpoint portainer.Endpoint
err := internal.UnmarshalEndpoint(v, &endpoint)
if err != nil {
return err
}
endpoints = append(endpoints, endpoint)
}
return nil
})
if err != nil {
return nil, err
}
return endpoints, nil
}
// Synchronize creates, updates and deletes endpoints inside a single transaction.
func (service *EndpointService) Synchronize(toCreate, toUpdate, toDelete []*portainer.Endpoint) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
for _, endpoint := range toCreate {
err := storeNewEndpoint(endpoint, bucket)
if err != nil {
return err
}
}
for _, endpoint := range toUpdate {
err := marshalAndStoreEndpoint(endpoint, bucket)
if err != nil {
return err
}
}
for _, endpoint := range toDelete {
err := bucket.Delete(internal.Itob(int(endpoint.ID)))
if err != nil {
return err
}
}
return nil
})
}
// CreateEndpoint assign an ID to a new endpoint and saves it.
func (service *EndpointService) CreateEndpoint(endpoint *portainer.Endpoint) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
err := storeNewEndpoint(endpoint, bucket)
if err != nil {
return err
}
return nil
})
}
// UpdateEndpoint updates an endpoint.
func (service *EndpointService) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
data, err := internal.MarshalEndpoint(endpoint)
if err != nil {
return err
}
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
err = bucket.Put(internal.Itob(int(ID)), data)
if err != nil {
return err
}
return nil
})
}
// DeleteEndpoint deletes an endpoint.
func (service *EndpointService) DeleteEndpoint(ID portainer.EndpointID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
err := bucket.Delete(internal.Itob(int(ID)))
if err != nil {
return err
}
return nil
})
}
func marshalAndStoreEndpoint(endpoint *portainer.Endpoint, bucket *bolt.Bucket) error {
data, err := internal.MarshalEndpoint(endpoint)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(endpoint.ID)), data)
if err != nil {
return err
}
return nil
}
func storeNewEndpoint(endpoint *portainer.Endpoint, bucket *bolt.Bucket) error {
id, _ := bucket.NextSequence()
endpoint.ID = portainer.EndpointID(id)
return marshalAndStoreEndpoint(endpoint, bucket)
}
+95
View File
@@ -0,0 +1,95 @@
package endpointgroup
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "endpoint_groups"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// EndpointGroup returns an endpoint group by ID.
func (service *Service) EndpointGroup(ID portainer.EndpointGroupID) (*portainer.EndpointGroup, error) {
var endpointGroup portainer.EndpointGroup
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &endpointGroup)
if err != nil {
return nil, err
}
return &endpointGroup, nil
}
// UpdateEndpointGroup updates an endpoint group.
func (service *Service) UpdateEndpointGroup(ID portainer.EndpointGroupID, endpointGroup *portainer.EndpointGroup) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, endpointGroup)
}
// DeleteEndpointGroup deletes an endpoint group.
func (service *Service) DeleteEndpointGroup(ID portainer.EndpointGroupID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
// EndpointGroups return an array containing all the endpoint groups.
func (service *Service) EndpointGroups() ([]portainer.EndpointGroup, error) {
var endpointGroups = make([]portainer.EndpointGroup, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var endpointGroup portainer.EndpointGroup
err := internal.UnmarshalObject(v, &endpointGroup)
if err != nil {
return err
}
endpointGroups = append(endpointGroups, endpointGroup)
}
return nil
})
return endpointGroups, err
}
// CreateEndpointGroup assign an ID to a new endpoint group and saves it.
func (service *Service) CreateEndpointGroup(endpointGroup *portainer.EndpointGroup) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
endpointGroup.ID = portainer.EndpointGroupID(id)
data, err := internal.MarshalObject(endpointGroup)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(endpointGroup.ID)), data)
})
}
+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
}
-77
View File
@@ -1,77 +0,0 @@
package internal
import (
"github.com/portainer/portainer"
"encoding/binary"
"encoding/json"
)
// MarshalUser encodes a user to binary format.
func MarshalUser(user *portainer.User) ([]byte, error) {
return json.Marshal(user)
}
// UnmarshalUser decodes a user from a binary data.
func UnmarshalUser(data []byte, user *portainer.User) error {
return json.Unmarshal(data, user)
}
// MarshalTeam encodes a team to binary format.
func MarshalTeam(team *portainer.Team) ([]byte, error) {
return json.Marshal(team)
}
// UnmarshalTeam decodes a team from a binary data.
func UnmarshalTeam(data []byte, team *portainer.Team) error {
return json.Unmarshal(data, team)
}
// MarshalTeamMembership encodes a team membership to binary format.
func MarshalTeamMembership(membership *portainer.TeamMembership) ([]byte, error) {
return json.Marshal(membership)
}
// UnmarshalTeamMembership decodes a team membership from a binary data.
func UnmarshalTeamMembership(data []byte, membership *portainer.TeamMembership) error {
return json.Unmarshal(data, membership)
}
// MarshalEndpoint encodes an endpoint to binary format.
func MarshalEndpoint(endpoint *portainer.Endpoint) ([]byte, error) {
return json.Marshal(endpoint)
}
// UnmarshalEndpoint decodes an endpoint from a binary data.
func UnmarshalEndpoint(data []byte, endpoint *portainer.Endpoint) error {
return json.Unmarshal(data, endpoint)
}
// MarshalResourceControl encodes a resource control object to binary format.
func MarshalResourceControl(rc *portainer.ResourceControl) ([]byte, error) {
return json.Marshal(rc)
}
// UnmarshalResourceControl decodes a resource control object from a binary data.
func UnmarshalResourceControl(data []byte, rc *portainer.ResourceControl) error {
return json.Unmarshal(data, rc)
}
// MarshalSettings encodes a settings object to binary format.
func MarshalSettings(settings *portainer.Settings) ([]byte, error) {
return json.Marshal(settings)
}
// UnmarshalSettings decodes a settings object from a binary data.
func UnmarshalSettings(data []byte, settings *portainer.Settings) error {
return json.Unmarshal(data, settings)
}
// Itob returns an 8-byte big endian representation of v.
// This function is typically used for encoding integer IDs to byte slices
// so that they can be used as BoltDB keys.
func Itob(v int) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, uint64(v))
return b
}
+15
View File
@@ -0,0 +1,15 @@
package internal
import (
"encoding/json"
)
// MarshalObject encodes an object to binary format
func MarshalObject(object interface{}) ([]byte, error) {
return json.Marshal(object)
}
// UnmarshalObject decodes an object from binary data
func UnmarshalObject(data []byte, object interface{}) error {
return json.Unmarshal(data, object)
}
-55
View File
@@ -1,55 +0,0 @@
package bolt
import "github.com/portainer/portainer"
// Migrator defines a service to migrate data after a Portainer version update.
type Migrator struct {
UserService *UserService
EndpointService *EndpointService
ResourceControlService *ResourceControlService
VersionService *VersionService
CurrentDBVersion int
store *Store
}
// NewMigrator creates a new Migrator.
func NewMigrator(store *Store, version int) *Migrator {
return &Migrator{
UserService: store.UserService,
EndpointService: store.EndpointService,
ResourceControlService: store.ResourceControlService,
VersionService: store.VersionService,
CurrentDBVersion: version,
store: store,
}
}
// Migrate checks the database version and migrate the existing data to the most recent data model.
func (m *Migrator) Migrate() error {
// Portainer < 1.12
if m.CurrentDBVersion == 0 {
err := m.updateAdminUserToDBVersion1()
if err != nil {
return err
}
}
// Portainer 1.12.x
if m.CurrentDBVersion == 1 {
err := m.updateResourceControlsToDBVersion2()
if err != nil {
return err
}
err = m.updateEndpointsToDBVersion2()
if err != nil {
return err
}
}
err := m.VersionService.StoreDBVersion(portainer.DBVersion)
if err != nil {
return err
}
return nil
}
@@ -1,19 +1,20 @@
package bolt
package migrator
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/user"
)
func (m *Migrator) updateAdminUserToDBVersion1() error {
u, err := m.UserService.UserByUsername("admin")
u, err := m.userService.UserByUsername("admin")
if err == nil {
admin := &portainer.User{
Username: "admin",
Password: u.Password,
Role: portainer.AdministratorRole,
}
err = m.UserService.CreateUser(admin)
err = m.userService.CreateUser(admin)
if err != nil {
return err
}
@@ -21,19 +22,15 @@ func (m *Migrator) updateAdminUserToDBVersion1() error {
if err != nil {
return err
}
} else if err != nil && err != portainer.ErrUserNotFound {
} else if err != nil && err != portainer.ErrObjectNotFound {
return err
}
return nil
}
func (m *Migrator) removeLegacyAdminUser() error {
return m.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
err := bucket.Delete([]byte("admin"))
if err != nil {
return err
}
return nil
return m.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(user.BucketName))
return bucket.Delete([]byte("admin"))
})
}
@@ -1,4 +1,4 @@
package bolt
package migrator
import (
"github.com/boltdb/bolt"
@@ -16,7 +16,7 @@ func (m *Migrator) updateResourceControlsToDBVersion2() error {
resourceControl.SubResourceIDs = []string{}
resourceControl.TeamAccesses = []portainer.TeamResourceAccess{}
owner, err := m.UserService.User(resourceControl.OwnerID)
owner, err := m.userService.User(resourceControl.OwnerID)
if err != nil {
return err
}
@@ -33,7 +33,7 @@ func (m *Migrator) updateResourceControlsToDBVersion2() error {
resourceControl.UserAccesses = []portainer.UserResourceAccess{userAccess}
}
err = m.ResourceControlService.CreateResourceControl(&resourceControl)
err = m.resourceControlService.CreateResourceControl(&resourceControl)
if err != nil {
return err
}
@@ -43,14 +43,14 @@ func (m *Migrator) updateResourceControlsToDBVersion2() error {
}
func (m *Migrator) updateEndpointsToDBVersion2() error {
legacyEndpoints, err := m.EndpointService.Endpoints()
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
endpoint.AuthorizedTeams = []portainer.TeamID{}
err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
@@ -61,12 +61,12 @@ func (m *Migrator) updateEndpointsToDBVersion2() error {
func (m *Migrator) retrieveLegacyResourceControls() ([]portainer.ResourceControl, error) {
legacyResourceControls := make([]portainer.ResourceControl, 0)
err := m.store.db.View(func(tx *bolt.Tx) error {
err := m.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte("containerResourceControl"))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var resourceControl portainer.ResourceControl
err := internal.UnmarshalResourceControl(v, &resourceControl)
err := internal.UnmarshalObject(v, &resourceControl)
if err != nil {
return err
}
@@ -78,7 +78,7 @@ func (m *Migrator) retrieveLegacyResourceControls() ([]portainer.ResourceControl
cursor = bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var resourceControl portainer.ResourceControl
err := internal.UnmarshalResourceControl(v, &resourceControl)
err := internal.UnmarshalObject(v, &resourceControl)
if err != nil {
return err
}
@@ -90,7 +90,7 @@ func (m *Migrator) retrieveLegacyResourceControls() ([]portainer.ResourceControl
cursor = bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var resourceControl portainer.ResourceControl
err := internal.UnmarshalResourceControl(v, &resourceControl)
err := internal.UnmarshalObject(v, &resourceControl)
if err != nil {
return err
}
+28
View File
@@ -0,0 +1,28 @@
package migrator
import "github.com/portainer/portainer"
func (m *Migrator) updateEndpointsToVersion11() error {
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
if endpoint.Type == portainer.AgentOnDockerEnvironment {
endpoint.TLSConfig.TLS = true
endpoint.TLSConfig.TLSSkipVerify = true
} else {
if endpoint.TLSConfig.TLSSkipVerify && !endpoint.TLSConfig.TLS {
endpoint.TLSConfig.TLSSkipVerify = false
}
}
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}
+127
View File
@@ -0,0 +1,127 @@
package migrator
import (
"strconv"
"strings"
"github.com/boltdb/bolt"
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/portainer/portainer/bolt/stack"
)
func (m *Migrator) updateEndpointsToVersion12() error {
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
endpoint.Tags = []string{}
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) updateEndpointGroupsToVersion12() error {
legacyEndpointGroups, err := m.endpointGroupService.EndpointGroups()
if err != nil {
return err
}
for _, group := range legacyEndpointGroups {
group.Tags = []string{}
err = m.endpointGroupService.UpdateEndpointGroup(group.ID, &group)
if err != nil {
return err
}
}
return nil
}
type legacyStack struct {
ID string `json:"Id"`
Name string `json:"Name"`
EndpointID portainer.EndpointID `json:"EndpointId"`
SwarmID string `json:"SwarmId"`
EntryPoint string `json:"EntryPoint"`
Env []portainer.Pair `json:"Env"`
ProjectPath string
}
func (m *Migrator) updateStacksToVersion12() error {
legacyStacks, err := m.retrieveLegacyStacks()
if err != nil {
return err
}
for _, legacyStack := range legacyStacks {
err := m.convertLegacyStack(&legacyStack)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) convertLegacyStack(s *legacyStack) error {
stackID := m.stackService.GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Name: s.Name,
Type: portainer.DockerSwarmStack,
SwarmID: s.SwarmID,
EndpointID: 0,
EntryPoint: s.EntryPoint,
Env: s.Env,
}
stack.ProjectPath = strings.Replace(s.ProjectPath, s.ID, strconv.Itoa(stackID), 1)
err := m.fileService.Rename(s.ProjectPath, stack.ProjectPath)
if err != nil {
return err
}
err = m.deleteLegacyStack(s.ID)
if err != nil {
return err
}
return m.stackService.CreateStack(stack)
}
func (m *Migrator) deleteLegacyStack(legacyID string) error {
return m.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(stack.BucketName))
return bucket.Delete([]byte(legacyID))
})
}
func (m *Migrator) retrieveLegacyStacks() ([]legacyStack, error) {
var legacyStacks = make([]legacyStack, 0)
err := m.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(stack.BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var stack legacyStack
err := internal.UnmarshalObject(v, &stack)
if err != nil {
return err
}
legacyStacks = append(legacyStacks, stack)
}
return nil
})
return legacyStacks, err
}
+17
View File
@@ -0,0 +1,17 @@
package migrator
import "github.com/portainer/portainer"
func (m *Migrator) updateSettingsToVersion13() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.LDAPSettings.AutoCreateUsers = false
legacySettings.LDAPSettings.GroupSearchSettings = []portainer.LDAPGroupSearchSettings{
portainer.LDAPGroupSearchSettings{},
}
return m.settingsService.UpdateSettings(legacySettings)
}
+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
}
+182
View File
@@ -0,0 +1,182 @@
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/resourcecontrol"
"github.com/portainer/portainer/bolt/settings"
"github.com/portainer/portainer/bolt/stack"
"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
resourceControlService *resourcecontrol.Service
settingsService *settings.Service
stackService *stack.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
ResourceControlService *resourcecontrol.Service
SettingsService *settings.Service
StackService *stack.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,
resourceControlService: parameters.ResourceControlService,
settingsService: parameters.SettingsService,
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
}
}
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)
}
-148
View File
@@ -1,148 +0,0 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// ResourceControlService represents a service for managing resource controls.
type ResourceControlService struct {
store *Store
}
// ResourceControl returns a ResourceControl object by ID
func (service *ResourceControlService) ResourceControl(ID portainer.ResourceControlID) (*portainer.ResourceControl, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(resourceControlBucketName))
value := bucket.Get(internal.Itob(int(ID)))
if value == nil {
return portainer.ErrResourceControlNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var resourceControl portainer.ResourceControl
err = internal.UnmarshalResourceControl(data, &resourceControl)
if err != nil {
return nil, err
}
return &resourceControl, nil
}
// ResourceControlByResourceID returns a ResourceControl object by checking if the resourceID is equal
// to the main ResourceID or in SubResourceIDs
func (service *ResourceControlService) ResourceControlByResourceID(resourceID string) (*portainer.ResourceControl, error) {
var resourceControl *portainer.ResourceControl
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(resourceControlBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var rc portainer.ResourceControl
err := internal.UnmarshalResourceControl(v, &rc)
if err != nil {
return err
}
if rc.ResourceID == resourceID {
resourceControl = &rc
}
for _, subResourceID := range rc.SubResourceIDs {
if subResourceID == resourceID {
resourceControl = &rc
}
}
}
if resourceControl == nil {
return portainer.ErrResourceControlNotFound
}
return nil
})
if err != nil {
return nil, err
}
return resourceControl, nil
}
// ResourceControls returns all the ResourceControl objects
func (service *ResourceControlService) ResourceControls() ([]portainer.ResourceControl, error) {
var rcs = make([]portainer.ResourceControl, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(resourceControlBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var resourceControl portainer.ResourceControl
err := internal.UnmarshalResourceControl(v, &resourceControl)
if err != nil {
return err
}
rcs = append(rcs, resourceControl)
}
return nil
})
if err != nil {
return nil, err
}
return rcs, nil
}
// CreateResourceControl creates a new ResourceControl object
func (service *ResourceControlService) CreateResourceControl(resourceControl *portainer.ResourceControl) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(resourceControlBucketName))
id, _ := bucket.NextSequence()
resourceControl.ID = portainer.ResourceControlID(id)
data, err := internal.MarshalResourceControl(resourceControl)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(resourceControl.ID)), data)
if err != nil {
return err
}
return nil
})
}
// UpdateResourceControl saves a ResourceControl object.
func (service *ResourceControlService) UpdateResourceControl(ID portainer.ResourceControlID, resourceControl *portainer.ResourceControl) error {
data, err := internal.MarshalResourceControl(resourceControl)
if err != nil {
return err
}
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(resourceControlBucketName))
err = bucket.Put(internal.Itob(int(ID)), data)
if err != nil {
return err
}
return nil
})
}
// DeleteResourceControl deletes a ResourceControl object by ID
func (service *ResourceControlService) DeleteResourceControl(ID portainer.ResourceControlID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(resourceControlBucketName))
err := bucket.Delete(internal.Itob(int(ID)))
if err != nil {
return err
}
return nil
})
}
+134
View File
@@ -0,0 +1,134 @@
package resourcecontrol
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "resource_control"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// ResourceControl returns a ResourceControl object by ID
func (service *Service) ResourceControl(ID portainer.ResourceControlID) (*portainer.ResourceControl, error) {
var resourceControl portainer.ResourceControl
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &resourceControl)
if err != nil {
return nil, err
}
return &resourceControl, nil
}
// ResourceControlByResourceID returns a ResourceControl object by checking if the resourceID is equal
// to the main ResourceID or in SubResourceIDs
func (service *Service) ResourceControlByResourceID(resourceID string) (*portainer.ResourceControl, error) {
var resourceControl *portainer.ResourceControl
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var rc portainer.ResourceControl
err := internal.UnmarshalObject(v, &rc)
if err != nil {
return err
}
if rc.ResourceID == resourceID {
resourceControl = &rc
break
}
for _, subResourceID := range rc.SubResourceIDs {
if subResourceID == resourceID {
resourceControl = &rc
break
}
}
}
if resourceControl == nil {
return portainer.ErrObjectNotFound
}
return nil
})
return resourceControl, err
}
// ResourceControls returns all the ResourceControl objects
func (service *Service) ResourceControls() ([]portainer.ResourceControl, error) {
var rcs = make([]portainer.ResourceControl, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var resourceControl portainer.ResourceControl
err := internal.UnmarshalObject(v, &resourceControl)
if err != nil {
return err
}
rcs = append(rcs, resourceControl)
}
return nil
})
return rcs, err
}
// CreateResourceControl creates a new ResourceControl object
func (service *Service) CreateResourceControl(resourceControl *portainer.ResourceControl) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
resourceControl.ID = portainer.ResourceControlID(id)
data, err := internal.MarshalObject(resourceControl)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(resourceControl.ID)), data)
})
}
// UpdateResourceControl saves a ResourceControl object.
func (service *Service) UpdateResourceControl(ID portainer.ResourceControlID, resourceControl *portainer.ResourceControl) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, resourceControl)
}
// DeleteResourceControl deletes a ResourceControl object by ID
func (service *Service) DeleteResourceControl(ID portainer.ResourceControlID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
+48
View File
@@ -0,0 +1,48 @@
package settings
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "settings"
settingsKey = "SETTINGS"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// Settings retrieve the settings object.
func (service *Service) Settings() (*portainer.Settings, error) {
var settings portainer.Settings
err := internal.GetObject(service.db, BucketName, []byte(settingsKey), &settings)
if err != nil {
return nil, err
}
return &settings, nil
}
// UpdateSettings persists a Settings object.
func (service *Service) UpdateSettings(settings *portainer.Settings) error {
return internal.UpdateObject(service.db, BucketName, []byte(settingsKey), settings)
}
-61
View File
@@ -1,61 +0,0 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// SettingsService represents a service to manage application settings.
type SettingsService struct {
store *Store
}
const (
dbSettingsKey = "SETTINGS"
)
// Settings retrieve the settings object.
func (service *SettingsService) Settings() (*portainer.Settings, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(settingsBucketName))
value := bucket.Get([]byte(dbSettingsKey))
if value == nil {
return portainer.ErrSettingsNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var settings portainer.Settings
err = internal.UnmarshalSettings(data, &settings)
if err != nil {
return nil, err
}
return &settings, nil
}
// StoreSettings persists a Settings object.
func (service *SettingsService) StoreSettings(settings *portainer.Settings) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(settingsBucketName))
data, err := internal.MarshalSettings(settings)
if err != nil {
return err
}
err = bucket.Put([]byte(dbSettingsKey), data)
if err != nil {
return err
}
return nil
})
}
+134
View File
@@ -0,0 +1,134 @@
package stack
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "stacks"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// Stack returns a stack object by ID.
func (service *Service) Stack(ID portainer.StackID) (*portainer.Stack, error) {
var stack portainer.Stack
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &stack)
if err != nil {
return nil, err
}
return &stack, nil
}
// StackByName returns a stack object by name.
func (service *Service) StackByName(name string) (*portainer.Stack, error) {
var stack *portainer.Stack
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var t portainer.Stack
err := internal.UnmarshalObject(v, &t)
if err != nil {
return err
}
if t.Name == name {
stack = &t
break
}
}
if stack == nil {
return portainer.ErrObjectNotFound
}
return nil
})
return stack, err
}
// Stacks returns an array containing all the stacks.
func (service *Service) Stacks() ([]portainer.Stack, error) {
var stacks = make([]portainer.Stack, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var stack portainer.Stack
err := internal.UnmarshalObject(v, &stack)
if err != nil {
return err
}
stacks = append(stacks, stack)
}
return nil
})
return stacks, err
}
// GetNextIdentifier returns the next identifier for a stack.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
}
// CreateStack creates a new stack.
func (service *Service) CreateStack(stack *portainer.Stack) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
// We manually manage sequences for stacks
err := bucket.SetSequence(uint64(stack.ID))
if err != nil {
return err
}
data, err := internal.MarshalObject(stack)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(stack.ID)), data)
})
}
// UpdateStack updates a stack.
func (service *Service) UpdateStack(ID portainer.StackID, stack *portainer.Stack) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, stack)
}
// DeleteStack deletes a stack.
func (service *Service) DeleteStack(ID portainer.StackID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
+76
View File
@@ -0,0 +1,76 @@
package tag
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "tags"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// Tags return an array containing all the tags.
func (service *Service) Tags() ([]portainer.Tag, error) {
var tags = make([]portainer.Tag, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var tag portainer.Tag
err := internal.UnmarshalObject(v, &tag)
if err != nil {
return err
}
tags = append(tags, tag)
}
return nil
})
return tags, err
}
// CreateTag creates a new tag.
func (service *Service) CreateTag(tag *portainer.Tag) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
tag.ID = portainer.TagID(id)
data, err := internal.MarshalObject(tag)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(tag.ID)), data)
})
}
// DeleteTag deletes a tag.
func (service *Service) DeleteTag(ID portainer.TagID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
+126
View File
@@ -0,0 +1,126 @@
package team
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "teams"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// Team returns a Team by ID
func (service *Service) Team(ID portainer.TeamID) (*portainer.Team, error) {
var team portainer.Team
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &team)
if err != nil {
return nil, err
}
return &team, nil
}
// TeamByName returns a team by name.
func (service *Service) TeamByName(name string) (*portainer.Team, error) {
var team *portainer.Team
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var t portainer.Team
err := internal.UnmarshalObject(v, &t)
if err != nil {
return err
}
if t.Name == name {
team = &t
break
}
}
if team == nil {
return portainer.ErrObjectNotFound
}
return nil
})
return team, err
}
// Teams return an array containing all the teams.
func (service *Service) Teams() ([]portainer.Team, error) {
var teams = make([]portainer.Team, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var team portainer.Team
err := internal.UnmarshalObject(v, &team)
if err != nil {
return err
}
teams = append(teams, team)
}
return nil
})
return teams, err
}
// UpdateTeam saves a Team.
func (service *Service) UpdateTeam(ID portainer.TeamID, team *portainer.Team) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, team)
}
// CreateTeam creates a new Team.
func (service *Service) CreateTeam(team *portainer.Team) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
team.ID = portainer.TeamID(id)
data, err := internal.MarshalObject(team)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(team.ID)), data)
})
}
// DeleteTeam deletes a Team.
func (service *Service) DeleteTeam(ID portainer.TeamID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
-217
View File
@@ -1,217 +0,0 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// TeamMembershipService represents a service for managing TeamMembership objects.
type TeamMembershipService struct {
store *Store
}
// TeamMembership returns a TeamMembership object by ID
func (service *TeamMembershipService) TeamMembership(ID portainer.TeamMembershipID) (*portainer.TeamMembership, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
value := bucket.Get(internal.Itob(int(ID)))
if value == nil {
return portainer.ErrTeamMembershipNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var membership portainer.TeamMembership
err = internal.UnmarshalTeamMembership(data, &membership)
if err != nil {
return nil, err
}
return &membership, nil
}
// TeamMemberships return an array containing all the TeamMembership objects.
func (service *TeamMembershipService) TeamMemberships() ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalTeamMembership(v, &membership)
if err != nil {
return err
}
memberships = append(memberships, membership)
}
return nil
})
if err != nil {
return nil, err
}
return memberships, nil
}
// TeamMembershipsByUserID return an array containing all the TeamMembership objects where the specified userID is present.
func (service *TeamMembershipService) TeamMembershipsByUserID(userID portainer.UserID) ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalTeamMembership(v, &membership)
if err != nil {
return err
}
if membership.UserID == userID {
memberships = append(memberships, membership)
}
}
return nil
})
if err != nil {
return nil, err
}
return memberships, nil
}
// TeamMembershipsByTeamID return an array containing all the TeamMembership objects where the specified teamID is present.
func (service *TeamMembershipService) TeamMembershipsByTeamID(teamID portainer.TeamID) ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalTeamMembership(v, &membership)
if err != nil {
return err
}
if membership.TeamID == teamID {
memberships = append(memberships, membership)
}
}
return nil
})
if err != nil {
return nil, err
}
return memberships, nil
}
// UpdateTeamMembership saves a TeamMembership object.
func (service *TeamMembershipService) UpdateTeamMembership(ID portainer.TeamMembershipID, membership *portainer.TeamMembership) error {
data, err := internal.MarshalTeamMembership(membership)
if err != nil {
return err
}
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
err = bucket.Put(internal.Itob(int(ID)), data)
if err != nil {
return err
}
return nil
})
}
// CreateTeamMembership creates a new TeamMembership object.
func (service *TeamMembershipService) CreateTeamMembership(membership *portainer.TeamMembership) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
id, _ := bucket.NextSequence()
membership.ID = portainer.TeamMembershipID(id)
data, err := internal.MarshalTeamMembership(membership)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(membership.ID)), data)
if err != nil {
return err
}
return nil
})
}
// DeleteTeamMembership deletes a TeamMembership object.
func (service *TeamMembershipService) DeleteTeamMembership(ID portainer.TeamMembershipID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
err := bucket.Delete(internal.Itob(int(ID)))
if err != nil {
return err
}
return nil
})
}
// DeleteTeamMembershipByUserID deletes all the TeamMembership object associated to a UserID.
func (service *TeamMembershipService) DeleteTeamMembershipByUserID(userID portainer.UserID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalTeamMembership(v, &membership)
if err != nil {
return err
}
if membership.UserID == userID {
err := bucket.Delete(internal.Itob(int(membership.ID)))
if err != nil {
return err
}
}
}
return nil
})
}
// DeleteTeamMembershipByTeamID deletes all the TeamMembership object associated to a TeamID.
func (service *TeamMembershipService) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalTeamMembership(v, &membership)
if err != nil {
return err
}
if membership.TeamID == teamID {
err := bucket.Delete(internal.Itob(int(membership.ID)))
if err != nil {
return err
}
}
}
return nil
})
}
-144
View File
@@ -1,144 +0,0 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// TeamService represents a service for managing teams.
type TeamService struct {
store *Store
}
// Team returns a Team by ID
func (service *TeamService) Team(ID portainer.TeamID) (*portainer.Team, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamBucketName))
value := bucket.Get(internal.Itob(int(ID)))
if value == nil {
return portainer.ErrTeamNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var team portainer.Team
err = internal.UnmarshalTeam(data, &team)
if err != nil {
return nil, err
}
return &team, nil
}
// TeamByName returns a team by name.
func (service *TeamService) TeamByName(name string) (*portainer.Team, error) {
var team *portainer.Team
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var t portainer.Team
err := internal.UnmarshalTeam(v, &t)
if err != nil {
return err
}
if t.Name == name {
team = &t
}
}
if team == nil {
return portainer.ErrTeamNotFound
}
return nil
})
if err != nil {
return nil, err
}
return team, nil
}
// Teams return an array containing all the teams.
func (service *TeamService) Teams() ([]portainer.Team, error) {
var teams = make([]portainer.Team, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var team portainer.Team
err := internal.UnmarshalTeam(v, &team)
if err != nil {
return err
}
teams = append(teams, team)
}
return nil
})
if err != nil {
return nil, err
}
return teams, nil
}
// UpdateTeam saves a Team.
func (service *TeamService) UpdateTeam(ID portainer.TeamID, team *portainer.Team) error {
data, err := internal.MarshalTeam(team)
if err != nil {
return err
}
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamBucketName))
err = bucket.Put(internal.Itob(int(ID)), data)
if err != nil {
return err
}
return nil
})
}
// CreateTeam creates a new Team.
func (service *TeamService) CreateTeam(team *portainer.Team) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamBucketName))
id, _ := bucket.NextSequence()
team.ID = portainer.TeamID(id)
data, err := internal.MarshalTeam(team)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(team.ID)), data)
if err != nil {
return err
}
return nil
})
}
// DeleteTeam deletes a Team.
func (service *TeamService) DeleteTeam(ID portainer.TeamID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamBucketName))
err := bucket.Delete(internal.Itob(int(ID)))
if err != nil {
return err
}
return nil
})
}
+197
View File
@@ -0,0 +1,197 @@
package teammembership
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "team_membership"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// TeamMembership returns a TeamMembership object by ID
func (service *Service) TeamMembership(ID portainer.TeamMembershipID) (*portainer.TeamMembership, error) {
var membership portainer.TeamMembership
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &membership)
if err != nil {
return nil, err
}
return &membership, nil
}
// TeamMemberships return an array containing all the TeamMembership objects.
func (service *Service) TeamMemberships() ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalObject(v, &membership)
if err != nil {
return err
}
memberships = append(memberships, membership)
}
return nil
})
return memberships, err
}
// TeamMembershipsByUserID return an array containing all the TeamMembership objects where the specified userID is present.
func (service *Service) TeamMembershipsByUserID(userID portainer.UserID) ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalObject(v, &membership)
if err != nil {
return err
}
if membership.UserID == userID {
memberships = append(memberships, membership)
}
}
return nil
})
return memberships, err
}
// TeamMembershipsByTeamID return an array containing all the TeamMembership objects where the specified teamID is present.
func (service *Service) TeamMembershipsByTeamID(teamID portainer.TeamID) ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalObject(v, &membership)
if err != nil {
return err
}
if membership.TeamID == teamID {
memberships = append(memberships, membership)
}
}
return nil
})
return memberships, err
}
// UpdateTeamMembership saves a TeamMembership object.
func (service *Service) UpdateTeamMembership(ID portainer.TeamMembershipID, membership *portainer.TeamMembership) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, membership)
}
// CreateTeamMembership creates a new TeamMembership object.
func (service *Service) CreateTeamMembership(membership *portainer.TeamMembership) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
membership.ID = portainer.TeamMembershipID(id)
data, err := internal.MarshalObject(membership)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(membership.ID)), data)
})
}
// DeleteTeamMembership deletes a TeamMembership object.
func (service *Service) DeleteTeamMembership(ID portainer.TeamMembershipID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
// DeleteTeamMembershipByUserID deletes all the TeamMembership object associated to a UserID.
func (service *Service) DeleteTeamMembershipByUserID(userID portainer.UserID) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalObject(v, &membership)
if err != nil {
return err
}
if membership.UserID == userID {
err := bucket.Delete(internal.Itob(int(membership.ID)))
if err != nil {
return err
}
}
}
return nil
})
}
// DeleteTeamMembershipByTeamID deletes all the TeamMembership object associated to a TeamID.
func (service *Service) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalObject(v, &membership)
if err != nil {
return err
}
if membership.TeamID == teamID {
err := bucket.Delete(internal.Itob(int(membership.ID)))
if err != nil {
return err
}
}
}
return nil
})
}
+95
View File
@@ -0,0 +1,95 @@
package template
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "templates"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// Templates return an array containing all the templates.
func (service *Service) Templates() ([]portainer.Template, error) {
var templates = make([]portainer.Template, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var template portainer.Template
err := internal.UnmarshalObject(v, &template)
if err != nil {
return err
}
templates = append(templates, template)
}
return nil
})
return templates, err
}
// Template returns a template by ID.
func (service *Service) Template(ID portainer.TemplateID) (*portainer.Template, error) {
var template portainer.Template
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &template)
if err != nil {
return nil, err
}
return &template, nil
}
// CreateTemplate creates a new template.
func (service *Service) CreateTemplate(template *portainer.Template) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
template.ID = portainer.TemplateID(id)
data, err := internal.MarshalObject(template)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(template.ID)), data)
})
}
// UpdateTemplate saves a template.
func (service *Service) UpdateTemplate(ID portainer.TemplateID, template *portainer.Template) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, template)
}
// DeleteTemplate deletes a template.
func (service *Service) DeleteTemplate(ID portainer.TemplateID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
+149
View File
@@ -0,0 +1,149 @@
package user
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "users"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// User returns a user by ID
func (service *Service) User(ID portainer.UserID) (*portainer.User, error) {
var user portainer.User
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &user)
if err != nil {
return nil, err
}
return &user, nil
}
// UserByUsername returns a user by username.
func (service *Service) UserByUsername(username string) (*portainer.User, error) {
var user *portainer.User
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var u portainer.User
err := internal.UnmarshalObject(v, &u)
if err != nil {
return err
}
if u.Username == username {
user = &u
break
}
}
if user == nil {
return portainer.ErrObjectNotFound
}
return nil
})
return user, err
}
// Users return an array containing all the users.
func (service *Service) Users() ([]portainer.User, error) {
var users = make([]portainer.User, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var user portainer.User
err := internal.UnmarshalObject(v, &user)
if err != nil {
return err
}
users = append(users, user)
}
return nil
})
return users, err
}
// UsersByRole return an array containing all the users with the specified role.
func (service *Service) UsersByRole(role portainer.UserRole) ([]portainer.User, error) {
var users = make([]portainer.User, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var user portainer.User
err := internal.UnmarshalObject(v, &user)
if err != nil {
return err
}
if user.Role == role {
users = append(users, user)
}
}
return nil
})
return users, err
}
// UpdateUser saves a user.
func (service *Service) UpdateUser(ID portainer.UserID, user *portainer.User) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, user)
}
// CreateUser creates a new user.
func (service *Service) CreateUser(user *portainer.User) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
user.ID = portainer.UserID(id)
data, err := internal.MarshalObject(user)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(user.ID)), data)
})
}
// DeleteUser deletes a user.
func (service *Service) DeleteUser(ID portainer.UserID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
-170
View File
@@ -1,170 +0,0 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// UserService represents a service for managing users.
type UserService struct {
store *Store
}
// User returns a user by ID
func (service *UserService) User(ID portainer.UserID) (*portainer.User, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
value := bucket.Get(internal.Itob(int(ID)))
if value == nil {
return portainer.ErrUserNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var user portainer.User
err = internal.UnmarshalUser(data, &user)
if err != nil {
return nil, err
}
return &user, nil
}
// UserByUsername returns a user by username.
func (service *UserService) UserByUsername(username string) (*portainer.User, error) {
var user *portainer.User
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var u portainer.User
err := internal.UnmarshalUser(v, &u)
if err != nil {
return err
}
if u.Username == username {
user = &u
}
}
if user == nil {
return portainer.ErrUserNotFound
}
return nil
})
if err != nil {
return nil, err
}
return user, nil
}
// Users return an array containing all the users.
func (service *UserService) Users() ([]portainer.User, error) {
var users = make([]portainer.User, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var user portainer.User
err := internal.UnmarshalUser(v, &user)
if err != nil {
return err
}
users = append(users, user)
}
return nil
})
if err != nil {
return nil, err
}
return users, nil
}
// UsersByRole return an array containing all the users with the specified role.
func (service *UserService) UsersByRole(role portainer.UserRole) ([]portainer.User, error) {
var users = make([]portainer.User, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var user portainer.User
err := internal.UnmarshalUser(v, &user)
if err != nil {
return err
}
if user.Role == role {
users = append(users, user)
}
}
return nil
})
if err != nil {
return nil, err
}
return users, nil
}
// UpdateUser saves a user.
func (service *UserService) UpdateUser(ID portainer.UserID, user *portainer.User) error {
data, err := internal.MarshalUser(user)
if err != nil {
return err
}
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
err = bucket.Put(internal.Itob(int(ID)), data)
if err != nil {
return err
}
return nil
})
}
// CreateUser creates a new user.
func (service *UserService) CreateUser(user *portainer.User) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
id, _ := bucket.NextSequence()
user.ID = portainer.UserID(id)
data, err := internal.MarshalUser(user)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(user.ID)), data)
if err != nil {
return err
}
return nil
})
}
// DeleteUser deletes a user.
func (service *UserService) DeleteUser(ID portainer.UserID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
err := bucket.Delete(internal.Itob(int(ID)))
if err != nil {
return err
}
return nil
})
}
+66
View File
@@ -0,0 +1,66 @@
package version
import (
"strconv"
"github.com/boltdb/bolt"
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "version"
versionKey = "DB_VERSION"
)
// Service represents a service to manage stored versions.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// DBVersion retrieves the stored database version.
func (service *Service) DBVersion() (int, error) {
var data []byte
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
value := bucket.Get([]byte(versionKey))
if value == nil {
return portainer.ErrObjectNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return 0, err
}
return strconv.Atoi(string(data))
}
// StoreDBVersion store the database version.
func (service *Service) StoreDBVersion(version int) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data := []byte(strconv.Itoa(version))
return bucket.Put([]byte(versionKey), data)
})
}
-58
View File
@@ -1,58 +0,0 @@
package bolt
import (
"strconv"
"github.com/portainer/portainer"
"github.com/boltdb/bolt"
)
// VersionService represents a service to manage stored versions.
type VersionService struct {
store *Store
}
const (
dBVersionKey = "DB_VERSION"
)
// DBVersion retrieves the stored database version.
func (service *VersionService) DBVersion() (int, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(versionBucketName))
value := bucket.Get([]byte(dBVersionKey))
if value == nil {
return portainer.ErrDBVersionNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return 0, err
}
dbVersion, err := strconv.Atoi(string(data))
if err != nil {
return 0, err
}
return dbVersion, nil
}
// StoreDBVersion store the database version.
func (service *VersionService) StoreDBVersion(version int) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(versionBucketName))
data := []byte(strconv.Itoa(version))
err := bucket.Put([]byte(dBVersionKey), data)
if err != nil {
return err
}
return nil
})
}
+72 -35
View File
@@ -1,12 +1,12 @@
package cli
import (
"log"
"time"
"github.com/portainer/portainer"
"os"
"path/filepath"
"strings"
"gopkg.in/alecthomas/kingpin.v2"
@@ -16,12 +16,15 @@ import (
type Service struct{}
const (
errInvalidEndpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix:// or tcp://")
errSocketNotFound = portainer.Error("Unable to locate Unix socket")
errEndpointsFileNotFound = portainer.Error("Unable to locate external endpoints file")
errInvalidSyncInterval = portainer.Error("Invalid synchronization interval")
errEndpointExcludeExternal = portainer.Error("Cannot use the -H flag mutually with --external-endpoints")
errNoAuthExcludeAdminPassword = portainer.Error("Cannot use --no-auth with --admin-password")
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
@@ -29,40 +32,58 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
kingpin.Version(version)
flags := &portainer.CLIFlags{
Endpoint: kingpin.Flag("host", "Dockerd endpoint").Short('H').String(),
ExternalEndpoints: kingpin.Flag("external-endpoints", "Path to a file defining available endpoints").String(),
SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source").Default(defaultSyncInterval).String(),
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(defaultNoAuth).Bool(),
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLSVerify).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(),
// Deprecated flags
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Short('t').String(),
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.Endpoint != "" && *flags.ExternalEndpoints != "" {
if *flags.EndpointURL != "" && *flags.ExternalEndpoints != "" {
return errEndpointExcludeExternal
}
err := validateEndpoint(*flags.Endpoint)
err := validateTemplateFile(*flags.TemplateFile)
if err != nil {
return err
}
err = validateEndpointURL(*flags.EndpointURL)
if err != nil {
return err
}
@@ -77,26 +98,34 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
return err
}
if *flags.NoAuth && (*flags.AdminPassword != "") {
err = validateSnapshotInterval(*flags.SnapshotInterval)
if err != nil {
return err
}
if *flags.NoAuth && (*flags.AdminPassword != "" || *flags.AdminPasswordFile != "") {
return errNoAuthExcludeAdminPassword
}
displayDeprecationWarnings(*flags.Templates, *flags.Logo, *flags.Labels)
if *flags.AdminPassword != "" && *flags.AdminPasswordFile != "" {
return errAdminPassExcludeAdminPassFile
}
return nil
}
func validateEndpoint(endpoint string) error {
if endpoint != "" {
if !strings.HasPrefix(endpoint, "unix://") && !strings.HasPrefix(endpoint, "tcp://") {
func validateEndpointURL(endpointURL string) error {
if endpointURL != "" {
if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") && !strings.HasPrefix(endpointURL, "npipe://") {
return errInvalidEndpointProtocol
}
if strings.HasPrefix(endpoint, "unix://") {
socketPath := strings.TrimPrefix(endpoint, "unix://")
if strings.HasPrefix(endpointURL, "unix://") || strings.HasPrefix(endpointURL, "npipe://") {
socketPath := strings.TrimPrefix(endpointURL, "unix://")
socketPath = strings.TrimPrefix(socketPath, "npipe://")
if _, err := os.Stat(socketPath); err != nil {
if os.IsNotExist(err) {
return errSocketNotFound
return errSocketOrNamedPipeNotFound
}
return err
}
@@ -117,6 +146,16 @@ func validateExternalEndpoints(externalEndpoints string) error {
return nil
}
func validateTemplateFile(templateFile string) error {
if _, err := os.Stat(templateFile); err != nil {
if os.IsNotExist(err) {
return errTemplateFileNotFound
}
return err
}
return nil
}
func validateSyncInterval(syncInterval string) error {
if syncInterval != defaultSyncInterval {
_, err := time.ParseDuration(syncInterval)
@@ -127,14 +166,12 @@ func validateSyncInterval(syncInterval string) error {
return nil
}
func displayDeprecationWarnings(templates, logo string, labels []portainer.Pair) {
if templates != "" {
log.Println("Warning: the --templates / -t flag is deprecated and will be removed in future versions.")
}
if logo != "" {
log.Println("Warning: the --logo flag is deprecated and will be removed in future versions.")
}
if labels != nil {
log.Println("Warning: the --hide-label / -l flag is deprecated and will be removed in future versions.")
func validateSnapshotInterval(snapshotInterval string) error {
if snapshotInterval != defaultSnapshotInterval {
_, err := time.ParseDuration(snapshotInterval)
if err != nil {
return errInvalidSnapshotInterval
}
}
return nil
}
+17 -13
View File
@@ -3,17 +3,21 @@
package cli
const (
defaultBindAddress = ":9000"
defaultDataDirectory = "/data"
defaultAssetsDirectory = "."
defaultNoAuth = "false"
defaultNoAnalytics = "false"
defaultTLSVerify = "false"
defaultTLSCACertPath = "/certs/ca.pem"
defaultTLSCertPath = "/certs/cert.pem"
defaultTLSKeyPath = "/certs/key.pem"
defaultSSL = "false"
defaultSSLCertPath = "/certs/portainer.crt"
defaultSSLKeyPath = "/certs/portainer.key"
defaultSyncInterval = "60s"
defaultBindAddress = ":9000"
defaultDataDirectory = "/data"
defaultAssetsDirectory = "./"
defaultNoAuth = "false"
defaultNoAnalytics = "false"
defaultTLS = "false"
defaultTLSSkipVerify = "false"
defaultTLSCACertPath = "/certs/ca.pem"
defaultTLSCertPath = "/certs/cert.pem"
defaultTLSKeyPath = "/certs/key.pem"
defaultSSL = "false"
defaultSSLCertPath = "/certs/portainer.crt"
defaultSSLKeyPath = "/certs/portainer.key"
defaultSyncInterval = "60s"
defaultSnapshot = "true"
defaultSnapshotInterval = "5m"
defaultTemplateFile = "/templates.json"
)
+17 -13
View File
@@ -1,17 +1,21 @@
package cli
const (
defaultBindAddress = ":9000"
defaultDataDirectory = "C:\\data"
defaultAssetsDirectory = "."
defaultNoAuth = "false"
defaultNoAnalytics = "false"
defaultTLSVerify = "false"
defaultTLSCACertPath = "C:\\certs\\ca.pem"
defaultTLSCertPath = "C:\\certs\\cert.pem"
defaultTLSKeyPath = "C:\\certs\\key.pem"
defaultSSL = "false"
defaultSSLCertPath = "C:\\certs\\portainer.crt"
defaultSSLKeyPath = "C:\\certs\\portainer.key"
defaultSyncInterval = "60s"
defaultBindAddress = ":9000"
defaultDataDirectory = "C:\\data"
defaultAssetsDirectory = "./"
defaultNoAuth = "false"
defaultNoAnalytics = "false"
defaultTLS = "false"
defaultTLSSkipVerify = "false"
defaultTLSCACertPath = "C:\\certs\\ca.pem"
defaultTLSCertPath = "C:\\certs\\cert.pem"
defaultTLSKeyPath = "C:\\certs\\key.pem"
defaultSSL = "false"
defaultSSLCertPath = "C:\\certs\\portainer.crt"
defaultSSLKeyPath = "C:\\certs\\portainer.key"
defaultSyncInterval = "60s"
defaultSnapshot = "true"
defaultSnapshotInterval = "5m"
defaultTemplateFile = "/templates.json"
)
+384 -62
View File
@@ -1,14 +1,23 @@
package main // import "github.com/portainer/portainer"
import (
"encoding/json"
"strings"
"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/file"
"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"
)
@@ -28,15 +37,15 @@ func initCLI() *portainer.CLIFlags {
}
func initFileService(dataStorePath string) portainer.FileService {
fileService, err := file.NewService(dataStorePath, "")
fileService, err := filesystem.NewService(dataStorePath, "")
if err != nil {
log.Fatal(err)
}
return fileService
}
func initStore(dataStorePath string) *bolt.Store {
store, err := bolt.NewStore(dataStorePath)
func initStore(dataStorePath string, fileService portainer.FileService) *bolt.Store {
store, err := bolt.NewStore(dataStorePath, fileService)
if err != nil {
log.Fatal(err)
}
@@ -46,6 +55,11 @@ func initStore(dataStorePath string) *bolt.Store {
log.Fatal(err)
}
err = store.Init()
if err != nil {
log.Fatal(err)
}
err = store.MigrateData()
if err != nil {
log.Fatal(err)
@@ -53,6 +67,14 @@ func initStore(dataStorePath string) *bolt.Store {
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()
@@ -64,45 +86,96 @@ func initJWTService(authenticationEnabled bool) portainer.JWTService {
return nil
}
func initDigitalSignatureService() portainer.DigitalSignatureService {
return &crypto.ECDSAService{}
}
func initCryptoService() portainer.CryptoService {
return &crypto.Service{}
}
func initEndpointWatcher(endpointService portainer.EndpointService, externalEnpointFile string, syncInterval string) bool {
authorizeEndpointMgmt := true
if externalEnpointFile != "" {
authorizeEndpointMgmt = false
log.Println("Using external endpoint definition. Endpoint management via the API will be disabled.")
endpointWatcher := cron.NewWatcher(endpointService, syncInterval)
err := endpointWatcher.WatchEndpointFile(externalEnpointFile)
if err != nil {
log.Fatal(err)
}
}
return authorizeEndpointMgmt
func initLDAPService() portainer.LDAPService {
return &ldap.Service{}
}
func initStatus(authorizeEndpointMgmt bool, flags *portainer.CLIFlags) *portainer.Status {
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(endpointService portainer.EndpointService, snapshotter portainer.Snapshotter, flags *portainer.CLIFlags) (portainer.JobScheduler, error) {
jobScheduler := cron.NewJobScheduler(endpointService, snapshotter)
if *flags.ExternalEndpoints != "" {
log.Println("Using external endpoint definition. Endpoint management via the API will be disabled.")
err := jobScheduler.ScheduleEndpointSyncJob(*flags.ExternalEndpoints, *flags.SyncInterval)
if err != nil {
return nil, err
}
}
if *flags.Snapshot {
err := jobScheduler.ScheduleSnapshotJob(*flags.SnapshotInterval)
if err != nil {
return nil, err
}
}
return jobScheduler, nil
}
func initStatus(endpointManagement, snapshot bool, flags *portainer.CLIFlags) *portainer.Status {
return &portainer.Status{
Analytics: !*flags.NoAnalytics,
Authentication: !*flags.NoAuth,
EndpointManagement: authorizeEndpointMgmt,
EndpointManagement: endpointManagement,
Snapshot: snapshot,
Version: portainer.APIVersion,
}
}
func initDockerHub(dockerHubService portainer.DockerHubService) error {
_, err := dockerHubService.DockerHub()
if err == portainer.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.ErrSettingsNotFound {
if err == portainer.ErrObjectNotFound {
settings := &portainer.Settings{
LogoURL: *flags.Logo,
DisplayExternalContributors: true,
}
if *flags.Templates != "" {
settings.TemplatesURL = *flags.Templates
} else {
settings.TemplatesURL = portainer.DefaultTemplatesURL
LogoURL: *flags.Logo,
AuthenticationMethod: portainer.AuthenticationInternal,
LDAPSettings: portainer.LDAPSettings{
AutoCreateUsers: true,
TLSConfig: portainer.TLSConfiguration{},
SearchSettings: []portainer.LDAPSearchSettings{
portainer.LDAPSearchSettings{},
},
GroupSearchSettings: []portainer.LDAPGroupSearchSettings{
portainer.LDAPGroupSearchSettings{},
},
},
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
SnapshotInterval: *flags.SnapshotInterval,
}
if *flags.Labels != nil {
@@ -111,7 +184,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
settings.BlackListedLabels = make([]portainer.Pair, 0)
}
return settingsService.StoreSettings(settings)
return settingsService.UpdateSettings(settings)
} else if err != nil {
return err
}
@@ -119,6 +192,58 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
return nil
}
func initTemplates(templateService portainer.TemplateService, fileService portainer.FileService, templateURL, templateFile string) error {
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
}
var templatesJSON []byte
if templateURL == "" {
return loadTemplatesFromFile(fileService, templateService, templateFile)
}
templatesJSON, err = client.Get(templateURL)
if err != nil {
log.Println("Unable to retrieve templates via HTTP")
return err
}
return unmarshalAndPersistTemplates(templateService, templatesJSON)
}
func loadTemplatesFromFile(fileService portainer.FileService, templateService portainer.TemplateService, templateFile string) error {
templatesJSON, err := fileService.GetFileContent(templateFile)
if err != nil {
log.Println("Unable to retrieve template via filesystem")
return err
}
return unmarshalAndPersistTemplates(templateService, templatesJSON)
}
func unmarshalAndPersistTemplates(templateService portainer.TemplateService, templateData []byte) error {
var templates []portainer.Template
err := json.Unmarshal(templateData, &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 {
@@ -127,63 +252,247 @@ func retrieveFirstEndpointFromDatabase(endpointService portainer.EndpointService
return &endpoints[0]
}
func loadAndParseKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
private, public, err := fileService.LoadKeyPair()
if err != nil {
return err
}
return signatureService.ParseKeyPair(private, public)
}
func generateAndStoreKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
private, public, err := signatureService.GenerateKeyPair()
if err != nil {
return err
}
privateHeader, publicHeader := signatureService.PEMHeaders()
return fileService.StoreKeyPair(private, public, privateHeader, publicHeader)
}
func initKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
existingKeyPair, err := fileService.KeyPairFilesExist()
if err != nil {
log.Fatal(err)
}
if existingKeyPair {
return loadAndParseKeyPair(fileService, signatureService)
}
return generateAndStoreKeyPair(fileService, signatureService)
}
func createTLSSecuredEndpoint(flags *portainer.CLIFlags, endpointService portainer.EndpointService, snapshotter portainer.Snapshotter) error {
tlsConfiguration := portainer.TLSConfiguration{
TLS: *flags.TLS,
TLSSkipVerify: *flags.TLSSkipVerify,
}
if *flags.TLS {
tlsConfiguration.TLSCACertPath = *flags.TLSCacert
tlsConfiguration.TLSCertPath = *flags.TLSCert
tlsConfiguration.TLSKeyPath = *flags.TLSKey
} else if !*flags.TLS && *flags.TLSSkipVerify {
tlsConfiguration.TLS = true
}
endpointID := endpointService.GetNextIdentifier()
endpoint := &portainer.Endpoint{
ID: portainer.EndpointID(endpointID),
Name: "primary",
URL: *flags.EndpointURL,
GroupID: portainer.EndpointGroupID(1),
Type: portainer.DockerEnvironment,
TLSConfig: tlsConfiguration,
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
Tags: []string{},
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
}
if strings.HasPrefix(endpoint.URL, "tcp://") {
tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(tlsConfiguration.TLSCACertPath, tlsConfiguration.TLSCertPath, tlsConfiguration.TLSKeyPath, tlsConfiguration.TLSSkipVerify)
if err != nil {
return err
}
agentOnDockerEnvironment, err := client.ExecutePingOperation(endpoint.URL, tlsConfig)
if err != nil {
return err
}
if agentOnDockerEnvironment {
endpoint.Type = portainer.AgentOnDockerEnvironment
}
}
return snapshotAndPersistEndpoint(endpoint, endpointService, snapshotter)
}
func createUnsecuredEndpoint(endpointURL string, endpointService portainer.EndpointService, snapshotter portainer.Snapshotter) error {
if strings.HasPrefix(endpointURL, "tcp://") {
_, err := client.ExecutePingOperation(endpointURL, nil)
if err != nil {
return err
}
}
endpointID := endpointService.GetNextIdentifier()
endpoint := &portainer.Endpoint{
ID: portainer.EndpointID(endpointID),
Name: "primary",
URL: endpointURL,
GroupID: portainer.EndpointGroupID(1),
Type: portainer.DockerEnvironment,
TLSConfig: portainer.TLSConfiguration{},
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
Tags: []string{},
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
}
return snapshotAndPersistEndpoint(endpoint, endpointService, snapshotter)
}
func snapshotAndPersistEndpoint(endpoint *portainer.Endpoint, endpointService portainer.EndpointService, snapshotter portainer.Snapshotter) error {
snapshot, err := snapshotter.CreateSnapshot(endpoint)
endpoint.Status = portainer.EndpointStatusUp
if err != nil {
log.Printf("http error: endpoint snapshot error (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
}
if snapshot != nil {
endpoint.Snapshots = []portainer.Snapshot{*snapshot}
}
return endpointService.CreateEndpoint(endpoint)
}
func initEndpoint(flags *portainer.CLIFlags, endpointService portainer.EndpointService, snapshotter portainer.Snapshotter) error {
if *flags.EndpointURL == "" {
return nil
}
endpoints, err := endpointService.Endpoints()
if err != nil {
return err
}
if len(endpoints) > 0 {
log.Println("Instance already has defined endpoints. Skipping the endpoint defined via CLI.")
return nil
}
if *flags.TLS || *flags.TLSSkipVerify {
return createTLSSecuredEndpoint(flags, endpointService, snapshotter)
}
return createUnsecuredEndpoint(*flags.EndpointURL, endpointService, snapshotter)
}
func main() {
flags := initCLI()
fileService := initFileService(*flags.Data)
store := initStore(*flags.Data)
store := initStore(*flags.Data, fileService)
defer store.Close()
jwtService := initJWTService(!*flags.NoAuth)
ldapService := initLDAPService()
gitService := initGitService()
cryptoService := initCryptoService()
authorizeEndpointMgmt := initEndpointWatcher(store.EndpointService, *flags.ExternalEndpoints, *flags.SyncInterval)
digitalSignatureService := initDigitalSignatureService()
err := initSettings(store.SettingsService, flags)
err := initKeyPair(fileService, digitalSignatureService)
if err != nil {
log.Fatal(err)
}
applicationStatus := initStatus(authorizeEndpointMgmt, flags)
clientFactory := initClientFactory(digitalSignatureService)
if *flags.Endpoint != "" {
var endpoints []portainer.Endpoint
endpoints, err := store.EndpointService.Endpoints()
snapshotter := initSnapshotter(clientFactory)
jobScheduler, err := initJobScheduler(store.EndpointService, snapshotter, flags)
if err != nil {
log.Fatal(err)
}
jobScheduler.Start()
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)
}
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)
}
if len(endpoints) == 0 {
endpoint := &portainer.Endpoint{
Name: "primary",
URL: *flags.Endpoint,
TLS: *flags.TLSVerify,
TLSCACertPath: *flags.TLSCacert,
TLSCertPath: *flags.TLSCert,
TLSKeyPath: *flags.TLSKey,
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
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.EndpointService.CreateEndpoint(endpoint)
err := store.UserService.CreateUser(user)
if err != nil {
log.Fatal(err)
}
} else {
log.Println("Instance already has defined endpoints. Skipping the endpoint defined via CLI.")
}
}
if *flags.AdminPassword != "" {
log.Printf("Creating admin user with password hash %s", *flags.AdminPassword)
user := &portainer.User{
Username: "admin",
Role: portainer.AdministratorRole,
Password: *flags.AdminPassword,
}
err := store.UserService.CreateUser(user)
if err != nil {
log.Fatal(err)
log.Println("Instance already has an administrator user defined. Skipping admin password related flags.")
}
}
@@ -192,22 +501,35 @@ func main() {
BindAddress: *flags.Addr,
AssetsPath: *flags.Assets,
AuthDisabled: *flags.NoAuth,
EndpointManagement: authorizeEndpointMgmt,
EndpointManagement: endpointManagement,
UserService: store.UserService,
TeamService: store.TeamService,
TeamMembershipService: store.TeamMembershipService,
EndpointService: store.EndpointService,
EndpointGroupService: store.EndpointGroupService,
ResourceControlService: store.ResourceControlService,
SettingsService: store.SettingsService,
RegistryService: store.RegistryService,
DockerHubService: store.DockerHubService,
StackService: store.StackService,
TagService: store.TagService,
TemplateService: store.TemplateService,
SwarmStackManager: swarmStackManager,
ComposeStackManager: composeStackManager,
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,
}
log.Printf("Starting Portainer on %s", *flags.Addr)
log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr)
err = server.Start()
if err != nil {
log.Fatal(err)
+60
View File
@@ -0,0 +1,60 @@
package cron
import (
"log"
"github.com/portainer/portainer"
)
type (
endpointSnapshotJob struct {
endpointService portainer.EndpointService
snapshotter portainer.Snapshotter
}
)
func newEndpointSnapshotJob(endpointService portainer.EndpointService, snapshotter portainer.Snapshotter) endpointSnapshotJob {
return endpointSnapshotJob{
endpointService: endpointService,
snapshotter: snapshotter,
}
}
func (job endpointSnapshotJob) Snapshot() error {
endpoints, err := job.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range endpoints {
if endpoint.Type == portainer.AzureEnvironment {
continue
}
snapshot, err := job.snapshotter.CreateSnapshot(&endpoint)
endpoint.Status = portainer.EndpointStatusUp
if err != nil {
log.Printf("cron 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 = job.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}
func (job endpointSnapshotJob) Run() {
err := job.Snapshot()
if err != nil {
log.Printf("cron error: snapshot job error (err=%s)\n", err)
}
}
@@ -4,7 +4,6 @@ import (
"encoding/json"
"io/ioutil"
"log"
"os"
"strings"
"github.com/portainer/portainer"
@@ -12,7 +11,6 @@ import (
type (
endpointSyncJob struct {
logger *log.Logger
endpointService portainer.EndpointService
endpointFilePath string
}
@@ -22,6 +20,16 @@ type (
endpointsToUpdate []*portainer.Endpoint
endpointsToDelete []*portainer.Endpoint
}
fileEndpoint struct {
Name string `json:"Name"`
URL string `json:"URL"`
TLS bool `json:"TLS,omitempty"`
TLSSkipVerify bool `json:"TLSSkipVerify,omitempty"`
TLSCACert string `json:"TLSCACert,omitempty"`
TLSCert string `json:"TLSCert,omitempty"`
TLSKey string `json:"TLSKey,omitempty"`
}
)
const (
@@ -31,15 +39,14 @@ const (
func newEndpointSyncJob(endpointFilePath string, endpointService portainer.EndpointService) endpointSyncJob {
return endpointSyncJob{
logger: log.New(os.Stderr, "", log.LstdFlags),
endpointService: endpointService,
endpointFilePath: endpointFilePath,
}
}
func endpointSyncError(err error, logger *log.Logger) bool {
func endpointSyncError(err error) bool {
if err != nil {
logger.Printf("Endpoint synchronization error: %s", err)
log.Printf("cron error: synchronization job error (err=%s)\n", err)
return true
}
return false
@@ -55,6 +62,28 @@ func isValidEndpoint(endpoint *portainer.Endpoint) bool {
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) {
@@ -66,22 +95,25 @@ func endpointExists(endpoint *portainer.Endpoint, endpoints []portainer.Endpoint
func mergeEndpointIfRequired(original, updated *portainer.Endpoint) *portainer.Endpoint {
var endpoint *portainer.Endpoint
if original.URL != updated.URL || original.TLS != updated.TLS ||
(updated.TLS && original.TLSCACertPath != updated.TLSCACertPath) ||
(updated.TLS && original.TLSCertPath != updated.TLSCertPath) ||
(updated.TLS && original.TLSKeyPath != updated.TLSKeyPath) {
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.TLS {
endpoint.TLS = true
endpoint.TLSCACertPath = updated.TLSCACertPath
endpoint.TLSCertPath = updated.TLSCertPath
endpoint.TLSKeyPath = updated.TLSKeyPath
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.TLS = false
endpoint.TLSCACertPath = ""
endpoint.TLSCertPath = ""
endpoint.TLSKeyPath = ""
endpoint.TLSConfig.TLS = false
endpoint.TLSConfig.TLSSkipVerify = false
endpoint.TLSConfig.TLSCACertPath = ""
endpoint.TLSConfig.TLSCertPath = ""
endpoint.TLSConfig.TLSKeyPath = ""
}
}
return endpoint
@@ -105,25 +137,23 @@ func (job endpointSyncJob) prepareSyncData(storedEndpoints, fileEndpoints []port
if fidx != -1 {
endpoint := mergeEndpointIfRequired(&storedEndpoints[idx], &fileEndpoints[fidx])
if endpoint != nil {
job.logger.Printf("New definition for a stored endpoint found in file, updating database. [name: %v] [url: %v]\n", endpoint.Name, endpoint.URL)
log.Printf("New definition for a stored endpoint found in file, updating database. [name: %v] [url: %v]\n", endpoint.Name, endpoint.URL)
endpointsToUpdate = append(endpointsToUpdate, endpoint)
} else {
job.logger.Printf("No change detected for a stored endpoint. [name: %v] [url: %v]\n", storedEndpoints[idx].Name, storedEndpoints[idx].URL)
}
} else {
job.logger.Printf("Stored endpoint not found in file (definition might be invalid), removing from database. [name: %v] [url: %v]", storedEndpoints[idx].Name, storedEndpoints[idx].URL)
log.Printf("Stored endpoint not found in file (definition might be invalid), removing from database. [name: %v] [url: %v]", storedEndpoints[idx].Name, storedEndpoints[idx].URL)
endpointsToDelete = append(endpointsToDelete, &storedEndpoints[idx])
}
}
for idx, endpoint := range fileEndpoints {
if endpoint.Name == "" || endpoint.URL == "" {
job.logger.Printf("Invalid file endpoint definition, skipping. [name: %v] [url: %v]", endpoint.Name, endpoint.URL)
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 {
job.logger.Printf("File endpoint not found in database, adding to database. [name: %v] [url: %v]", fileEndpoints[idx].Name, fileEndpoints[idx].URL)
log.Printf("File endpoint not found in database, adding to database. [name: %v] [url: %v]", fileEndpoints[idx].Name, fileEndpoints[idx].URL)
endpointsToCreate = append(endpointsToCreate, &fileEndpoints[idx])
}
}
@@ -137,13 +167,13 @@ func (job endpointSyncJob) prepareSyncData(storedEndpoints, fileEndpoints []port
func (job endpointSyncJob) Sync() error {
data, err := ioutil.ReadFile(job.endpointFilePath)
if endpointSyncError(err, job.logger) {
if endpointSyncError(err) {
return err
}
var fileEndpoints []portainer.Endpoint
var fileEndpoints []fileEndpoint
err = json.Unmarshal(data, &fileEndpoints)
if endpointSyncError(err, job.logger) {
if endpointSyncError(err) {
return err
}
@@ -152,23 +182,25 @@ func (job endpointSyncJob) Sync() error {
}
storedEndpoints, err := job.endpointService.Endpoints()
if endpointSyncError(err, job.logger) {
if endpointSyncError(err) {
return err
}
sync := job.prepareSyncData(storedEndpoints, fileEndpoints)
convertedFileEndpoints := convertFileEndpoints(fileEndpoints)
sync := job.prepareSyncData(storedEndpoints, convertedFileEndpoints)
if sync.requireSync() {
err = job.endpointService.Synchronize(sync.endpointsToCreate, sync.endpointsToUpdate, sync.endpointsToDelete)
if endpointSyncError(err, job.logger) {
if endpointSyncError(err) {
return err
}
job.logger.Printf("Endpoint synchronization ended. [created: %v] [updated: %v] [deleted: %v]", len(sync.endpointsToCreate), len(sync.endpointsToUpdate), len(sync.endpointsToDelete))
log.Printf("Endpoint synchronization ended. [created: %v] [updated: %v] [deleted: %v]", len(sync.endpointsToCreate), len(sync.endpointsToUpdate), len(sync.endpointsToDelete))
}
return nil
}
func (job endpointSyncJob) Run() {
job.logger.Println("Endpoint synchronization job started.")
log.Println("cron: synchronization job started")
err := job.Sync()
endpointSyncError(err, job.logger)
endpointSyncError(err)
}
+86
View File
@@ -0,0 +1,86 @@
package cron
import (
"log"
"github.com/portainer/portainer"
"github.com/robfig/cron"
)
// JobScheduler represents a service for managing crons.
type JobScheduler struct {
cron *cron.Cron
endpointService portainer.EndpointService
snapshotter portainer.Snapshotter
endpointFilePath string
endpointSyncInterval string
}
// NewJobScheduler initializes a new service.
func NewJobScheduler(endpointService portainer.EndpointService, snapshotter portainer.Snapshotter) *JobScheduler {
return &JobScheduler{
cron: cron.New(),
endpointService: endpointService,
snapshotter: snapshotter,
}
}
// ScheduleEndpointSyncJob schedules a cron job to synchronize the endpoints from a file
func (scheduler *JobScheduler) ScheduleEndpointSyncJob(endpointFilePath string, interval string) error {
scheduler.endpointFilePath = endpointFilePath
scheduler.endpointSyncInterval = interval
job := newEndpointSyncJob(endpointFilePath, scheduler.endpointService)
err := job.Sync()
if err != nil {
return err
}
return scheduler.cron.AddJob("@every "+interval, job)
}
// ScheduleSnapshotJob schedules a cron job to create endpoint snapshots
func (scheduler *JobScheduler) ScheduleSnapshotJob(interval string) error {
job := newEndpointSnapshotJob(scheduler.endpointService, scheduler.snapshotter)
err := job.Snapshot()
if err != nil {
return err
}
return scheduler.cron.AddJob("@every "+interval, job)
}
// UpdateSnapshotJob will update the schedules to match the new snapshot interval
func (scheduler *JobScheduler) UpdateSnapshotJob(interval string) {
// TODO: the cron library do not support removing/updating schedules.
// As a work-around we need to re-create the cron and reschedule the jobs.
// We should update the library.
jobs := scheduler.cron.Entries()
scheduler.cron.Stop()
scheduler.cron = cron.New()
for _, job := range jobs {
switch job.Job.(type) {
case endpointSnapshotJob:
scheduler.ScheduleSnapshotJob(interval)
case endpointSyncJob:
scheduler.ScheduleEndpointSyncJob(scheduler.endpointFilePath, scheduler.endpointSyncInterval)
default:
log.Println("Unsupported job")
}
}
scheduler.cron.Start()
}
// Start starts the scheduled jobs
func (scheduler *JobScheduler) Start() {
if len(scheduler.cron.Entries()) > 0 {
scheduler.cron.Start()
}
}
-40
View File
@@ -1,40 +0,0 @@
package cron
import (
"github.com/portainer/portainer"
"github.com/robfig/cron"
)
// Watcher represents a service for managing crons.
type Watcher struct {
Cron *cron.Cron
EndpointService portainer.EndpointService
syncInterval string
}
// NewWatcher initializes a new service.
func NewWatcher(endpointService portainer.EndpointService, syncInterval string) *Watcher {
return &Watcher{
Cron: cron.New(),
EndpointService: endpointService,
syncInterval: syncInterval,
}
}
// WatchEndpointFile starts a cron job to synchronize the endpoints from a file
func (watcher *Watcher) WatchEndpointFile(endpointFilePath string) error {
job := newEndpointSyncJob(endpointFilePath, watcher.EndpointService)
err := job.Sync()
if err != nil {
return err
}
err = watcher.Cron.AddJob("@every "+watcher.syncInterval, job)
if err != nil {
return err
}
watcher.Cron.Start()
return nil
}
+125
View File
@@ -0,0 +1,125 @@
package crypto
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/md5"
"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
}
// 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
}
// Sign creates a signature from a message.
// It automatically hash the message using MD5 and creates a signature from
// that hash.
// It then encodes the generated signature in base64.
func (service *ECDSAService) Sign(message string) (string, error) {
digest := md5.New()
digest.Write([]byte(message))
hash := digest.Sum(nil)
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
}
+47 -14
View File
@@ -6,21 +6,54 @@ import (
"io/ioutil"
)
// CreateTLSConfiguration initializes a tls.Config using a CA certificate, a certificate and a key
func CreateTLSConfiguration(caCertPath, certPath, keyPath string) (*tls.Config, error) {
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return nil, err
// 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}
}
caCert, err := ioutil.ReadFile(caCertPath)
if err != nil {
return nil, err
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
config := &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caCertPool,
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
}
+103
View File
@@ -0,0 +1,103 @@
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
func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint) (*client.Client, error) {
if endpoint.Type == portainer.AzureEnvironment {
return nil, unsupportedEnvironmentType
} else if endpoint.Type == portainer.AgentOnDockerEnvironment {
return createAgentClient(endpoint, factory.signatureService)
}
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) (*client.Client, error) {
httpCli, err := httpClient(endpoint)
if err != nil {
return nil, err
}
signature, err := signatureService.Sign(portainer.PortainerAgentSignatureMessage)
if err != nil {
return nil, err
}
headers := map[string]string{
portainer.PortainerAgentPublicKeyHeader: signatureService.EncodedPublicKey(),
portainer.PortainerAgentSignatureHeader: signature,
}
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{
Timeout: time.Second * 10,
Transport: transport,
}, nil
}
+135
View File
@@ -0,0 +1,135 @@
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 = 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
}
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
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)
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)
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)
return nil
}
+27
View File
@@ -0,0 +1,27 @@
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
}
return snapshot(cli)
}
+34 -15
View File
@@ -4,52 +4,71 @@ package portainer
const (
ErrUnauthorized = Error("Unauthorized")
ErrResourceAccessDenied = Error("Access denied to resource")
ErrResourceNotFound = Error("Unable to find resource")
ErrUnsupportedDockerAPI = Error("Unsupported Docker API response")
ErrObjectNotFound = Error("Object not found inside the database")
ErrMissingSecurityContext = Error("Unable to find security details in request context")
)
// User errors.
const (
ErrUserNotFound = Error("User not found")
ErrUserAlreadyExists = Error("User already exists")
ErrInvalidUsername = Error("Invalid username. White spaces are not allowed.")
ErrAdminAlreadyInitialized = Error("Admin user already initialized")
ErrUserAlreadyExists = Error("User already exists")
ErrInvalidUsername = Error("Invalid username. White spaces are not allowed")
ErrAdminAlreadyInitialized = Error("An administrator user already exists")
ErrAdminCannotRemoveSelf = Error("Cannot remove your own user account. Contact another administrator")
ErrCannotRemoveLastLocalAdmin = Error("Cannot remove the last local administrator account")
)
// Team errors.
const (
ErrTeamNotFound = Error("Team not found")
ErrTeamAlreadyExists = Error("Team already exists")
)
// TeamMembership errors.
const (
ErrTeamMembershipNotFound = Error("Team membership not found")
ErrTeamMembershipAlreadyExists = Error("Team membership already exists for this user and team.")
ErrTeamMembershipAlreadyExists = Error("Team membership already exists for this user and team")
)
// ResourceControl errors.
const (
ErrResourceControlNotFound = Error("Resource control not found")
ErrResourceControlAlreadyExists = Error("A resource control is already applied on this resource")
ErrInvalidResourceControlType = Error("Unsupported resource control type")
)
// Endpoint errors.
const (
ErrEndpointNotFound = Error("Endpoint not found")
ErrEndpointAccessDenied = Error("Access denied to endpoint")
)
// Version errors.
// Azure environment errors
const (
ErrDBVersionNotFound = Error("DB version not found")
ErrAzureInvalidCredentials = Error("Invalid Azure credentials")
)
// Settings errors.
// Endpoint group errors.
const (
ErrSettingsNotFound = Error("Settings not found")
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.
+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.Sign(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
}
-143
View File
@@ -1,143 +0,0 @@
package file
import (
"github.com/portainer/portainer"
"io"
"os"
"path"
"strconv"
)
const (
// TLSStorePath represents the subfolder where TLS files are stored in the file store folder.
TLSStorePath = "tls"
// 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"
)
// 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),
}
// Checking if a mount directory exists is broken with Go on Windows.
// This will need to be reviewed after the issue has been fixed in Go.
// See: https://github.com/portainer/portainer/issues/474
// err := createDirectoryIfNotExist(dataStorePath, 0755)
// if err != nil {
// return nil, err
// }
err := service.createDirectoryInStoreIfNotExist(TLSStorePath)
if err != nil {
return nil, err
}
return service, nil
}
// StoreTLSFile creates a subfolder in the TLSStorePath and stores a new file with the content from r.
func (service *Service) StoreTLSFile(endpointID portainer.EndpointID, fileType portainer.TLSFileType, r io.Reader) error {
ID := strconv.Itoa(int(endpointID))
endpointStorePath := path.Join(TLSStorePath, ID)
err := service.createDirectoryInStoreIfNotExist(endpointStorePath)
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(endpointStorePath, fileName)
err = service.createFileInStore(tlsFilePath, r)
if err != nil {
return err
}
return nil
}
// GetPathForTLSFile returns the absolute path to a specific TLS file for an endpoint.
func (service *Service) GetPathForTLSFile(endpointID portainer.EndpointID, 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
}
ID := strconv.Itoa(int(endpointID))
return path.Join(service.fileStorePath, TLSStorePath, ID, fileName), nil
}
// DeleteTLSFiles deletes a folder containing the TLS files for an endpoint.
func (service *Service) DeleteTLSFiles(endpointID portainer.EndpointID) error {
ID := strconv.Itoa(int(endpointID))
endpointPath := path.Join(service.fileStorePath, TLSStorePath, ID)
err := os.RemoveAll(endpointPath)
if err != nil {
return err
}
return nil
}
// createDirectoryInStoreIfNotExist creates a new directory in the file store if it doesn't exists on the file system.
func (service *Service) createDirectoryInStoreIfNotExist(name string) error {
path := path.Join(service.fileStorePath, name)
return createDirectoryIfNotExist(path, 0700)
}
// createDirectoryIfNotExist creates a directory if it doesn't exists on the file system.
func createDirectoryIfNotExist(path string, mode uint32) error {
_, err := os.Stat(path)
if os.IsNotExist(err) {
err = os.Mkdir(path, os.FileMode(mode))
if err != nil {
return err
}
} else if err != nil {
return err
}
return nil
}
// 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
}
+320
View File
@@ -0,0 +1,320 @@
package filesystem
import (
"bytes"
"encoding/json"
"encoding/pem"
"io/ioutil"
"github.com/portainer/portainer"
"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"
)
// 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
}
return service, 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
}
// 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
}
+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
}
+121
View File
@@ -0,0 +1,121 @@
package client
import (
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
"github.com/portainer/portainer"
)
// 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 * 5,
},
}
}
// 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.
func Get(url string) ([]byte, error) {
client := &http.Client{
Timeout: time.Second * 3,
}
response, err := client.Get(url)
if err != nil {
return nil, err
}
defer response.Body.Close()
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
}
+28 -17
View File
@@ -4,27 +4,38 @@ import (
"encoding/json"
"log"
"net/http"
"strings"
)
// errorResponse is a generic response for sending a error.
type errorResponse struct {
Err string `json:"err,omitempty"`
}
// WriteErrorResponse writes an error message to the response and logger.
func WriteErrorResponse(w http.ResponseWriter, err error, code int, logger *log.Logger) {
if logger != nil {
logger.Printf("http error: %s (code=%d)", err, code)
type (
// LoggerHandler defines a HTTP handler that includes a HandlerError return pointer
LoggerHandler func(http.ResponseWriter, *http.Request) *HandlerError
// HandlerError represents an error raised inside a HTTP handler
HandlerError struct {
StatusCode int
Message string
Err error
}
errorResponse struct {
Err string `json:"err,omitempty"`
}
)
w.WriteHeader(code)
json.NewEncoder(w).Encode(&errorResponse{Err: err.Error()})
func (handler LoggerHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
err := handler(rw, r)
if err != nil {
writeErrorResponse(rw, err)
}
}
// WriteMethodNotAllowedResponse writes an error message to the response and sets the Allow header.
func WriteMethodNotAllowedResponse(w http.ResponseWriter, allowedMethods []string) {
w.Header().Set("Allow", strings.Join(allowedMethods, ", "))
w.WriteHeader(http.StatusMethodNotAllowed)
json.NewEncoder(w).Encode(&errorResponse{Err: http.StatusText(http.StatusMethodNotAllowed)})
func writeErrorResponse(rw http.ResponseWriter, err *HandlerError) {
log.Printf("http error: %s (err=%s) (code=%d)\n", err.Message, err.Err, err.StatusCode)
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(err.StatusCode)
json.NewEncoder(rw).Encode(&errorResponse{Err: err.Message})
}
// WriteError is a convenience function that creates a new HandlerError before calling writeErrorResponse.
// For use outside of the standard http handlers.
func WriteError(rw http.ResponseWriter, code int, message string, err error) {
writeErrorResponse(rw, &HandlerError{code, message, err})
}
-112
View File
@@ -1,112 +0,0 @@
package handler
import (
"github.com/portainer/portainer"
"encoding/json"
"log"
"net/http"
"os"
"github.com/asaskevich/govalidator"
"github.com/gorilla/mux"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security"
)
// AuthHandler represents an HTTP API handler for managing authentication.
type AuthHandler struct {
*mux.Router
Logger *log.Logger
authDisabled bool
UserService portainer.UserService
CryptoService portainer.CryptoService
JWTService portainer.JWTService
}
const (
// ErrInvalidCredentialsFormat is an error raised when credentials format is not valid
ErrInvalidCredentialsFormat = portainer.Error("Invalid credentials format")
// ErrInvalidCredentials is an error raised when credentials for a user are invalid
ErrInvalidCredentials = portainer.Error("Invalid credentials")
// ErrAuthDisabled is an error raised when trying to access the authentication endpoints
// when the server has been started with the --no-auth flag
ErrAuthDisabled = portainer.Error("Authentication is disabled")
)
// NewAuthHandler returns a new instance of AuthHandler.
func NewAuthHandler(bouncer *security.RequestBouncer, authDisabled bool) *AuthHandler {
h := &AuthHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
authDisabled: authDisabled,
}
h.Handle("/auth",
bouncer.PublicAccess(http.HandlerFunc(h.handlePostAuth)))
return h
}
func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
httperror.WriteMethodNotAllowedResponse(w, []string{http.MethodPost})
return
}
if handler.authDisabled {
httperror.WriteErrorResponse(w, ErrAuthDisabled, http.StatusServiceUnavailable, handler.Logger)
return
}
var req postAuthRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err := govalidator.ValidateStruct(req)
if err != nil {
httperror.WriteErrorResponse(w, ErrInvalidCredentialsFormat, http.StatusBadRequest, handler.Logger)
return
}
var username = req.Username
var password = req.Password
u, err := handler.UserService.UserByUsername(username)
if err == portainer.ErrUserNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
err = handler.CryptoService.CompareHashAndData(u.Password, password)
if err != nil {
httperror.WriteErrorResponse(w, ErrInvalidCredentials, http.StatusUnprocessableEntity, handler.Logger)
return
}
tokenData := &portainer.TokenData{
ID: u.ID,
Username: u.Username,
Role: u.Role,
}
token, err := handler.JWTService.GenerateToken(tokenData)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, &postAuthResponse{JWT: token}, handler.Logger)
}
type postAuthRequest struct {
Username string `valid:"required"`
Password string `valid:"required"`
}
type postAuthResponse struct {
JWT string `json:"jwt"`
}
+187
View File
@@ -0,0 +1,187 @@
package auth
import (
"log"
"net/http"
"strings"
"github.com/asaskevich/govalidator"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
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
}
+43
View File
@@ -0,0 +1,43 @@
package auth
import (
"net/http"
"github.com/gorilla/mux"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"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
}
// 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",
rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.authenticate)))).Methods(http.MethodPost)
return h
}
-94
View File
@@ -1,94 +0,0 @@
package handler
import (
"strconv"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/proxy"
"github.com/portainer/portainer/http/security"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
)
// DockerHandler represents an HTTP API handler for proxying requests to the Docker API.
type DockerHandler struct {
*mux.Router
Logger *log.Logger
EndpointService portainer.EndpointService
TeamMembershipService portainer.TeamMembershipService
ProxyManager *proxy.Manager
}
// NewDockerHandler returns a new instance of DockerHandler.
func NewDockerHandler(bouncer *security.RequestBouncer) *DockerHandler {
h := &DockerHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.PathPrefix("/{id}/").Handler(
bouncer.AuthenticatedAccess(http.HandlerFunc(h.proxyRequestsToDockerAPI)))
return h
}
func (handler *DockerHandler) checkEndpointAccessControl(endpoint *portainer.Endpoint, userID portainer.UserID) bool {
for _, authorizedUserID := range endpoint.AuthorizedUsers {
if authorizedUserID == userID {
return true
}
}
memberships, _ := handler.TeamMembershipService.TeamMembershipsByUserID(userID)
for _, authorizedTeamID := range endpoint.AuthorizedTeams {
for _, membership := range memberships {
if membership.TeamID == authorizedTeamID {
return true
}
}
}
return false
}
func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
parsedID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
endpointID := portainer.EndpointID(parsedID)
endpoint, err := handler.EndpointService.Endpoint(endpointID)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if tokenData.Role != portainer.AdministratorRole && !handler.checkEndpointAccessControl(endpoint, tokenData.ID) {
httperror.WriteErrorResponse(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger)
return
}
var proxy http.Handler
proxy = handler.ProxyManager.GetProxy(string(endpointID))
if proxy == nil {
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
}
http.StripPrefix("/"+id, proxy).ServeHTTP(w, r)
}
@@ -0,0 +1,19 @@
package dockerhub
import (
"net/http"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/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"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
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"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"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
}
-356
View File
@@ -1,356 +0,0 @@
package handler
import (
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/proxy"
"github.com/portainer/portainer/http/security"
"encoding/json"
"log"
"net/http"
"os"
"strconv"
"github.com/asaskevich/govalidator"
"github.com/gorilla/mux"
)
// EndpointHandler represents an HTTP API handler for managing Docker endpoints.
type EndpointHandler struct {
*mux.Router
Logger *log.Logger
authorizeEndpointManagement bool
EndpointService portainer.EndpointService
FileService portainer.FileService
ProxyManager *proxy.Manager
}
const (
// ErrEndpointManagementDisabled is an error raised when trying to access the endpoints management endpoints
// when the server has been started with the --external-endpoints flag
ErrEndpointManagementDisabled = portainer.Error("Endpoint management is disabled")
)
// NewEndpointHandler returns a new instance of EndpointHandler.
func NewEndpointHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bool) *EndpointHandler {
h := &EndpointHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
authorizeEndpointManagement: authorizeEndpointManagement,
}
h.Handle("/endpoints",
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePostEndpoints))).Methods(http.MethodPost)
h.Handle("/endpoints",
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetEndpoints))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}",
bouncer.AdministratorAccess(http.HandlerFunc(h.handleGetEndpoint))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}",
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutEndpoint))).Methods(http.MethodPut)
h.Handle("/endpoints/{id}/access",
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutEndpointAccess))).Methods(http.MethodPut)
h.Handle("/endpoints/{id}",
bouncer.AdministratorAccess(http.HandlerFunc(h.handleDeleteEndpoint))).Methods(http.MethodDelete)
return h
}
// handleGetEndpoints handles GET requests on /endpoints
func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *http.Request) {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
filteredEndpoints, err := security.FilterEndpoints(endpoints, securityContext)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, filteredEndpoints, handler.Logger)
}
// handlePostEndpoints handles POST requests on /endpoints
func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *http.Request) {
if !handler.authorizeEndpointManagement {
httperror.WriteErrorResponse(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
return
}
var req postEndpointsRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err := govalidator.ValidateStruct(req)
if err != nil {
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
endpoint := &portainer.Endpoint{
Name: req.Name,
URL: req.URL,
PublicURL: req.PublicURL,
TLS: req.TLS,
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
}
err = handler.EndpointService.CreateEndpoint(endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if req.TLS {
caCertPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCA)
endpoint.TLSCACertPath = caCertPath
certPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCert)
endpoint.TLSCertPath = certPath
keyPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileKey)
endpoint.TLSKeyPath = keyPath
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
encodeJSON(w, &postEndpointsResponse{ID: int(endpoint.ID)}, handler.Logger)
}
type postEndpointsRequest struct {
Name string `valid:"required"`
URL string `valid:"required"`
PublicURL string `valid:"-"`
TLS bool
}
type postEndpointsResponse struct {
ID int `json:"Id"`
}
// handleGetEndpoint handles GET requests on /endpoints/:id
func (handler *EndpointHandler) handleGetEndpoint(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
endpointID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrEndpointNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, endpoint, handler.Logger)
}
// handlePutEndpointAccess handles PUT requests on /endpoints/:id/access
func (handler *EndpointHandler) handlePutEndpointAccess(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
endpointID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
var req putEndpointAccessRequest
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err = govalidator.ValidateStruct(req)
if err != nil {
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrEndpointNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if req.AuthorizedUsers != nil {
authorizedUserIDs := []portainer.UserID{}
for _, value := range req.AuthorizedUsers {
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
}
endpoint.AuthorizedUsers = authorizedUserIDs
}
if req.AuthorizedTeams != nil {
authorizedTeamIDs := []portainer.TeamID{}
for _, value := range req.AuthorizedTeams {
authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value))
}
endpoint.AuthorizedTeams = authorizedTeamIDs
}
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
type putEndpointAccessRequest struct {
AuthorizedUsers []int `valid:"-"`
AuthorizedTeams []int `valid:"-"`
}
// handlePutEndpoint handles PUT requests on /endpoints/:id
func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http.Request) {
if !handler.authorizeEndpointManagement {
httperror.WriteErrorResponse(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
return
}
vars := mux.Vars(r)
id := vars["id"]
endpointID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
var req putEndpointsRequest
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err = govalidator.ValidateStruct(req)
if err != nil {
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrEndpointNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if req.Name != "" {
endpoint.Name = req.Name
}
if req.URL != "" {
endpoint.URL = req.URL
}
if req.PublicURL != "" {
endpoint.PublicURL = req.PublicURL
}
if req.TLS {
endpoint.TLS = true
caCertPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCA)
endpoint.TLSCACertPath = caCertPath
certPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCert)
endpoint.TLSCertPath = certPath
keyPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileKey)
endpoint.TLSKeyPath = keyPath
} else {
endpoint.TLS = false
endpoint.TLSCACertPath = ""
endpoint.TLSCertPath = ""
endpoint.TLSKeyPath = ""
err = handler.FileService.DeleteTLSFiles(endpoint.ID)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
_, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
type putEndpointsRequest struct {
Name string `valid:"-"`
URL string `valid:"-"`
PublicURL string `valid:"-"`
TLS bool `valid:"-"`
}
// handleDeleteEndpoint handles DELETE requests on /endpoints/:id
func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *http.Request) {
if !handler.authorizeEndpointManagement {
httperror.WriteErrorResponse(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
return
}
vars := mux.Vars(r)
id := vars["id"]
endpointID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrEndpointNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
handler.ProxyManager.DeleteProxy(string(endpointID))
err = handler.EndpointService.DeleteEndpoint(portainer.EndpointID(endpointID))
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if endpoint.TLS {
err = handler.FileService.DeleteTLSFiles(portainer.EndpointID(endpointID))
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
}
@@ -0,0 +1,66 @@
package endpointgroups
import (
"net/http"
"github.com/asaskevich/govalidator"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
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"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
// 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"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
// 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/portainer/http/error"
"github.com/portainer/portainer/http/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"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
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"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
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"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"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"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"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"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"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,43 @@
package endpointproxy
import (
"strconv"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"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}
}
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,56 @@
package endpointproxy
import (
"strconv"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"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.GetExtensionProxy(proxyExtensionKey)
if proxy == nil {
proxy, err = handler.ProxyManager.CreateAndRegisterExtensionProxy(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"
"github.com/portainer/portainer"
"github.com/portainer/portainer/crypto"
"github.com/portainer/portainer/http/client"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
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 stack 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: payload.URL,
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
}
@@ -0,0 +1,48 @@
package endpoints
import (
"net/http"
"strconv"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
// DELETE request on /api/endpoints/:id
func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
if !handler.authorizeEndpointManagement {
return &httperror.HandlerError{http.StatusServiceUnavailable, "Endpoint management is disabled", ErrEndpointManagementDisabled}
}
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.TLSConfig.TLS {
folder := strconv.Itoa(endpointID)
err = handler.FileService.DeleteTLSFiles(folder)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove TLS files from disk", err}
}
}
err = handler.EndpointService.DeleteEndpoint(portainer.EndpointID(endpointID))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove endpoint from the database", err}
}
handler.ProxyManager.DeleteProxy(string(endpointID))
handler.ProxyManager.DeleteExtensionProxies(string(endpointID))
return response.Empty(w)
}
@@ -0,0 +1,73 @@
package endpoints
import (
"net/http"
"github.com/asaskevich/govalidator"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
type endpointExtensionAddPayload struct {
Type int
URL string
}
func (payload *endpointExtensionAddPayload) Validate(r *http.Request) error {
if payload.Type != 1 {
return portainer.Error("Invalid type value. Value must be one of: 1 (Storidge)")
}
if payload.Type == 1 && govalidator.IsNull(payload.URL) {
return portainer.Error("Invalid extension URL")
}
return nil
}
// POST request on /api/endpoints/:id/extensions
func (handler *Handler) endpointExtensionAdd(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}
}
var payload endpointExtensionAddPayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
extensionType := portainer.EndpointExtensionType(payload.Type)
var extension *portainer.EndpointExtension
for _, ext := range endpoint.Extensions {
if ext.Type == extensionType {
extension = &ext
}
}
if extension != nil {
extension.URL = payload.URL
} else {
extension = &portainer.EndpointExtension{
Type: extensionType,
URL: payload.URL,
}
endpoint.Extensions = append(endpoint.Extensions, *extension)
}
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
}
return response.JSON(w, extension)
}
@@ -0,0 +1,43 @@
package endpoints
import (
"net/http"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
// DELETE request on /api/endpoints/:id/extensions/:extensionType
func (handler *Handler) endpointExtensionRemove(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}
}
extensionType, err := request.RetrieveNumericRouteVariableValue(r, "extensionType")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid extension type route variable", err}
}
for idx, ext := range endpoint.Extensions {
if ext.Type == portainer.EndpointExtensionType(extensionType) {
endpoint.Extensions = append(endpoint.Extensions[:idx], endpoint.Extensions[idx+1:]...)
}
}
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
}
return response.Empty(w)
}
@@ -0,0 +1,34 @@
package endpoints
import (
"net/http"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
// GET request on /api/endpoints/:id
func (handler *Handler) endpointInspect(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}
}
hideFields(endpoint)
return response.JSON(w, endpoint)
}
@@ -0,0 +1,34 @@
package endpoints
import (
"net/http"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/response"
"github.com/portainer/portainer/http/security"
)
// GET request on /api/endpoints
func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
}
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}
}
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
for _, endpoint := range filteredEndpoints {
hideFields(&endpoint)
}
return response.JSON(w, filteredEndpoints)
}
@@ -0,0 +1,42 @@
package endpoints
import (
"log"
"net/http"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/response"
)
// POST request on /api/endpoints/snapshot
func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
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.Type == portainer.AzureEnvironment {
continue
}
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.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
}
}
return response.Empty(w)
}
@@ -0,0 +1,142 @@
package endpoints
import (
"net/http"
"strconv"
"github.com/portainer/portainer"
"github.com/portainer/portainer/http/client"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
type endpointUpdatePayload struct {
Name string
URL string
PublicURL string
GroupID int
TLS bool
TLSSkipVerify bool
TLSSkipClientVerify bool
AzureApplicationID string
AzureTenantID string
AzureAuthenticationKey string
Tags []string
}
func (payload *endpointUpdatePayload) Validate(r *http.Request) error {
return nil
}
// PUT request on /api/endpoints/:id
func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
if !handler.authorizeEndpointManagement {
return &httperror.HandlerError{http.StatusServiceUnavailable, "Endpoint management is disabled", ErrEndpointManagementDisabled}
}
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
}
var payload endpointUpdatePayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", 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 payload.Name != "" {
endpoint.Name = payload.Name
}
if payload.URL != "" {
endpoint.URL = payload.URL
}
if payload.PublicURL != "" {
endpoint.PublicURL = payload.PublicURL
}
if payload.GroupID != 0 {
endpoint.GroupID = portainer.EndpointGroupID(payload.GroupID)
}
if payload.Tags != nil {
endpoint.Tags = payload.Tags
}
if endpoint.Type == portainer.AzureEnvironment {
credentials := endpoint.AzureCredentials
if payload.AzureApplicationID != "" {
credentials.ApplicationID = payload.AzureApplicationID
}
if payload.AzureTenantID != "" {
credentials.TenantID = payload.AzureTenantID
}
if payload.AzureAuthenticationKey != "" {
credentials.AuthenticationKey = payload.AzureAuthenticationKey
}
httpClient := client.NewHTTPClient()
_, authErr := httpClient.ExecuteAzureAuthenticationRequest(&credentials)
if authErr != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate against Azure", authErr}
}
endpoint.AzureCredentials = credentials
}
folder := strconv.Itoa(endpointID)
if payload.TLS {
endpoint.TLSConfig.TLS = true
endpoint.TLSConfig.TLSSkipVerify = payload.TLSSkipVerify
if !payload.TLSSkipVerify {
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
endpoint.TLSConfig.TLSCACertPath = caCertPath
} else {
endpoint.TLSConfig.TLSCACertPath = ""
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCA)
}
if !payload.TLSSkipClientVerify {
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
endpoint.TLSConfig.TLSCertPath = certPath
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
endpoint.TLSConfig.TLSKeyPath = keyPath
} else {
endpoint.TLSConfig.TLSCertPath = ""
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCert)
endpoint.TLSConfig.TLSKeyPath = ""
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileKey)
}
} else {
endpoint.TLSConfig.TLS = false
endpoint.TLSConfig.TLSSkipVerify = false
endpoint.TLSConfig.TLSCACertPath = ""
endpoint.TLSConfig.TLSCertPath = ""
endpoint.TLSConfig.TLSKeyPath = ""
err = handler.FileService.DeleteTLSFiles(folder)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove TLS files from disk", err}
}
}
_, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register HTTP proxy for the endpoint", err}
}
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
}
return response.JSON(w, endpoint)
}
@@ -0,0 +1,67 @@
package endpoints
import (
"net/http"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
type endpointUpdateAccessPayload struct {
AuthorizedUsers []int
AuthorizedTeams []int
}
func (payload *endpointUpdateAccessPayload) Validate(r *http.Request) error {
return nil
}
// PUT request on /api/endpoints/:id/access
func (handler *Handler) endpointUpdateAccess(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
if !handler.authorizeEndpointManagement {
return &httperror.HandlerError{http.StatusServiceUnavailable, "Endpoint management is disabled", ErrEndpointManagementDisabled}
}
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
}
var payload endpointUpdateAccessPayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", 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 payload.AuthorizedUsers != nil {
authorizedUserIDs := []portainer.UserID{}
for _, value := range payload.AuthorizedUsers {
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
}
endpoint.AuthorizedUsers = authorizedUserIDs
}
if payload.AuthorizedTeams != nil {
authorizedTeamIDs := []portainer.TeamID{}
for _, value := range payload.AuthorizedTeams {
authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value))
}
endpoint.AuthorizedTeams = authorizedTeamIDs
}
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
}
return response.JSON(w, endpoint)
}
+64
View File
@@ -0,0 +1,64 @@
package endpoints
import (
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/proxy"
"github.com/portainer/portainer/http/security"
"net/http"
"github.com/gorilla/mux"
)
const (
// ErrEndpointManagementDisabled is an error raised when trying to access the endpoints management endpoints
// when the server has been started with the --external-endpoints flag
ErrEndpointManagementDisabled = portainer.Error("Endpoint management is disabled")
)
func hideFields(endpoint *portainer.Endpoint) {
endpoint.AzureCredentials = portainer.AzureCredentials{}
}
// Handler is the HTTP handler used to handle endpoint operations.
type Handler struct {
*mux.Router
authorizeEndpointManagement bool
requestBouncer *security.RequestBouncer
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
FileService portainer.FileService
ProxyManager *proxy.Manager
Snapshotter portainer.Snapshotter
}
// NewHandler creates a handler to manage endpoint operations.
func NewHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bool) *Handler {
h := &Handler{
Router: mux.NewRouter(),
authorizeEndpointManagement: authorizeEndpointManagement,
requestBouncer: bouncer,
}
h.Handle("/endpoints",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointCreate))).Methods(http.MethodPost)
h.Handle("/endpoints/snapshot",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost)
h.Handle("/endpoints",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointList))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointInspect))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointUpdate))).Methods(http.MethodPut)
h.Handle("/endpoints/{id}/access",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointUpdateAccess))).Methods(http.MethodPut)
h.Handle("/endpoints/{id}",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointDelete))).Methods(http.MethodDelete)
h.Handle("/endpoints/{id}/extensions",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointExtensionAdd))).Methods(http.MethodPost)
h.Handle("/endpoints/{id}/extensions/{extensionType}",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointExtensionRemove))).Methods(http.MethodDelete)
return h
}
-61
View File
@@ -1,61 +0,0 @@
package handler
import (
"os"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"log"
"net/http"
"path"
"strings"
)
// FileHandler represents an HTTP API handler for managing static files.
type FileHandler struct {
http.Handler
Logger *log.Logger
allowedDirectories map[string]bool
}
// NewFileHandler returns a new instance of FileHandler.
func NewFileHandler(assetPath string) *FileHandler {
h := &FileHandler{
Handler: http.FileServer(http.Dir(assetPath)),
Logger: log.New(os.Stderr, "", log.LstdFlags),
allowedDirectories: map[string]bool{
"/": true,
"/css": true,
"/js": true,
"/images": true,
"/fonts": true,
},
}
return h
}
func isHTML(acceptContent []string) bool {
for _, accept := range acceptContent {
if strings.Contains(accept, "text/html") {
return true
}
}
return false
}
func (handler *FileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
requestDirectory := path.Dir(r.URL.Path)
if !handler.allowedDirectories[requestDirectory] {
httperror.WriteErrorResponse(w, portainer.ErrResourceNotFound, http.StatusNotFound, handler.Logger)
return
}
if !isHTML(r.Header["Accept"]) {
w.Header().Set("Cache-Control", "max-age=31536000")
} else {
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
}
handler.Handler.ServeHTTP(w, r)
}

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