Compare commits

...

101 Commits

Author SHA1 Message Date
Dmitry Salakhov 39e9dca7b8 bump version to 1.24.2 (#4927) 2021-03-18 13:02:39 +13:00
Chaim Lev-Ari cfdd38c55e fix(endpoints): create roles array in edge endpoint (#4742) 2021-03-14 20:55:32 +01:00
Chaim Lev-Ari a12a0b61dc feat(containers): enforce disable bind mounts (#4110) (#4467)
* feat(containers): enforce disable bind mounts (#4110)

* feat(containers): enforce disable bind mounts

* refactor(docker): move check for endpoint admin to a function

* feat(docker): check if service has bind mounts

* feat(services): allow bind mounts for endpoint admin

* feat(container): enable bind mounts for endpoint admin

* fix(services): fix typo

* fix(docker): tag user as admin when auth is disabled
2021-03-04 11:50:35 +01:00
Steven Kang d2cdbf789e feat(build): introduced buildx for 1.24 branch (#4850) 2021-02-16 09:49:37 +13:00
Chaim Lev-Ari 06db4e0ad4 fix(auth): skip security checks with --no-auth flag (#4513)
* fix(stacks): skip security checks if no-auth

* fix(containers): skip security check when auth is disabled

* fix(volumes): show browse if auth is disabled
2021-01-18 09:31:23 +13:00
Chaim Lev-Ari 9f92e0aee3 feat(settings): introduce setting to disable container caps for non-admins (#4109) (#4510)
* feat(settings): introduce settings to allow/disable

* feat(settings): update the setting

* feat(docker): prevent user from using caps if disabled

* refactor(stacks): revert file

* style(api): remove portainer ns
2020-12-09 17:15:19 +13:00
Anthony Lapenna f347d97daf chore(version): bump version number 2020-07-23 10:28:34 +12:00
Anthony Lapenna d5cee5b8b1 feat(core/extensions): add the ability to update a license (#4081)
* feat(core/extensions): add the ability to update a license

* feat(core/extensions): trigger data upgrade if extension is not enabled yet

* feat(core/extensions): trigger data upgrade if extension is not enabled yet

* feat(core/extensions): trigger data upgrade if extension is not enabled yet

* feat(core/extensions): trigger data upgrade if extension is not enabled yet
2020-07-22 21:13:51 +12:00
Anthony Lapenna 4da6824bc7 feat(database): review database migration (#4054) 2020-07-17 17:04:32 +12:00
Chaim Lev-Ari 80b6b6e300 fix(registries): filter gitlab repos without tags (#4048) 2020-07-16 20:57:52 +12:00
Anthony Lapenna 484dab5932 feat(database): trigger missing database migration for AllowHostNamespaceForRegularUsers setting (#4035) 2020-07-13 22:27:22 +12:00
Chaim Lev-Ari f8bd075ce4 feat(containers): disable edit container on security setting restricting regular users (#4033)
* feat(settings): add info about container edit disable

* feat(settings): set security settings

* feat(containers): hide recreate button when setting is enabled

* feat(settings): rephrase security notice

* fix(settings): save allowHostNamespaceForRegularUsers to state
2020-07-13 22:26:23 +12:00
Chaim Lev-Ari cd58c16b4e feat(settings): hide stacks for non admin when settings is set (#4025)
* refactor(settings): replace disableDeviceMapping with allow

* feat(dashboard): hide stacks if settings disabled and non admin

* refactor(sidebar): check if user is endpoint admin

* feat(settings): set the default value for stack management

* feat(settings): rename field label

* fix(sidebar): refresh show stacks state
2020-07-13 18:36:47 +12:00
Chaim Lev-Ari 5ebb03cb4e feat(settings): add setting to disable device mapping for regular users (#4017)
* feat(settings): introduce device mapping service

* feat(containers): hide devices field when setting is on

* feat(containers): prevent passing of devices when not allowed

* feat(stacks): prevent non admin from device mapping

* feat(stacks): disallow swarm stack creation for user

* refactor(settings): replace disableDeviceMapping with allow

* fix(stacks): remove check for disable device mappings from swarm

* feat(settings): rename field to disable

* feat(settings): supply default value for disableDeviceMapping

* feat(container): check for endpoint admin
2020-07-13 16:32:56 +12:00
Chaim Lev-Ari dffcd3fdfd feat(settings): replace cookies with local storage (#3979)
* feat(cookies): use secured cookies in frontend

* fix(datatables): persist state changes

* fix(datatables): persist order

* feat(sidebar): use local storage to store toggle state

* feat(config): use local storage instead of cookies
2020-07-10 11:51:31 +12:00
Chaim Lev-Ari 3f7687e78a feat(server): support minimum tls v1.2 (#4019)
* feat(crypto): use tls 1.2

* feat(crypto): use secure cipher suites

* feat(server): accept tls1.2 connections

* refactor(crypto): create base tls config

* refactor(server): use basic tls config

* fix(server): remove unused import

* refactor(crypto): rename tls conf factory
2020-07-10 11:48:01 +12:00
Maxime Bajeux 0f58ece899 feat(containers): prevent non-admin users from running containers using the host namespace pid (#3970)
* feat(containers): Prevent non-admin users from running containers using the host namespace pid

* feat(containers): add rbac check for swarm stack too

* feat(containers): remove forgotten conflict

* feat(containers): init EnableHostNamespaceUse to true and return 403 on forbidden action

* feat(containers): change enableHostNamespaceUse to restrictHostNamespaceUse in html

* feat(settings): rename EnableHostNamespaceUse to AllowHostNamespaceForRegularUsers
2020-07-08 09:48:34 +12:00
Chaim Lev-Ari b0ad212858 fix(registries): hide zero tags repositories (#3985) 2020-07-07 10:59:33 +12:00
Chaim Lev-Ari 7eb2fd3424 feat(stacks): add a setting to disable the creation of stacks for non-admin users (#3932)
* feat(settings): introduce a setting to prevent non-admin from stack creation

* feat(settings): update stack creation setting

* feat(settings): fail stack creation if user is non admin

* fix(settings): save preventStackCreation setting to state

* feat(stacks): disable add button when settings is enabled

* format(stacks): remove line

* feat(stacks): setting to hide stacks from users

* feat(settings): rename disable stacks setting

* refactor(settings): rename setting to disableStackManagementForRegularUsers
2020-07-01 09:34:43 +12:00
Maxime Bajeux 4c0d8ce732 feat(containers): Ensure users cannot create privileged containers via the API (#3969)
* feat(containers): Ensure users cannot create privileged containers via the API

* feat(containers): add rbac check in stack creation
2020-06-30 17:13:37 +12:00
Anthony Lapenna e1cc4bc9a1 chore(version): bump version number 2020-06-16 17:22:51 +12:00
Anthony Lapenna 13f712d06d Merge branch 'release/1.24.0' 2020-05-29 14:34:39 +12:00
Anthony Lapenna bfcdeecac9 chore(version): bump version number 2020-05-29 14:34:32 +12:00
Chaim Lev-Ari babc509115 fix(yarn): downgrade router version (#3857) 2020-05-25 20:00:33 +12:00
Maxime Bajeux ecbee3ee3d fix(templates): replace volume input with a select (#3853)
* prevent non admins to create volume which they do not have access

* fix(volumes): replace volume input by a select
2020-05-22 11:35:34 +12:00
Maxime Bajeux 10772a3ecd fix(networks): network without ipv6 should not be expand (#3844) 2020-05-21 12:40:23 +12:00
Chaim Lev-Ari 2260107811 feat(edge-stacks): add refresh to edit stack page (#3835) 2020-05-18 20:05:00 +12:00
Chaim Lev-Ari 42e7db0ae7 fix(prettier): auto format html files (#3836) 2020-05-18 19:08:45 +12:00
Maxime Bajeux ebac85b462 feat(volumes): add a switch to use CIFS volumes (#3823)
* feat(volumes): add a switch to use CIFS volumes

* feat(volumes): switch between nfs and cifs

* feat(volumes): autofix sharepoint, hide driveroptions and allow to create unnammed volume

* feat(volumes): change cifs version select options

* feat(volumes): change few things
2020-05-15 13:28:51 +12:00
Chaim Lev-Ari 8eac1d2221 feat(edge-compute): add support for Edge stacks (#3827)
* feat(api): introduce Edge group API (#3639)

* feat(edge-groups): add object definition and service definition

* feat(edge-groups): implement bolt layer

* feat(edge-groups): bind service to server

* feat(edge-group): add edge-group create http handler

* feat(edge-groups): add list method to edge group handler

* feat(edge-group): add inspect http handler

* feat(edge-groups): add delete edge-group handler

* feat(edge-groups): add update group handler

* style(db): order by alphabetical order

* fix(edge-groups): rewrite http error messages

Co-Authored-By: Anthony Lapenna <anthony.lapenna@portainer.io>

* fix(main): order by alphabetical order

* refactor(edge-group): relocate fetch group

* fix(edge-group): reset tagids/endpoints if dynamic

* refactor(server): order by alphabetical order

* refactor(server): order by alphabetical order

Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>

* Introduce a new setting to enable Edge compute features (#3654)

* feat(edge-compute): add edge compute setting

* feat(edge-compute): add edge compute group to sidebar

* fix(settings): rename settings form group

* fix(settings): align form control

* Edge group associated endpoints (#3659)

* chore(version): bump version number

* chore(version): bump version number

* feat(endpoints): filter by endpoint type (#3646)

* refactor(tags): migrate tags to have association objects

* refactor(tags): refactor tag management (#3628)

* refactor(tags): replace tags with tag ids

* refactor(tags): revert tags to be strings and add tagids

* refactor(tags): enable search by tag in home view

* refactor(tags): show endpoint tags

* refactor(endpoints): expect tagIds on create payload

* refactor(endpoints): expect tagIds on update payload

* refactor(endpoints): replace TagIds to TagIDs

* refactor(endpoints): set endpoint group to get TagIDs

* refactor(endpoints): refactor tag-selector to receive tag-ids

* refactor(endpoints): show tags in multi-endpoint-selector

* chore(tags): revert reformat

* refactor(endpoints): remove unneeded bind

* refactor(endpoints): change param tags to tagids in endpoint create

* refactor(endpoints): remove console.log

* refactor(tags): remove deleted tag from endpoint and endpoint group

* fix(endpoints): show loading label while loading tags

* chore(go): remove obsolete import labels

* chore(db): add db version comment

* fix(db): add tag service to migrator

* refactor(db): add error checks in migrator

* style(db): sort props in alphabetical order

* style(tags): fix typo

Co-Authored-By: Anthony Lapenna <anthony.lapenna@portainer.io>

* refactor(endpoints): replace tagsMap with tag string representation

* refactor(tags): rewrite tag delete to be more readable

* refactor(home): rearange code to match former style

* refactor(tags): guard against missing model in tag-selector

* refactor(tags): rename vars in tag_delete

* refactor(tags): allow any authenticated user to fetch tag list

* refactor(endpoints): replace controller function with class

* refactor(endpoints): replace function with helper

* refactor(endpoints): replace controller with class

* refactor(tags): revert tags-selector to use 1 way bindings

* refactor(endpoints): load empty tag array instead of nil

* refactor(endpoints): revert default tag ids

* refactor(endpoints): use function in place

* refactor(tags): use lodash

* style(tags): use parens in arrow functions

* fix(tags): remove tag from tag model

Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>

* refactor(tags): create tag association when creating tag

* refactor(tags): delete tag association when deleting tag

* refactor(db): handle error in tag association create

* feat(endpoint-group): update tag assoc when creating endpoint group

* feat(endpoint-group): update tag association when updating group

* feat(endpoint-groups): remove group from tag associations

* feat(endpoints): associate endpoint with tag on create

* feat(endpoints): edit tag association when updating endpoint

* fix(tags): fix merge problems

* refactor(tags): remove tag association resource

* fix(db): use regular tags map

* style(tags): reorder props and imports

* refactor(endpoint-groups): replace tag-association with tag

* feat(edge-group): get associated endpoints when fetching

* refactor(tags): refactor algo to update endpoint and group tags

* refactor(edge-group): rename variable

* refactor(tags): move calc of tags to remove to global function

* fix(tags): update tag after adding association

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>

* fix(edge-groups): associate groups only with edge endpoints (#3667)

* fix(edge-groups): check endpoint type when adding to edge-group

* fix(edge-groups): return only edge endpoints for dynamic groups

* fix(edge-compute): load edge compute setting on public setting (#3665)

* Edge group list (#3644)

* feat(edge-groups): add edge module

* feat(edge-groups):  add edge-group service

* feat(edge-group): add groups list view

* feat(edge-groups): add link to groups in the sidebar

* feat(edge-group): show endpoints count and group type

* feat(edge-group): enable removal of edge groups

* refactor(edge-groups): replace datatable controller with class

* refactor(edge-groups): replace function with class

* fix(edge-groups): sort items by endpoints count and group type

* refactor(edge-groups): use generic datatable-header component

* feat(app): add trace for ui router

* fix(edge-compute): add ng injection to onEnter guard

* fix(edge-compute): add ng injection to onEnter guard

* style(edge-compute): remove space

* refactor(edge-compute): import angular

* fix(app): remove ui router trace

* refactor(product): revert app.js

* fix(edge-compute): remove admin guard from edge routes

* fix(edge-groups): change label of empty datatable

Co-Authored-By: Anthony Lapenna <anthony.lapenna@portainer.io>

* refactor(edge-groups): rename service

* fix(edge-groups): replace icon in sidebar

Co-Authored-By: Anthony Lapenna <anthony.lapenna@portainer.io>

* refactor(edge-groups): remove datatable controller

* refactor(edge-groups): move datatable icon to binding

* refactor(edge-groups): use vanilla datatable header

* refactor(datatable): remove datatable header

Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>

* refactor(edge): rename edge group to Edge group

* feat(edge-groups): edge group creation view (#3671)

* feat(edge-groups): add create group view

* feat(edge-groups): allow to choose group type

* feat(edge-groups): implement create service handler

* feat(edge-group): filter by edge endpoints

* refactor(edge-groups): rename to camel case

* refactor(edge-groups): replace controller with class

* feat(endpoints): filter endpoints by type

* refactor(edge-groups): remove comments and unneccesary async keyword

* refactor(edge-group): use $async service

* fix(edge-groups): replace view title

Co-Authored-By: Anthony Lapenna <anthony.lapenna@portainer.io>

* fix(edge-groups): change icon

Co-Authored-By: Anthony Lapenna <anthony.lapenna@portainer.io>

* fix(edge-groups): change icon

Co-Authored-By: Anthony Lapenna <anthony.lapenna@portainer.io>

* refactor(edge-groups): remove obsolete function

* feat(edge-groups): add empty list messages

* feat(edge-group): add description to group types

* refactor(edge-groups): add finally block

* feat(endpoints): search server in multi-endpoint-selector

Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>

* feat(edge-group) edit view (#3672)

* feat(edge-groups): add edit group view

* refactor(edge-group): replace edit controller with class

* refactor(edge-groups): remove async keyword

* refactor(edge-groups): use $async service

* refactor(edge-group): remove unnecessary functions

* fix(endpoints): group by groups in endpoint-selector

* feat(edge-groups): minor UI update

* fix(edge-groups): provide defaults for edge group (#3682)

* feat(edge-stacks): add basic views and sidebar link (#3689)

* feat(edge-stacks): add mock routes

* feat(edge-stacks): add link to stacks on sidebar

* feat(edge-stacks): add edge stacks view

* feat(edge-stacks): add create view

* feat(edge-stacks): add edit view

* fix(edge-stacks): use class in controller

* feat(edge-stacks): add edge-stacks api (#3688)

* feat(edge-stack): add edge stack types

* feat(edge-stacks): add edge stack service interface

* feat(edge-stacks): implement store

* feat(edge-stacks): bind service to datastore

* feat(edge-stacks): bind service to server

* feat(edge-stack): create basic api

* feat(edge-stack): create stack api

* feat(edge-stacks): update api

* refacotor(edge-stack): rename files

* feat(edge-stack): update endpoint status

* style(edge-stacks): remove comments

* feat(edge-stacks): use edge stacks folder for files

* fix(edge-stacks): replace bucket name

Co-Authored-By: Anthony Lapenna <anthony.lapenna@portainer.io>

* fix(edge-stacks): replace unmarshal function

Co-Authored-By: Anthony Lapenna <anthony.lapenna@portainer.io>

* fix(edge-stacks): replace edge stacks path

Co-Authored-By: Anthony Lapenna <anthony.lapenna@portainer.io>

Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>

* chore(git): merge develop to edge compute (#3692)

* feat(support): make support type dynamic (#3621)

* chore(version): bump version number

* chore(version): bump version number

* feat(endpoints): filter by endpoint type (#3646)

* chore(assets): double UI image resolutions for HiDPI displays (#3648)

Fixes #3069

Prevents users seeing blurry logos and other images when using a hidpi
display (like scaled 4k, or a Retina display).

These images have been recreated manually with 2x the original
resolution but should resemble the originals as much as possible.

They have also been run through pngcrush for compression.

* fix(services): enforce minimum replica count of 0 (#3653)

* fix(services): enforce minimum replica count of 0

Fixes #3652

Prevents replica count from being set below zero and causing an error.

* fix(services): enforce replica count is an integer

Prevents users entering decimals in the replica count

* refactor(tags): refactor tag management (#3628)

* refactor(tags): replace tags with tag ids

* refactor(tags): revert tags to be strings and add tagids

* refactor(tags): enable search by tag in home view

* refactor(tags): show endpoint tags

* refactor(endpoints): expect tagIds on create payload

* refactor(endpoints): expect tagIds on update payload

* refactor(endpoints): replace TagIds to TagIDs

* refactor(endpoints): set endpoint group to get TagIDs

* refactor(endpoints): refactor tag-selector to receive tag-ids

* refactor(endpoints): show tags in multi-endpoint-selector

* chore(tags): revert reformat

* refactor(endpoints): remove unneeded bind

* refactor(endpoints): change param tags to tagids in endpoint create

* refactor(endpoints): remove console.log

* refactor(tags): remove deleted tag from endpoint and endpoint group

* fix(endpoints): show loading label while loading tags

* chore(go): remove obsolete import labels

* chore(db): add db version comment

* fix(db): add tag service to migrator

* refactor(db): add error checks in migrator

* style(db): sort props in alphabetical order

* style(tags): fix typo

Co-Authored-By: Anthony Lapenna <anthony.lapenna@portainer.io>

* refactor(endpoints): replace tagsMap with tag string representation

* refactor(tags): rewrite tag delete to be more readable

* refactor(home): rearange code to match former style

* refactor(tags): guard against missing model in tag-selector

* refactor(tags): rename vars in tag_delete

* refactor(tags): allow any authenticated user to fetch tag list

* refactor(endpoints): replace controller function with class

* refactor(endpoints): replace function with helper

* refactor(endpoints): replace controller with class

* refactor(tags): revert tags-selector to use 1 way bindings

* refactor(endpoints): load empty tag array instead of nil

* refactor(endpoints): revert default tag ids

* refactor(endpoints): use function in place

* refactor(tags): use lodash

* style(tags): use parens in arrow functions

* fix(tags): remove tag from tag model

Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>

* chore(yarn): change start:client to start webpack dev server (#3595)

* chore(yarn): change start:client to start webpack dev server

* Update package.json

Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>

* create tag from tag selector (#3640)

* feat(tags): add button to save tag when doesn't exist

* feat(endpoints): allow the creating of tags in endpoint edit

* feat(groups): allow user to create tags in create group

* feat(groups): allow user to create tags in edit group

* feat(endpoint): allow user to create tags from endpoint create

* feat(tags): allow the creation of a new tag from dropdown

* feat(tag): replace "add" with "create"

* feat(tags): show tags input when not tags

* feat(tags): hide create message when not allowed

* refactor(tags): replace component controller with class

* refactor(tags): replace native methods with lodash

* refactor(tags): remove unused onChangeTags function

* refactor(tags): remove on-change binding

* style(tags): remove white space

* refactor(endpoint-groups): move controller to separate file

* fix(groups): allow admin to create tag in group form

* refactor(endpoints): wrap async function with try catch and $async

* style(tags): wrap arrow function args with parenthesis

* refactor(endpoints): return $async functions

* refactor(tags): throw error in the format Notification expects

* chore(yarn): add start:client script back (#3691)

* feat(endpoints): filter by ids and/or tag ids (#3690)

* feat(endpoints): add filter by tagIds

* refactor(endpoints): change endpoints service to query by tagIds

* fix(endpoints): filter by tags

* feat(endpoints): filter by endpoint groups tags

* feat(endpoints): filter by ids

Co-authored-by: itsconquest <william.conquest@portainer.io>
Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
Co-authored-by: Ben Brooks <ben@bbrks.me>
Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>

* Chore merge develop to edge compute (#3702)

* feat(support): make support type dynamic (#3621)

* chore(version): bump version number

* chore(version): bump version number

* feat(endpoints): filter by endpoint type (#3646)

* chore(assets): double UI image resolutions for HiDPI displays (#3648)

Fixes #3069

Prevents users seeing blurry logos and other images when using a hidpi
display (like scaled 4k, or a Retina display).

These images have been recreated manually with 2x the original
resolution but should resemble the originals as much as possible.

They have also been run through pngcrush for compression.

* fix(services): enforce minimum replica count of 0 (#3653)

* fix(services): enforce minimum replica count of 0

Fixes #3652

Prevents replica count from being set below zero and causing an error.

* fix(services): enforce replica count is an integer

Prevents users entering decimals in the replica count

* refactor(tags): refactor tag management (#3628)

* refactor(tags): replace tags with tag ids

* refactor(tags): revert tags to be strings and add tagids

* refactor(tags): enable search by tag in home view

* refactor(tags): show endpoint tags

* refactor(endpoints): expect tagIds on create payload

* refactor(endpoints): expect tagIds on update payload

* refactor(endpoints): replace TagIds to TagIDs

* refactor(endpoints): set endpoint group to get TagIDs

* refactor(endpoints): refactor tag-selector to receive tag-ids

* refactor(endpoints): show tags in multi-endpoint-selector

* chore(tags): revert reformat

* refactor(endpoints): remove unneeded bind

* refactor(endpoints): change param tags to tagids in endpoint create

* refactor(endpoints): remove console.log

* refactor(tags): remove deleted tag from endpoint and endpoint group

* fix(endpoints): show loading label while loading tags

* chore(go): remove obsolete import labels

* chore(db): add db version comment

* fix(db): add tag service to migrator

* refactor(db): add error checks in migrator

* style(db): sort props in alphabetical order

* style(tags): fix typo

Co-Authored-By: Anthony Lapenna <anthony.lapenna@portainer.io>

* refactor(endpoints): replace tagsMap with tag string representation

* refactor(tags): rewrite tag delete to be more readable

* refactor(home): rearange code to match former style

* refactor(tags): guard against missing model in tag-selector

* refactor(tags): rename vars in tag_delete

* refactor(tags): allow any authenticated user to fetch tag list

* refactor(endpoints): replace controller function with class

* refactor(endpoints): replace function with helper

* refactor(endpoints): replace controller with class

* refactor(tags): revert tags-selector to use 1 way bindings

* refactor(endpoints): load empty tag array instead of nil

* refactor(endpoints): revert default tag ids

* refactor(endpoints): use function in place

* refactor(tags): use lodash

* style(tags): use parens in arrow functions

* fix(tags): remove tag from tag model

Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>

* chore(yarn): change start:client to start webpack dev server (#3595)

* chore(yarn): change start:client to start webpack dev server

* Update package.json

Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>

* create tag from tag selector (#3640)

* feat(tags): add button to save tag when doesn't exist

* feat(endpoints): allow the creating of tags in endpoint edit

* feat(groups): allow user to create tags in create group

* feat(groups): allow user to create tags in edit group

* feat(endpoint): allow user to create tags from endpoint create

* feat(tags): allow the creation of a new tag from dropdown

* feat(tag): replace "add" with "create"

* feat(tags): show tags input when not tags

* feat(tags): hide create message when not allowed

* refactor(tags): replace component controller with class

* refactor(tags): replace native methods with lodash

* refactor(tags): remove unused onChangeTags function

* refactor(tags): remove on-change binding

* style(tags): remove white space

* refactor(endpoint-groups): move controller to separate file

* fix(groups): allow admin to create tag in group form

* refactor(endpoints): wrap async function with try catch and $async

* style(tags): wrap arrow function args with parenthesis

* refactor(endpoints): return $async functions

* refactor(tags): throw error in the format Notification expects

* chore(yarn): add start:client script back (#3691)

* feat(endpoints): filter by ids and/or tag ids (#3690)

* feat(endpoints): add filter by tagIds

* refactor(endpoints): change endpoints service to query by tagIds

* fix(endpoints): filter by tags

* feat(endpoints): filter by endpoint groups tags

* feat(endpoints): filter by ids

* refactor(project): sort portainer types and interface definitions (#3694)

* refactor(portainer): sort types

* style(portainer): add comment about role service

* refactor(portainer): sort interface types

* refactor(portainer): sort enums

* Update README.md

* Update README.md

* Update README.md

* chore(project): add prettier for code format (#3645)

* chore(project): install prettier and lint-staged

* chore(project): apply prettier to html too

* chore(project): git ignore eslintcache

* chore(project): add a comment about format script

* chore(prettier): update printWidth

* chore(prettier): remove useTabs option

* chore(prettier): add HTML validation

* refactor(prettier): fix closing tags

* feat(prettier): define angular parser for html templates

* style(prettier): run prettier on codebase

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>

* chore(prettier): run format on client codebase

Co-authored-by: itsconquest <william.conquest@portainer.io>
Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
Co-authored-by: Ben Brooks <ben@bbrks.me>
Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>
Co-authored-by: Neil Cresswell <neil@cresswell.net.nz>

* feat(edge-stacks): create basic edge stack service (#3704)

Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>

* feat(edge-groups): Provide a switch to use AND or OR for tags (#3695)

* feat(edge-groups): add switch to form

* feat(project): add property to EdgeGroup

* feat(edge-groups): save mustHaveAllTags

* feat(edge-groups): fetch associated endpoints (AND and OR)

* feat(edge-groups): add AND selector

* feat(edge-groups): default to AND

* fix(edge-groups): rewrite selector options

Co-Authored-By: Anthony Lapenna <anthony.lapenna@portainer.io>

* refactor(endpoints): move margin to schedule form

* fix(edge-groups): move the selector to top of group

* refactor(edge-groups): replace partialMatch property

Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>

* feat(edge-stacks): add Edge stack creation view (#3705)

* feat(edge-stacks): basic creation view

* feat(edge-stacks): add group selector

* feat(edge-stack): create edge stack

* fix(code-editor): apply digest cycle after editor is changed

* style(project): reformat constants file

* feat(edge-stacks): add a note about missing edge groups

* fix(edge-stacks): add groups when creating stack from file

* feat(edge-groups): add associated endpoints table (#3710)

* feat(edge-groups): load associated endpoints

* feat(endpoints): add option to filter endpoint by partial match tags

* feat(edge-groups): query endpoints by PartialMatch

* feat(edge-groups): reload endpoints when form changes

* feat(edge-groups): remove columns

* feat(edge-group): remove url column

* refactor(edge-group): remove props

* feat(edge-stacks): add list view (#3713)

* feat(edge-stacks): basic datatable

* feat(edge-stacks): remove stack

* refactor(edge-stacks): convert to class

* refactor(edge-stacks): replace id with stackId

* feat(edge-stacks) edit edge stack view (#3716)

* feat(edge-stack): load file content

* feat(edge-stack): edit view

* feat(edge-stack): enable update stack

* refactor(edge-stacks): move form to component

* feat(edge-stacks): add endpoints status

* feat(edge-stacks): minor UI update

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>

* feat(edge-groups) prevent deletion of edge group used by an edge stack (#3722)

* feat(edge-groups): show if group belonges to edge stack

* feat(edge-group): protect deletion of used edge group

* feat(edge-groups): diable selection of used group

* feat(edge-groups): add inuse tag (#3739)

* feat(edge-groups): add inuse tag

* Update app/edge/components/groups-datatable/groupsDatatable.html

Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>

* feat(edge-stack): update stack version when stack file is changed (#3746)

* feat(edge-stack): update version when stack file is changed

* refactor(edge-stacks): move update of version to clientside

* feat(edge-groups): replace Edge group endpoint selector (#3738)

* feat(edge-groups): replace selector

* feat(edge-group): add selector in edit form

* feat(edge-groups): show tags in endpoint selector

* feat(edge-groups): show the endpoint group name

* fix(edge-group): remove element from associated endpoints

* feat(edge-groups): add group column

* feat(edge-groups): move endpoints to other column

* fix(groups): disable sort

* refactor(endpoints): toggle backend pagination as a property

* fix(endpoints): show group name in group-association-table

* feat(endpoints): truncate table columns

* fix(endpoints): update group association table colspan

* fix(endpoint-groups): show dash when no tags

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>

* feat(edge-stacks): add api for edge to query stack config (#3748)

* refactor(http): move edge validation to bouncer

* feat(edge-stacks): add api for edge to query stack config

* style(edge-stack): remove parentheses

* Update api/http/security/bouncer.go

* refactor(edge-stacks): move config inspect to endpoints handler

* refactor(endpoints): move stack inspect to edge handler

* style(security): fix typo

Co-Authored-By: Anthony Lapenna <anthony.lapenna@portainer.io>

* refactor(endpoints): rename file

Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>

* feat(edge-groups): add dynamic group endpoints table (#3780)

* fix(edge-stacks): update version when updating stack files (#3778)

* feat(edgestacks): change status permission to edge enpoints

* feat(edge-compute): add stack info to edge status inspect (#3764)

* feat(edge-compute): create helper functions

* feat(endpoints): add relation object and service

* feat(db): create endpoint relation migration

* feat(endpoints): create relation when creating endpoint

* feat(endpoints): update relation when updating endpoint

* feat(endpoints): delete relation when deleting endpoint

* feat(endpoint): add stack status to endpoint_status

* feat(edge-stacks): connect new edge stack to endpoint

* refactor(edgestack): return errors.New

* refactor(edgestacks): return error

* refactor(edgegroup): endpoint can be related only if edge endpoint

* feat(endpoints): update relation only when tags or groups were changd

* refactor(tags): change tags functions to set functions

* refactor(edgestack): return a list of endpoints for a list of edgegroups

* feat(edgestacks): update relation when updating stack

* feat(edgestacks): remove relations when deleting edge stack

* feat(edgegroup): update related endpoints

* feat(endpoint-group): update endpoints relations on create

* feat(endpointgroup): add relatd stacks to endpoint when added to group

* feat(endpoint-groups): update relation when group is changed

* feat(endpointgroup): when deleting group, update its endpoints relations

* feat(tags): update related endpoints when deleting tag

* refactor(edge-compute): use pointers

* refactor(endpointgroup): handle unassociated endpoint

* fix(edgestack): show correct stack status

* fix(endpoint): remove deleted endpoint from related tags

* feat(edge-stacks): change acknowledged status color to blue (#3810)

* feat(edge-compute): provide stack name to edge endpoint (#3809)

* feat(edge-groups): when no tags selected show empty list of endpoints (#3811)

* feat(edge-groups): when no tags selected show empty list of endpoints

* fix(edge-group): change empty associated endpoint text

* fix(edge-compute): add missing relations updates (#3817)

* fix(endpoint): remove deleted endpoint from edge group

* fix(tags): remove deleted tag from edge group

* fix(endpoint): remove deleted endpoint from edge stack

* fix(edge-groups): remove clearing of edgeGroup fields

* fix(edge-groups): show dynamic edge groups without tags

* fix(edge-compute): use sequential delete in resources (#3818)

* fix(endpoints): delete endpoints on by one

* fix(tags): remove tags one by one

* fix(groups): remove endpoint groups one by one

* fix(edge-stacks): remove stack one by one

* fix(edge-groups): remove edge group one by one

* fix(edge-stacks): add link to root in breadcrumbs

* style(edge): add empty line after errors

* refactor(tags): remove old function

* refactor(endpoints): revert changes to multi-endpoint-selector

* feat(edge-stacks): support Edge stack templates (#3812)

* feat(edge-compute): fetch templates from url

* feat(edge-stacks): fetch edge templates

* feat(edge-stacks): choose template and save

* feat(edge-stacks): add placeholder to templates select

* feat(edge-templates): show info

* fix(edge-stacks): fix typo

* feat(edge-templates): replace template url

* feat(edge-compute): use custom url if available

* fix(edge-stacks): show error message when failing

* feat(edge-compute): show description in template

* feat(edge-templates): change access to route

* style(edge-compute): change EdgeTemplatesURL description

Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>

Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>

Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>
Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
Co-authored-by: itsconquest <william.conquest@portainer.io>
Co-authored-by: Ben Brooks <ben@bbrks.me>
Co-authored-by: Neil Cresswell <neil@cresswell.net.nz>
2020-05-14 14:14:28 +12:00
Anthony Lapenna 8e09b935cd feat(cli): add deprecation warnings (#3826) 2020-05-13 16:21:17 +12:00
Anthony Lapenna 9dcd223134 feat(stacks): prevent external stack removal by a non-administrator user (#3800)
* fix(stacks): prevent external stacks removal by non admin

* feat(stacks): add RBAC checks for external stack removals

Co-authored-by: Maxime Bajeux <max.bajeux@gmail.com>
2020-05-13 15:37:35 +12:00
Anthony Lapenna 29c0584454 fix(api): update restricted volume browsing operation logic (#3798)
* fix(api): prevent a potential panic

* fix(api): update restricted volume browsing operation logic
2020-05-12 16:08:01 +12:00
Anthony Lapenna 5c274f5b0c docs(readme): update README 2020-05-12 10:30:12 +12:00
Maxime Bajeux b3af91cea3 feat(volumes): Revise the UX for creation of NFS volumes (#3815)
* feat(volumes): Revise the UX for creation of NFS volumes

* feat(volume-creation): minor UI update

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
2020-05-12 10:23:42 +12:00
Anthony Lapenna c8f55ac896 feat(project): update pull-dog configuration 2020-05-11 11:23:56 +12:00
Anthony Lapenna 659e4486db feat(project): add pull-dog config 2020-05-11 09:31:07 +12:00
pull-dog-user cc091ee589 feat(project): enable Pull Dog for the repository (#3801)
* Create pull-dog.json

* Create docker-compose.pull-dog.yml

* Update pull-dog.json

* Update docker-compose.pull-dog.yml
2020-05-11 09:25:30 +12:00
Maxime Bajeux 8046fb0438 fix(volumes): add unicity check on volumes (#3779)
* fix(volumes): add unicity check on volumes

* fix(volumes): add header to volume creation request

* fix(volumes): change few things
2020-05-09 09:40:49 +12:00
Chaim Lev-Ari 7fa73d1147 fix(endpoints): show line breaks in edge run commands (#3794) 2020-05-06 17:45:17 +12:00
Shivam Goyal bfd6cca33f feat(image-build): fix typo (#3787) 2020-05-04 19:54:09 +12:00
Maxime Bajeux 7fe7ce1a0a fix(container-creation): ensure exposedPorts exists (#3770) 2020-04-29 14:33:37 +12:00
Maxime Bajeux 7f0ce61413 feat(networks): remove the ability to create host network (#3771) 2020-04-29 14:05:32 +12:00
Maxime Bajeux 3de533042d feat(networks): add ipv6 support (#3717)
* feat(portainer-core): add ipv6 support

* feat(networks): add few changes

* refacto(networks): write regex once

* fix(networks): fix indentation

* refacto(networks): use foreach instead map and pluralize ipvxconfig

* refacto(networks): pluralize ipvxconfig

* feat(networks): support ipv6 with ports

* feat(networks): add an explicit error message

* fix(networks): hide ipv6 configuration when creating macvlan
2020-04-28 12:34:54 +12:00
Dan Underwood b2f36a3bbe fix(networking): convert from localhost addresses to 127.0.0.1 (#3411) 2020-04-27 13:54:37 +12:00
Anthony Lapenna 3d5bdab620 feat(project): add alpine based Dockerfile (#3759) 2020-04-27 13:46:12 +12:00
Anthony Lapenna fee20248ea Update CONTRIBUTING.md 2020-04-21 11:45:01 +12:00
Mariell f525c8d022 feat(container-creation): add support for --init (#2111) (#3714) 2020-04-15 13:09:42 +12:00
Chaim Lev-Ari bba622a500 chore(eslint): add rule to sort imports (#3715)
* chore(eslint): add plugin to sort imports

* chore(eslint): sort imports

* chore(eslint): add eslint-config-prettier
2020-04-15 11:46:34 +12:00
Chaim Lev-Ari cf5056d9c0 chore(project): add prettier for code format (#3645)
* chore(project): install prettier and lint-staged

* chore(project): apply prettier to html too

* chore(project): git ignore eslintcache

* chore(project): add a comment about format script

* chore(prettier): update printWidth

* chore(prettier): remove useTabs option

* chore(prettier): add HTML validation

* refactor(prettier): fix closing tags

* feat(prettier): define angular parser for html templates

* style(prettier): run prettier on codebase

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
2020-04-11 09:54:53 +12:00
Neil Cresswell 6663073be1 Update README.md 2020-04-09 10:01:20 +12:00
Neil Cresswell 18a38d597a Update README.md 2020-04-09 10:00:29 +12:00
Neil Cresswell aeea88be36 Update README.md 2020-04-09 09:59:14 +12:00
Chaim Lev-Ari 6da38d466b refactor(project): sort portainer types and interface definitions (#3694)
* refactor(portainer): sort types

* style(portainer): add comment about role service

* refactor(portainer): sort interface types

* refactor(portainer): sort enums
2020-04-09 09:26:11 +12:00
Chaim Lev-Ari 2542d30a09 feat(endpoints): filter by ids and/or tag ids (#3690)
* feat(endpoints): add filter by tagIds

* refactor(endpoints): change endpoints service to query by tagIds

* fix(endpoints): filter by tags

* feat(endpoints): filter by endpoint groups tags

* feat(endpoints): filter by ids
2020-04-08 21:14:50 +12:00
Chaim Lev-Ari df13f3b4cc chore(yarn): add start:client script back (#3691) 2020-04-08 21:03:52 +12:00
Chaim Lev-Ari db8b3d6e5a create tag from tag selector (#3640)
* feat(tags): add button to save tag when doesn't exist

* feat(endpoints): allow the creating of tags in endpoint edit

* feat(groups): allow user to create tags in create group

* feat(groups): allow user to create tags in edit group

* feat(endpoint): allow user to create tags from endpoint create

* feat(tags): allow the creation of a new tag from dropdown

* feat(tag): replace "add" with "create"

* feat(tags): show tags input when not tags

* feat(tags): hide create message when not allowed

* refactor(tags): replace component controller with class

* refactor(tags): replace native methods with lodash

* refactor(tags): remove unused onChangeTags function

* refactor(tags): remove on-change binding

* style(tags): remove white space

* refactor(endpoint-groups): move controller to separate file

* fix(groups): allow admin to create tag in group form

* refactor(endpoints): wrap async function with try catch and $async

* style(tags): wrap arrow function args with parenthesis

* refactor(endpoints): return $async functions

* refactor(tags): throw error in the format Notification expects
2020-04-08 19:56:24 +12:00
Chaim Lev-Ari dd6262cf69 chore(yarn): change start:client to start webpack dev server (#3595)
* chore(yarn): change start:client to start webpack dev server

* Update package.json

Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>
2020-04-06 09:06:59 +12:00
Chaim Lev-Ari edd86f2506 refactor(tags): refactor tag management (#3628)
* refactor(tags): replace tags with tag ids

* refactor(tags): revert tags to be strings and add tagids

* refactor(tags): enable search by tag in home view

* refactor(tags): show endpoint tags

* refactor(endpoints): expect tagIds on create payload

* refactor(endpoints): expect tagIds on update payload

* refactor(endpoints): replace TagIds to TagIDs

* refactor(endpoints): set endpoint group to get TagIDs

* refactor(endpoints): refactor tag-selector to receive tag-ids

* refactor(endpoints): show tags in multi-endpoint-selector

* chore(tags): revert reformat

* refactor(endpoints): remove unneeded bind

* refactor(endpoints): change param tags to tagids in endpoint create

* refactor(endpoints): remove console.log

* refactor(tags): remove deleted tag from endpoint and endpoint group

* fix(endpoints): show loading label while loading tags

* chore(go): remove obsolete import labels

* chore(db): add db version comment

* fix(db): add tag service to migrator

* refactor(db): add error checks in migrator

* style(db): sort props in alphabetical order

* style(tags): fix typo

Co-Authored-By: Anthony Lapenna <anthony.lapenna@portainer.io>

* refactor(endpoints): replace tagsMap with tag string representation

* refactor(tags): rewrite tag delete to be more readable

* refactor(home): rearange code to match former style

* refactor(tags): guard against missing model in tag-selector

* refactor(tags): rename vars in tag_delete

* refactor(tags): allow any authenticated user to fetch tag list

* refactor(endpoints): replace controller function with class

* refactor(endpoints): replace function with helper

* refactor(endpoints): replace controller with class

* refactor(tags): revert tags-selector to use 1 way bindings

* refactor(endpoints): load empty tag array instead of nil

* refactor(endpoints): revert default tag ids

* refactor(endpoints): use function in place

* refactor(tags): use lodash

* style(tags): use parens in arrow functions

* fix(tags): remove tag from tag model

Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>
2020-03-29 22:54:14 +13:00
Ben Brooks fe89a4fc01 fix(services): enforce minimum replica count of 0 (#3653)
* fix(services): enforce minimum replica count of 0

Fixes #3652

Prevents replica count from being set below zero and causing an error.

* fix(services): enforce replica count is an integer

Prevents users entering decimals in the replica count
2020-03-29 13:25:50 +13:00
Ben Brooks 00bef100ee chore(assets): double UI image resolutions for HiDPI displays (#3648)
Fixes #3069

Prevents users seeing blurry logos and other images when using a hidpi
display (like scaled 4k, or a Retina display).

These images have been recreated manually with 2x the original
resolution but should resemble the originals as much as possible.

They have also been run through pngcrush for compression.
2020-03-27 16:49:20 +13:00
Chaim Lev-Ari ae7f46c8ef feat(endpoints): filter by endpoint type (#3646) 2020-03-26 18:44:27 +13:00
Anthony Lapenna 78558f9c8e chore(version): bump version number 2020-03-20 11:37:35 +13:00
Anthony Lapenna 5a3caab9c4 Merge tag '1.23.2' into develop
Release 1.23.2
2020-03-20 11:35:49 +13:00
Anthony Lapenna 5396a069f2 Merge branch 'release/1.23.2' 2020-03-20 11:35:43 +13:00
Anthony Lapenna 2a92fcb802 chore(version): bump version number 2020-03-20 11:35:36 +13:00
itsconquest 2c400eb3b4 feat(support): make support type dynamic (#3621) 2020-03-19 09:38:56 +13:00
Anthony Lapenna a11a348893 fix(containers): do not persist container status filter (#3615) 2020-03-16 14:37:59 +13:00
itsconquest d022853059 feat(support): add new offerings (#3608)
* feat(support): add new offerings

* feat(support): refactor for simplicity

* feat(support): rename for clarity
2020-03-16 11:35:55 +13:00
xAt0mZ bfdb4dba12 fix(container-creation): validate runtime property (#3581)
Co-authored-by: linquize <linquize2@yahoo.com>
2020-02-26 16:31:59 +13:00
William 8d7bae0560 fix(dependencies): bump go-winio lib (#3569)
* fix(dependencies): bump go-winio lib

* fix(api): update gomod

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
2020-02-25 18:56:34 +13:00
Anthony Lapenna e0d83db609 fix(authentication/ldap): fix an issue with authorizations not updated after ldap login (#3577) 2020-02-25 18:54:32 +13:00
Steven Kang ad5f51964c fix(build): Remove -ErrorAction:SilentlyContinue (#3571) 2020-02-22 18:18:09 +13:00
Anthony Lapenna 9cc8448418 chore(version): bump version number 2020-02-20 09:24:06 +13:00
Anthony Lapenna b2cc6be007 fix(api): update gomod 2020-02-20 08:20:45 +13:00
Anthony Lapenna be0b01611f Merge tag '1.23.1' into develop
Release 1.23.1
2020-02-18 13:46:17 +13:00
Anthony Lapenna bcda7e2d7e Merge branch 'release/1.23.1' 2020-02-18 13:46:10 +13:00
Anthony Lapenna d0e998ddc4 chore(version): bump version number 2020-02-18 13:46:04 +13:00
William 1f7e5fec4f fix(settings/authentication): patch LDAP connectivity check (#3561) 2020-02-18 12:25:30 +13:00
Anthony Lapenna d3a625e22f feat(api): update gomod 2020-02-15 09:09:45 +13:00
William eff1b79a4a fix(networks): patch datatable (#3557) 2020-02-12 20:41:41 +01:00
William 0330b16776 fix(container-create): patch dns (#3556) 2020-02-11 15:23:56 +01:00
William 97a0ea4a31 fix(templates): select volume name not object (#3549) 2020-02-06 09:07:23 +13:00
William 167d4319b5 fix(authentication): frontend changes (#3456)
* fix(authentication): frontend changes

* fix(authentication): suggested changes

* fix(authentication): support AnonymousMode boolean

* feat(authentication): support empty vals + move from scope to formValues

* feat(authentication): allow test of TLS & anon

* feat(authentication): remove unneeded whitespace

* feat(authentication): remove un-needed whitespace

* feat(refactor): rebase + cleanup logic
2020-02-06 09:06:22 +13:00
Mike Church 6f59f130a1 feat(dashboard): add health status to home page and dashboard (#3489)
* feat(dashboard): add health status to home page and dashboard

* fix(dashboard): code review updates, using builtin for substring search
2020-02-05 07:59:29 +13:00
William cc8d3c8639 refactor(UX): disable/remove uneeded UI elements (#3530)
* refactor(UX): disable/remove uneeded UI elements

* refactor(UX): rm missed th in container network

* refactor(UX): minor formatting improvement
2020-01-31 16:49:59 +01:00
Hugo Hromic f4c461d7fb feat(settings/ldap): preserve ReaderDN in database if empty in settings payload (#3537)
* Allows to avoid changing any current value when using LDAP Anonymous Mode
2020-01-31 13:17:19 +13:00
William 6c492d2290 fix(UX): prevent task expand on row selection (#3531)
* fix(UX): prevent task expand on row selection

* refactor(UX): apply service expand logic to networks
2020-01-29 14:44:02 +01:00
William 8bea0988dd fix(api): lower Docker client API version for backwards support (#3534) 2020-01-29 17:36:28 +13:00
William 8dda67c8d0 refactor(UX): fix grammar (#3528) 2020-01-27 14:02:04 +13:00
William 7365afa1bb feature(UX): sort dropdowns alphabetically (#3524) 2020-01-25 09:53:48 +13:00
Kirill K 1ef29f2671 feat: add setting to change DNS servers (#3511)
* feat: add setting to change DNS servers

* style: fixing codeclimate warning

Looks like conditional was excessive, it works as expected even without
it.

* style: rename 'DNS Server 1/2' to 'Primary/Secondary DNS Server'

Signed-off-by: Kirill K <kovalev.kirill.a@gmail.com>

* style: rename variables in code to match UI naming

* feat: add tooltips on DNS servers input fields

Signed-off-by: Kirill K <kovalev.kirill.a@gmail.com>

* Revert "feat: add tooltips on DNS servers input fields"

This reverts commit b83ef50825e9fa015668627dae309d70f481b86d.

* style: secondary DNS placeholder

Signed-off-by: Kirill K <kovalev.kirill.a@gmail.com>
2020-01-24 15:49:28 +01:00
William fa5bb9b1be feat(stack-creation): add note for 2FA (#3509)
* refactor(stack-creation): add note for 2FA

* Update app/portainer/views/stacks/create/createstack.html

Co-Authored-By: Anthony Lapenna <anthony.lapenna@portainer.io>

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
2020-01-23 09:17:04 +13:00
Hugo Hromic 2ba195adaa feat(api): implement anonymous mode for LDAP connection (#3460)
* When enabled, ReaderDN and Password will not be used
* Anonymous mode is set to `true` by default on fresh installations
2020-01-22 11:14:07 +13:00
William 9da08bc792 refactor(endpoint-creation): remove unneeded port (#3467)
* refactor(endpoint-creation): remove unneeded port

* refactor(endpoint-creation): remove from clipboard
2020-01-22 09:17:41 +13:00
William 17bc17f638 fix(api): fix an issue with ownership for services and stacks (#3512) 2020-01-21 08:09:30 +13:00
William efae49d92b chore(project): require Portainer logs (#3508) 2020-01-19 18:46:40 +13:00
Iceyer 58c00401e9 fix: atob convert unicode in config failed (#3415)
atob only support decode ascii char, when this unicode in config, it will show unknown char.
2019-12-09 17:52:02 +01:00
Anthony Lapenna e9f6861df0 feat(api): add go module support (#3408)
* feat(api): experiment with go mod

* feat(api): experiment with go mod

* feat(api): experiment with go mod

* feat(api): add go module support

* refactor(api): go fmt
2019-12-05 17:02:27 +13:00
Anthony Lapenna bba13f69ad chore(version): bump version number 2019-12-04 17:07:21 +13:00
Anthony Lapenna 36020dd8bc Merge tag '1.23.0' into develop
Release 1.23.0
2019-12-04 17:05:49 +13:00
881 changed files with 39841 additions and 29861 deletions
+5 -268
View File
@@ -10,6 +10,10 @@ globals:
extends:
- 'eslint:recommended'
- prettier
plugins:
- import
parserOptions:
ecmaVersion: 2018
@@ -17,276 +21,9 @@ parserOptions:
ecmaFeatures:
modules: true
# # http://eslint.org/docs/rules/
rules:
# # Possible Errors
# no-await-in-loop: off
# no-cond-assign: error
# no-console: off
# no-constant-condition: error
no-control-regex: off
# no-debugger: error
# no-dupe-args: error
# no-dupe-keys: error
# no-duplicate-case: error
# no-empty-character-class: error
no-empty: warn
# no-ex-assign: error
# no-extra-boolean-cast: error
# no-extra-parens: off
# no-extra-semi: error
# no-func-assign: error
# no-inner-declarations:
# - error
# - functions
# no-invalid-regexp: error
# no-irregular-whitespace: error
# no-negated-in-lhs: error
# no-obj-calls: error
# no-prototype-builtins: off
# no-regex-spaces: error
# no-sparse-arrays: error
# no-template-curly-in-string: off
# no-unexpected-multiline: error
# no-unreachable: error
# no-unsafe-finally: off
# no-unsafe-negation: off
# use-isnan: error
# valid-jsdoc: off
# valid-typeof: error
# # Best Practices
# accessor-pairs: error
# array-callback-return: off
# block-scoped-var: off
# class-methods-use-this: off
# complexity:
# - error
# - 6
# consistent-return: off
# curly: off
# default-case: off
# dot-location: off
# dot-notation: off
# eqeqeq: error
# guard-for-in: error
# no-alert: error
# no-caller: error
# no-case-declarations: error
# no-div-regex: error
# no-else-return: off
no-empty-function: warn
# no-empty-pattern: error
# no-eq-null: error
# no-eval: error
# no-extend-native: error
# no-extra-bind: error
# no-extra-label: off
# no-fallthrough: error
# no-floating-decimal: off
# no-global-assign: off
# no-implicit-coercion: off
# no-implied-eval: error
# no-invalid-this: off
# no-iterator: error
# no-labels:
# - error
# - allowLoop: true
# allowSwitch: true
# no-lone-blocks: error
# no-loop-func: error
# no-magic-number: off
# no-multi-spaces: off
# no-multi-str: off
# no-native-reassign: error
# no-new-func: error
# no-new-wrappers: error
# no-new: error
# no-octal-escape: error
# no-octal: error
# no-param-reassign: off
# no-proto: error
# no-redeclare: error
# no-restricted-properties: off
# no-return-assign: error
# no-return-await: off
# no-script-url: error
# no-self-assign: off
# no-self-compare: error
# no-sequences: off
# no-throw-literal: off
# no-unmodified-loop-condition: off
# no-unused-expressions: error
# no-unused-labels: off
# no-useless-call: error
# no-useless-concat: error
no-useless-escape: off
# no-useless-return: off
# no-void: error
# no-warning-comments: off
# no-with: error
# prefer-promise-reject-errors: off
# radix: error
# require-await: off
# vars-on-top: off
# wrap-iife: error
# yoda: off
# # Strict
# strict: off
# # Variables
# init-declarations: off
# no-catch-shadow: error
# no-delete-var: error
# no-label-var: error
# no-restricted-globals: off
# no-shadow-restricted-names: error
# no-shadow: off
# no-undef-init: error
# no-undef: off
# no-undefined: off
# no-unused-vars:
# - warn
# -
# vars: local
# no-use-before-define: off
# # Node.js and CommonJS
# callback-return: error
# global-require: error
# handle-callback-err: error
# no-mixed-requires: off
# no-new-require: off
# no-path-concat: error
# no-process-env: off
# no-process-exit: error
# no-restricted-modules: off
# no-sync: off
# # Stylistic Issues
# array-bracket-spacing: off
# block-spacing: off
# brace-style: off
# camelcase: off
# capitalized-comments: off
# comma-dangle:
# - error
# - never
# comma-spacing: off
# comma-style: off
# computed-property-spacing: off
# consistent-this: off
# eol-last: off
# func-call-spacing: off
# func-name-matching: off
# func-names: off
# func-style: off
# id-length: off
# id-match: off
# indent: off
# jsx-quotes: off
# key-spacing: off
# keyword-spacing: off
# line-comment-position: off
# linebreak-style:
# - error
# - unix
# lines-around-comment: off
# lines-around-directive: off
# max-depth: off
# max-len: off
# max-nested-callbacks: off
# max-params: off
# max-statements-per-line: off
# max-statements:
# - error
# - 30
# multiline-ternary: off
# new-cap: off
# new-parens: off
# newline-after-var: off
# newline-before-return: off
# newline-per-chained-call: off
# no-array-constructor: off
# no-bitwise: off
# no-continue: off
# no-inline-comments: off
# no-lonely-if: off
# no-mixed-operators: off
# no-mixed-spaces-and-tabs: off
# no-multi-assign: off
# no-multiple-empty-lines: off
# no-negated-condition: off
# no-nested-ternary: off
# no-new-object: off
# no-plusplus: off
# no-restricted-syntax: off
# no-spaced-func: off
# no-tabs: off
# no-ternary: off
# no-trailing-spaces: off
# no-underscore-dangle: off
# no-unneeded-ternary: off
# object-curly-newline: off
# object-curly-spacing: off
# object-property-newline: off
# one-var-declaration-per-line: off
# one-var: off
# operator-assignment: off
# operator-linebreak: off
# padded-blocks: off
# quote-props: off
# quotes:
# - error
# - single
# require-jsdoc: off
# semi-spacing: off
# semi:
# - error
# - always
# sort-keys: off
# sort-vars: off
# space-before-blocks: off
# space-before-function-paren: off
# space-in-parens: off
# space-infix-ops: off
# space-unary-ops: off
# spaced-comment: off
# template-tag-spacing: off
# unicode-bom: off
# wrap-regex: off
# # ECMAScript 6
# arrow-body-style: off
# arrow-parens: off
# arrow-spacing: off
# constructor-super: off
# generator-star-spacing: off
# no-class-assign: off
# no-confusing-arrow: off
# no-const-assign: off
# no-dupe-class-members: off
# no-duplicate-imports: off
# no-new-symbol: off
# no-restricted-imports: off
# no-this-before-super: off
# no-useless-computed-key: off
# no-useless-constructor: off
# no-useless-rename: off
# no-var: off
# object-shorthand: off
# prefer-arrow-callback: off
# prefer-const: off
# prefer-destructuring: off
# prefer-numeric-literals: off
# prefer-rest-params: off
# prefer-reflect: off
# prefer-spread: off
# prefer-template: off
# require-yield: off
# rest-spread-spacing: off
# sort-imports: off
# symbol-description: off
# template-curly-spacing: off
# yield-star-spacing: off
import/order: error
+4
View File
@@ -26,6 +26,10 @@ A clear and concise description of what the bug is.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Portainer Logs**
Provide the logs of your Portainer container or Service.
You can see how [here](https://portainer.readthedocs.io/en/stable/faq.html#how-do-i-get-the-logs-from-portainer)
**Steps to reproduce the issue:**
1. Go to '...'
2. Click on '....'
+2 -1
View File
@@ -4,4 +4,5 @@ dist
portainer-checksum.txt
api/cmd/portainer/portainer*
.tmp
.vscode
.vscode
.eslintcache
+13
View File
@@ -0,0 +1,13 @@
{
"printWidth": 180,
"singleQuote": true,
"htmlWhitespaceSensitivity": "strict",
"overrides": [
{
"files": ["*.html"],
"options": {
"parser": "angular"
}
}
]
}
+1 -15
View File
@@ -15,21 +15,7 @@ For example, if you work on a bugfix for the issue #361, you could name the bran
## Issues open to contribution
Want to contribute but don't know where to start?
Some of the open issues are labeled with prefix `exp/`, this is used to mark them as available for contributors to work on. All of these have an attributed difficulty level:
* **beginner**: a task that should be accessible with users not familiar with the codebase
* **intermediate**: a task that require some understanding of the project codebase or some experience in
either AngularJS or Golang
* **advanced**: a task that require a deep understanding of the project codebase
You can use Github filters to list these issues:
* beginner labeled issues: https://github.com/portainer/portainer/labels/exp%2Fbeginner
* intermediate labeled issues: https://github.com/portainer/portainer/labels/exp%2Fintermediate
* advanced labeled issues: https://github.com/portainer/portainer/labels/exp%2Fadvanced
Want to contribute but don't know where to start? Have a look at the issues labeled with the `good first issue` label: https://github.com/portainer/portainer/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22
## Commit Message Format
+15 -21
View File
@@ -3,15 +3,14 @@
</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)
[![Microbadger](https://images.microbadger.com/badges/image/portainer/portainer.svg)](http://microbadger.com/images/portainer/portainer 'Image size')
[![Build Status](https://portainer.visualstudio.com/Portainer%20CI/_apis/build/status/Portainer%20CI?branchName=develop)](https://portainer.visualstudio.com/Portainer%20CI/_build/latest?definitionId=3&branchName=develop)
[![Code Climate](https://codeclimate.com/github/portainer/portainer/badges/gpa.svg)](https://codeclimate.com/github/portainer/portainer)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6)
**_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 (can be deployed as Linux container or a Windows native container, supports other platforms too).
**_Portainer_** allows you to manage all your Docker resources (containers, images, volumes, networks and more) ! It is compatible with the *standalone Docker* engine and with *Docker Swarm mode*.
**_Portainer_** allows you to manage all your Docker resources (containers, images, volumes, networks and more) ! It is compatible with the _standalone Docker_ engine and with _Docker Swarm mode_.
## Demo
@@ -29,36 +28,31 @@ Unlike the public demo, the playground sessions are deleted after 4 hours. Apart
## Getting started
* [Deploy Portainer](https://portainer.readthedocs.io/en/latest/deployment.html)
* [Documentation](https://portainer.readthedocs.io)
- [Deploy Portainer](https://www.portainer.io/installation/)
- [Documentation](https://www.portainer.io/documentation/)
## Getting help
**NOTE**: You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
For FORMAL Support, please purchase a support subscription from here: https://www.portainer.io/products-services/portainer-business-support/
* Issues: https://github.com/portainer/portainer/issues
* FAQ: https://portainer.readthedocs.io/en/latest/faq.html
* Slack (chat): https://portainer.io/slack/
For community support: You can find more information about Portainer's community support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
- Issues: https://github.com/portainer/portainer/issues
- FAQ: https://www.portainer.io/documentation/faqs/
- Slack (chat): https://portainer.io/slack/
## Reporting bugs and contributing
* Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new).
* Want to help us build **_portainer_**? Follow our [contribution guidelines](https://portainer.readthedocs.io/en/latest/contribute.html) to build it locally and make a pull request. We need all the help we can get!
- Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new).
- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://www.portainer.io/documentation/how-to-contribute/) to build it locally and make a pull request. We need all the help we can get!
## Security
* Here at Portainer, we believe in [responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure) of security issues. If you have found a security issue, please report it to <security@portainer.io>.
- Here at Portainer, we believe in [responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure) of security issues. If you have found a security issue, please report it to <security@portainer.io>.
## Limitations
**_Portainer_** has full support for the following Docker versions:
* Docker 1.10 to the latest version
* Standalone Docker Swarm >= 1.2.3 _(**NOTE:** Use of Standalone Docker Swarm is being discouraged since the introduction of built-in Swarm Mode in Docker. While older versions of Portainer had support for Standalone Docker Swarm, Portainer 1.17.0 and newer **do not** support it. However, the built-in Swarm Mode of Docker is fully supported.)_
Partial support for the following Docker versions (some features may not be available):
* Docker 1.9
Portainer supports "Current - 2 docker versions only. Prior versions may operate, however these are not supported.
## Licensing
@@ -68,4 +62,4 @@ Portainer also contains the following code, which is licensed under the [MIT lic
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]
rdash-angular: Copyright (c) [2014][elliot hesp]
+64 -38
View File
@@ -5,6 +5,9 @@ import (
"path"
"time"
"github.com/portainer/portainer/api/bolt/edgegroup"
"github.com/portainer/portainer/api/bolt/edgestack"
"github.com/portainer/portainer/api/bolt/endpointrelation"
"github.com/portainer/portainer/api/bolt/tunnelserver"
"github.com/boltdb/bolt"
@@ -36,28 +39,31 @@ const (
// Store defines the implementation of portainer.DataStore using
// BoltDB as the storage system.
type Store struct {
path string
db *bolt.DB
checkForDataMigration bool
fileService portainer.FileService
RoleService *role.Service
DockerHubService *dockerhub.Service
EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service
ExtensionService *extension.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
SettingsService *settings.Service
StackService *stack.Service
TagService *tag.Service
TeamMembershipService *teammembership.Service
TeamService *team.Service
TemplateService *template.Service
TunnelServerService *tunnelserver.Service
UserService *user.Service
VersionService *version.Service
WebhookService *webhook.Service
ScheduleService *schedule.Service
path string
db *bolt.DB
checkForDataMigration bool
fileService portainer.FileService
RoleService *role.Service
DockerHubService *dockerhub.Service
EdgeGroupService *edgegroup.Service
EdgeStackService *edgestack.Service
EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service
EndpointRelationService *endpointrelation.Service
ExtensionService *extension.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
SettingsService *settings.Service
StackService *stack.Service
TagService *tag.Service
TeamMembershipService *teammembership.Service
TeamService *team.Service
TemplateService *template.Service
TunnelServerService *tunnelserver.Service
UserService *user.Service
VersionService *version.Service
WebhookService *webhook.Service
ScheduleService *schedule.Service
}
// NewStore initializes a new Store and the associated services
@@ -117,22 +123,24 @@ func (store *Store) MigrateData() error {
if version < portainer.DBVersion {
migratorParams := &migrator.Parameters{
DB: store.db,
DatabaseVersion: version,
EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService,
ExtensionService: store.ExtensionService,
RegistryService: store.RegistryService,
ResourceControlService: store.ResourceControlService,
RoleService: store.RoleService,
ScheduleService: store.ScheduleService,
SettingsService: store.SettingsService,
StackService: store.StackService,
TeamMembershipService: store.TeamMembershipService,
TemplateService: store.TemplateService,
UserService: store.UserService,
VersionService: store.VersionService,
FileService: store.fileService,
DB: store.db,
DatabaseVersion: version,
EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService,
EndpointRelationService: store.EndpointRelationService,
ExtensionService: store.ExtensionService,
RegistryService: store.RegistryService,
ResourceControlService: store.ResourceControlService,
RoleService: store.RoleService,
ScheduleService: store.ScheduleService,
SettingsService: store.SettingsService,
StackService: store.StackService,
TagService: store.TagService,
TeamMembershipService: store.TeamMembershipService,
TemplateService: store.TemplateService,
UserService: store.UserService,
VersionService: store.VersionService,
FileService: store.fileService,
}
migrator := migrator.NewMigrator(migratorParams)
@@ -160,6 +168,18 @@ func (store *Store) initServices() error {
}
store.DockerHubService = dockerhubService
edgeStackService, err := edgestack.NewService(store.db)
if err != nil {
return err
}
store.EdgeStackService = edgeStackService
edgeGroupService, err := edgegroup.NewService(store.db)
if err != nil {
return err
}
store.EdgeGroupService = edgeGroupService
endpointgroupService, err := endpointgroup.NewService(store.db)
if err != nil {
return err
@@ -172,6 +192,12 @@ func (store *Store) initServices() error {
}
store.EndpointService = endpointService
endpointRelationService, err := endpointrelation.NewService(store.db)
if err != nil {
return err
}
store.EndpointRelationService = endpointRelationService
extensionService, err := extension.NewService(store.db)
if err != nil {
return err
+94
View File
@@ -0,0 +1,94 @@
package edgegroup
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "edgegroups"
)
// Service represents a service for managing Edge group 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
}
// EdgeGroups return an array containing all the Edge groups.
func (service *Service) EdgeGroups() ([]portainer.EdgeGroup, error) {
var groups = make([]portainer.EdgeGroup, 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 group portainer.EdgeGroup
err := internal.UnmarshalObjectWithJsoniter(v, &group)
if err != nil {
return err
}
groups = append(groups, group)
}
return nil
})
return groups, err
}
// EdgeGroup returns an Edge group by ID.
func (service *Service) EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGroup, error) {
var group portainer.EdgeGroup
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &group)
if err != nil {
return nil, err
}
return &group, nil
}
// UpdateEdgeGroup updates an Edge group.
func (service *Service) UpdateEdgeGroup(ID portainer.EdgeGroupID, group *portainer.EdgeGroup) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, group)
}
// DeleteEdgeGroup deletes an Edge group.
func (service *Service) DeleteEdgeGroup(ID portainer.EdgeGroupID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
// CreateEdgeGroup assign an ID to a new Edge group and saves it.
func (service *Service) CreateEdgeGroup(group *portainer.EdgeGroup) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
group.ID = portainer.EdgeGroupID(id)
data, err := internal.MarshalObject(group)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(group.ID)), data)
})
}
+101
View File
@@ -0,0 +1,101 @@
package edgestack
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "edge_stack"
)
// Service represents a service for managing Edge stack 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
}
// EdgeStacks returns an array containing all edge stacks
func (service *Service) EdgeStacks() ([]portainer.EdgeStack, error) {
var stacks = make([]portainer.EdgeStack, 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.EdgeStack
err := internal.UnmarshalObject(v, &stack)
if err != nil {
return err
}
stacks = append(stacks, stack)
}
return nil
})
return stacks, err
}
// EdgeStack returns an Edge stack by ID.
func (service *Service) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStack, error) {
var stack portainer.EdgeStack
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &stack)
if err != nil {
return nil, err
}
return &stack, nil
}
// CreateEdgeStack assign an ID to a new Edge stack and saves it.
func (service *Service) CreateEdgeStack(edgeStack *portainer.EdgeStack) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
if edgeStack.ID == 0 {
id, _ := bucket.NextSequence()
edgeStack.ID = portainer.EdgeStackID(id)
}
data, err := internal.MarshalObject(edgeStack)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(edgeStack.ID)), data)
})
}
// UpdateEdgeStack updates an Edge stack.
func (service *Service) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, edgeStack)
}
// DeleteEdgeStack deletes an Edge stack.
func (service *Service) DeleteEdgeStack(ID portainer.EdgeStackID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
// GetNextIdentifier returns the next identifier for an endpoint.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
}
@@ -0,0 +1,68 @@
package endpointrelation
import (
"github.com/boltdb/bolt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "endpoint_relations"
)
// Service represents a service for managing endpoint relation 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
}
// EndpointRelation returns a Endpoint relation object by EndpointID
func (service *Service) EndpointRelation(endpointID portainer.EndpointID) (*portainer.EndpointRelation, error) {
var endpointRelation portainer.EndpointRelation
identifier := internal.Itob(int(endpointID))
err := internal.GetObject(service.db, BucketName, identifier, &endpointRelation)
if err != nil {
return nil, err
}
return &endpointRelation, nil
}
// CreateEndpointRelation saves endpointRelation
func (service *Service) CreateEndpointRelation(endpointRelation *portainer.EndpointRelation) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data, err := internal.MarshalObject(endpointRelation)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(endpointRelation.EndpointID)), data)
})
}
// UpdateEndpointRelation updates an Endpoint relation object
func (service *Service) UpdateEndpointRelation(EndpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error {
identifier := internal.Itob(int(EndpointID))
return internal.UpdateObject(service.db, BucketName, identifier, endpointRelation)
}
// DeleteEndpointRelation deletes an Endpoint relation object
func (service *Service) DeleteEndpointRelation(EndpointID portainer.EndpointID) error {
identifier := internal.Itob(int(EndpointID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
+1 -1
View File
@@ -16,7 +16,7 @@ func (store *Store) Init() error {
Labels: []portainer.Pair{},
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Tags: []string{},
TagIDs: []portainer.TagID{},
}
err = store.EndpointGroupService.CreateEndpointGroup(unassignedGroup)
+94
View File
@@ -0,0 +1,94 @@
package migrator
import (
"github.com/portainer/portainer/api"
)
func (m *Migrator) updateTagsToDBVersion23() error {
tags, err := m.tagService.Tags()
if err != nil {
return err
}
for _, tag := range tags {
tag.EndpointGroups = make(map[portainer.EndpointGroupID]bool)
tag.Endpoints = make(map[portainer.EndpointID]bool)
err = m.tagService.UpdateTag(tag.ID, &tag)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) updateEndpointsAndEndpointGroupsToDBVersion23() error {
tags, err := m.tagService.Tags()
if err != nil {
return err
}
tagsNameMap := make(map[string]portainer.Tag)
for _, tag := range tags {
tagsNameMap[tag.Name] = tag
}
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range endpoints {
endpointTags := make([]portainer.TagID, 0)
for _, tagName := range endpoint.Tags {
tag, ok := tagsNameMap[tagName]
if ok {
endpointTags = append(endpointTags, tag.ID)
tag.Endpoints[endpoint.ID] = true
}
}
endpoint.TagIDs = endpointTags
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
relation := &portainer.EndpointRelation{
EndpointID: endpoint.ID,
EdgeStacks: map[portainer.EdgeStackID]bool{},
}
err = m.endpointRelationService.CreateEndpointRelation(relation)
if err != nil {
return err
}
}
endpointGroups, err := m.endpointGroupService.EndpointGroups()
if err != nil {
return err
}
for _, endpointGroup := range endpointGroups {
endpointGroupTags := make([]portainer.TagID, 0)
for _, tagName := range endpointGroup.Tags {
tag, ok := tagsNameMap[tagName]
if ok {
endpointGroupTags = append(endpointGroupTags, tag.ID)
tag.EndpointGroups[endpointGroup.ID] = true
}
}
endpointGroup.TagIDs = endpointGroupTags
err = m.endpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
if err != nil {
return err
}
}
for _, tag := range tagsNameMap {
err = m.tagService.UpdateTag(tag.ID, &tag)
if err != nil {
return err
}
}
return nil
}
+14
View File
@@ -0,0 +1,14 @@
package migrator
func (m *Migrator) updateSettingsToDBVersion24() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.AllowDeviceMappingForRegularUsers = true
legacySettings.AllowStackManagementForRegularUsers = true
legacySettings.AllowHostNamespaceForRegularUsers = true
return m.settingsService.UpdateSettings(legacySettings)
}
+12
View File
@@ -0,0 +1,12 @@
package migrator
func (m *Migrator) updateSettingsToDBVersion25() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.AllowContainerCapabilitiesForRegularUsers = true
return m.settingsService.UpdateSettings(legacySettings)
}
+86 -49
View File
@@ -2,9 +2,10 @@ package migrator
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/endpoint"
"github.com/portainer/portainer/api/bolt/endpointgroup"
"github.com/portainer/portainer/api/bolt/endpointrelation"
"github.com/portainer/portainer/api/bolt/extension"
"github.com/portainer/portainer/api/bolt/registry"
"github.com/portainer/portainer/api/bolt/resourcecontrol"
@@ -12,6 +13,7 @@ import (
"github.com/portainer/portainer/api/bolt/schedule"
"github.com/portainer/portainer/api/bolt/settings"
"github.com/portainer/portainer/api/bolt/stack"
"github.com/portainer/portainer/api/bolt/tag"
"github.com/portainer/portainer/api/bolt/teammembership"
"github.com/portainer/portainer/api/bolt/template"
"github.com/portainer/portainer/api/bolt/user"
@@ -21,64 +23,70 @@ import (
type (
// Migrator defines a service to migrate data after a Portainer version update.
Migrator struct {
currentDBVersion int
db *bolt.DB
endpointGroupService *endpointgroup.Service
endpointService *endpoint.Service
extensionService *extension.Service
registryService *registry.Service
resourceControlService *resourcecontrol.Service
roleService *role.Service
scheduleService *schedule.Service
settingsService *settings.Service
stackService *stack.Service
teamMembershipService *teammembership.Service
templateService *template.Service
userService *user.Service
versionService *version.Service
fileService portainer.FileService
currentDBVersion int
db *bolt.DB
endpointGroupService *endpointgroup.Service
endpointService *endpoint.Service
endpointRelationService *endpointrelation.Service
extensionService *extension.Service
registryService *registry.Service
resourceControlService *resourcecontrol.Service
roleService *role.Service
scheduleService *schedule.Service
settingsService *settings.Service
stackService *stack.Service
tagService *tag.Service
teamMembershipService *teammembership.Service
templateService *template.Service
userService *user.Service
versionService *version.Service
fileService portainer.FileService
}
// Parameters represents the required parameters to create a new Migrator instance.
Parameters struct {
DB *bolt.DB
DatabaseVersion int
EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service
ExtensionService *extension.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
RoleService *role.Service
ScheduleService *schedule.Service
SettingsService *settings.Service
StackService *stack.Service
TeamMembershipService *teammembership.Service
TemplateService *template.Service
UserService *user.Service
VersionService *version.Service
FileService portainer.FileService
DB *bolt.DB
DatabaseVersion int
EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service
EndpointRelationService *endpointrelation.Service
ExtensionService *extension.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
RoleService *role.Service
ScheduleService *schedule.Service
SettingsService *settings.Service
StackService *stack.Service
TagService *tag.Service
TeamMembershipService *teammembership.Service
TemplateService *template.Service
UserService *user.Service
VersionService *version.Service
FileService portainer.FileService
}
)
// NewMigrator creates a new Migrator.
func NewMigrator(parameters *Parameters) *Migrator {
return &Migrator{
db: parameters.DB,
currentDBVersion: parameters.DatabaseVersion,
endpointGroupService: parameters.EndpointGroupService,
endpointService: parameters.EndpointService,
extensionService: parameters.ExtensionService,
registryService: parameters.RegistryService,
resourceControlService: parameters.ResourceControlService,
roleService: parameters.RoleService,
scheduleService: parameters.ScheduleService,
settingsService: parameters.SettingsService,
teamMembershipService: parameters.TeamMembershipService,
templateService: parameters.TemplateService,
stackService: parameters.StackService,
userService: parameters.UserService,
versionService: parameters.VersionService,
fileService: parameters.FileService,
db: parameters.DB,
currentDBVersion: parameters.DatabaseVersion,
endpointGroupService: parameters.EndpointGroupService,
endpointService: parameters.EndpointService,
endpointRelationService: parameters.EndpointRelationService,
extensionService: parameters.ExtensionService,
registryService: parameters.RegistryService,
resourceControlService: parameters.ResourceControlService,
roleService: parameters.RoleService,
scheduleService: parameters.ScheduleService,
settingsService: parameters.SettingsService,
tagService: parameters.TagService,
teamMembershipService: parameters.TeamMembershipService,
templateService: parameters.TemplateService,
stackService: parameters.StackService,
userService: parameters.UserService,
versionService: parameters.VersionService,
fileService: parameters.FileService,
}
}
@@ -301,5 +309,34 @@ func (m *Migrator) Migrate() error {
}
}
// Portainer 1.24.0
if m.currentDBVersion < 23 {
err := m.updateTagsToDBVersion23()
if err != nil {
return err
}
err = m.updateEndpointsAndEndpointGroupsToDBVersion23()
if err != nil {
return err
}
}
// Portainer 1.24.1
if m.currentDBVersion < 24 {
err := m.updateSettingsToDBVersion24()
if err != nil {
return err
}
}
// Portainer 1.24.2
if m.currentDBVersion < 25 {
err := m.updateSettingsToDBVersion25()
if err != nil {
return err
}
}
return m.versionService.StoreDBVersion(portainer.DBVersion)
}
+19
View File
@@ -52,6 +52,19 @@ func (service *Service) Tags() ([]portainer.Tag, error) {
return tags, err
}
// Tag returns a tag by ID.
func (service *Service) Tag(ID portainer.TagID) (*portainer.Tag, error) {
var tag portainer.Tag
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &tag)
if err != nil {
return nil, err
}
return &tag, nil
}
// CreateTag creates a new tag.
func (service *Service) CreateTag(tag *portainer.Tag) error {
return service.db.Update(func(tx *bolt.Tx) error {
@@ -69,6 +82,12 @@ func (service *Service) CreateTag(tag *portainer.Tag) error {
})
}
// UpdateTag updates a tag.
func (service *Service) UpdateTag(ID portainer.TagID, tag *portainer.Tag) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, tag)
}
// DeleteTag deletes a tag.
func (service *Service) DeleteTag(ID portainer.TagID) error {
identifier := internal.Itob(int(ID))
+1 -1
View File
@@ -179,7 +179,7 @@ func (service *Service) snapshotEnvironment(endpointID portainer.EndpointID, tun
}
endpointURL := endpoint.URL
endpoint.URL = fmt.Sprintf("tcp://localhost:%d", tunnelPort)
endpoint.URL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnelPort)
snapshot, err := service.snapshotter.CreateSnapshot(endpoint)
if err != nil {
return err
+30 -5
View File
@@ -1,6 +1,7 @@
package cli
import (
"log"
"time"
"github.com/portainer/portainer/api"
@@ -38,8 +39,8 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
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(),
ExternalEndpoints: kingpin.Flag("external-endpoints", "Path to a file defining available endpoints (deprecated)").String(),
NoAuth: kingpin.Flag("no-auth", "Disable authentication (deprecated)").Default(defaultNoAuth).Bool(),
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAnalytics).Bool(),
TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(),
TLSSkipVerify: kingpin.Flag("tlsskipverify", "Disable TLS server verification").Default(defaultTLSSkipVerify).Bool(),
@@ -49,15 +50,15 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
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(),
SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source (deprecated)").Default(defaultSyncInterval).String(),
Snapshot: kingpin.Flag("snapshot", "Start a background job to create endpoint snapshots (deprecated)").Default(defaultSnapshot).Bool(),
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each endpoint snapshot job").Default(defaultSnapshotInterval).String(),
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
Templates: kingpin.Flag("templates", "URL to the templates definitions.").Short('t').String(),
TemplateFile: kingpin.Flag("template-file", "Path to the templates (app) definitions on the filesystem").Default(defaultTemplateFile).String(),
TemplateFile: kingpin.Flag("template-file", "Path to the App templates definitions on the filesystem (deprecated)").Default(defaultTemplateFile).String(),
}
kingpin.Parse()
@@ -76,6 +77,8 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
// ValidateFlags validates the values of the flags.
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
displayDeprecationWarnings(flags)
if *flags.EndpointURL != "" && *flags.ExternalEndpoints != "" {
return errEndpointExcludeExternal
}
@@ -116,6 +119,28 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
return nil
}
func displayDeprecationWarnings(flags *portainer.CLIFlags) {
if *flags.ExternalEndpoints != "" {
log.Println("Warning: the --external-endpoint flag is deprecated and will likely be removed in a future version of Portainer.")
}
if *flags.SyncInterval != defaultSyncInterval {
log.Println("Warning: the --sync-interval flag is deprecated and will likely be removed in a future version of Portainer.")
}
if *flags.NoAuth {
log.Println("Warning: the --no-auth flag is deprecated and will likely be removed in a future version of Portainer.")
}
if !*flags.Snapshot {
log.Println("Warning: the --no-snapshot flag is deprecated and will likely be removed in a future version of Portainer.")
}
if *flags.TemplateFile != "" {
log.Println("Warning: the --template-file flag is deprecated and will likely be removed in a future version of Portainer.")
}
}
func validateEndpointURL(endpointURL string) error {
if endpointURL != "" {
if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") && !strings.HasPrefix(endpointURL, "npipe://") {
+56 -48
View File
@@ -9,7 +9,7 @@ import (
"github.com/portainer/portainer/api/chisel"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt"
"github.com/portainer/portainer/api/cli"
"github.com/portainer/portainer/api/cron"
@@ -259,6 +259,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
LogoURL: *flags.Logo,
AuthenticationMethod: portainer.AuthenticationInternal,
LDAPSettings: portainer.LDAPSettings{
AnonymousMode: true,
AutoCreateUsers: true,
TLSConfig: portainer.TLSConfiguration{},
SearchSettings: []portainer.LDAPSearchSettings{
@@ -268,13 +269,17 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
portainer.LDAPGroupSearchSettings{},
},
},
OAuthSettings: portainer.OAuthSettings{},
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowVolumeBrowserForRegularUsers: false,
EnableHostManagementFeatures: false,
SnapshotInterval: *flags.SnapshotInterval,
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
OAuthSettings: portainer.OAuthSettings{},
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowVolumeBrowserForRegularUsers: false,
AllowDeviceMappingForRegularUsers: true,
AllowStackManagementForRegularUsers: true,
AllowContainerCapabilitiesForRegularUsers: true,
EnableHostManagementFeatures: false,
AllowHostNamespaceForRegularUsers: true,
SnapshotInterval: *flags.SnapshotInterval,
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
}
if *flags.Templates != "" {
@@ -396,7 +401,7 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, endpointService portain
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{},
Tags: []string{},
TagIDs: []portainer.TagID{},
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
}
@@ -439,7 +444,7 @@ func createUnsecuredEndpoint(endpointURL string, endpointService portainer.Endpo
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{},
Tags: []string{},
TagIDs: []portainer.TagID{},
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
}
@@ -650,44 +655,47 @@ func main() {
}
var server portainer.Server = &http.Server{
ReverseTunnelService: reverseTunnelService,
Status: applicationStatus,
BindAddress: *flags.Addr,
AssetsPath: *flags.Assets,
AuthDisabled: *flags.NoAuth,
EndpointManagement: endpointManagement,
RoleService: store.RoleService,
UserService: store.UserService,
TeamService: store.TeamService,
TeamMembershipService: store.TeamMembershipService,
EndpointService: store.EndpointService,
EndpointGroupService: store.EndpointGroupService,
ExtensionService: store.ExtensionService,
ResourceControlService: store.ResourceControlService,
SettingsService: store.SettingsService,
RegistryService: store.RegistryService,
DockerHubService: store.DockerHubService,
StackService: store.StackService,
ScheduleService: store.ScheduleService,
TagService: store.TagService,
TemplateService: store.TemplateService,
WebhookService: store.WebhookService,
SwarmStackManager: swarmStackManager,
ComposeStackManager: composeStackManager,
ExtensionManager: extensionManager,
CryptoService: cryptoService,
JWTService: jwtService,
FileService: fileService,
LDAPService: ldapService,
GitService: gitService,
SignatureService: digitalSignatureService,
JobScheduler: jobScheduler,
Snapshotter: snapshotter,
SSL: *flags.SSL,
SSLCert: *flags.SSLCert,
SSLKey: *flags.SSLKey,
DockerClientFactory: clientFactory,
JobService: jobService,
ReverseTunnelService: reverseTunnelService,
Status: applicationStatus,
BindAddress: *flags.Addr,
AssetsPath: *flags.Assets,
AuthDisabled: *flags.NoAuth,
EndpointManagement: endpointManagement,
RoleService: store.RoleService,
UserService: store.UserService,
TeamService: store.TeamService,
TeamMembershipService: store.TeamMembershipService,
EdgeGroupService: store.EdgeGroupService,
EdgeStackService: store.EdgeStackService,
EndpointService: store.EndpointService,
EndpointGroupService: store.EndpointGroupService,
EndpointRelationService: store.EndpointRelationService,
ExtensionService: store.ExtensionService,
ResourceControlService: store.ResourceControlService,
SettingsService: store.SettingsService,
RegistryService: store.RegistryService,
DockerHubService: store.DockerHubService,
StackService: store.StackService,
ScheduleService: store.ScheduleService,
TagService: store.TagService,
TemplateService: store.TemplateService,
WebhookService: store.WebhookService,
SwarmStackManager: swarmStackManager,
ComposeStackManager: composeStackManager,
ExtensionManager: extensionManager,
CryptoService: cryptoService,
JWTService: jwtService,
FileService: fileService,
LDAPService: ldapService,
GitService: gitService,
SignatureService: digitalSignatureService,
JobScheduler: jobScheduler,
Snapshotter: snapshotter,
SSL: *flags.SSL,
SSLCert: *flags.SSLCert,
SSLKey: *flags.SSLKey,
DockerClientFactory: clientFactory,
JobService: jobService,
}
log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr)
+1 -1
View File
@@ -2,7 +2,7 @@ package cron
import (
"github.com/portainer/portainer/api"
"github.com/robfig/cron"
"github.com/robfig/cron/v3"
)
// JobScheduler represents a service for managing crons
+18
View File
@@ -6,6 +6,24 @@ import (
"io/ioutil"
)
// CreateTLSConfiguration creates a basic tls.Config to be used by servers with recommended TLS settings
func CreateServerTLSConfiguration() *tls.Config {
return &tls.Config{
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
tls.TLS_AES_128_GCM_SHA256,
tls.TLS_AES_256_GCM_SHA384,
tls.TLS_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
},
}
}
// 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) {
+3 -3
View File
@@ -7,14 +7,14 @@ import (
"time"
"github.com/docker/docker/client"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
)
const (
unsupportedEnvironmentType = portainer.Error("Environment not supported")
defaultDockerRequestTimeout = 60
dockerClientVersion = "1.40"
dockerClientVersion = "1.37"
)
// ClientFactory is used to create Docker clients
@@ -81,7 +81,7 @@ func createEdgeClient(endpoint *portainer.Endpoint, reverseTunnelService portain
}
tunnel := reverseTunnelService.GetTunnelDetails(endpoint.ID)
endpointURL := fmt.Sprintf("http://localhost:%d", tunnel.Port)
endpointURL := fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
return client.NewClientWithOpts(
client.WithHost(endpointURL),
+11
View File
@@ -3,6 +3,7 @@ package docker
import (
"context"
"log"
"strings"
"time"
"github.com/docker/docker/api/types"
@@ -126,6 +127,8 @@ func snapshotContainers(snapshot *portainer.Snapshot, cli *client.Client) error
runningContainers := 0
stoppedContainers := 0
healthyContainers := 0
unhealthyContainers := 0
stacks := make(map[string]struct{})
for _, container := range containers {
if container.State == "exited" {
@@ -134,6 +137,12 @@ func snapshotContainers(snapshot *portainer.Snapshot, cli *client.Client) error
runningContainers++
}
if strings.Contains(container.Status, "(healthy)") {
healthyContainers++
} else if strings.Contains(container.Status, "(unhealthy)") {
unhealthyContainers++
}
for k, v := range container.Labels {
if k == "com.docker.compose.project" {
stacks[v] = struct{}{}
@@ -143,6 +152,8 @@ func snapshotContainers(snapshot *portainer.Snapshot, cli *client.Client) error
snapshot.RunningContainerCount = runningContainers
snapshot.StoppedContainerCount = stoppedContainers
snapshot.HealthyContainerCount = healthyContainers
snapshot.UnhealthyContainerCount = unhealthyContainers
snapshot.StackCount += len(stacks)
snapshot.SnapshotRaw.Containers = containers
return nil
+54
View File
@@ -0,0 +1,54 @@
package portainer
// EdgeGroupRelatedEndpoints returns a list of endpoints related to this Edge group
func EdgeGroupRelatedEndpoints(edgeGroup *EdgeGroup, endpoints []Endpoint, endpointGroups []EndpointGroup) []EndpointID {
if !edgeGroup.Dynamic {
return edgeGroup.Endpoints
}
endpointIDs := []EndpointID{}
for _, endpoint := range endpoints {
if endpoint.Type != EdgeAgentEnvironment {
continue
}
var endpointGroup EndpointGroup
for _, group := range endpointGroups {
if endpoint.GroupID == group.ID {
endpointGroup = group
break
}
}
if edgeGroupRelatedToEndpoint(edgeGroup, &endpoint, &endpointGroup) {
endpointIDs = append(endpointIDs, endpoint.ID)
}
}
return endpointIDs
}
// edgeGroupRelatedToEndpoint returns true is edgeGroup is associated with endpoint
func edgeGroupRelatedToEndpoint(edgeGroup *EdgeGroup, endpoint *Endpoint, endpointGroup *EndpointGroup) bool {
if !edgeGroup.Dynamic {
for _, endpointID := range edgeGroup.Endpoints {
if endpoint.ID == endpointID {
return true
}
}
return false
}
endpointTags := TagSet(endpoint.TagIDs)
if endpointGroup.TagIDs != nil {
endpointTags = TagUnion(endpointTags, TagSet(endpointGroup.TagIDs))
}
edgeGroupTags := TagSet(edgeGroup.TagIDs)
if edgeGroup.PartialMatch {
intersection := TagIntersection(endpointTags, edgeGroupTags)
return len(intersection) != 0
}
return TagContains(edgeGroupTags, endpointTags)
}
+27
View File
@@ -0,0 +1,27 @@
package portainer
import "errors"
// EdgeStackRelatedEndpoints returns a list of endpoints related to this Edge stack
func EdgeStackRelatedEndpoints(edgeGroupIDs []EdgeGroupID, endpoints []Endpoint, endpointGroups []EndpointGroup, edgeGroups []EdgeGroup) ([]EndpointID, error) {
edgeStackEndpoints := []EndpointID{}
for _, edgeGroupID := range edgeGroupIDs {
var edgeGroup *EdgeGroup
for _, group := range edgeGroups {
if group.ID == edgeGroupID {
edgeGroup = &group
break
}
}
if edgeGroup == nil {
return nil, errors.New("Edge group was not found")
}
edgeStackEndpoints = append(edgeStackEndpoints, EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups)...)
}
return edgeStackEndpoints, nil
}
+25
View File
@@ -0,0 +1,25 @@
package portainer
// EndpointRelatedEdgeStacks returns a list of Edge stacks related to this Endpoint
func EndpointRelatedEdgeStacks(endpoint *Endpoint, endpointGroup *EndpointGroup, edgeGroups []EdgeGroup, edgeStacks []EdgeStack) []EdgeStackID {
relatedEdgeGroupsSet := map[EdgeGroupID]bool{}
for _, edgeGroup := range edgeGroups {
if edgeGroupRelatedToEndpoint(&edgeGroup, endpoint, endpointGroup) {
relatedEdgeGroupsSet[edgeGroup.ID] = true
}
}
relatedEdgeStacks := []EdgeStackID{}
for _, edgeStack := range edgeStacks {
for _, edgeGroupID := range edgeStack.EdgeGroups {
if relatedEdgeGroupsSet[edgeGroupID] {
relatedEdgeStacks = append(relatedEdgeStacks, edgeStack.ID)
break
}
}
}
return relatedEdgeStacks
}
+1 -1
View File
@@ -123,7 +123,7 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, dataPa
endpointURL := endpoint.URL
if endpoint.Type == portainer.EdgeAgentEnvironment {
tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID)
endpointURL = fmt.Sprintf("tcp://localhost:%d", tunnel.Port)
endpointURL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnel.Port)
}
args = append(args, "-H", endpointURL)
+28
View File
@@ -29,6 +29,8 @@ const (
ComposeStorePath = "compose"
// ComposeFileDefaultName represents the default name of a compose file.
ComposeFileDefaultName = "docker-compose.yml"
// EdgeStackStorePath represents the subfolder where edge stack files are stored in the file store folder.
EdgeStackStorePath = "edge_stacks"
// 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.
@@ -121,6 +123,32 @@ func (service *Service) StoreStackFileFromBytes(stackIdentifier, fileName string
return path.Join(service.fileStorePath, stackStorePath), nil
}
// GetEdgeStackProjectPath returns the absolute path on the FS for a edge stack based
// on its identifier.
func (service *Service) GetEdgeStackProjectPath(edgeStackIdentifier string) string {
return path.Join(service.fileStorePath, EdgeStackStorePath, edgeStackIdentifier)
}
// StoreEdgeStackFileFromBytes creates a subfolder in the EdgeStackStorePath and stores a new file from bytes.
// It returns the path to the folder where the file is stored.
func (service *Service) StoreEdgeStackFileFromBytes(edgeStackIdentifier, fileName string, data []byte) (string, error) {
stackStorePath := path.Join(EdgeStackStorePath, edgeStackIdentifier)
err := service.createDirectoryInStore(stackStorePath)
if err != nil {
return "", err
}
composeFilePath := path.Join(stackStorePath, fileName)
r := bytes.NewReader(data)
err = service.createFileInStore(composeFilePath, r)
if err != nil {
return "", err
}
return path.Join(service.fileStorePath, stackStorePath), nil
}
// StoreRegistryManagementFileFromBytes creates a subfolder in the
// ExtensionRegistryManagementStorePath and stores a new file from bytes.
// It returns the path to the folder where the file is stored.
+40
View File
@@ -0,0 +1,40 @@
module github.com/portainer/portainer/api
go 1.13
require (
github.com/Microsoft/go-winio v0.4.14
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a
github.com/boltdb/bolt v1.3.1
github.com/containerd/containerd v1.3.1 // indirect
github.com/coreos/go-semver v0.3.0
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/docker/cli v0.0.0-20191126203649-54d085b857e9
github.com/docker/docker v0.0.0-00010101000000-000000000000
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814
github.com/gofrs/uuid v3.2.0+incompatible
github.com/gorilla/mux v1.7.3
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/websocket v1.4.1
github.com/imdario/mergo v0.3.8 // indirect
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389
github.com/json-iterator/go v1.1.8
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
github.com/mattn/go-shellwords v1.0.6 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
github.com/portainer/libcompose v0.5.3
github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33
github.com/robfig/cron/v3 v3.0.0
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/asn1-ber.v1 v1.0.0-00010101000000-000000000000 // indirect
gopkg.in/ldap.v2 v2.5.1
gopkg.in/src-d/go-git.v4 v4.13.1
)
replace github.com/docker/docker => github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203
replace gopkg.in/asn1-ber.v1 => github.com/go-asn1-ber/asn1-ber v1.3.1
+286
View File
@@ -0,0 +1,286 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.3.8 h1:dvxbxtpTIjdAbx2OtL26p4eq0iEvys/U5yrsTJb3NZI=
github.com/Microsoft/go-winio v0.3.8/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/Microsoft/hcsshim v0.8.6 h1:ZfF0+zZeYdzMIVMZHKtDKJvLHj76XCuVae/jNkjj0IA=
github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2 h1:axBiC50cNZOs7ygH5BgQp4N+aYrZ2DNpWZ1KG3VOSOM=
github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2/go.mod h1:jnzFpU88PccN/tPPhCpnNU8mZphvKxYM9lLNkd8e+os=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/containerd/containerd v1.3.1 h1:LdbWxLhkAIxGO7h3mATHkyav06WuDs/yTWxIljJOTks=
github.com/containerd/containerd v1.3.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc h1:TP+534wVlf61smEIq1nwLLAjQVEK2EADoW3CX9AuT+8=
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 h1:74lLNRzvsdIlkTgfDSMuaPjBr4cf6k7pwQQANm/yLKU=
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/docker/cli v0.0.0-20190711175710-5b38d82aa076/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v0.0.0-20191126203649-54d085b857e9 h1:Q6D6b2iRKhvtL3Wj9p0SyPOvUDJ1ht62mbiBoNJ3Aus=
github.com/docker/cli v0.0.0-20191126203649-54d085b857e9/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ=
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
github.com/docker/engine v1.4.2-0.20191127222017-3152f9436292 h1:qQ7mw+CVWpRj5DWBL4CVHtBbGQdlPCj4j1evDh0ethw=
github.com/docker/engine v1.4.2-0.20191127222017-3152f9436292/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY=
github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203 h1:QeBh8wW8pIZKlXxlMOQ8hSCMdJA+2Z/bD/iDyCAS8XU=
github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY=
github.com/docker/engine v1.13.1 h1:Cks33UT9YBW5Xyc3MtGDq2IPgqfJtJ+qkFaxc2b0Euc=
github.com/docker/engine v1.13.1/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY=
github.com/docker/go-connections v0.3.0 h1:3lOnM9cSzgGwx8VfK/NGOW5fLQ0GjIlCkaktF+n1M6o=
github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-metrics v0.0.0-20181218153428-b84716841b82 h1:X0fj836zx99zFu83v/M79DuBn84IL/Syx1SY6Y5ZEMA=
github.com/docker/go-metrics v0.0.0-20181218153428-b84716841b82/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814 h1:gWvniJ4GbFfkf700kykAImbLiEMU0Q3QN9hQ26Js1pU=
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814/go.mod h1:secRm32Ro77eD23BmPVbgLbWN+JWDw7pJszenjxI4bI=
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-asn1-ber/asn1-ber v1.3.1 h1:gvPdv/Hr++TRFCl0UbPFHC54P9N9jgsRPnmnr419Uck=
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v0.0.0-20160317213430-0eeaf8392f5b/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jpillora/ansi v0.0.0-20170202005112-f496b27cd669 h1:l5rH/CnVVu+HPxjtxjM90nHrm4nov3j3RF9/62UjgLs=
github.com/jpillora/ansi v0.0.0-20170202005112-f496b27cd669/go.mod h1:kOeLNvjNBGSV3uYtFjvb72+fnZCMFJF1XDvRIjdom0g=
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389 h1:K3JsoRqX6C4gmTvY4jqtFGCfK8uToj9DMahciJaoWwE=
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389/go.mod h1:wHQUFFnFySoqdAOzjHkTvb4DsVM1h/73PS9l2vnioRM=
github.com/jpillora/requestlog v0.0.0-20181015073026-df8817be5f82 h1:7ufdyC3aMxFcCv+ABZy/dmIVGKFoGNBCqOgLYPIckD8=
github.com/jpillora/requestlog v0.0.0-20181015073026-df8817be5f82/go.mod h1:w8buj+yNfmLEP0ENlbG/FRnK6bVmuhqXnukYCs9sDvY=
github.com/jpillora/sizestr v0.0.0-20160130011556-e2ea2fa42fb9 h1:0c9jcgBtHRtDU//jTrcCgWG6UHjMZytiq/3WhraNgUM=
github.com/jpillora/sizestr v0.0.0-20160130011556-e2ea2fa42fb9/go.mod h1:1ffp+CRe0eAwwRb0/BownUAjMBsmTLwgAvRbfj9dRwE=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c h1:N7A4JCA2G+j5fuFxCsJqjFU/sZe0mj8H0sSoSwbaikw=
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c/go.mod h1:Nn5wlyECw3iJrzi0AhIWg+AJUb4PlRQVW4/3XHH1LZA=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v0.0.0-20150511174710-5cf931ef8f76/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-shellwords v1.0.6 h1:9Jok5pILi5S1MnDirGVTufYGtksUs/V2BWUP3ZkeUUI=
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microsoft/go-winio v0.4.8 h1:N4SmTFXUK7/jnn/UG/gm2mrHiYu9LVGvtsvULyody/c=
github.com/microsoft/go-winio v0.4.8/go.mod h1:kcIxxtKZE55DEncT/EOvFiygPobhUWpSDqDb47poQOU=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE=
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420 h1:Yu3681ykYHDfLoI6XVjL4JWmkE+3TX9yfIWwRCh1kFM=
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/image-spec v0.0.0-20170515205857-f03dbe35d449 h1:Aq8iG72akPb/kszE7ksZ5ldV+JYPYii/KZOxlpJF07s=
github.com/opencontainers/image-spec v0.0.0-20170515205857-f03dbe35d449/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/runc v0.0.0-20161109192122-51371867a01c h1:iOMba/KmaXgSX5PFKu1u6s+DZXiq+EzPayawa76w6aA=
github.com/opencontainers/runc v0.0.0-20161109192122-51371867a01c/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 h1:lNCW6THrCKBiJBpz8kbVGjC7MgdCGKwuvBgc7LoD6sw=
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/portainer/libcompose v0.5.3 h1:tE4WcPuGvo+NKeDkDWpwNavNLZ5GHIJ4RvuZXsI9uI8=
github.com/portainer/libcompose v0.5.3/go.mod h1:7SKd/ho69rRKHDFSDUwkbMcol2TMKU5OslDsajr8Ro8=
github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2 h1:0PfgGLys9yHr4rtnirg0W0Cjvv6/DzxBIZk5sV59208=
github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2/go.mod h1:/wIeGwJOMYc1JplE/OvYMO5korce39HddIfI8VKGyAM=
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 h1:H8HR2dHdBf8HANSkUyVw4o8+4tegGcd+zyKZ3e599II=
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33/go.mod h1:Y2TfgviWI4rT2qaOTHr+hq6MdKIE5YjgQAu7qwptTV0=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3 h1:CTwfnzjQ+8dS6MhHHu4YswVAD99sL2wjPqP+VkURmKE=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E=
github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
github.com/urfave/cli v1.21.0/go.mod h1:lxDj6qX9Q6lWQxIrbrT0nwecwUtRnhVZAJjJZrVUZZQ=
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.1.0 h1:ngVtJC9TY/lg0AA/1k48FYhBrhRoFlEmWzsehpNAaZg=
github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 h1:anGSYQpPhQwXlwsu5wmfq0nWkCNaMEMUwAv13Y92hd8=
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20181017193950-04a2e542c03f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c h1:jceGD5YNJGgGMkJz79agzOln1K9TaZUjv5ird16qniQ=
golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.22.1 h1:/7cs52RnTJmD43s3uxzlq2U7nqVTd/37viQwMrMNlOM=
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ldap.v2 v2.5.1 h1:wiu0okdNfjlBzg6UWvd1Hn8Y+Ux17/u/4nlk4CQr6tU=
gopkg.in/ldap.v2 v2.5.1/go.mod h1:oI0cpe/D7HRtBQl8aTg+ZmzFUAvu4lsv3eLXMLGFxWk=
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg=
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE=
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+6
View File
@@ -79,6 +79,11 @@ func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
}
err = handler.AuthorizationService.UpdateUsersAuthorizations()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
}
return handler.writeToken(w, user)
}
@@ -175,6 +180,7 @@ func (handler *Handler) addUserIntoTeams(user *portainer.User, settings *portain
}
}
}
return nil
}
@@ -0,0 +1,104 @@
package edgegroups
import (
"github.com/portainer/portainer/api"
)
type endpointSetType map[portainer.EndpointID]bool
func (handler *Handler) getEndpointsByTags(tagIDs []portainer.TagID, partialMatch bool) ([]portainer.EndpointID, error) {
if len(tagIDs) == 0 {
return []portainer.EndpointID{}, nil
}
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
return nil, err
}
groupEndpoints := mapEndpointGroupToEndpoints(endpoints)
tags := []portainer.Tag{}
for _, tagID := range tagIDs {
tag, err := handler.TagService.Tag(tagID)
if err != nil {
return nil, err
}
tags = append(tags, *tag)
}
setsOfEndpoints := mapTagsToEndpoints(tags, groupEndpoints)
var endpointSet endpointSetType
if partialMatch {
endpointSet = setsUnion(setsOfEndpoints)
} else {
endpointSet = setsIntersection(setsOfEndpoints)
}
results := []portainer.EndpointID{}
for _, endpoint := range endpoints {
if _, ok := endpointSet[endpoint.ID]; ok && endpoint.Type == portainer.EdgeAgentEnvironment {
results = append(results, endpoint.ID)
}
}
return results, nil
}
func mapEndpointGroupToEndpoints(endpoints []portainer.Endpoint) map[portainer.EndpointGroupID]endpointSetType {
groupEndpoints := map[portainer.EndpointGroupID]endpointSetType{}
for _, endpoint := range endpoints {
groupID := endpoint.GroupID
if groupEndpoints[groupID] == nil {
groupEndpoints[groupID] = endpointSetType{}
}
groupEndpoints[groupID][endpoint.ID] = true
}
return groupEndpoints
}
func mapTagsToEndpoints(tags []portainer.Tag, groupEndpoints map[portainer.EndpointGroupID]endpointSetType) []endpointSetType {
sets := []endpointSetType{}
for _, tag := range tags {
set := tag.Endpoints
for groupID := range tag.EndpointGroups {
for endpointID := range groupEndpoints[groupID] {
set[endpointID] = true
}
}
sets = append(sets, set)
}
return sets
}
func setsIntersection(sets []endpointSetType) endpointSetType {
if len(sets) == 0 {
return endpointSetType{}
}
intersectionSet := sets[0]
for _, set := range sets {
for endpointID := range intersectionSet {
if !set[endpointID] {
delete(intersectionSet, endpointID)
}
}
}
return intersectionSet
}
func setsUnion(sets []endpointSetType) endpointSetType {
unionSet := endpointSetType{}
for _, set := range sets {
for endpointID := range set {
unionSet[endpointID] = true
}
}
return unionSet
}
@@ -0,0 +1,83 @@
package edgegroups
import (
"net/http"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
)
type edgeGroupCreatePayload struct {
Name string
Dynamic bool
TagIDs []portainer.TagID
Endpoints []portainer.EndpointID
PartialMatch bool
}
func (payload *edgeGroupCreatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
return portainer.Error("Invalid Edge group name")
}
if payload.Dynamic && (payload.TagIDs == nil || len(payload.TagIDs) == 0) {
return portainer.Error("TagIDs is mandatory for a dynamic Edge group")
}
if !payload.Dynamic && (payload.Endpoints == nil || len(payload.Endpoints) == 0) {
return portainer.Error("Endpoints is mandatory for a static Edge group")
}
return nil
}
func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload edgeGroupCreatePayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge groups from the database", err}
}
for _, edgeGroup := range edgeGroups {
if edgeGroup.Name == payload.Name {
return &httperror.HandlerError{http.StatusBadRequest, "Edge group name must be unique", portainer.Error("Edge group name must be unique")}
}
}
edgeGroup := &portainer.EdgeGroup{
Name: payload.Name,
Dynamic: payload.Dynamic,
TagIDs: []portainer.TagID{},
Endpoints: []portainer.EndpointID{},
PartialMatch: payload.PartialMatch,
}
if edgeGroup.Dynamic {
edgeGroup.TagIDs = payload.TagIDs
} else {
endpointIDs := []portainer.EndpointID{}
for _, endpointID := range payload.Endpoints {
endpoint, err := handler.EndpointService.Endpoint(endpointID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint from the database", err}
}
if endpoint.Type == portainer.EdgeAgentEnvironment {
endpointIDs = append(endpointIDs, endpoint.ID)
}
}
edgeGroup.Endpoints = endpointIDs
}
err = handler.EdgeGroupService.CreateEdgeGroup(edgeGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the Edge group inside the database", err}
}
return response.JSON(w, edgeGroup)
}
@@ -0,0 +1,45 @@
package edgegroups
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
)
func (handler *Handler) edgeGroupDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge group identifier route variable", err}
}
_, err = handler.EdgeGroupService.EdgeGroup(portainer.EdgeGroupID(edgeGroupID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge group with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge group with the specified identifier inside the database", err}
}
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge stacks from the database", err}
}
for _, edgeStack := range edgeStacks {
for _, groupID := range edgeStack.EdgeGroups {
if groupID == portainer.EdgeGroupID(edgeGroupID) {
return &httperror.HandlerError{http.StatusForbidden, "Edge group is used by an Edge stack", portainer.Error("Edge group is used by an Edge stack")}
}
}
}
err = handler.EdgeGroupService.DeleteEdgeGroup(portainer.EdgeGroupID(edgeGroupID))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the Edge group from the database", err}
}
return response.Empty(w)
}
@@ -0,0 +1,35 @@
package edgegroups
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
)
func (handler *Handler) edgeGroupInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge group identifier route variable", err}
}
edgeGroup, err := handler.EdgeGroupService.EdgeGroup(portainer.EdgeGroupID(edgeGroupID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge group with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge group with the specified identifier inside the database", err}
}
if edgeGroup.Dynamic {
endpoints, err := handler.getEndpointsByTags(edgeGroup.TagIDs, edgeGroup.PartialMatch)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints and endpoint groups for Edge group", err}
}
edgeGroup.Endpoints = endpoints
}
return response.JSON(w, edgeGroup)
}
@@ -0,0 +1,55 @@
package edgegroups
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
)
type decoratedEdgeGroup struct {
portainer.EdgeGroup
HasEdgeStack bool `json:"HasEdgeStack"`
}
func (handler *Handler) edgeGroupList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge groups from the database", err}
}
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge stacks from the database", err}
}
usedEdgeGroups := make(map[portainer.EdgeGroupID]bool)
for _, stack := range edgeStacks {
for _, groupID := range stack.EdgeGroups {
usedEdgeGroups[groupID] = true
}
}
decoratedEdgeGroups := []decoratedEdgeGroup{}
for _, orgEdgeGroup := range edgeGroups {
edgeGroup := decoratedEdgeGroup{
EdgeGroup: orgEdgeGroup,
}
if edgeGroup.Dynamic {
endpoints, err := handler.getEndpointsByTags(edgeGroup.TagIDs, edgeGroup.PartialMatch)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints and endpoint groups for Edge group", err}
}
edgeGroup.Endpoints = endpoints
}
edgeGroup.HasEdgeStack = usedEdgeGroups[edgeGroup.ID]
decoratedEdgeGroups = append(decoratedEdgeGroups, edgeGroup)
}
return response.JSON(w, decoratedEdgeGroups)
}
@@ -0,0 +1,154 @@
package edgegroups
import (
"net/http"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
)
type edgeGroupUpdatePayload struct {
Name string
Dynamic bool
TagIDs []portainer.TagID
Endpoints []portainer.EndpointID
PartialMatch *bool
}
func (payload *edgeGroupUpdatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
return portainer.Error("Invalid Edge group name")
}
if payload.Dynamic && (payload.TagIDs == nil || len(payload.TagIDs) == 0) {
return portainer.Error("TagIDs is mandatory for a dynamic Edge group")
}
if !payload.Dynamic && (payload.Endpoints == nil || len(payload.Endpoints) == 0) {
return portainer.Error("Endpoints is mandatory for a static Edge group")
}
return nil
}
func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge group identifier route variable", err}
}
var payload edgeGroupUpdatePayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
edgeGroup, err := handler.EdgeGroupService.EdgeGroup(portainer.EdgeGroupID(edgeGroupID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge group with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge group with the specified identifier inside the database", err}
}
if payload.Name != "" {
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge groups from the database", err}
}
for _, edgeGroup := range edgeGroups {
if edgeGroup.Name == payload.Name && edgeGroup.ID != portainer.EdgeGroupID(edgeGroupID) {
return &httperror.HandlerError{http.StatusBadRequest, "Edge group name must be unique", portainer.Error("Edge group name must be unique")}
}
}
edgeGroup.Name = payload.Name
}
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err}
}
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err}
}
oldRelatedEndpoints := portainer.EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups)
edgeGroup.Dynamic = payload.Dynamic
if edgeGroup.Dynamic {
edgeGroup.TagIDs = payload.TagIDs
} else {
endpointIDs := []portainer.EndpointID{}
for _, endpointID := range payload.Endpoints {
endpoint, err := handler.EndpointService.Endpoint(endpointID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint from the database", err}
}
if endpoint.Type == portainer.EdgeAgentEnvironment {
endpointIDs = append(endpointIDs, endpoint.ID)
}
}
edgeGroup.Endpoints = endpointIDs
}
if payload.PartialMatch != nil {
edgeGroup.PartialMatch = *payload.PartialMatch
}
err = handler.EdgeGroupService.UpdateEdgeGroup(edgeGroup.ID, edgeGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Edge group changes inside the database", err}
}
newRelatedEndpoints := portainer.EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups)
endpointsToUpdate := append(newRelatedEndpoints, oldRelatedEndpoints...)
for _, endpointID := range endpointsToUpdate {
err = handler.updateEndpoint(endpointID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Endpoint relation changes inside the database", err}
}
}
return response.JSON(w, edgeGroup)
}
func (handler *Handler) updateEndpoint(endpointID portainer.EndpointID) error {
relation, err := handler.EndpointRelationService.EndpointRelation(endpointID)
if err != nil {
return err
}
endpoint, err := handler.EndpointService.Endpoint(endpointID)
if err != nil {
return err
}
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID)
if err != nil {
return err
}
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
if err != nil {
return err
}
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
if err != nil {
return err
}
edgeStackSet := map[portainer.EdgeStackID]bool{}
endpointEdgeStacks := portainer.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks)
for _, edgeStackID := range endpointEdgeStacks {
edgeStackSet[edgeStackID] = true
}
relation.EdgeStacks = edgeStackSet
return handler.EndpointRelationService.UpdateEndpointRelation(endpoint.ID, relation)
}
+39
View File
@@ -0,0 +1,39 @@
package edgegroups
import (
"net/http"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
)
// Handler is the HTTP handler used to handle endpoint group operations.
type Handler struct {
*mux.Router
EdgeGroupService portainer.EdgeGroupService
EdgeStackService portainer.EdgeStackService
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
EndpointRelationService portainer.EndpointRelationService
TagService portainer.TagService
}
// NewHandler creates a handler to manage endpoint group operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
}
h.Handle("/edge_groups",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeGroupCreate))).Methods(http.MethodPost)
h.Handle("/edge_groups",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeGroupList))).Methods(http.MethodGet)
h.Handle("/edge_groups/{id}",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeGroupInspect))).Methods(http.MethodGet)
h.Handle("/edge_groups/{id}",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeGroupUpdate))).Methods(http.MethodPut)
h.Handle("/edge_groups/{id}",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeGroupDelete))).Methods(http.MethodDelete)
return h
}
@@ -0,0 +1,289 @@
package edgestacks
import (
"errors"
"net/http"
"strconv"
"strings"
"time"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
)
// POST request on /api/endpoint_groups
func (handler *Handler) edgeStackCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
method, err := request.RetrieveQueryParameter(r, "method", false)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: method", err}
}
edgeStack, err := handler.createSwarmStack(method, r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Edge stack", err}
}
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err}
}
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err}
}
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from database", err}
}
relatedEndpoints, err := portainer.EdgeStackRelatedEndpoints(edgeStack.EdgeGroups, endpoints, endpointGroups, edgeGroups)
for _, endpointID := range relatedEndpoints {
relation, err := handler.EndpointRelationService.EndpointRelation(endpointID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err}
}
relation.EdgeStacks[edgeStack.ID] = true
err = handler.EndpointRelationService.UpdateEndpointRelation(endpointID, relation)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err}
}
}
return response.JSON(w, edgeStack)
}
func (handler *Handler) createSwarmStack(method string, r *http.Request) (*portainer.EdgeStack, error) {
switch method {
case "string":
return handler.createSwarmStackFromFileContent(r)
case "repository":
return handler.createSwarmStackFromGitRepository(r)
case "file":
return handler.createSwarmStackFromFileUpload(r)
}
return nil, errors.New("Invalid value for query parameter: method. Value must be one of: string, repository or file")
}
type swarmStackFromFileContentPayload struct {
Name string
StackFileContent string
EdgeGroups []portainer.EdgeGroupID
}
func (payload *swarmStackFromFileContentPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
return portainer.Error("Invalid stack name")
}
if govalidator.IsNull(payload.StackFileContent) {
return portainer.Error("Invalid stack file content")
}
if payload.EdgeGroups == nil || len(payload.EdgeGroups) == 0 {
return portainer.Error("Edge Groups are mandatory for an Edge stack")
}
return nil
}
func (handler *Handler) createSwarmStackFromFileContent(r *http.Request) (*portainer.EdgeStack, error) {
var payload swarmStackFromFileContentPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return nil, err
}
err = handler.validateUniqueName(payload.Name)
if err != nil {
return nil, err
}
stackID := handler.EdgeStackService.GetNextIdentifier()
stack := &portainer.EdgeStack{
ID: portainer.EdgeStackID(stackID),
Name: payload.Name,
EntryPoint: filesystem.ComposeFileDefaultName,
CreationDate: time.Now().Unix(),
EdgeGroups: payload.EdgeGroups,
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus),
Version: 1,
}
stackFolder := strconv.Itoa(int(stack.ID))
projectPath, err := handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil {
return nil, err
}
stack.ProjectPath = projectPath
err = handler.EdgeStackService.CreateEdgeStack(stack)
if err != nil {
return nil, err
}
return stack, nil
}
type swarmStackFromGitRepositoryPayload struct {
Name string
RepositoryURL string
RepositoryReferenceName string
RepositoryAuthentication bool
RepositoryUsername string
RepositoryPassword string
ComposeFilePathInRepository string
EdgeGroups []portainer.EdgeGroupID
}
func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
return portainer.Error("Invalid stack name")
}
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
return portainer.Error("Invalid repository URL. Must correspond to a valid URL format")
}
if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) {
return portainer.Error("Invalid repository credentials. Username and password must be specified when authentication is enabled")
}
if govalidator.IsNull(payload.ComposeFilePathInRepository) {
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
}
if payload.EdgeGroups == nil || len(payload.EdgeGroups) == 0 {
return portainer.Error("Edge Groups are mandatory for an Edge stack")
}
return nil
}
func (handler *Handler) createSwarmStackFromGitRepository(r *http.Request) (*portainer.EdgeStack, error) {
var payload swarmStackFromGitRepositoryPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return nil, err
}
err = handler.validateUniqueName(payload.Name)
if err != nil {
return nil, err
}
stackID := handler.EdgeStackService.GetNextIdentifier()
stack := &portainer.EdgeStack{
ID: portainer.EdgeStackID(stackID),
Name: payload.Name,
EntryPoint: payload.ComposeFilePathInRepository,
CreationDate: time.Now().Unix(),
EdgeGroups: payload.EdgeGroups,
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus),
Version: 1,
}
projectPath := handler.FileService.GetEdgeStackProjectPath(strconv.Itoa(int(stack.ID)))
stack.ProjectPath = projectPath
gitCloneParams := &cloneRepositoryParameters{
url: payload.RepositoryURL,
referenceName: payload.RepositoryReferenceName,
path: projectPath,
authentication: payload.RepositoryAuthentication,
username: payload.RepositoryUsername,
password: payload.RepositoryPassword,
}
err = handler.cloneGitRepository(gitCloneParams)
if err != nil {
return nil, err
}
err = handler.EdgeStackService.CreateEdgeStack(stack)
if err != nil {
return nil, err
}
return stack, nil
}
type swarmStackFromFileUploadPayload struct {
Name string
StackFileContent []byte
EdgeGroups []portainer.EdgeGroupID
}
func (payload *swarmStackFromFileUploadPayload) Validate(r *http.Request) error {
name, err := request.RetrieveMultiPartFormValue(r, "Name", false)
if err != nil {
return portainer.Error("Invalid stack name")
}
payload.Name = name
composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "file")
if err != nil {
return portainer.Error("Invalid Compose file. Ensure that the Compose file is uploaded correctly")
}
payload.StackFileContent = composeFileContent
var edgeGroups []portainer.EdgeGroupID
err = request.RetrieveMultiPartFormJSONValue(r, "EdgeGroups", &edgeGroups, false)
if err != nil || len(edgeGroups) == 0 {
return portainer.Error("Edge Groups are mandatory for an Edge stack")
}
payload.EdgeGroups = edgeGroups
return nil
}
func (handler *Handler) createSwarmStackFromFileUpload(r *http.Request) (*portainer.EdgeStack, error) {
payload := &swarmStackFromFileUploadPayload{}
err := payload.Validate(r)
if err != nil {
return nil, err
}
err = handler.validateUniqueName(payload.Name)
if err != nil {
return nil, err
}
stackID := handler.EdgeStackService.GetNextIdentifier()
stack := &portainer.EdgeStack{
ID: portainer.EdgeStackID(stackID),
Name: payload.Name,
EntryPoint: filesystem.ComposeFileDefaultName,
CreationDate: time.Now().Unix(),
EdgeGroups: payload.EdgeGroups,
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus),
Version: 1,
}
stackFolder := strconv.Itoa(int(stack.ID))
projectPath, err := handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil {
return nil, err
}
stack.ProjectPath = projectPath
err = handler.EdgeStackService.CreateEdgeStack(stack)
if err != nil {
return nil, err
}
return stack, nil
}
func (handler *Handler) validateUniqueName(name string) error {
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
if err != nil {
return err
}
for _, stack := range edgeStacks {
if strings.EqualFold(stack.Name, name) {
return portainer.Error("Edge stack name must be unique")
}
}
return nil
}
@@ -0,0 +1,62 @@
package edgestacks
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
)
func (handler *Handler) edgeStackDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeStackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid edge stack identifier route variable", err}
}
edgeStack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(edgeStackID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge stack with the specified identifier inside the database", err}
}
err = handler.EdgeStackService.DeleteEdgeStack(portainer.EdgeStackID(edgeStackID))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the edge stack from the database", err}
}
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err}
}
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err}
}
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from database", err}
}
relatedEndpoints, err := portainer.EdgeStackRelatedEndpoints(edgeStack.EdgeGroups, endpoints, endpointGroups, edgeGroups)
for _, endpointID := range relatedEndpoints {
relation, err := handler.EndpointRelationService.EndpointRelation(endpointID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err}
}
delete(relation.EdgeStacks, edgeStack.ID)
err = handler.EndpointRelationService.UpdateEndpointRelation(endpointID, relation)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err}
}
}
return response.Empty(w)
}
@@ -0,0 +1,37 @@
package edgestacks
import (
"net/http"
"path"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
)
type stackFileResponse struct {
StackFileContent string `json:"StackFileContent"`
}
// GET request on /api/edge_stacks/:id/file
func (handler *Handler) edgeStackFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid edge stack identifier route variable", err}
}
stack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(stackID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge stack with the specified identifier inside the database", err}
}
stackFileContent, err := handler.FileService.GetFileContent(path.Join(stack.ProjectPath, stack.EntryPoint))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Compose file from disk", err}
}
return response.JSON(w, &stackFileResponse{StackFileContent: string(stackFileContent)})
}
@@ -0,0 +1,26 @@
package edgestacks
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
)
func (handler *Handler) edgeStackInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeStackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid edge stack identifier route variable", err}
}
edgeStack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(edgeStackID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge stack with the specified identifier inside the database", err}
}
return response.JSON(w, edgeStack)
}
@@ -0,0 +1,17 @@
package edgestacks
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
)
func (handler *Handler) edgeStackList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stacks from the database", err}
}
return response.JSON(w, edgeStacks)
}
@@ -0,0 +1,76 @@
package edgestacks
import (
"net/http"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
)
type updateStatusPayload struct {
Error string
Status *portainer.EdgeStackStatusType
EndpointID *portainer.EndpointID
}
func (payload *updateStatusPayload) Validate(r *http.Request) error {
if payload.Status == nil {
return portainer.Error("Invalid status")
}
if payload.EndpointID == nil {
return portainer.Error("Invalid EndpointID")
}
if *payload.Status == portainer.StatusError && govalidator.IsNull(payload.Error) {
return portainer.Error("Error message is mandatory when status is error")
}
return nil
}
func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
}
stack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(stackID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
}
var payload updateStatusPayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(*payload.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.AuthorizedEdgeEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
stack.Status[*payload.EndpointID] = portainer.EdgeStackStatus{
Type: *payload.Status,
Error: payload.Error,
EndpointID: *payload.EndpointID,
}
err = handler.EdgeStackService.UpdateEdgeStack(stack.ID, stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err}
}
return response.JSON(w, stack)
}
@@ -0,0 +1,156 @@
package edgestacks
import (
"net/http"
"strconv"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
)
type updateEdgeStackPayload struct {
StackFileContent string
Version *int
Prune *bool
EdgeGroups []portainer.EdgeGroupID
}
func (payload *updateEdgeStackPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.StackFileContent) {
return portainer.Error("Invalid stack file content")
}
if payload.EdgeGroups != nil && len(payload.EdgeGroups) == 0 {
return portainer.Error("Edge Groups are mandatory for an Edge stack")
}
return nil
}
func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
}
stack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(stackID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
}
var payload updateEdgeStackPayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
if payload.EdgeGroups != nil {
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err}
}
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err}
}
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from database", err}
}
oldRelated, err := portainer.EdgeStackRelatedEndpoints(stack.EdgeGroups, endpoints, endpointGroups, edgeGroups)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack related endpoints from database", err}
}
newRelated, err := portainer.EdgeStackRelatedEndpoints(payload.EdgeGroups, endpoints, endpointGroups, edgeGroups)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack related endpoints from database", err}
}
oldRelatedSet := EndpointSet(oldRelated)
newRelatedSet := EndpointSet(newRelated)
endpointsToRemove := map[portainer.EndpointID]bool{}
for endpointID := range oldRelatedSet {
if !newRelatedSet[endpointID] {
endpointsToRemove[endpointID] = true
}
}
for endpointID := range endpointsToRemove {
relation, err := handler.EndpointRelationService.EndpointRelation(endpointID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err}
}
delete(relation.EdgeStacks, stack.ID)
err = handler.EndpointRelationService.UpdateEndpointRelation(endpointID, relation)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err}
}
}
endpointsToAdd := map[portainer.EndpointID]bool{}
for endpointID := range newRelatedSet {
if !oldRelatedSet[endpointID] {
endpointsToAdd[endpointID] = true
}
}
for endpointID := range endpointsToAdd {
relation, err := handler.EndpointRelationService.EndpointRelation(endpointID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err}
}
relation.EdgeStacks[stack.ID] = true
err = handler.EndpointRelationService.UpdateEndpointRelation(endpointID, relation)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err}
}
}
stack.EdgeGroups = payload.EdgeGroups
}
if payload.Prune != nil {
stack.Prune = *payload.Prune
}
stackFolder := strconv.Itoa(int(stack.ID))
_, err = handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist updated Compose file on disk", err}
}
if payload.Version != nil && *payload.Version != stack.Version {
stack.Version = *payload.Version
stack.Status = map[portainer.EndpointID]portainer.EdgeStackStatus{}
}
err = handler.EdgeStackService.UpdateEdgeStack(stack.ID, stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err}
}
return response.JSON(w, stack)
}
func EndpointSet(endpointIDs []portainer.EndpointID) map[portainer.EndpointID]bool {
set := map[portainer.EndpointID]bool{}
for _, endpointID := range endpointIDs {
set[endpointID] = true
}
return set
}
+17
View File
@@ -0,0 +1,17 @@
package edgestacks
type cloneRepositoryParameters struct {
url string
referenceName string
path string
authentication bool
username string
password string
}
func (handler *Handler) cloneGitRepository(parameters *cloneRepositoryParameters) error {
if parameters.authentication {
return handler.GitService.ClonePrivateRepositoryWithBasicAuth(parameters.url, parameters.referenceName, parameters.path, parameters.username, parameters.password)
}
return handler.GitService.ClonePublicRepository(parameters.url, parameters.referenceName, parameters.path)
}
+46
View File
@@ -0,0 +1,46 @@
package edgestacks
import (
"net/http"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
)
// Handler is the HTTP handler used to handle endpoint group operations.
type Handler struct {
*mux.Router
requestBouncer *security.RequestBouncer
EdgeGroupService portainer.EdgeGroupService
EdgeStackService portainer.EdgeStackService
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
EndpointRelationService portainer.EndpointRelationService
FileService portainer.FileService
GitService portainer.GitService
}
// NewHandler creates a handler to manage endpoint group operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
requestBouncer: bouncer,
}
h.Handle("/edge_stacks",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackCreate))).Methods(http.MethodPost)
h.Handle("/edge_stacks",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackList))).Methods(http.MethodGet)
h.Handle("/edge_stacks/{id}",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackInspect))).Methods(http.MethodGet)
h.Handle("/edge_stacks/{id}",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackUpdate))).Methods(http.MethodPut)
h.Handle("/edge_stacks/{id}",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackDelete))).Methods(http.MethodDelete)
h.Handle("/edge_stacks/{id}/file",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackFile))).Methods(http.MethodGet)
h.Handle("/edge_stacks/{id}/status",
bouncer.PublicAccess(httperror.LoggerHandler(h.edgeStackStatusUpdate))).Methods(http.MethodPut)
return h
}
@@ -0,0 +1,49 @@
package edgetemplates
import (
"encoding/json"
"log"
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/client"
)
// GET request on /api/edgetemplates
func (handler *Handler) edgeTemplateList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
settings, err := handler.SettingsService.Settings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
}
url := portainer.EdgeTemplatesURL
if settings.TemplatesURL != "" {
url = settings.TemplatesURL
}
var templateData []byte
templateData, err = client.Get(url, 0)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve external templates", err}
}
var templates []portainer.Template
err = json.Unmarshal(templateData, &templates)
if err != nil {
log.Printf("[DEBUG] [http,edge,templates] [failed parsing edge templates] [body: %s]", templateData)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to parse external templates", err}
}
filteredTemplates := []portainer.Template{}
for _, template := range templates {
if template.Type == portainer.EdgeStackTemplate {
filteredTemplates = append(filteredTemplates, template)
}
}
return response.JSON(w, filteredTemplates)
}
+31
View File
@@ -0,0 +1,31 @@
package edgetemplates
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/gorilla/mux"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
)
// Handler is the HTTP handler used to handle edge endpoint operations.
type Handler struct {
*mux.Router
requestBouncer *security.RequestBouncer
SettingsService portainer.SettingsService
}
// NewHandler creates a handler to manage endpoint operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
requestBouncer: bouncer,
}
h.Handle("/edge_templates",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeTemplateList))).Methods(http.MethodGet)
return h
}
@@ -0,0 +1,60 @@
package endpointedge
import (
"net/http"
"path"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
)
type configResponse struct {
Prune bool
StackFileContent string
Name string
}
// GET request on api/endpoints/:id/edge/stacks/:stackId
func (handler *Handler) endpointEdgeStackInspect(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.AuthorizedEdgeEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
edgeStackID, err := request.RetrieveNumericRouteVariableValue(r, "stackId")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid edge stack identifier route variable", err}
}
edgeStack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(edgeStackID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge stack with the specified identifier inside the database", err}
}
stackFileContent, err := handler.FileService.GetFileContent(path.Join(edgeStack.ProjectPath, edgeStack.EntryPoint))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Compose file from disk", err}
}
return response.JSON(w, configResponse{
Prune: edgeStack.Prune,
StackFileContent: string(stackFileContent),
Name: edgeStack.Name,
})
}
+33
View File
@@ -0,0 +1,33 @@
package endpointedge
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/gorilla/mux"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
)
// Handler is the HTTP handler used to handle edge endpoint operations.
type Handler struct {
*mux.Router
requestBouncer *security.RequestBouncer
EndpointService portainer.EndpointService
EdgeStackService portainer.EdgeStackService
FileService portainer.FileService
}
// NewHandler creates a handler to manage endpoint operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
requestBouncer: bouncer,
}
h.Handle("/{id}/edge/stacks/{stackId}",
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointEdgeStackInspect))).Methods(http.MethodGet)
return h
}
@@ -14,15 +14,15 @@ type endpointGroupCreatePayload struct {
Name string
Description string
AssociatedEndpoints []portainer.EndpointID
Tags []string
TagIDs []portainer.TagID
}
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{}
if payload.TagIDs == nil {
payload.TagIDs = []portainer.TagID{}
}
return nil
}
@@ -40,7 +40,7 @@ func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Reque
Description: payload.Description,
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Tags: payload.Tags,
TagIDs: payload.TagIDs,
}
err = handler.EndpointGroupService.CreateEndpointGroup(endpointGroup)
@@ -63,10 +63,29 @@ func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Reque
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
}
err = handler.updateEndpointRelations(&endpoint, endpointGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relations changes inside the database", err}
}
break
}
}
}
for _, tagID := range endpointGroup.TagIDs {
tag, err := handler.TagService.Tag(tagID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve tag from the database", err}
}
tag.EndpointGroups[endpointGroup.ID] = true
err = handler.TagService.UpdateTag(tagID, tag)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist tag changes inside the database", err}
}
}
return response.JSON(w, endpointGroup)
}
@@ -20,7 +20,7 @@ func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Reque
return &httperror.HandlerError{http.StatusForbidden, "Unable to remove the default 'Unassigned' group", portainer.ErrCannotRemoveDefaultGroup}
}
_, err = handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
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 {
@@ -46,6 +46,11 @@ func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Reque
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
}
err = handler.updateEndpointRelations(&endpoint, nil)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relations changes inside the database", err}
}
}
}
@@ -56,5 +61,19 @@ func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Reque
}
}
for _, tagID := range endpointGroup.TagIDs {
tag, err := handler.TagService.Tag(tagID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve tag from the database", err}
}
delete(tag.EndpointGroups, endpointGroup.ID)
err = handler.TagService.UpdateTag(tagID, tag)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist tag changes inside the database", err}
}
}
return response.Empty(w)
}
@@ -42,5 +42,10 @@ func (handler *Handler) endpointGroupAddEndpoint(w http.ResponseWriter, r *http.
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
}
err = handler.updateEndpointRelations(endpoint, endpointGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relations changes inside the database", err}
}
return response.Empty(w)
}
@@ -42,5 +42,10 @@ func (handler *Handler) endpointGroupDeleteEndpoint(w http.ResponseWriter, r *ht
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
}
err = handler.updateEndpointRelations(endpoint, nil)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relations changes inside the database", err}
}
return response.Empty(w)
}
@@ -13,7 +13,7 @@ import (
type endpointGroupUpdatePayload struct {
Name string
Description string
Tags []string
TagIDs []portainer.TagID
UserAccessPolicies portainer.UserAccessPolicies
TeamAccessPolicies portainer.TeamAccessPolicies
}
@@ -50,8 +50,44 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque
endpointGroup.Description = payload.Description
}
if payload.Tags != nil {
endpointGroup.Tags = payload.Tags
tagsChanged := false
if payload.TagIDs != nil {
payloadTagSet := portainer.TagSet(payload.TagIDs)
endpointGroupTagSet := portainer.TagSet((endpointGroup.TagIDs))
union := portainer.TagUnion(payloadTagSet, endpointGroupTagSet)
intersection := portainer.TagIntersection(payloadTagSet, endpointGroupTagSet)
tagsChanged = len(union) > len(intersection)
if tagsChanged {
removeTags := portainer.TagDifference(endpointGroupTagSet, payloadTagSet)
for tagID := range removeTags {
tag, err := handler.TagService.Tag(tagID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a tag inside the database", err}
}
delete(tag.EndpointGroups, endpointGroup.ID)
err = handler.TagService.UpdateTag(tag.ID, tag)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist tag changes inside the database", err}
}
}
endpointGroup.TagIDs = payload.TagIDs
for _, tagID := range payload.TagIDs {
tag, err := handler.TagService.Tag(tagID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a tag inside the database", err}
}
tag.EndpointGroups[endpointGroup.ID] = true
err = handler.TagService.UpdateTag(tag.ID, tag)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist tag changes inside the database", err}
}
}
}
}
updateAuthorizations := false
@@ -77,5 +113,22 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque
}
}
if tagsChanged {
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 == endpointGroup.ID {
err = handler.updateEndpointRelations(&endpoint, endpointGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relations changes inside the database", err}
}
}
}
}
return response.JSON(w, endpointGroup)
}
@@ -0,0 +1,42 @@
package endpointgroups
import portainer "github.com/portainer/portainer/api"
func (handler *Handler) updateEndpointRelations(endpoint *portainer.Endpoint, endpointGroup *portainer.EndpointGroup) error {
if endpoint.Type != portainer.EdgeAgentEnvironment {
return nil
}
if endpointGroup == nil {
unassignedGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(1))
if err != nil {
return err
}
endpointGroup = unassignedGroup
}
endpointRelation, err := handler.EndpointRelationService.EndpointRelation(endpoint.ID)
if err != nil {
return err
}
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
if err != nil {
return err
}
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
if err != nil {
return err
}
endpointStacks := portainer.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks)
stacksSet := map[portainer.EdgeStackID]bool{}
for _, edgeStackID := range endpointStacks {
stacksSet[edgeStackID] = true
}
endpointRelation.EdgeStacks = stacksSet
return handler.EndpointRelationService.UpdateEndpointRelation(endpoint.ID, endpointRelation)
}
+7 -3
View File
@@ -12,9 +12,13 @@ import (
// Handler is the HTTP handler used to handle endpoint group operations.
type Handler struct {
*mux.Router
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
AuthorizationService *portainer.AuthorizationService
AuthorizationService *portainer.AuthorizationService
EdgeGroupService portainer.EdgeGroupService
EdgeStackService portainer.EdgeStackService
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
EndpointRelationService portainer.EndpointRelationService
TagService portainer.TagService
}
// NewHandler creates a handler to manage endpoint group operations.
+64 -18
View File
@@ -12,7 +12,7 @@ import (
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/client"
)
@@ -32,7 +32,7 @@ type endpointCreatePayload struct {
AzureApplicationID string
AzureTenantID string
AzureAuthenticationKey string
Tags []string
TagIDs []portainer.TagID
}
func (payload *endpointCreatePayload) Validate(r *http.Request) error {
@@ -54,14 +54,14 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
}
payload.GroupID = groupID
var tags []string
err = request.RetrieveMultiPartFormJSONValue(r, "Tags", &tags, true)
var tagIDs []portainer.TagID
err = request.RetrieveMultiPartFormJSONValue(r, "TagIds", &tagIDs, true)
if err != nil {
return portainer.Error("Invalid Tags parameter")
return portainer.Error("Invalid TagIds parameter")
}
payload.Tags = tags
if payload.Tags == nil {
payload.Tags = make([]string, 0)
payload.TagIDs = tagIDs
if payload.TagIDs == nil {
payload.TagIDs = make([]portainer.TagID, 0)
}
useTLS, _ := request.RetrieveBooleanMultiPartFormValue(r, "TLS", true)
@@ -146,6 +146,38 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *
return endpointCreationError
}
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group inside the database", err}
}
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from the database", err}
}
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stacks from the database", err}
}
relationObject := &portainer.EndpointRelation{
EndpointID: endpoint.ID,
EdgeStacks: map[portainer.EdgeStackID]bool{},
}
if endpoint.Type == portainer.EdgeAgentEnvironment {
relatedEdgeStacks := portainer.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks)
for _, stackID := range relatedEdgeStacks {
relationObject.EdgeStacks[stackID] = true
}
}
err = handler.EndpointRelationService.CreateEndpointRelation(relationObject)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the relation object inside the database", err}
}
return response.JSON(w, endpoint)
}
@@ -187,7 +219,7 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{},
AzureCredentials: credentials,
Tags: payload.Tags,
TagIDs: payload.TagIDs,
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
}
@@ -229,13 +261,13 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
TLSConfig: portainer.TLSConfiguration{
TLS: false,
},
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
Tags: payload.Tags,
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
EdgeKey: edgeKey,
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{},
TagIDs: payload.TagIDs,
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
EdgeKey: edgeKey,
}
err = handler.saveEndpointAndUpdateAuthorizations(endpoint)
@@ -278,7 +310,7 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload)
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{},
Tags: payload.Tags,
TagIDs: payload.TagIDs,
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
}
@@ -322,7 +354,7 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload)
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{},
Tags: payload.Tags,
TagIDs: payload.TagIDs,
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
}
@@ -377,6 +409,20 @@ func (handler *Handler) saveEndpointAndUpdateAuthorizations(endpoint *portainer.
return handler.AuthorizationService.UpdateUsersAuthorizations()
}
for _, tagID := range endpoint.TagIDs {
tag, err := handler.TagService.Tag(tagID)
if err != nil {
return err
}
tag.Endpoints[endpoint.ID] = true
err = handler.TagService.UpdateTag(tagID, tag)
if err != nil {
return err
}
}
return nil
}
@@ -50,5 +50,75 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
}
}
err = handler.EndpointRelationService.DeleteEndpointRelation(endpoint.ID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove endpoint relation from the database", err}
}
for _, tagID := range endpoint.TagIDs {
tag, err := handler.TagService.Tag(tagID)
if err != nil {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find tag inside the database", err}
}
delete(tag.Endpoints, endpoint.ID)
err = handler.TagService.UpdateTag(tagID, tag)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist tag relation inside the database", err}
}
}
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from the database", err}
}
for idx := range edgeGroups {
edgeGroup := &edgeGroups[idx]
endpointIdx := findEndpointIndex(edgeGroup.Endpoints, endpoint.ID)
if endpointIdx != -1 {
edgeGroup.Endpoints = removeElement(edgeGroup.Endpoints, endpointIdx)
err = handler.EdgeGroupService.UpdateEdgeGroup(edgeGroup.ID, edgeGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update edge group", err}
}
}
}
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stacks from the database", err}
}
for idx := range edgeStacks {
edgeStack := &edgeStacks[idx]
if _, ok := edgeStack.Status[endpoint.ID]; ok {
delete(edgeStack.Status, endpoint.ID)
err = handler.EdgeStackService.UpdateEdgeStack(edgeStack.ID, edgeStack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update edge stack", err}
}
}
}
return response.Empty(w)
}
func findEndpointIndex(tags []portainer.EndpointID, searchEndpointID portainer.EndpointID) int {
for idx, tagID := range tags {
if searchEndpointID == tagID {
return idx
}
}
return -1
}
func removeElement(arr []portainer.EndpointID, index int) []portainer.EndpointID {
if index < 0 {
return arr
}
lastTagIdx := len(arr) - 1
arr[index] = arr[lastTagIdx]
return arr[:lastTagIdx]
}
+143 -12
View File
@@ -5,7 +5,7 @@ import (
"strconv"
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
"github.com/portainer/libhttp/request"
@@ -28,6 +28,15 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
groupID, _ := request.RetrieveNumericQueryParameter(r, "groupId", true)
limit, _ := request.RetrieveNumericQueryParameter(r, "limit", true)
endpointType, _ := request.RetrieveNumericQueryParameter(r, "type", true)
var tagIDs []portainer.TagID
request.RetrieveJSONQueryParameter(r, "tagIds", &tagIDs, true)
tagsPartialMatch, _ := request.RetrieveBooleanQueryParameter(r, "tagsPartialMatch", true)
var endpointIDs []portainer.EndpointID
request.RetrieveJSONQueryParameter(r, "endpointIds", &endpointIDs, true)
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
if err != nil {
@@ -46,12 +55,32 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
if endpointIDs != nil {
filteredEndpoints = filteredEndpointsByIds(filteredEndpoints, endpointIDs)
}
if groupID != 0 {
filteredEndpoints = filterEndpointsByGroupID(filteredEndpoints, portainer.EndpointGroupID(groupID))
}
if search != "" {
filteredEndpoints = filterEndpointsBySearchCriteria(filteredEndpoints, endpointGroups, search)
tags, err := handler.TagService.Tags()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve tags from the database", err}
}
tagsMap := make(map[portainer.TagID]string)
for _, tag := range tags {
tagsMap[tag.ID] = tag.Name
}
filteredEndpoints = filterEndpointsBySearchCriteria(filteredEndpoints, endpointGroups, tagsMap, search)
}
if endpointType != 0 {
filteredEndpoints = filterEndpointsByType(filteredEndpoints, portainer.EndpointType(endpointType))
}
if tagIDs != nil {
filteredEndpoints = filteredEndpointsByTags(filteredEndpoints, tagIDs, endpointGroups, tagsPartialMatch)
}
filteredEndpointCount := len(filteredEndpoints)
@@ -97,17 +126,17 @@ func filterEndpointsByGroupID(endpoints []portainer.Endpoint, endpointGroupID po
return filteredEndpoints
}
func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, searchCriteria string) []portainer.Endpoint {
func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
if endpointMatchSearchCriteria(&endpoint, searchCriteria) {
endpointTags := convertTagIDsToTags(tagsMap, endpoint.TagIDs)
if endpointMatchSearchCriteria(&endpoint, endpointTags, searchCriteria) {
filteredEndpoints = append(filteredEndpoints, endpoint)
continue
}
if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, searchCriteria) {
if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, tagsMap, searchCriteria) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
@@ -115,7 +144,7 @@ func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGro
return filteredEndpoints
}
func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, searchCriteria string) bool {
func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, searchCriteria string) bool {
if strings.Contains(strings.ToLower(endpoint.Name), searchCriteria) {
return true
}
@@ -129,8 +158,7 @@ func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, searchCriteria st
} else if endpoint.Status == portainer.EndpointStatusDown && searchCriteria == "down" {
return true
}
for _, tag := range endpoint.Tags {
for _, tag := range tags {
if strings.Contains(strings.ToLower(tag), searchCriteria) {
return true
}
@@ -139,14 +167,14 @@ func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, searchCriteria st
return false
}
func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGroups []portainer.EndpointGroup, searchCriteria string) bool {
func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) bool {
for _, group := range endpointGroups {
if group.ID == endpoint.GroupID {
if strings.Contains(strings.ToLower(group.Name), searchCriteria) {
return true
}
for _, tag := range group.Tags {
tags := convertTagIDsToTags(tagsMap, group.TagIDs)
for _, tag := range tags {
if strings.Contains(strings.ToLower(tag), searchCriteria) {
return true
}
@@ -156,3 +184,106 @@ func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGrou
return false
}
func filterEndpointsByType(endpoints []portainer.Endpoint, endpointType portainer.EndpointType) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
if endpoint.Type == endpointType {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func convertTagIDsToTags(tagsMap map[portainer.TagID]string, tagIDs []portainer.TagID) []string {
tags := make([]string, 0)
for _, tagID := range tagIDs {
tags = append(tags, tagsMap[tagID])
}
return tags
}
func filteredEndpointsByTags(endpoints []portainer.Endpoint, tagIDs []portainer.TagID, endpointGroups []portainer.EndpointGroup, partialMatch bool) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
endpointGroup := getEndpointGroup(endpoint.GroupID, endpointGroups)
endpointMatched := false
if partialMatch {
endpointMatched = endpointPartialMatchTags(endpoint, endpointGroup, tagIDs)
} else {
endpointMatched = endpointFullMatchTags(endpoint, endpointGroup, tagIDs)
}
if endpointMatched {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func getEndpointGroup(groupID portainer.EndpointGroupID, groups []portainer.EndpointGroup) portainer.EndpointGroup {
var endpointGroup portainer.EndpointGroup
for _, group := range groups {
if group.ID == groupID {
endpointGroup = group
break
}
}
return endpointGroup
}
func endpointPartialMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.EndpointGroup, tagIDs []portainer.TagID) bool {
tagSet := make(map[portainer.TagID]bool)
for _, tagID := range tagIDs {
tagSet[tagID] = true
}
for _, tagID := range endpoint.TagIDs {
if tagSet[tagID] {
return true
}
}
for _, tagID := range endpointGroup.TagIDs {
if tagSet[tagID] {
return true
}
}
return false
}
func endpointFullMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.EndpointGroup, tagIDs []portainer.TagID) bool {
missingTags := make(map[portainer.TagID]bool)
for _, tagID := range tagIDs {
missingTags[tagID] = true
}
for _, tagID := range endpoint.TagIDs {
if missingTags[tagID] {
delete(missingTags, tagID)
}
}
for _, tagID := range endpointGroup.TagIDs {
if missingTags[tagID] {
delete(missingTags, tagID)
}
}
return len(missingTags) == 0
}
func filteredEndpointsByIds(endpoints []portainer.Endpoint, ids []portainer.EndpointID) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
idsSet := make(map[portainer.EndpointID]bool)
for _, id := range ids {
idsSet[id] = true
}
for _, endpoint := range endpoints {
if idsSet[endpoint.ID] {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
@@ -1,7 +1,6 @@
package endpoints
import (
"errors"
"net/http"
httperror "github.com/portainer/libhttp/error"
@@ -10,12 +9,18 @@ import (
"github.com/portainer/portainer/api"
)
type stackStatusResponse struct {
ID portainer.EdgeStackID
Version int
}
type endpointStatusInspectResponse struct {
Status string `json:"status"`
Port int `json:"port"`
Schedules []portainer.EdgeSchedule `json:"schedules"`
CheckinInterval int `json:"checkin"`
Credentials string `json:"credentials"`
Stacks []stackStatusResponse `json:"stacks"`
}
// GET request on /api/endpoints/:id/status
@@ -32,20 +37,14 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
if endpoint.Type != portainer.EdgeAgentEnvironment {
return &httperror.HandlerError{http.StatusInternalServerError, "Status unavailable for non Edge agent endpoints", errors.New("Status unavailable")}
}
edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader)
if edgeIdentifier == "" {
return &httperror.HandlerError{http.StatusForbidden, "Missing Edge identifier", errors.New("missing Edge identifier")}
}
if endpoint.EdgeID != "" && endpoint.EdgeID != edgeIdentifier {
return &httperror.HandlerError{http.StatusForbidden, "Invalid Edge identifier", errors.New("invalid Edge identifier")}
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
if endpoint.EdgeID == "" {
edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader)
endpoint.EdgeID = edgeIdentifier
err := handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
@@ -73,5 +72,27 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req
handler.ReverseTunnelService.SetTunnelStatusToActive(endpoint.ID)
}
relation, err := handler.EndpointRelationService.EndpointRelation(endpoint.ID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve relation object from the database", err}
}
edgeStacksStatus := []stackStatusResponse{}
for stackID := range relation.EdgeStacks {
stack, err := handler.EdgeStackService.EdgeStack(stackID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack from the database", err}
}
stackStatus := stackStatusResponse{
ID: stack.ID,
Version: stack.Version,
}
edgeStacksStatus = append(edgeStacksStatus, stackStatus)
}
statusResponse.Stacks = edgeStacksStatus
return response.JSON(w, statusResponse)
}
+80 -4
View File
@@ -24,7 +24,7 @@ type endpointUpdatePayload struct {
AzureApplicationID *string
AzureTenantID *string
AzureAuthenticationKey *string
Tags []string
TagIDs []portainer.TagID
UserAccessPolicies portainer.UserAccessPolicies
TeamAccessPolicies portainer.TeamAccessPolicies
}
@@ -69,12 +69,52 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
endpoint.PublicURL = *payload.PublicURL
}
groupIDChanged := false
if payload.GroupID != nil {
endpoint.GroupID = portainer.EndpointGroupID(*payload.GroupID)
groupID := portainer.EndpointGroupID(*payload.GroupID)
groupIDChanged = groupID != endpoint.GroupID
endpoint.GroupID = groupID
}
if payload.Tags != nil {
endpoint.Tags = payload.Tags
tagsChanged := false
if payload.TagIDs != nil {
payloadTagSet := portainer.TagSet(payload.TagIDs)
endpointTagSet := portainer.TagSet((endpoint.TagIDs))
union := portainer.TagUnion(payloadTagSet, endpointTagSet)
intersection := portainer.TagIntersection(payloadTagSet, endpointTagSet)
tagsChanged = len(union) > len(intersection)
if tagsChanged {
removeTags := portainer.TagDifference(endpointTagSet, payloadTagSet)
for tagID := range removeTags {
tag, err := handler.TagService.Tag(tagID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a tag inside the database", err}
}
delete(tag.Endpoints, endpoint.ID)
err = handler.TagService.UpdateTag(tag.ID, tag)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist tag changes inside the database", err}
}
}
endpoint.TagIDs = payload.TagIDs
for _, tagID := range payload.TagIDs {
tag, err := handler.TagService.Tag(tagID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a tag inside the database", err}
}
tag.Endpoints[endpoint.ID] = true
err = handler.TagService.UpdateTag(tag.ID, tag)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist tag changes inside the database", err}
}
}
}
}
updateAuthorizations := false
@@ -184,5 +224,41 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
}
}
if endpoint.Type == portainer.EdgeAgentEnvironment && (groupIDChanged || tagsChanged) {
relation, err := handler.EndpointRelationService.EndpointRelation(endpoint.ID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation inside the database", err}
}
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint group inside the database", err}
}
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from the database", err}
}
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stacks from the database", err}
}
edgeStackSet := map[portainer.EdgeStackID]bool{}
endpointEdgeStacks := portainer.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks)
for _, edgeStackID := range endpointEdgeStacks {
edgeStackSet[edgeStackID] = true
}
relation.EdgeStacks = edgeStackSet
err = handler.EndpointRelationService.UpdateEndpointRelation(endpoint.ID, relation)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation changes inside the database", err}
}
}
return response.JSON(w, endpoint)
}
+7 -4
View File
@@ -29,15 +29,19 @@ type Handler struct {
*mux.Router
authorizeEndpointManagement bool
requestBouncer *security.RequestBouncer
AuthorizationService *portainer.AuthorizationService
EdgeGroupService portainer.EdgeGroupService
EdgeStackService portainer.EdgeStackService
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
EndpointRelationService portainer.EndpointRelationService
FileService portainer.FileService
ProxyManager *proxy.Manager
Snapshotter portainer.Snapshotter
JobService portainer.JobService
ProxyManager *proxy.Manager
ReverseTunnelService portainer.ReverseTunnelService
SettingsService portainer.SettingsService
AuthorizationService *portainer.AuthorizationService
Snapshotter portainer.Snapshotter
TagService portainer.TagService
}
// NewHandler creates a handler to manage endpoint operations.
@@ -70,6 +74,5 @@ func NewHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bo
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost)
h.Handle("/endpoints/{id}/status",
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointStatusInspect))).Methods(http.MethodGet)
return h
}
@@ -41,16 +41,21 @@ func (handler *Handler) extensionCreate(w http.ResponseWriter, r *http.Request)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions status from the database", err}
}
for _, existingExtension := range extensions {
if existingExtension.ID == extensionID && existingExtension.Enabled {
return &httperror.HandlerError{http.StatusConflict, "Unable to enable extension", portainer.ErrExtensionAlreadyEnabled}
}
}
extension := &portainer.Extension{
ID: extensionID,
}
for _, existingExtension := range extensions {
if existingExtension.ID == extensionID && (existingExtension.Enabled || !existingExtension.License.Valid) {
if existingExtension.License.LicenseKey == payload.License {
return &httperror.HandlerError{http.StatusConflict, "Unable to enable extension", portainer.ErrExtensionAlreadyEnabled}
}
_ = handler.ExtensionManager.DisableExtension(&existingExtension)
extension.Enabled = true
}
}
extensionDefinitions, err := handler.ExtensionManager.FetchExtensionDefinitions()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extension definitions", err}
@@ -68,15 +73,14 @@ func (handler *Handler) extensionCreate(w http.ResponseWriter, r *http.Request)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to enable extension", err}
}
extension.Enabled = true
if extension.ID == portainer.RBACExtension {
if extension.ID == portainer.RBACExtension && !extension.Enabled {
err = handler.upgradeRBACData()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "An error occured during database update", err}
}
}
extension.Enabled = true
err = handler.ExtensionService.Persist(extension)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist extension status inside the database", err}
@@ -46,10 +46,21 @@ func (handler *Handler) extensionUpload(w http.ResponseWriter, r *http.Request)
}
extensionID := portainer.ExtensionID(extensionIdentifier)
extensions, err := handler.ExtensionService.Extensions()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions status from the database", err}
}
extension := &portainer.Extension{
ID: extensionID,
}
for _, existingExtension := range extensions {
if existingExtension.ID == extensionID && (existingExtension.Enabled || !existingExtension.License.Valid) {
extension.Enabled = true
}
}
_ = handler.ExtensionManager.DisableExtension(extension)
err = handler.ExtensionManager.InstallExtension(extension, payload.License, payload.ArchiveFileName, payload.ExtensionArchive)
@@ -57,15 +68,15 @@ func (handler *Handler) extensionUpload(w http.ResponseWriter, r *http.Request)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to install extension", err}
}
extension.Enabled = true
if extension.ID == portainer.RBACExtension {
if extension.ID == portainer.RBACExtension && !extension.Enabled {
err = handler.upgradeRBACData()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "An error occured during database update", err}
}
}
extension.Enabled = true
err = handler.ExtensionService.Persist(extension)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist extension status inside the database", err}
+16
View File
@@ -4,6 +4,10 @@ import (
"net/http"
"strings"
"github.com/portainer/portainer/api/http/handler/edgegroups"
"github.com/portainer/portainer/api/http/handler/edgestacks"
"github.com/portainer/portainer/api/http/handler/edgetemplates"
"github.com/portainer/portainer/api/http/handler/endpointedge"
"github.com/portainer/portainer/api/http/handler/support"
"github.com/portainer/portainer/api/http/handler/schedules"
@@ -37,6 +41,10 @@ import (
type Handler struct {
AuthHandler *auth.Handler
DockerHubHandler *dockerhub.Handler
EdgeGroupsHandler *edgegroups.Handler
EdgeStacksHandler *edgestacks.Handler
EdgeTemplatesHandler *edgetemplates.Handler
EndpointEdgeHandler *endpointedge.Handler
EndpointGroupHandler *endpointgroups.Handler
EndpointHandler *endpoints.Handler
EndpointProxyHandler *endpointproxy.Handler
@@ -68,6 +76,12 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/dockerhub"):
http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/edge_stacks"):
http.StripPrefix("/api", h.EdgeStacksHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/edge_groups"):
http.StripPrefix("/api", h.EdgeGroupsHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/edge_templates"):
http.StripPrefix("/api", h.EdgeTemplatesHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/endpoint_groups"):
http.StripPrefix("/api", h.EndpointGroupHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/endpoints"):
@@ -78,6 +92,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
case strings.Contains(r.URL.Path, "/azure/"):
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
case strings.Contains(r.URL.Path, "/edge/"):
http.StripPrefix("/api/endpoints", h.EndpointEdgeHandler).ServeHTTP(w, r)
default:
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
}
+26 -16
View File
@@ -6,18 +6,23 @@ import (
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
)
type publicSettingsResponse struct {
LogoURL string `json:"LogoURL"`
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
ExternalTemplates bool `json:"ExternalTemplates"`
OAuthLoginURI string `json:"OAuthLoginURI"`
LogoURL string `json:"LogoURL"`
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
ExternalTemplates bool `json:"ExternalTemplates"`
OAuthLoginURI string `json:"OAuthLoginURI"`
AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"`
AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"`
AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"`
AllowContainerCapabilitiesForRegularUsers bool `json:"AllowContainerCapabilitiesForRegularUsers"`
}
// GET request on /api/settings/public
@@ -28,18 +33,23 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *
}
publicSettings := &publicSettingsResponse{
LogoURL: settings.LogoURL,
AuthenticationMethod: settings.AuthenticationMethod,
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers,
EnableHostManagementFeatures: settings.EnableHostManagementFeatures,
ExternalTemplates: false,
LogoURL: settings.LogoURL,
AuthenticationMethod: settings.AuthenticationMethod,
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers,
EnableHostManagementFeatures: settings.EnableHostManagementFeatures,
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers,
AllowStackManagementForRegularUsers: settings.AllowStackManagementForRegularUsers,
ExternalTemplates: false,
AllowContainerCapabilitiesForRegularUsers: settings.AllowContainerCapabilitiesForRegularUsers,
OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login",
settings.OAuthSettings.AuthorizationURI,
settings.OAuthSettings.ClientID,
settings.OAuthSettings.RedirectURI,
settings.OAuthSettings.Scopes),
AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers,
}
if settings.TemplatesURL != "" {
+43 -13
View File
@@ -7,23 +7,28 @@ import (
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
)
type settingsUpdatePayload struct {
LogoURL *string
BlackListedLabels []portainer.Pair
AuthenticationMethod *int
LDAPSettings *portainer.LDAPSettings
OAuthSettings *portainer.OAuthSettings
AllowBindMountsForRegularUsers *bool
AllowPrivilegedModeForRegularUsers *bool
AllowVolumeBrowserForRegularUsers *bool
EnableHostManagementFeatures *bool
SnapshotInterval *string
TemplatesURL *string
EdgeAgentCheckinInterval *int
LogoURL *string
BlackListedLabels []portainer.Pair
AuthenticationMethod *int
LDAPSettings *portainer.LDAPSettings
OAuthSettings *portainer.OAuthSettings
AllowBindMountsForRegularUsers *bool
AllowPrivilegedModeForRegularUsers *bool
AllowVolumeBrowserForRegularUsers *bool
EnableHostManagementFeatures *bool
SnapshotInterval *string
TemplatesURL *string
EdgeAgentCheckinInterval *int
EnableEdgeComputeFeatures *bool
AllowStackManagementForRegularUsers *bool
AllowHostNamespaceForRegularUsers *bool
AllowDeviceMappingForRegularUsers *bool
AllowContainerCapabilitiesForRegularUsers *bool
}
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
@@ -69,11 +74,16 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
}
if payload.LDAPSettings != nil {
ldapReaderDN := settings.LDAPSettings.ReaderDN
ldapPassword := settings.LDAPSettings.Password
if payload.LDAPSettings.ReaderDN != "" {
ldapReaderDN = payload.LDAPSettings.ReaderDN
}
if payload.LDAPSettings.Password != "" {
ldapPassword = payload.LDAPSettings.Password
}
settings.LDAPSettings = *payload.LDAPSettings
settings.LDAPSettings.ReaderDN = ldapReaderDN
settings.LDAPSettings.Password = ldapPassword
}
@@ -104,6 +114,22 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
settings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures
}
if payload.EnableEdgeComputeFeatures != nil {
settings.EnableEdgeComputeFeatures = *payload.EnableEdgeComputeFeatures
}
if payload.AllowStackManagementForRegularUsers != nil {
settings.AllowStackManagementForRegularUsers = *payload.AllowStackManagementForRegularUsers
}
if payload.AllowHostNamespaceForRegularUsers != nil {
settings.AllowHostNamespaceForRegularUsers = *payload.AllowHostNamespaceForRegularUsers
}
if payload.AllowContainerCapabilitiesForRegularUsers != nil {
settings.AllowContainerCapabilitiesForRegularUsers = *payload.AllowContainerCapabilitiesForRegularUsers
}
if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval {
err := handler.updateSnapshotInterval(settings, *payload.SnapshotInterval)
if err != nil {
@@ -115,6 +141,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
settings.EdgeAgentCheckinInterval = *payload.EdgeAgentCheckinInterval
}
if payload.AllowDeviceMappingForRegularUsers != nil {
settings.AllowDeviceMappingForRegularUsers = *payload.AllowDeviceMappingForRegularUsers
}
tlsError := handler.updateTLS(settings)
if tlsError != nil {
return tlsError
+30 -12
View File
@@ -1,7 +1,6 @@
package stacks
import (
"errors"
"net/http"
"path"
"regexp"
@@ -11,7 +10,7 @@ import (
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/security"
)
@@ -283,6 +282,7 @@ type composeStackDeploymentConfig struct {
dockerhub *portainer.DockerHub
registries []portainer.Registry
isAdmin bool
user *portainer.User
}
func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) (*composeStackDeploymentConfig, *httperror.HandlerError) {
@@ -302,12 +302,21 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai
}
filteredRegistries := security.FilterRegistries(registries, securityContext)
var user *portainer.User
if !handler.authDisabled {
user, err = handler.UserService.User(securityContext.UserID)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err}
}
}
config := &composeStackDeploymentConfig{
stack: stack,
endpoint: endpoint,
dockerhub: dockerhub,
registries: filteredRegistries,
isAdmin: securityContext.IsAdmin,
user: user,
}
return config, nil
@@ -324,20 +333,29 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig)
return err
}
if !settings.AllowBindMountsForRegularUsers && !config.isAdmin {
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
if !handler.authDisabled {
isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
if err != nil {
return err
}
valid, err := handler.isValidStackFile(stackContent)
if err != nil {
return err
}
if !valid {
return errors.New("bind-mount disabled for non administrator users")
if (!settings.AllowBindMountsForRegularUsers ||
!settings.AllowPrivilegedModeForRegularUsers ||
!settings.AllowHostNamespaceForRegularUsers ||
!settings.AllowDeviceMappingForRegularUsers ||
!settings.AllowContainerCapabilitiesForRegularUsers) && !isAdminOrEndpointAdmin {
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
if err != nil {
return err
}
err = handler.isValidStackFile(stackContent, settings)
if err != nil {
return err
}
}
}
+26 -12
View File
@@ -1,7 +1,6 @@
package stacks
import (
"errors"
"net/http"
"path"
"strconv"
@@ -10,7 +9,7 @@ import (
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/security"
)
@@ -292,6 +291,7 @@ type swarmStackDeploymentConfig struct {
registries []portainer.Registry
prune bool
isAdmin bool
user *portainer.User
}
func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint, prune bool) (*swarmStackDeploymentConfig, *httperror.HandlerError) {
@@ -311,6 +311,14 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine
}
filteredRegistries := security.FilterRegistries(registries, securityContext)
var user *portainer.User
if !handler.authDisabled {
user, err = handler.UserService.User(securityContext.UserID)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err}
}
}
config := &swarmStackDeploymentConfig{
stack: stack,
endpoint: endpoint,
@@ -318,6 +326,7 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine
registries: filteredRegistries,
prune: prune,
isAdmin: securityContext.IsAdmin,
user: user,
}
return config, nil
@@ -329,20 +338,25 @@ func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) err
return err
}
if !settings.AllowBindMountsForRegularUsers && !config.isAdmin {
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
if !handler.authDisabled {
isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
if err != nil {
return err
}
valid, err := handler.isValidStackFile(stackContent)
if err != nil {
return err
}
if !valid {
return errors.New("bind-mount disabled for non administrator users")
if !settings.AllowBindMountsForRegularUsers && !isAdminOrEndpointAdmin {
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
if err != nil {
return err
}
err = handler.isValidStackFile(stackContent, settings)
if err != nil {
return err
}
}
}
+58 -3
View File
@@ -1,12 +1,13 @@
package stacks
import (
"errors"
"net/http"
"sync"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
)
@@ -15,6 +16,8 @@ type Handler struct {
stackCreationMutex *sync.Mutex
stackDeletionMutex *sync.Mutex
requestBouncer *security.RequestBouncer
authDisabled bool
*mux.Router
FileService portainer.FileService
GitService portainer.GitService
@@ -31,9 +34,10 @@ type Handler struct {
}
// NewHandler creates a handler to manage stack operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
func NewHandler(bouncer *security.RequestBouncer, authDisabled bool) *Handler {
h := &Handler{
Router: mux.NewRouter(),
authDisabled: authDisabled,
stackCreationMutex: &sync.Mutex{},
stackDeletionMutex: &sync.Mutex{},
requestBouncer: bouncer,
@@ -56,7 +60,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
}
func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedRequestContext, endpointID portainer.EndpointID, resourceControl *portainer.ResourceControl) (bool, error) {
if securityContext.IsAdmin {
if securityContext.IsAdmin || handler.authDisabled {
return true, nil
}
@@ -87,3 +91,54 @@ func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedR
}
return false, nil
}
func (handler *Handler) userCanCreateStack(securityContext *security.RestrictedRequestContext, endpointID portainer.EndpointID) (bool, error) {
if securityContext.IsAdmin || handler.authDisabled {
return true, nil
}
_, err := handler.ExtensionService.Extension(portainer.RBACExtension)
if err == portainer.ErrObjectNotFound {
return false, nil
} else if err != nil && err != portainer.ErrObjectNotFound {
return false, err
}
user, err := handler.UserService.User(securityContext.UserID)
if err != nil {
return false, err
}
_, ok := user.EndpointAuthorizations[endpointID][portainer.EndpointResourcesAccess]
if ok {
return true, nil
}
return false, nil
}
func (handler *Handler) userIsAdminOrEndpointAdmin(user *portainer.User, endpointID portainer.EndpointID) (bool, error) {
isAdmin := user.Role == portainer.AdministratorRole
rbacExtension, err := handler.ExtensionService.Extension(portainer.RBACExtension)
if err != nil && err != portainer.ErrObjectNotFound {
return false, errors.New("Unable to verify if RBAC extension is loaded")
}
endpointResourceAccess := false
_, ok := user.EndpointAuthorizations[portainer.EndpointID(endpointID)][portainer.EndpointResourcesAccess]
if ok {
endpointResourceAccess = true
}
if rbacExtension != nil {
if isAdmin || endpointResourceAccess {
return true, nil
}
} else {
if isAdmin {
return true, nil
}
}
return false, nil
}
+49 -8
View File
@@ -10,7 +10,7 @@ import (
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
)
@@ -43,6 +43,29 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
}
settings, err := handler.SettingsService.Settings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
}
if !settings.AllowStackManagementForRegularUsers {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err}
}
canCreate, err := handler.userCanCreateStack(securityContext, portainer.EndpointID(endpointID))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack creation", err}
}
if !canCreate {
errMsg := "Stack creation is disabled for non-admin users"
return &httperror.HandlerError{http.StatusForbidden, errMsg, errors.New(errMsg)}
}
}
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}
@@ -97,10 +120,10 @@ func (handler *Handler) createSwarmStack(w http.ResponseWriter, r *http.Request,
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", errors.New(request.ErrInvalidQueryParameter)}
}
func (handler *Handler) isValidStackFile(stackFileContent []byte) (bool, error) {
func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *portainer.Settings) error {
composeConfigYAML, err := loader.ParseYAML(stackFileContent)
if err != nil {
return false, err
return err
}
composeConfigFile := types.ConfigFile{
@@ -117,19 +140,37 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte) (bool, error)
options.SkipInterpolation = true
})
if err != nil {
return false, err
return err
}
for key := range composeConfig.Services {
service := composeConfig.Services[key]
for _, volume := range service.Volumes {
if volume.Type == "bind" {
return false, nil
if !settings.AllowBindMountsForRegularUsers {
for _, volume := range service.Volumes {
if volume.Type == "bind" {
return errors.New("bind-mount disabled for non administrator users")
}
}
}
if !settings.AllowPrivilegedModeForRegularUsers && service.Privileged == true {
return errors.New("privileged mode disabled for non administrator users")
}
if !settings.AllowHostNamespaceForRegularUsers && service.Pid == "host" {
return errors.New("pid host disabled for non administrator users")
}
if !settings.AllowDeviceMappingForRegularUsers && service.Devices != nil && len(service.Devices) > 0 {
return errors.New("device mapping disabled for non administrator users")
}
if !settings.AllowContainerCapabilitiesForRegularUsers && (len(service.CapAdd) > 0 || len(service.CapDrop) > 0) {
return errors.New("container capabilities disabled for non administrator users")
}
}
return true, nil
return nil
}
func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *portainer.Stack, userID portainer.UserID) *httperror.HandlerError {
+39 -13
View File
@@ -9,7 +9,7 @@ import (
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
)
// DELETE request on /api/stacks/:id?external=<external>&endpointId=<endpointId>
@@ -21,9 +21,14 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
externalStack, _ := request.RetrieveBooleanQueryParameter(r, "external", true)
if externalStack {
return handler.deleteExternalStack(r, w, stackID)
return handler.deleteExternalStack(r, w, stackID, securityContext)
}
id, err := strconv.Atoi(stackID)
@@ -68,11 +73,6 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
@@ -106,7 +106,38 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
return response.Empty(w)
}
func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWriter, stackName string) *httperror.HandlerError {
func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWriter, stackName string, securityContext *security.RestrictedRequestContext) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
}
user, err := handler.UserService.User(securityContext.UserID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err}
}
rbacExtension, err := handler.ExtensionService.Extension(portainer.RBACExtension)
if err != nil && err != portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify if RBAC extension is loaded", err}
}
endpointResourceAccess := false
_, ok := user.EndpointAuthorizations[portainer.EndpointID(endpointID)][portainer.EndpointResourcesAccess]
if ok {
endpointResourceAccess = true
}
if rbacExtension != nil {
if !securityContext.IsAdmin && !endpointResourceAccess {
return &httperror.HandlerError{http.StatusUnauthorized, "Permission denied to delete the stack", portainer.ErrUnauthorized}
}
} else {
if !securityContext.IsAdmin {
return &httperror.HandlerError{http.StatusUnauthorized, "Permission denied to delete the stack", portainer.ErrUnauthorized}
}
}
stack, err := handler.StackService.StackByName(stackName)
if err != nil && err != portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for stack existence inside the database", err}
@@ -115,11 +146,6 @@ func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWrit
return &httperror.HandlerError{http.StatusBadRequest, "A stack with this name exists inside the database. Cannot use external delete method", portainer.ErrStackNotExternal}
}
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err}
+7 -2
View File
@@ -12,7 +12,12 @@ import (
// Handler is the HTTP handler used to handle tag operations.
type Handler struct {
*mux.Router
TagService portainer.TagService
TagService portainer.TagService
EdgeGroupService portainer.EdgeGroupService
EdgeStackService portainer.EdgeStackService
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
EndpointRelationService portainer.EndpointRelationService
}
// NewHandler creates a handler to manage tag operations.
@@ -23,7 +28,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
h.Handle("/tags",
bouncer.AdminAccess(httperror.LoggerHandler(h.tagCreate))).Methods(http.MethodPost)
h.Handle("/tags",
bouncer.AdminAccess(httperror.LoggerHandler(h.tagList))).Methods(http.MethodGet)
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.tagList))).Methods(http.MethodGet)
h.Handle("/tags/{id}",
bouncer.AdminAccess(httperror.LoggerHandler(h.tagDelete))).Methods(http.MethodDelete)
+3 -1
View File
@@ -41,7 +41,9 @@ func (handler *Handler) tagCreate(w http.ResponseWriter, r *http.Request) *httpe
}
tag := &portainer.Tag{
Name: payload.Name,
Name: payload.Name,
EndpointGroups: map[portainer.EndpointGroupID]bool{},
Endpoints: map[portainer.EndpointID]bool{},
}
err = handler.TagService.CreateTag(tag)
+116 -1
View File
@@ -15,11 +15,126 @@ func (handler *Handler) tagDelete(w http.ResponseWriter, r *http.Request) *httpe
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid tag identifier route variable", err}
}
tagID := portainer.TagID(id)
err = handler.TagService.DeleteTag(portainer.TagID(id))
tag, err := handler.TagService.Tag(tagID)
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a tag with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a tag with the specified identifier inside the database", err}
}
for endpointID := range tag.Endpoints {
endpoint, err := handler.EndpointService.Endpoint(endpointID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint from the database", err}
}
tagIdx := findTagIndex(endpoint.TagIDs, tagID)
if tagIdx != -1 {
endpoint.TagIDs = removeElement(endpoint.TagIDs, tagIdx)
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
}
}
}
for endpointGroupID := range tag.EndpointGroups {
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(endpointGroupID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint group from the database", err}
}
tagIdx := findTagIndex(endpointGroup.TagIDs, tagID)
if tagIdx != -1 {
endpointGroup.TagIDs = removeElement(endpointGroup.TagIDs, tagIdx)
err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint group", err}
}
}
}
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
}
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from the database", err}
}
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stacks from the database", err}
}
for _, endpoint := range endpoints {
if (tag.Endpoints[endpoint.ID] || tag.EndpointGroups[endpoint.GroupID]) && endpoint.Type == portainer.EdgeAgentEnvironment {
err = handler.updateEndpointRelations(endpoint, edgeGroups, edgeStacks)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint relations in the database", err}
}
}
}
for idx := range edgeGroups {
edgeGroup := &edgeGroups[idx]
tagIdx := findTagIndex(edgeGroup.TagIDs, tagID)
if tagIdx != -1 {
edgeGroup.TagIDs = removeElement(edgeGroup.TagIDs, tagIdx)
err = handler.EdgeGroupService.UpdateEdgeGroup(edgeGroup.ID, edgeGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint group", err}
}
}
}
err = handler.TagService.DeleteTag(tagID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the tag from the database", err}
}
return response.Empty(w)
}
func (handler *Handler) updateEndpointRelations(endpoint portainer.Endpoint, edgeGroups []portainer.EdgeGroup, edgeStacks []portainer.EdgeStack) error {
endpointRelation, err := handler.EndpointRelationService.EndpointRelation(endpoint.ID)
if err != nil {
return err
}
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID)
if err != nil {
return err
}
endpointStacks := portainer.EndpointRelatedEdgeStacks(&endpoint, endpointGroup, edgeGroups, edgeStacks)
stacksSet := map[portainer.EdgeStackID]bool{}
for _, edgeStackID := range endpointStacks {
stacksSet[edgeStackID] = true
}
endpointRelation.EdgeStacks = stacksSet
return handler.EndpointRelationService.UpdateEndpointRelation(endpoint.ID, endpointRelation)
}
func findTagIndex(tags []portainer.TagID, searchTagID portainer.TagID) int {
for idx, tagID := range tags {
if searchTagID == tagID {
return idx
}
}
return -1
}
func removeElement(arr []portainer.TagID, index int) []portainer.TagID {
if index < 0 {
return arr
}
lastTagIdx := len(arr) - 1
arr[index] = arr[lastTagIdx]
return arr[:lastTagIdx]
}
+1 -1
View File
@@ -4,11 +4,11 @@ import (
"net/http"
"github.com/asaskevich/govalidator"
"github.com/gofrs/uuid"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
"github.com/satori/go.uuid"
)
type webhookCreatePayload struct {
+1 -1
View File
@@ -14,7 +14,7 @@ import (
func (handler *Handler) proxyEdgeAgentWebsocketRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error {
tunnel := handler.ReverseTunnelService.GetTunnelDetails(params.endpoint.ID)
endpointURL, err := url.Parse(fmt.Sprintf("http://localhost:%d", tunnel.Port))
endpointURL, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port))
if err != nil {
return err
}
+2 -1
View File
@@ -34,7 +34,7 @@ func (factory *ProxyFactory) newDockerLocalProxy(endpoint *portainer.Endpoint) (
func (factory *ProxyFactory) newDockerHTTPProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
if endpoint.Type == portainer.EdgeAgentEnvironment {
tunnel := factory.reverseTunnelService.GetTunnelDetails(endpoint.ID)
endpoint.URL = fmt.Sprintf("http://localhost:%d", tunnel.Port)
endpoint.URL = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
}
endpointURL, err := url.Parse(endpoint.URL)
@@ -68,6 +68,7 @@ func (factory *ProxyFactory) newDockerHTTPProxy(endpoint *portainer.Endpoint) (h
ExtensionService: factory.extensionService,
SignatureService: factory.signatureService,
DockerClientFactory: factory.dockerClientFactory,
AuthDisabled: factory.authDisabled,
}
dockerTransport, err := docker.NewTransport(transportParameters, httpTransport)
@@ -158,7 +158,7 @@ func (transport *Transport) applyAccessControlOnResource(parameters *resourceOpe
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
}
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess || portainer.UserCanAccessResource(executor.operationContext.userID, executor.operationContext.userTeamIDs, resourceControl) {
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess || (resourceControl != nil && portainer.UserCanAccessResource(executor.operationContext.userID, executor.operationContext.userTeamIDs, resourceControl)) {
responseObject = decorateObject(responseObject, resourceControl)
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
}
+3 -3
View File
@@ -6,7 +6,7 @@ import (
"github.com/docker/docker/client"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
)
@@ -74,8 +74,8 @@ func (transport *Transport) configInspectOperation(response *http.Response, exec
// selectorConfigLabels retrieve the labels object associated to the config object.
// Labels are available under the "Spec.Labels" property.
// API schema references:
// https://docs.docker.com/engine/api/v1.40/#operation/ConfigList
// https://docs.docker.com/engine/api/v1.40/#operation/ConfigInspect
// https://docs.docker.com/engine/api/v1.37/#operation/ConfigList
// https://docs.docker.com/engine/api/v1.37/#operation/ConfigInspect
func selectorConfigLabels(responseObject map[string]interface{}) map[string]interface{} {
secretSpec := responseutils.GetJSONObject(responseObject, "Spec")
if secretSpec != nil {
+99 -6
View File
@@ -1,12 +1,17 @@
package docker
import (
"bytes"
"context"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"github.com/docker/docker/client"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
"github.com/portainer/portainer/api/http/security"
)
const (
@@ -19,16 +24,19 @@ func getInheritedResourceControlFromContainerLabels(dockerClient *client.Client,
return nil, err
}
serviceName := container.Config.Labels[resourceLabelForDockerServiceID]
if serviceName != "" {
serviceResourceControl := portainer.GetResourceControlByResourceIDAndType(serviceName, portainer.ServiceResourceControl, resourceControls)
if serviceResourceControl != nil {
return serviceResourceControl, nil
}
}
swarmStackName := container.Config.Labels[resourceLabelForDockerSwarmStackName]
if swarmStackName != "" {
return portainer.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
}
serviceName := container.Config.Labels[resourceLabelForDockerServiceID]
if serviceName != "" {
return portainer.GetResourceControlByResourceIDAndType(serviceName, portainer.ServiceResourceControl, resourceControls), nil
}
composeStackName := container.Config.Labels[resourceLabelForDockerComposeStackName]
if composeStackName != "" {
return portainer.GetResourceControlByResourceIDAndType(composeStackName, portainer.StackResourceControl, resourceControls), nil
@@ -144,3 +152,88 @@ func containerHasBlackListedLabel(containerLabels map[string]interface{}, labelB
return false
}
func (transport *Transport) decorateContainerCreationOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) {
type PartialContainer struct {
HostConfig struct {
Privileged bool `json:"Privileged"`
PidMode string `json:"PidMode"`
Devices []interface{} `json:"Devices"`
CapAdd []string `json:"CapAdd"`
CapDrop []string `json:"CapDrop"`
Binds []string `json:"Binds"`
} `json:"HostConfig"`
}
forbiddenResponse := &http.Response{
StatusCode: http.StatusForbidden,
}
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return nil, err
}
isAdminOrEndpointAdmin, err := transport.isAdminOrEndpointAdmin(request)
if err != nil {
return nil, err
}
if !isAdminOrEndpointAdmin {
settings, err := transport.settingsService.Settings()
if err != nil {
return nil, err
}
if !settings.AllowPrivilegedModeForRegularUsers ||
!settings.AllowHostNamespaceForRegularUsers ||
!settings.AllowDeviceMappingForRegularUsers ||
!settings.AllowContainerCapabilitiesForRegularUsers ||
!settings.AllowBindMountsForRegularUsers {
body, err := ioutil.ReadAll(request.Body)
if err != nil {
return nil, err
}
partialContainer := &PartialContainer{}
err = json.Unmarshal(body, partialContainer)
if err != nil {
return nil, err
}
if !settings.AllowPrivilegedModeForRegularUsers && partialContainer.HostConfig.Privileged {
return forbiddenResponse, errors.New("forbidden to use privileged mode")
}
if !settings.AllowHostNamespaceForRegularUsers && partialContainer.HostConfig.PidMode == "host" {
return forbiddenResponse, errors.New("forbidden to use pid host namespace")
}
if !settings.AllowDeviceMappingForRegularUsers && len(partialContainer.HostConfig.Devices) > 0 {
return nil, errors.New("forbidden to use device mapping")
}
if !settings.AllowContainerCapabilitiesForRegularUsers && (len(partialContainer.HostConfig.CapAdd) > 0 || len(partialContainer.HostConfig.CapDrop) > 0) {
return nil, errors.New("forbidden to use container capabilities")
}
if !settings.AllowBindMountsForRegularUsers && (len(partialContainer.HostConfig.Binds) > 0) {
return forbiddenResponse, errors.New("forbidden to use bind mounts")
}
request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
}
}
response, err := transport.executeDockerRequest(request)
if err != nil {
return response, err
}
if response.StatusCode == http.StatusCreated {
err = transport.decorateGenericResourceCreationResponse(response, resourceIdentifierAttribute, resourceType, tokenData.ID)
}
return response, err
}
+3 -4
View File
@@ -6,9 +6,8 @@ import (
"github.com/docker/docker/client"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
"github.com/portainer/portainer/api"
)
const (
@@ -75,8 +74,8 @@ func (transport *Transport) secretInspectOperation(response *http.Response, exec
// selectorSecretLabels retrieve the labels object associated to the secret object.
// Labels are available under the "Spec.Labels" property.
// API schema references:
// https://docs.docker.com/engine/api/v1.40/#operation/SecretList
// https://docs.docker.com/engine/api/v1.40/#operation/SecretInspect
// https://docs.docker.com/engine/api/v1.37/#operation/SecretList
// https://docs.docker.com/engine/api/v1.37/#operation/SecretInspect
func selectorSecretLabels(responseObject map[string]interface{}) map[string]interface{} {
secretSpec := responseutils.GetJSONObject(responseObject, "Spec")
if secretSpec != nil {
+55
View File
@@ -1,7 +1,11 @@
package docker
import (
"bytes"
"context"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"github.com/docker/docker/api/types"
@@ -84,3 +88,54 @@ func selectorServiceLabels(responseObject map[string]interface{}) map[string]int
}
return nil
}
func (transport *Transport) decorateServiceCreationOperation(request *http.Request) (*http.Response, error) {
type PartialService struct {
TaskTemplate struct {
ContainerSpec struct {
Mounts []struct {
Type string
}
}
}
}
forbiddenResponse := &http.Response{
StatusCode: http.StatusForbidden,
}
isAdminOrEndpointAdmin, err := transport.isAdminOrEndpointAdmin(request)
if err != nil {
return nil, err
}
if !isAdminOrEndpointAdmin {
settings, err := transport.settingsService.Settings()
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(request.Body)
if err != nil {
return nil, err
}
partialService := &PartialService{}
err = json.Unmarshal(body, partialService)
if err != nil {
return nil, err
}
if !settings.AllowBindMountsForRegularUsers && (len(partialService.TaskTemplate.ContainerSpec.Mounts) > 0) {
for _, mount := range partialService.TaskTemplate.ContainerSpec.Mounts {
if mount.Type == "bind" {
return forbiddenResponse, errors.New("forbidden to use bind mounts")
}
}
}
request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
}
return transport.replaceRegistryAuthenticationHeader(request)
}
+58 -14
View File
@@ -13,7 +13,7 @@ import (
"github.com/portainer/portainer/api/docker"
"github.com/docker/docker/client"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
"github.com/portainer/portainer/api/http/security"
)
@@ -38,6 +38,7 @@ type (
extensionService portainer.ExtensionService
dockerClient *client.Client
dockerClientFactory *docker.ClientFactory
authDisabled bool
}
// TransportParameters is used to create a new Transport
@@ -54,6 +55,7 @@ type (
ReverseTunnelService portainer.ReverseTunnelService
ExtensionService portainer.ExtensionService
DockerClientFactory *docker.ClientFactory
AuthDisabled bool
}
restrictedDockerOperationContext struct {
@@ -94,6 +96,7 @@ func NewTransport(parameters *TransportParameters, httpTransport *http.Transport
dockerClientFactory: parameters.DockerClientFactory,
HTTPTransport: httpTransport,
dockerClient: dockerClient,
authDisabled: parameters.AuthDisabled,
}
return transport, nil
@@ -171,11 +174,13 @@ func (transport *Transport) proxyAgentRequest(r *http.Request) (*http.Response,
switch {
case strings.HasPrefix(requestPath, "/browse"):
// host file browser request
volumeIDParameter, found := r.URL.Query()["volumeID"]
if !found || len(volumeIDParameter) < 1 {
return transport.administratorOperation(r)
}
// volume browser request
return transport.restrictedResourceOperation(r, volumeIDParameter[0], portainer.VolumeResourceControl, true)
}
@@ -207,7 +212,7 @@ func (transport *Transport) proxyConfigRequest(request *http.Request) (*http.Res
func (transport *Transport) proxyContainerRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
case "/containers/create":
return transport.decorateGenericResourceCreationOperation(request, containerObjectIdentifier, portainer.ContainerResourceControl)
return transport.decorateContainerCreationOperation(request, containerObjectIdentifier, portainer.ContainerResourceControl)
case "/containers/prune":
return transport.administratorOperation(request)
@@ -243,7 +248,7 @@ func (transport *Transport) proxyContainerRequest(request *http.Request) (*http.
func (transport *Transport) proxyServiceRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
case "/services/create":
return transport.replaceRegistryAuthenticationHeader(request)
return transport.decorateServiceCreationOperation(request)
case "/services":
return transport.rewriteOperation(request, transport.serviceListOperation)
@@ -273,7 +278,7 @@ func (transport *Transport) proxyServiceRequest(request *http.Request) (*http.Re
func (transport *Transport) proxyVolumeRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
case "/volumes/create":
return transport.decorateGenericResourceCreationOperation(request, volumeObjectIdentifier, portainer.VolumeResourceControl)
return transport.decorateVolumeResourceCreationOperation(request, volumeObjectIdentifier, portainer.VolumeResourceControl)
case "/volumes/prune":
return transport.administratorOperation(request)
@@ -443,10 +448,16 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r
return nil, err
}
// Return access denied for all roles except endpoint-administrator
_, userCanBrowse := user.EndpointAuthorizations[transport.endpoint.ID][portainer.OperationDockerAgentBrowseList]
if rbacExtension != nil && !settings.AllowVolumeBrowserForRegularUsers && !userCanBrowse {
return responseutils.WriteAccessDeniedResponse()
if !settings.AllowVolumeBrowserForRegularUsers {
if rbacExtension == nil {
return responseutils.WriteAccessDeniedResponse()
}
// Return access denied for all roles except endpoint-administrator
_, userCanBrowse := user.EndpointAuthorizations[transport.endpoint.ID][portainer.OperationDockerAgentBrowseList]
if !userCanBrowse {
return responseutils.WriteAccessDeniedResponse()
}
}
}
@@ -549,12 +560,12 @@ func (transport *Transport) interceptAndRewriteRequest(request *http.Request, op
// on the resourceIdentifierAttribute parameter then generate a new resource control associated to that resource
// with a random token and rewrites the response by decorating the original response with a ResourceControl object.
// The generic Docker API response format is JSON object:
// https://docs.docker.com/engine/api/v1.40/#operation/ContainerCreate
// https://docs.docker.com/engine/api/v1.40/#operation/NetworkCreate
// https://docs.docker.com/engine/api/v1.40/#operation/VolumeCreate
// https://docs.docker.com/engine/api/v1.40/#operation/ServiceCreate
// https://docs.docker.com/engine/api/v1.40/#operation/SecretCreate
// https://docs.docker.com/engine/api/v1.40/#operation/ConfigCreate
// https://docs.docker.com/engine/api/v1.37/#operation/ContainerCreate
// https://docs.docker.com/engine/api/v1.37/#operation/NetworkCreate
// https://docs.docker.com/engine/api/v1.37/#operation/VolumeCreate
// https://docs.docker.com/engine/api/v1.37/#operation/ServiceCreate
// https://docs.docker.com/engine/api/v1.37/#operation/SecretCreate
// https://docs.docker.com/engine/api/v1.37/#operation/ConfigCreate
func (transport *Transport) decorateGenericResourceCreationResponse(response *http.Response, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType, userID portainer.UserID) error {
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
if err != nil {
@@ -725,3 +736,36 @@ func (transport *Transport) createOperationContext(request *http.Request) (*rest
return operationContext, nil
}
func (transport *Transport) isAdminOrEndpointAdmin(request *http.Request) (bool, error) {
if transport.authDisabled {
return true, nil
}
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return false, err
}
if tokenData.Role == portainer.AdministratorRole {
return true, nil
}
user, err := transport.userService.User(tokenData.ID)
if err != nil {
return false, err
}
rbacExtension, err := transport.extensionService.Extension(portainer.RBACExtension)
if err != nil && err != portainer.ErrObjectNotFound {
return false, err
}
if rbacExtension == nil {
return false, nil
}
_, endpointResourceAccess := user.EndpointAuthorizations[portainer.EndpointID(transport.endpoint.ID)][portainer.EndpointResourcesAccess]
return endpointResourceAccess, nil
}
+40 -1
View File
@@ -2,12 +2,14 @@ package docker
import (
"context"
"errors"
"net/http"
"github.com/docker/docker/client"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
"github.com/portainer/portainer/api/http/security"
)
const (
@@ -87,3 +89,40 @@ func (transport *Transport) volumeInspectOperation(response *http.Response, exec
func selectorVolumeLabels(responseObject map[string]interface{}) map[string]interface{} {
return responseutils.GetJSONObject(responseObject, "Labels")
}
func (transport *Transport) decorateVolumeResourceCreationOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) {
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return nil, err
}
volumeID := request.Header.Get("X-Portainer-VolumeName")
if volumeID != "" {
cli := transport.dockerClient
agentTargetHeader := request.Header.Get(portainer.PortainerAgentTargetHeader)
if agentTargetHeader != "" {
dockerClient, err := transport.dockerClientFactory.CreateClient(transport.endpoint, agentTargetHeader)
if err != nil {
return nil, err
}
defer dockerClient.Close()
cli = dockerClient
}
_, err = cli.VolumeInspect(context.Background(), volumeID)
if err == nil {
return nil, errors.New("a volume with the same name already exists")
}
}
response, err := transport.executeDockerRequest(request)
if err != nil {
return response, err
}
if response.StatusCode == http.StatusCreated {
err = transport.decorateGenericResourceCreationResponse(response, resourceIdentifierAttribute, resourceType, tokenData.ID)
}
return response, err
}
+1
View File
@@ -24,6 +24,7 @@ func (factory ProxyFactory) newOSBasedLocalProxy(path string, endpoint *portaine
ExtensionService: factory.extensionService,
SignatureService: factory.signatureService,
DockerClientFactory: factory.dockerClientFactory,
AuthDisabled: factory.authDisabled,
}
proxy := &dockerLocalProxy{}
+1
View File
@@ -25,6 +25,7 @@ func (factory ProxyFactory) newOSBasedLocalProxy(path string, endpoint *portaine
ExtensionService: factory.extensionService,
SignatureService: factory.signatureService,
DockerClientFactory: factory.dockerClientFactory,
AuthDisabled: factory.authDisabled,
}
proxy := &dockerLocalProxy{}
+4 -1
View File
@@ -6,7 +6,7 @@ import (
"net/http/httputil"
"net/url"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/docker"
)
@@ -32,6 +32,7 @@ type (
reverseTunnelService portainer.ReverseTunnelService
extensionService portainer.ExtensionService
dockerClientFactory *docker.ClientFactory
authDisabled bool
}
// ProxyFactoryParameters is used to create a new ProxyFactory
@@ -47,6 +48,7 @@ type (
ReverseTunnelService portainer.ReverseTunnelService
ExtensionService portainer.ExtensionService
DockerClientFactory *docker.ClientFactory
AuthDisabled bool
}
)
@@ -64,6 +66,7 @@ func NewProxyFactory(parameters *ProxyFactoryParameters) *ProxyFactory {
reverseTunnelService: parameters.ReverseTunnelService,
extensionService: parameters.ExtensionService,
dockerClientFactory: parameters.DockerClientFactory,
authDisabled: parameters.AuthDisabled,
}
}
+4 -2
View File
@@ -4,8 +4,8 @@ import (
"net/http"
"strconv"
"github.com/orcaman/concurrent-map"
"github.com/portainer/portainer/api"
cmap "github.com/orcaman/concurrent-map"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/http/proxy/factory"
)
@@ -34,6 +34,7 @@ type (
ReverseTunnelService portainer.ReverseTunnelService
ExtensionService portainer.ExtensionService
DockerClientFactory *docker.ClientFactory
AuthDisabled bool
}
)
@@ -51,6 +52,7 @@ func NewManager(parameters *ManagerParams) *Manager {
ReverseTunnelService: parameters.ReverseTunnelService,
ExtensionService: parameters.ExtensionService,
DockerClientFactory: parameters.DockerClientFactory,
AuthDisabled: parameters.AuthDisabled,
}
return &Manager{
+20
View File
@@ -1,6 +1,8 @@
package security
import (
"errors"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api"
@@ -143,6 +145,24 @@ func (bouncer *RequestBouncer) AuthorizedEndpointOperation(r *http.Request, endp
return nil
}
// AuthorizedEdgeEndpointOperation verifies that the request was received from a valid Edge endpoint
func (bouncer *RequestBouncer) AuthorizedEdgeEndpointOperation(r *http.Request, endpoint *portainer.Endpoint) error {
if endpoint.Type != portainer.EdgeAgentEnvironment {
return errors.New("Invalid endpoint type")
}
edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader)
if edgeIdentifier == "" {
return errors.New("missing Edge identifier")
}
if endpoint.EdgeID != "" && endpoint.EdgeID != edgeIdentifier {
return errors.New("invalid Edge identifier")
}
return nil
}
func (bouncer *RequestBouncer) checkEndpointOperationAuthorization(r *http.Request, endpoint *portainer.Endpoint) error {
tokenData, err := RetrieveTokenData(r)
if err != nil {
+106 -49
View File
@@ -3,11 +3,16 @@ package http
import (
"time"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/handler/edgegroups"
"github.com/portainer/portainer/api/http/handler/edgestacks"
"github.com/portainer/portainer/api/http/handler/edgetemplates"
"github.com/portainer/portainer/api/http/handler/endpointedge"
"github.com/portainer/portainer/api/http/handler/support"
"github.com/portainer/portainer/api/http/handler/roles"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/http/handler"
"github.com/portainer/portainer/api/http/handler/auth"
@@ -41,45 +46,48 @@ import (
// Server implements the portainer.Server interface
type Server struct {
BindAddress string
AssetsPath string
AuthDisabled bool
EndpointManagement bool
Status *portainer.Status
ReverseTunnelService portainer.ReverseTunnelService
ExtensionManager portainer.ExtensionManager
ComposeStackManager portainer.ComposeStackManager
CryptoService portainer.CryptoService
SignatureService portainer.DigitalSignatureService
JobScheduler portainer.JobScheduler
Snapshotter portainer.Snapshotter
RoleService portainer.RoleService
DockerHubService portainer.DockerHubService
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
FileService portainer.FileService
GitService portainer.GitService
JWTService portainer.JWTService
LDAPService portainer.LDAPService
ExtensionService portainer.ExtensionService
RegistryService portainer.RegistryService
ResourceControlService portainer.ResourceControlService
ScheduleService portainer.ScheduleService
SettingsService portainer.SettingsService
StackService portainer.StackService
SwarmStackManager portainer.SwarmStackManager
TagService portainer.TagService
TeamService portainer.TeamService
TeamMembershipService portainer.TeamMembershipService
TemplateService portainer.TemplateService
UserService portainer.UserService
WebhookService portainer.WebhookService
Handler *handler.Handler
SSL bool
SSLCert string
SSLKey string
DockerClientFactory *docker.ClientFactory
JobService portainer.JobService
BindAddress string
AssetsPath string
AuthDisabled bool
EndpointManagement bool
Status *portainer.Status
ReverseTunnelService portainer.ReverseTunnelService
ExtensionManager portainer.ExtensionManager
ComposeStackManager portainer.ComposeStackManager
CryptoService portainer.CryptoService
SignatureService portainer.DigitalSignatureService
JobScheduler portainer.JobScheduler
Snapshotter portainer.Snapshotter
RoleService portainer.RoleService
DockerHubService portainer.DockerHubService
EdgeGroupService portainer.EdgeGroupService
EdgeStackService portainer.EdgeStackService
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
EndpointRelationService portainer.EndpointRelationService
FileService portainer.FileService
GitService portainer.GitService
JWTService portainer.JWTService
LDAPService portainer.LDAPService
ExtensionService portainer.ExtensionService
RegistryService portainer.RegistryService
ResourceControlService portainer.ResourceControlService
ScheduleService portainer.ScheduleService
SettingsService portainer.SettingsService
StackService portainer.StackService
SwarmStackManager portainer.SwarmStackManager
TagService portainer.TagService
TeamService portainer.TeamService
TeamMembershipService portainer.TeamMembershipService
TemplateService portainer.TemplateService
UserService portainer.UserService
WebhookService portainer.WebhookService
Handler *handler.Handler
SSL bool
SSLCert string
SSLKey string
DockerClientFactory *docker.ClientFactory
JobService portainer.JobService
}
// Start starts the HTTP server
@@ -96,6 +104,7 @@ func (server *Server) Start() error {
ReverseTunnelService: server.ReverseTunnelService,
ExtensionService: server.ExtensionService,
DockerClientFactory: server.DockerClientFactory,
AuthDisabled: server.AuthDisabled,
}
proxyManager := proxy.NewManager(proxyManagerParameters)
@@ -144,21 +153,54 @@ func (server *Server) Start() error {
var dockerHubHandler = dockerhub.NewHandler(requestBouncer)
dockerHubHandler.DockerHubService = server.DockerHubService
var edgeGroupsHandler = edgegroups.NewHandler(requestBouncer)
edgeGroupsHandler.EdgeGroupService = server.EdgeGroupService
edgeGroupsHandler.EdgeStackService = server.EdgeStackService
edgeGroupsHandler.EndpointService = server.EndpointService
edgeGroupsHandler.EndpointGroupService = server.EndpointGroupService
edgeGroupsHandler.EndpointRelationService = server.EndpointRelationService
edgeGroupsHandler.TagService = server.TagService
var edgeStacksHandler = edgestacks.NewHandler(requestBouncer)
edgeStacksHandler.EdgeGroupService = server.EdgeGroupService
edgeStacksHandler.EdgeStackService = server.EdgeStackService
edgeStacksHandler.EndpointService = server.EndpointService
edgeStacksHandler.EndpointGroupService = server.EndpointGroupService
edgeStacksHandler.EndpointRelationService = server.EndpointRelationService
edgeStacksHandler.FileService = server.FileService
edgeStacksHandler.GitService = server.GitService
var edgeTemplatesHandler = edgetemplates.NewHandler(requestBouncer)
edgeTemplatesHandler.SettingsService = server.SettingsService
var endpointHandler = endpoints.NewHandler(requestBouncer, server.EndpointManagement)
endpointHandler.AuthorizationService = authorizationService
endpointHandler.EdgeGroupService = server.EdgeGroupService
endpointHandler.EdgeStackService = server.EdgeStackService
endpointHandler.EndpointService = server.EndpointService
endpointHandler.EndpointGroupService = server.EndpointGroupService
endpointHandler.EndpointRelationService = server.EndpointRelationService
endpointHandler.FileService = server.FileService
endpointHandler.ProxyManager = proxyManager
endpointHandler.Snapshotter = server.Snapshotter
endpointHandler.JobService = server.JobService
endpointHandler.ProxyManager = proxyManager
endpointHandler.ReverseTunnelService = server.ReverseTunnelService
endpointHandler.SettingsService = server.SettingsService
endpointHandler.AuthorizationService = authorizationService
endpointHandler.Snapshotter = server.Snapshotter
endpointHandler.TagService = server.TagService
var endpointEdgeHandler = endpointedge.NewHandler(requestBouncer)
endpointEdgeHandler.EdgeStackService = server.EdgeStackService
endpointEdgeHandler.EndpointService = server.EndpointService
endpointEdgeHandler.FileService = server.FileService
var endpointGroupHandler = endpointgroups.NewHandler(requestBouncer)
endpointGroupHandler.EndpointGroupService = server.EndpointGroupService
endpointGroupHandler.EndpointService = server.EndpointService
endpointGroupHandler.AuthorizationService = authorizationService
endpointGroupHandler.EdgeGroupService = server.EdgeGroupService
endpointGroupHandler.EdgeStackService = server.EdgeStackService
endpointGroupHandler.EndpointService = server.EndpointService
endpointGroupHandler.EndpointGroupService = server.EndpointGroupService
endpointGroupHandler.EndpointRelationService = server.EndpointRelationService
endpointGroupHandler.TagService = server.TagService
var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer)
endpointProxyHandler.EndpointService = server.EndpointService
@@ -206,7 +248,7 @@ func (server *Server) Start() error {
settingsHandler.ExtensionService = server.ExtensionService
settingsHandler.AuthorizationService = authorizationService
var stackHandler = stacks.NewHandler(requestBouncer)
var stackHandler = stacks.NewHandler(requestBouncer, server.AuthDisabled)
stackHandler.FileService = server.FileService
stackHandler.StackService = server.StackService
stackHandler.EndpointService = server.EndpointService
@@ -221,6 +263,11 @@ func (server *Server) Start() error {
stackHandler.ExtensionService = server.ExtensionService
var tagHandler = tags.NewHandler(requestBouncer)
tagHandler.EdgeGroupService = server.EdgeGroupService
tagHandler.EdgeStackService = server.EdgeStackService
tagHandler.EndpointService = server.EndpointService
tagHandler.EndpointGroupService = server.EndpointGroupService
tagHandler.EndpointRelationService = server.EndpointRelationService
tagHandler.TagService = server.TagService
var teamHandler = teams.NewHandler(requestBouncer)
@@ -266,8 +313,12 @@ func (server *Server) Start() error {
RoleHandler: roleHandler,
AuthHandler: authHandler,
DockerHubHandler: dockerHubHandler,
EdgeGroupsHandler: edgeGroupsHandler,
EdgeStacksHandler: edgeStacksHandler,
EdgeTemplatesHandler: edgeTemplatesHandler,
EndpointGroupHandler: endpointGroupHandler,
EndpointHandler: endpointHandler,
EndpointEdgeHandler: endpointEdgeHandler,
EndpointProxyHandler: endpointProxyHandler,
FileHandler: fileHandler,
MOTDHandler: motdHandler,
@@ -289,8 +340,14 @@ func (server *Server) Start() error {
SchedulesHanlder: schedulesHandler,
}
if server.SSL {
return http.ListenAndServeTLS(server.BindAddress, server.SSLCert, server.SSLKey, server.Handler)
httpServer := &http.Server{
Addr: server.BindAddress,
Handler: server.Handler,
}
return http.ListenAndServe(server.BindAddress, server.Handler)
if server.SSL {
httpServer.TLSConfig = crypto.CreateServerTLSConfiguration()
return httpServer.ListenAndServeTLS(server.SSLCert, server.SSLKey)
}
return httpServer.ListenAndServe()
}
+11 -7
View File
@@ -4,7 +4,7 @@ import (
"fmt"
"strings"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
"gopkg.in/ldap.v2"
@@ -92,9 +92,11 @@ func (*Service) AuthenticateUser(username, password string, settings *portainer.
}
defer connection.Close()
err = connection.Bind(settings.ReaderDN, settings.Password)
if err != nil {
return err
if !settings.AnonymousMode {
err = connection.Bind(settings.ReaderDN, settings.Password)
if err != nil {
return err
}
}
userDN, err := searchUser(username, connection, settings.SearchSettings)
@@ -118,9 +120,11 @@ func (*Service) GetUserGroups(username string, settings *portainer.LDAPSettings)
}
defer connection.Close()
err = connection.Bind(settings.ReaderDN, settings.Password)
if err != nil {
return nil, err
if !settings.AnonymousMode {
err = connection.Bind(settings.ReaderDN, settings.Password)
if err != nil {
return nil, err
}
}
userDN, err := searchUser(username, connection, settings.SearchSettings)
+1 -1
View File
@@ -39,7 +39,7 @@ func (manager *ComposeStackManager) createClient(endpoint *portainer.Endpoint) (
endpointURL := endpoint.URL
if endpoint.Type == portainer.EdgeAgentEnvironment {
tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID)
endpointURL = fmt.Sprintf("tcp://localhost:%d", tunnel.Port)
endpointURL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnel.Port)
}
clientOpts := client.Options{
+842 -731
View File
File diff suppressed because it is too large Load Diff
+6 -2
View File
@@ -54,7 +54,7 @@ info:
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
version: "1.23.0"
version: "1.24.1"
title: "Portainer API"
contact:
email: "info@portainer.io"
@@ -3174,7 +3174,7 @@ definitions:
description: "Is analytics enabled"
Version:
type: "string"
example: "1.23.0"
example: "1.24.1"
description: "Portainer API version"
PublicSettingsInspectResponse:
type: "object"
@@ -3296,6 +3296,10 @@ definitions:
LDAPSettings:
type: "object"
properties:
AnonymousMode:
type: "boolean"
example: true
description: "Enable this option if the server is configured for Anonymous access. When enabled, ReaderDN and Password will not be used."
ReaderDN:
type: "string"
example: "cn=readonly-account,dc=ldap,dc=domain,dc=tld"
+1 -1
View File
@@ -1,5 +1,5 @@
{
"packageName": "portainer",
"packageVersion": "1.23.0",
"packageVersion": "1.24.1",
"projectName": "portainer"
}
+71
View File
@@ -0,0 +1,71 @@
package portainer
type tagSet map[TagID]bool
// TagSet converts an array of ids to a set
func TagSet(tagIDs []TagID) tagSet {
set := map[TagID]bool{}
for _, tagID := range tagIDs {
set[tagID] = true
}
return set
}
// TagIntersection returns a set intersection of the provided sets
func TagIntersection(sets ...tagSet) tagSet {
intersection := tagSet{}
if len(sets) == 0 {
return intersection
}
setA := sets[0]
for tag := range setA {
inAll := true
for _, setB := range sets {
if !setB[tag] {
inAll = false
break
}
}
if inAll {
intersection[tag] = true
}
}
return intersection
}
// TagUnion returns a set union of provided sets
func TagUnion(sets ...tagSet) tagSet {
union := tagSet{}
for _, set := range sets {
for tag := range set {
union[tag] = true
}
}
return union
}
// TagContains return true if setA contains setB
func TagContains(setA tagSet, setB tagSet) bool {
containedTags := 0
for tag := range setB {
if setA[tag] {
containedTags++
}
}
return containedTags == len(setA)
}
// TagDifference returns the set difference tagsA - tagsB
func TagDifference(setA tagSet, setB tagSet) tagSet {
set := tagSet{}
for tag := range setA {
if !setB[tag] {
set[tag] = true
}
}
return set
}
+4 -2
View File
@@ -4,6 +4,7 @@ import angular from 'angular';
import './agent/_module';
import './azure/_module';
import './docker/__module';
import './edge/__module';
import './portainer/__module';
angular.module('portainer', [
@@ -29,15 +30,16 @@ angular.module('portainer', [
'portainer.agent',
'portainer.azure',
'portainer.docker',
'portainer.edge',
'portainer.extensions',
'portainer.integrations',
'rzModule',
'moment-picker'
'moment-picker',
]);
if (require) {
var req = require.context('./', true, /^(.*\.(js$))[^.]*$/im);
req.keys().forEach(function(key) {
req.keys().forEach(function (key) {
req(key);
});
}

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