Compare commits

..

132 Commits

Author SHA1 Message Date
yi-portainer
114073ae59 * update github banner image 2021-08-04 15:00:34 +12:00
Yi Chen
f1e2bb14a9 * update readme as needed (#5387) 2021-08-04 14:50:50 +12:00
dbuduev
ed2c65c1e6 feat(logger): Init logrus [DTD-55] (#5232) 2021-08-04 11:26:22 +12:00
cong meng
51ef2c2aa9 fix(advance deploy): EE-1141 A standard user can escalate to cluster administrator privileges on Kubernetes (#5325)
* fix(advance deploy): EE-1141 A standard user can escalate to cluster administrator privileges on Kubernetes

* fix(advance deploy): EE-1141 reuse existing token cache when do deployment

* fix: EE-1141 use user's SA token to exec pod command

* fix: EE-1141 stop advanced-deploy or pod-exec if user's SA token is empty

* fix: EE-1141 resolve merge conflicts

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-08-04 11:11:24 +12:00
cong meng
5652bac004 feat: EE-424 Provide a way to re-associate an Edge endpoint to a new Edge agent (#5266)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-08-02 18:08:40 +12:00
zees-dev
ce31de5e9e feat(kubernetes/resource-usage): k8s resource usage for cluster, node and namespace EE-3 EE-1112 (#5301)
* backported resource usage functionality from EE

* utilising view bound endpoint object instead of depracated EndpointProvider

* refactor flatmap

* addressed merge conflict issues
2021-07-28 14:26:03 +12:00
Matt Hook
cee7ac26e9 Fix dockerhub pro account rate-limit issue (#5352) 2021-07-27 10:49:28 +12:00
Chaim Lev-Ari
c943ac498f feat(stacks): allow standalone to edit env vars (#5255)
Co-authored-by: Tobias Holler <mail@toubs.de>
2021-07-26 13:48:30 +03:00
Richard Wei
49f25e9c4c fix(docker):Fix image pulled errorDetails not showing EE-936 (#5336)
* fix image pulled errorDetails not showing

* code clean up for errorDetail detection
2021-07-24 08:51:34 +12:00
dbuduev
7d6b1edd48 feat(k8s): Introduce the ability to restrict access to default namespace (EE-745) (#5337) 2021-07-23 17:10:46 +12:00
Richard Wei
c26af1449c fix(app): Fix ports displayed twice when using docker EE-706 (#5239)
* fix duplicate port showing using docker

* fix changes from review by using lodash for filter

* move container filter in filter folder

* change filter name to unique for reuse
2021-07-23 11:29:01 +12:00
Richard Wei
09c5bada3e fix(app): fix create stack with capital letters or space issue EE-908 (#5236)
* fix(app): fix create stack with capital letters or space issue

* replace ComposeWrapper with ComposeStackManager
2021-07-23 09:53:42 +12:00
Chaim Lev-Ari
fe07815fc7 fix(images): ensure latest image (#5274) 2021-07-22 12:19:48 +03:00
Richard Wei
c56c236e3a fix(stack): show warning if endpoint is selected (#5234)
* fix/EE-916/Invalid warning in stack details

* fix typo for isEndpointSelected function

* check yarmlError is valid

* combine yamlError and isEndpointSelected into one linie
2021-07-22 16:21:25 +12:00
Hui
68453482af fix(swagger): add swagger annotation for pull and redeploy stack 2021-07-22 11:40:53 +12:00
Chaim Lev-Ari
7b2269fbba feat(endpoints): filter endpoints by a list of types (#5308)
* feat(endpoints): filter endpoints by a list of types

* docs(endpoints): update api docs for endpoint list
2021-07-21 10:16:22 +03:00
Chaim Lev-Ari
bd47bb8cdc chore(lint): add lint command (#5106) 2021-07-21 17:45:35 +12:00
Chaim Lev-Ari
f9ffb1a712 refactor(stacks): use docker-compose-wrapper library (#4979) 2021-07-21 13:56:28 +12:00
Chaim Lev-Ari
592f7024e1 fix(stacks): prevent stack creation when container_name already exists (#5211) 2021-07-21 13:55:06 +12:00
Richard Wei
00fc629c1c fix charts x label padding (#5327) 2021-07-21 13:54:22 +12:00
Chaim Lev-Ari
6a9b386df8 fix(kube/nodes): show node events (#5246) 2021-07-20 16:49:33 +03:00
Dmitry Salakhov
8aa3bfc59c fix(namespace): update portainer-config when delete a namespace (#5330) 2021-07-20 14:05:31 +12:00
fhanportainer
308f828446 fix(k8s): fixed generating kube auction summary issue (#5331) 2021-07-19 19:45:20 +12:00
Chaim Lev-Ari
89756b2e01 fix(kube/app): show resource allocation (#5317) 2021-07-19 10:44:48 +03:00
Chaim Lev-Ari
db16299aab feat(docker/volumes): change how volume resource id is calculated (#5067)
[EE-494]
2021-07-19 10:43:49 +03:00
Chaim Lev-Ari
72117693fb feat(stacks): update stopped stack (#5215)
[EE-554]
2021-07-19 10:41:50 +03:00
LP B
179df06267 feat(app): rework private registries and support private registries in kubernetes EE-30 (#5131)
* feat(app): rework private registries and support private registries in kubernetes

[EE-30]

feat(api): backport private registries backend changes (#5072)

* feat(api/bolt): backport bolt changes

* feat(api/exec): backport exec changes

* feat(api/http): backport http/handler/dockerhub changes

* feat(api/http): backport http/handler/endpoints changes

* feat(api/http): backport http/handler/registries changes

* feat(api/http): backport http/handler/stacks changes

* feat(api/http): backport http/handler changes

* feat(api/http): backport http/proxy/factory/azure changes

* feat(api/http): backport http/proxy/factory/docker changes

* feat(api/http): backport http/proxy/factory/utils changes

* feat(api/http): backport http/proxy/factory/kubernetes changes

* feat(api/http): backport http/proxy/factory changes

* feat(api/http): backport http/security changes

* feat(api/http): backport http changes

* feat(api/internal): backport internal changes

* feat(api): backport api changes

* feat(api/kubernetes): backport kubernetes changes

* fix(api/http): changes on backend following backport

feat(app): backport private registries frontend changes (#5056)

* feat(app/docker): backport docker/components changes

* feat(app/docker): backport docker/helpers changes

* feat(app/docker): backport docker/views/container changes

* feat(app/docker): backport docker/views/images changes

* feat(app/docker): backport docker/views/registries changes

* feat(app/docker): backport docker/views/services changes

* feat(app/docker): backport docker changes

* feat(app/kubernetes): backport kubernetes/components changes

* feat(app/kubernetes): backport kubernetes/converters changes

* feat(app/kubernetes): backport kubernetes/models changes

* feat(app/kubernetes): backport kubernetes/registries changes

* feat(app/kubernetes): backport kubernetes/services changes

* feat(app/kubernetes): backport kubernetes/views/applications changes

* feat(app/kubernetes): backport kubernetes/views/configurations changes

* feat(app/kubernetes): backport kubernetes/views/configure changes

* feat(app/kubernetes): backport kubernetes/views/resource-pools changes

* feat(app/kubernetes): backport kubernetes/views changes

* feat(app/portainer): backport portainer/components/accessManagement changes

* feat(app/portainer): backport portainer/components/datatables changes

* feat(app/portainer): backport portainer/components/forms changes

* feat(app/portainer): backport portainer/components/registry-details changes

* feat(app/portainer): backport portainer/models changes

* feat(app/portainer): backport portainer/rest changes

* feat(app/portainer): backport portainer/services changes

* feat(app/portainer): backport portainer/views changes

* feat(app/portainer): backport portainer changes

* feat(app): backport app changes

* config(project): gitignore + jsconfig changes

gitignore all files under api/cmd/portainer but main.go and enable Code Editor autocomplete on import ... from '@/...'

fix(app): fix pull rate limit checker

fix(app/registries): sidebar menus and registry accesses users filtering

fix(api): add missing kube client factory

fix(kube): fetch dockerhub pull limits (#5133)

fix(app): pre review fixes (#5142)

* fix(app/registries): remove checkbox for endpointRegistries view

* fix(endpoints): allow access to default namespace

* fix(docker): fetch pull limits

* fix(kube/ns): show selected registries for non admin

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

chore(webpack): ignore missing sourcemaps

fix(registries): fetch registry config from url

feat(kube/registries): ignore not found when deleting secret

feat(db): move migration to db 31

fix(registries): fix bugs in PR EE-869 (#5169)

* fix(registries): hide role

* fix(endpoints): set empty access policy to edge endpoint

* fix(registry): remove double arguments

* fix(admin): ignore warning

* feat(kube/configurations): tag registry secrets (#5157)

* feat(kube/configurations): tag registry secrets

* feat(kube/secrets): show registry secrets for admins

* fix(registries): move dockerhub to beginning

* refactor(registries): use endpoint scoped registries

feat(registries): filter by namespace if supplied

feat(access-managment): filter users for registry (#5191)

* refactor(access-manage): move users selector to component

* feat(access-managment): filter users for registry

refactor(registries): sync code with CE (#5200)

* refactor(registry): add inspect handler under endpoints

* refactor(endpoint): sync endpoint_registries_list

* refactor(endpoints): sync registry_access

* fix(db): rename migration functions

* fix(registries): show accesses for admin

* fix(kube): set token on transport

* refactor(kube): move secret help to bottom

* fix(kuberentes): remove shouldLog parameter

* style(auth): add description of security.IsAdmin

* feat(security): allow admin access to registry

* feat(edge): connect to edge endpoint when creating client

* style(portainer): change deprecation version

* refactor(sidebar): hide manage

* refactor(containers): revert changes

* style(container): remove whitespace

* fix(endpoint): add handler to registy on endpointService

* refactor(image): use endpointService.registries

* fix(kueb/namespaces): rename resource pool to namespace

* fix(kube/namespace): move selected registries

* fix(api/registries): hide accesses on registry creation

Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>

refactor(api): remove code duplication after rebase

fix(app/registries): replace last registry api usage by endpoint registry api

fix(api/endpoints): update registry access policies on endpoint deletion (#5226)

[EE-1027]

fix(db): update db version

* fix(dockerhub): fetch rate limits

* fix(registry/tests): supply restricred context

* fix(registries): show proget registry only when selected

* fix(registry): create dockerhub registry

* feat(db): move migrations to db 32

Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>
2021-07-14 21:15:21 +12:00
Dmitry Salakhov
0f5407da40 feat(tech): bump golang to v1.16 EE-515 (#4993)
* bump golang to v1.16

* Update build/linux/toolkit.Dockerfile

Co-authored-by: dbuduev <dbuduev@gmail.com>
2021-07-14 13:10:42 +12:00
Chaim Lev-Ari
2fd95d87eb fix(volumes): fetch resource by docker name (#5216) 2021-07-13 18:09:58 +12:00
cong meng
33b428eb7f EE-1110 Ingress routes and their mapping to a application name are not deleted when the application is deleted (#5291)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-07-09 10:41:04 +12:00
Chaim Lev-Ari
c6b770d697 feat(edgestack): remove deploy message (#5279)
[EE-392]
2021-07-08 11:39:52 +12:00
fhanportainer
d48f6bd02c fix(ingress): fixed hostname field when having multiple ingresses EE-1072 (#5273) 2021-07-05 18:17:20 +12:00
Stéphane Busso
340805f880 fix download logs (#5243) 2021-07-05 11:10:10 +12:00
zees-dev
f6c5c552aa feat(oauth/team-memberships): oauth team memberships teaser EE-341 (#5088)
* EE oauth team memberships feature teaser

* bugfix: deleting a default team should reset default team id to 0

* error wrapping, refactor team deletion code
2021-07-02 18:20:10 +12:00
dbuduev
90a472c08b feat(registry): Add ProGet registry type EE-703 (#5196)
* intermediate commit

* feat(registry): backport ProGet registry to CE (#954)

* backport EE changes

* label updates and remove auth-toggle

Co-authored-by: Dennis Buduev <dennis.buduev@portainer.io>
2021-07-01 14:57:15 +12:00
Richard Wei
8b80eb1731 fix(app):Set resource assignment default to off EE-1043 (#5248)
test passed.
2021-06-30 19:15:28 +12:00
yi-portainer
d2404458ea Merge branch 'release/2.6' into develop 2021-06-25 00:02:42 +12:00
Chaim Lev-Ari
1ddf76dbda fix(git-form): show git form and clear auth values (#5224)
* fix(custom-templates): show git form

fix [EE-1025]

* fix(git-form): empty auth values when auth is off
2021-06-23 12:33:22 +12:00
Chaim Lev-Ari
6a39a5cf44 fix(git-form): show git form and clear auth values (#5224)
* fix(custom-templates): show git form

fix [EE-1025]

* fix(git-form): empty auth values when auth is off
2021-06-22 21:41:50 +12:00
cong meng
a13ad8927f fix(stack) ignore username and password when authentication is disabled EE-161 (#5222)
* fix(stack) ignore username and password when authentication is disabled EE-161

* fix(stack) ignore username and password when authentication is disabled for stack creation EE-161

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-06-22 19:59:05 +12:00
cong meng
8e3751d0b7 fix(stack) Unable to update and redeploy a stack created from a git repository if it has failed once EE-1012 (#5212)
testing passed
2021-06-22 12:58:54 +12:00
Dmitry Salakhov
89f53458c6 fix(stack): allow standard users use advanced deployment (#5205) 2021-06-21 09:53:48 +12:00
cong meng
5466e68f50 fix(ACI): At least one team or user should be specified when creating a restricted container in Azure ACI EE-578 (#5204)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-06-18 11:30:18 +12:00
Stéphane Busso
60ef6d0270 Bump version to 2.6.0 2021-06-17 16:55:11 +12:00
Hui
caa6c15032 feat(k8s): advanced deployment from Git repo EE-447 (#5166)
* feat(stack): UI updates in git repo deployment method for k8s EE-640. (#5097)

* feat(stack): UI updates in git repo deployment method for k8s EE-640.

* feat(stack): supports the combination of GIT + COMPOSE.

* feat(stack): rename variable

* feat(stack): add git repo deployment method for k8s EE-638

* cleanup

* update payload validation rules

* make repo ref optional in frond end

Co-authored-by: fhanportainer <79428273+fhanportainer@users.noreply.github.com>
2021-06-16 23:47:32 +02:00
cong meng
6b759438b8 fix(k8s) cleaning up namespace access policies when removing users orteams from endpoint or endpoint group EE-718 (#5184)
* fix(k8s) cleaning up namespace access policies when removing users or teams from endpoint or endpoint group EE-718

* fix(k8s) minor code cleanup EE-718

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-06-16 20:15:29 +12:00
Hui
2170ad49ef fix(DB): downgrade DB version from 31 to 30 EE-955 (#5193)
* downgrade DB version from 31 to 30

* rename unit test func

* refactor migration func for DB 30

* move test helper func

* use struct method
2021-06-16 19:58:30 +12:00
yi-portainer
6a88c2ae36 Merge branch 'release/2.5' into develop 2021-06-16 17:31:00 +12:00
Alice Groux
7f96220a09 feat(k8s/advanced-deployment): allow standard users to see and use advanced deployment feature EE-446 (#5050) 2021-06-16 17:28:44 +12:00
Dmitry Salakhov
0b93714de4 feat(stacks): redeploy git stack [EE-161] (#5139)
* feat(git): save git config when creating stack (#5048)

* feat(git): save git config when creating stack

* chore(fs): test fileExists

* fix(git): fix tests to use CloneRepository

* refactor(git): move options to new object

* feat(stacks): redeploy git stack api (#5112)

* feat(stacks): redeploy git stacks form

[EE-666]

* feat(stack): show loading after confirmation

* fix(stacks): show same size description

* fix(stacks): reload state when deployed

* feat(stacks): set stopped stacks status to activate when updating

* feat(stacks): backup stack folder before cloning

* feat(stacks): don't accept prune and env on update git

Co-authored-by: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>
2021-06-16 09:11:35 +12:00
cong meng
296ecc5960 fix(k8s) Adding a Kube app does not allow Global to be set after removing persisted folder EE-563 (#5143)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-06-15 15:53:31 +12:00
Chaim Lev-Ari
d7bc4f9b96 fix(stacks): show missing status stacks (#5047)
Co-authored-by: dbuduev <dbuduev@gmail.com>
2021-06-14 14:40:00 +02:00
itsconquest
a5e8cf62d2 feat(UX): introduce new env variables UI (#4175)
* feat(app): introduce new env vars ui

feat(app): introduce new env vars ui

feat(UX): WIP new env variables UI

feat(UX): update button and placeholder

feat(UX): mention .env file in message

feat(UX): allow add/remove value & load correctly

feat(UX): restrict filesize to 1MB

feat(UX): vertical align error message

feat(UX): fill UI from file & when switching modes

feat(UX): strip un-needed newline character

feat(UX): introduce component to other views

feat(UX): fix title alignment

feat(UX): only populate editor on mode switch when key exists

feat(UX): prevent trimming of whitespace on values

feat(UX): change editor to async

feat(UX): add message describing use

feat(UX): Refactor variable text to editorText

refactor(app): rename env vars controller

refactor(app): move env var explanation to parent

refactor(app): order env var panels

refactor(app): move simple env vars mode to component

refactor(app): parse env vars

refactor(app): move styles to css

refactor(app): rename functions

refactor(container): parse env vars

refactor(env-vars): move utils to helper module

refactor(env-vars): use util function for parse dot env file

fix(env-vars): ignore comments

refactor(services): use env vars utils

refactor(env-vars): rename files

refactor(env-panel): use utils

style(stack): revert EnvContent to Env

style(service): revert EnvContent to Env

style(container): revert EnvContent to Env

refactor(env-vars): support default value

refactor(service): use new env var component

refactor(env-var): use one way data flow

refactor(containers): remove unused function

* fix(env-vars): prevent using non .env files

* refactor(env-vars): move env vars items to a component

* feat(app): fixed env vars form validation in Stack

* feat(services): disable env form submit if invalid

* fix(app): show key pairs correctly

* fix(env-var): use the same validation as with kubernetes

* fix(env-vars): parse env var

Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>
Co-authored-by: Felix Han <felix.han@portainer.io>
2021-06-14 18:59:07 +12:00
zees-dev
6e9f472723 feat(container-stats): introduce container block I/O stats (#5017)
* feat(container-stats):introduce container block io stats

* Change charts to 2x2 view

* fix(container-stats): handle missing io stats by detecting stats based on op codes

Co-authored-by: DarkAEther <30438425+DarkAEther@users.noreply.github.com>
2021-06-14 15:57:00 +12:00
Hui
49bd139466 fix swagger param (#5183) 2021-06-14 14:45:57 +12:00
cong meng
dc180d85c5 Feat 4612 real time metrics for kube nodes (#4708)
* feat(k8s/node): display realtime node metrics GH#4612

* feat(k8s): show observation timestamp instead of real timestamp GH#4612

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-06-14 12:29:41 +12:00
Maxime Bajeux
45ceece1a9 feat(application): Invalid environment variable form validation when creating an application (#5019) 2021-06-14 11:06:54 +12:00
Chaim Lev-Ari
0b85684168 fix(app): parse response with null body (#4654)
* fix(app): parse response with null body

* style(docker): add comment explaining change

* fix(images): show correct error when failing import

* fix(images): use async await
2021-06-11 12:05:54 +12:00
Hui
f674573cdf feat(OAuth): Add SSO support for OAuth EE-390 (#5087)
* add updateSettingsToDB28 func and test

* update DBversion const

* migration func naming modification

* feat(oauth): add sso, hide internal auth teaser and logout options. (#5039)

* cleanup and make helper func for unit testing

* dbversion update

* feat(publicSettings): public settings response modification for OAuth SSO EE-608 (#5062)

* feat(oauth): updated logout logic with logoutUrl. (#5064)

* add exclusive token generation for OAuth

* swagger annotation revision

* add unit test

* updates based on tech review feedback

* feat(oauth): updated oauth settings model

* feat(oauth): added oauth logout url

* feat(oauth): fixed SSO toggle and logout issue.

* set SSO to ON by default

* update migrator unit test

* set SSO to true by default for new instance

* prevent applying the SSO logout url to the initial admin user

Co-authored-by: fhanportainer <79428273+fhanportainer@users.noreply.github.com>
Co-authored-by: Felix Han <felix.han@portainer.io>
2021-06-11 10:09:04 +12:00
Richard Wei
14ac005627 fix(app):fix local k8s endpoint not saved EE-825 (#5162) 2021-06-11 09:36:17 +12:00
cong meng
26ead28d7b Feat(stacks): orphaned stacks #4397 (#4834)
* feat(stack): add the ability for an administrator user to manage orphaned stacks (#4397)

* feat(stack): apply small font size to the information text of associate (#4397)

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-06-10 14:52:33 +12:00
zees-dev
eae2f5c9fc feat(kubernetes/summary): summary of k8s actions upon deploying/updating resources EE-436 (#5137)
* feat EE-440/EE-436 kubernetes-resources-summary-panel

* bugfix: returning created resources after update

* fixed patch based bugs - displaying accurate updates for k8s resources

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-06-10 10:38:23 +12:00
cong meng
1f2a90a722 fix(frontend): When a docker endpoint is selected, configuring a newly added k8s agent fails EE-821 (#5115)
* fix(frontend): When a docker endpoint is selected, configuring a newly added k8s agent fails EE-821

* fix(frontend): restore endpointID in a finally block EE-821

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-06-09 21:54:36 +02:00
fhanportainer
267968e099 fix(aci): fixed aci with persistence or networking issue. (#4996) 2021-06-10 01:34:19 +12:00
cong meng
defd929366 Fix(kube) advanced deployment CE-83 (#4866)
* refactor(http/kube): convert compose format

* feat(kube/deploy): deploy to agent

* feat(kube/deploy): show more details about error

* refactor(kube): return string from deploy

* feat(kube/deploy): revert to use local kubectl

* Revert "feat(kube/deploy): revert to use local kubectl"

This reverts commit 7c4a1c70

* feat(kube/deploy): GH#4321 use the v2 version of agent api instead of v3

Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-06-09 01:55:17 +02:00
testA113
2fb17c9cf9 Merge pull request #4983 from portainer/feat/EE-352/CE-truncate-image-name-in-tables
feat(k8s): truncate image name in tables
2021-06-04 15:20:26 +12:00
dbuduev
c8d78ad15f Merge pull request #5146 from portainer/feat/EE-872/test-scaffolding
feat(bolt): implement bolt db test store EE-872
2021-06-04 13:44:56 +12:00
Dennis Buduev
96a6129d8a feat(bolt): implement boltdb test store EE-872 2021-06-04 13:33:18 +12:00
Alice Groux
b8660ed2a0 feat(k8s/applications): reorder placement policies and select mandatory by default (#5063) 2021-06-03 13:42:44 +02:00
Chaim Lev-Ari
9ec1f2ed6d fix(endpoints): set sysctl setting for new endpoints (#5028) 2021-06-03 11:36:54 +02:00
yi-portainer
8bfa5132cd Merge branch 'release/2.5' into develop 2021-06-03 20:39:54 +12:00
wheresolivia
cafcebe27e Merge pull request #4668 from portainer/feat-4667-custom-portainer-folder
chore(dev-build): custom portainer data folder
2021-06-03 13:28:33 +12:00
wheresolivia
ea6df891c3 Merge pull request #5014 from portainer/feat/EE-445/resourcepool-namespace
feat(k8s): replace resourcepool with namespace EE-445
2021-06-02 11:30:20 +12:00
Chaim Lev-Ari
230f8fddc3 fix(kube): replace remaining resource pool texts 2021-06-01 11:56:47 +03:00
Chaim Lev-Ari
6734f0ab74 feat(k8s): replace resource pool with name space 2021-06-01 11:52:05 +03:00
Chaim Lev-Ari
3e60167aeb feat(k8s/applications): default to isolated application 2021-06-01 11:52:05 +03:00
Chaim Lev-Ari
8a4902f15a feat(k8s/applications): rephrase descriptions 2021-06-01 11:52:05 +03:00
yi-portainer
1d46f2bb35 * update portainer version to 2.5.1 2021-05-28 10:21:29 +12:00
yi-portainer
dde0467b89 Merge branch 'release/2.5' into develop 2021-05-28 10:16:38 +12:00
wheresolivia
a2a197b14b Merge pull request #5033 from portainer/fix/CE-575/type-downgrade-error
fix(portainer): Fix the typo in the downgrade error message
2021-05-27 16:46:48 +12:00
cong meng
ee403ca32a fix(image) Confirmation modal on builder output view EE-816 (#5114)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-05-27 13:52:02 +12:00
fhanportainer
d7fcfee2a2 fix(templates): checking windows endpoint and template properties. (#5108)
* fix(templates): checking windows endpoint and template properties.

* fix(templates): removed debug code.

* fix(templates): fixed type issue in custom template.
2021-05-27 08:56:13 +12:00
cong meng
3018801fc0 fix(image) Confirmation modal on builder output view EE-816 (#5107)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-05-26 17:11:32 +12:00
fhanportainer
6bfbf58cdb fix(template): fixed disabled deploy button EE-812 (#5105) 2021-05-25 18:55:50 +02:00
dbuduev
3568fe9e52 feat(git) git clone improvements [EE-451] (#5070) 2021-05-24 17:27:07 +12:00
yi-portainer
2270de73ee Merge branch 'release/2.5' into develop 2021-05-24 08:53:10 +12:00
Chaim Lev-Ari
819faa3948 fix(k8s/proxy): proxy healthz request to k8s api (#5090) 2021-05-21 00:20:08 +02:00
wheresolivia
ef8794c2b9 Merge pull request #5079 from portainer/fix/EE-769/code-editor-prompt-on-change
fix(stacks): check for editor change before setting as dirty
2021-05-20 18:44:46 +12:00
Felix Han
5618794927 fix(k8s-config): check for config editor change before setting as dirty 2021-05-20 11:46:17 +12:00
Felix Han
47d462f085 fix(web-editor): check for editor change before setting as dirty. 2021-05-20 10:22:07 +12:00
zees-dev
0114766d50 Merge pull request #5086 from portainer/revert-5084-feat/EE-341/EE-777/oauth-memberships-teaser
Revert "feat(oauth/team-memberships): EE oauth team memberships teaser"
2021-05-20 10:21:11 +12:00
Stéphane Busso
2b94aa5aa6 Revert "feat(oauth/team-memberships): EE oauth team memberships teaser" 2021-05-20 10:03:59 +12:00
cong meng
746e738f1d Merge pull request #5084 from portainer/feat/EE-341/EE-777/oauth-memberships-teaser
feat(oauth/team-memberships): EE oauth team memberships teaser
2021-05-20 09:21:10 +12:00
zees-dev
29f5008c5f EE oauth team memberships feature teaser 2021-05-19 16:15:46 +12:00
Felix Han
e54d99fd3d fix(stacks): remove line breaks in web editors value 2021-05-19 12:09:11 +12:00
Chaim Lev-Ari
b3784792fe fix(stacks): show containers only for standalone (#5080) 2021-05-18 23:06:04 +02:00
Chaim Lev-Ari
87e7d8ada8 fix(stacks): check for editor change before setting as dirty 2021-05-18 14:08:23 +03:00
yi-portainer
af03d91e39 Merge branch 'release/2.5' into develop 2021-05-18 17:02:31 +12:00
yi-portainer
71635834c7 * update portainer version to 2.5.0
(cherry picked from commit 43702c2516)
2021-05-13 18:32:42 +12:00
yi-portainer
43702c2516 * update portainer version to 2.5.0 2021-05-13 18:30:34 +12:00
Chaim Lev-Ari
a21798f518 fix(docker/containers): show sysctl control (#5051) 2021-05-12 02:29:35 +02:00
dbuduev
3641158daf fix: docker-compose use custom config.json to access private images (#5058)
cherry-picking commit a6b289c9.

Co-authored-by: Dmitry Salakhov <to@dimasalakhov.com>
2021-05-11 23:05:00 +02:00
Chaim Lev-Ari
0ac6274712 fix(docker/services): create a service webhook (#5052) 2021-05-11 10:59:42 +12:00
Chaim Lev-Ari
886d6764be fix(docker): set image pulls as valid if failed fetching (#5055) 2021-05-11 09:24:29 +12:00
Chaim Lev-Ari
39e24ec93f fix(docker): set image pulls as valid if failed fetching (#5007) 2021-05-07 15:38:58 +12:00
Chaim Lev-Ari
b7980f1b60 fix(k8s/ingress): remove only selected ingress (#5035)
* fix(k8s/ingress): remove only selected ingress

* fix(k8s/ingress): remove ingress from namespace
2021-05-07 09:49:56 +12:00
Maxime Bajeux
ce04944ce6 fix(portainer): Fix the type in the downgrade error message 2021-05-05 11:44:00 +02:00
Hui
564bea7575 fix(ACI): ACI UAC breaks when redeploying container with same name asone already existing EE-645 (#5030)
* add existing continer instance checking logic

* modify response status code and err message

* return json instead of plain text for err msg

* Update api/http/proxy/factory/azure/containergroup.go

* Update api/http/proxy/factory/azure/containergroup.go

* Update api/http/proxy/factory/azure/containergroup.go

Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>
2021-05-05 20:26:31 +12:00
Chaim Lev-Ari
dcc77e50e5 fix(docker/images): show image selector advanced mode (#5032) 2021-05-05 20:16:59 +12:00
Stéphane Busso
317ebe2bfc Revert "feat(edge) EE-596 Update the version of agent to 2.4.0 in agent deploy command on the adding edge screen (#5021)" (#5031)
This reverts commit 7e2ce3ffc2.
2021-05-05 16:24:20 +12:00
zees-dev
daabce2b8f Merge pull request #4406 from ricmatsui/feat1654-colorize-logs
feat(log-viewer): add ansi color support for logs
2021-05-03 09:25:24 +12:00
cong meng
7e2ce3ffc2 feat(edge) EE-596 Update the version of agent to 2.4.0 in agent deploy command on the adding edge screen (#5021)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-04-29 16:25:09 +12:00
Alice Groux
d99358ea8e feat(k8s/container): realtime metrics (#4416)
* feat(k8s/container): metrics layout

* feat(k8s/container): memory graph

* feat(k8s/container): cpu usage percent

* feat(k8s/metrics): metrics api validation to enable metrics server

* feat(k8s/pods): update error metrics view

* feat(k8s/container): improve stopRepeater function

* feat(k8s/pods): display empty view instead of empty graphs

* feat(k8s/pods): fix CPU usage

* feat(k8s/configure): fix the metrics server test

* feat(k8s/pod): fix cpu issue

* feat(k8s/pod): fix toaster for non register pods in metrics server

* feat(k8s/service): remove options before 30 secondes for refresh rate

* feat(k8s/pod): fix default value for the refresh rate

* feat(k8s/pod): fix rebase
2021-04-29 13:10:14 +12:00
Alice Groux
befccacc27 feat(k8s/ingress): create multiple ingress network per kubernetes namespace (#4464)
* feat(k8s/ingress): introduce multiple hosts per ingress

* feat(k8s/ingress): host selector in app create/edit

* feat(k8s/ingress): save empty hosts

* feat(k8s/ingress): fix empty host

* feat(k8s/ingress): rename inputs + ensure hostnames unicity + fix remove hostname and routes

* feat(k8s/ingress): fix duplicates hostname validation

* feat(k8s/application): fix rebase

* feat(k8s/resource-pool): fix error messages for ingress (wip)

* fix(k8s/resource-pool): ingress duplicates detection
2021-04-28 05:51:13 +12:00
yi-portainer
ca849e31a1 * update version to 2.4 2021-04-21 12:49:09 +12:00
wheresolivia
335bfb81ba Merge pull request #4965 from portainer/feat(backup)-backup-restore-system
feat(backup): Add backup/restore to the server [EE-386] [EE-378] [CE-452]
2021-04-21 12:16:39 +12:00
wheresolivia
ba2e1d1f60 Merge pull request #4986 from portainer/feat/CE-414/add-UAC-to-ACI
feat(ACI): add UAC to ACI
2021-04-21 11:45:19 +12:00
Ricardo Matsui
a7fc7816d1 Merge branch 'develop' into feat1654-colorize-logs 2021-04-15 22:38:43 -07:00
alice groux
872a8262f1 feat(k8s): add full name on hovering over the image name 2021-04-14 14:59:17 +02:00
Felix Han
5b26ef2036 feat(ACI): updated function name 2021-04-14 16:08:49 +12:00
Felix Han
effb0f6272 Merge branch 'feat/CE-414/add-UAC-to-ACI' of https://github.com/portainer/portainer into feat/CE-414/add-UAC-to-ACI 2021-04-14 16:06:16 +12:00
alice groux
c339afb562 feat(k8s): cut image name to 64 chars with truncate filter in all applications datatables 2021-04-13 16:09:37 +02:00
Chaim Lev-Ari
78661b50ca chore(dev-build): custom portainer data folder 2021-04-12 08:49:07 +03:00
fhanportainer
0aec8fd423 EE-379: add S3 stubs to CE (#4967) 2021-04-08 13:32:59 +12:00
Dmitry Salakhov
8bf662c13a that shouldn't be removed 2021-04-07 16:49:27 +12:00
Dmitry Salakhov
fc9511dc97 UI 2021-04-07 13:21:58 +12:00
Dmitry Salakhov
6d8f5e7479 go 1.13 compatibility 2021-04-07 12:12:19 +12:00
Dmitry Salakhov
a3ec2f8e85 feat(backup): Add backup/restore to the server 2021-04-06 22:08:43 +12:00
Felix Han
e3e7e84821 feat(ACI): add UAC to ACI 2021-03-30 10:58:56 +13:00
Ricardo Matsui
3f9ff8460f fix(log-viewer): fix copy logs and log status 2020-10-28 23:43:53 -07:00
Ricardo Matsui
ae3809cefd fix(log-viewer): fix formatting last line without newline 2020-10-26 16:36:12 -07:00
Ricardo Matsui
8e246c203c feat(log-viewer): add ansi color support for logs 2020-10-24 01:01:09 -07:00
494 changed files with 14774 additions and 5038 deletions

2
.gitignore vendored
View File

@@ -11,3 +11,5 @@ api/cmd/portainer/portainer*
__debug_bin
api/docs
.idea
.env

View File

@@ -0,0 +1,4 @@
{
"go.lintTool": "golangci-lint",
"go.lintFlags": ["--fast", "-E", "exportloopref"]
}

View File

@@ -1,16 +1,12 @@
<p align="center">
<img title="portainer" src='https://github.com/portainer/portainer/blob/develop/app/assets/images/logo_alt.png?raw=true' />
<img title="portainer" src='https://github.com/portainer/portainer/blob/develop/app/assets/images/portainer-github-banner.png?raw=true' />
</p>
[![Docker Pulls](https://img.shields.io/docker/pulls/portainer/portainer.svg)](https://hub.docker.com/r/portainer/portainer/)
[![Microbadger](https://images.microbadger.com/badges/image/portainer/portainer.svg)](http://microbadger.com/images/portainer/portainer 'Image size')
[![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 CE** is a lightweight universal management GUI that can be used to **easily** manage Docker, Swarm, Kubernetes and ACI environments. It is designed to be as **simple** to deploy as it is to use.
**_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 consists of a single container that can run on any cluster. It can be deployed as a Linux container or a Windows native container.
**Portainer** allows you to manage all your orchestrator resources (containers, images, volumes, networks and more) through a super-simple graphical interface.
## Demo
@@ -18,30 +14,38 @@ You can try out the public demo instance: http://demo.portainer.io/ (login with
Please note that the public demo cluster is **reset every 15min**.
Alternatively, you can deploy a copy of the demo stack inside a [play-with-docker (PWD)](https://labs.play-with-docker.com) playground:
## Latest Version
- Browse [PWD/?stack=portainer-demo/play-with-docker/docker-stack.yml](http://play-with-docker.com/?stack=https://raw.githubusercontent.com/portainer/portainer-demo/master/play-with-docker/docker-stack.yml)
- Sign in with your [Docker ID](https://docs.docker.com/docker-id)
- Follow [these](https://github.com/portainer/portainer-demo/blob/master/play-with-docker/docker-stack.yml#L5-L8) steps.
Portainer CE is updated regularly. We aim to do an update release every couple of months.
Unlike the public demo, the playground sessions are deleted after 4 hours. Apart from that, all the settings are the same, including default credentials.
**The latest version of Portainer is 2.6.x** And you can find the release notes [here.](https://www.portainer.io/blog/new-portainer-ce-2.6.0-release)
Portainer is on version 2, the second number denotes the month of release.
## Getting started
- [Deploy Portainer](https://documentation.portainer.io/quickstart/)
- [Documentation](https://documentation.portainer.io)
- [Building Portainer](https://documentation.portainer.io/contributing/instructions/)
- [Contribute to the project](https://documentation.portainer.io/contributing/instructions/)
## Features & Functions
View [this](https://www.portainer.io/products) table to see all of the Portainer CE functionality and compare to Portainer Business.
- [Portainer CE for Docker / Docker Swarm](https://www.portainer.io/solutions/docker)
- [Portainer CE for Kubernetes](https://www.portainer.io/solutions/kubernetes-ui)
- [Portainer CE for Azure ACI](https://www.portainer.io/solutions/serverless-containers)
## Getting help
For FORMAL Support, please purchase a support subscription from here: https://www.portainer.io/products/portainer-business
Portainer CE is an open source project and is supported by the community. You can buy a supported version of Portainer at portainer.io
For community support: You can find more information about Portainer's community support framework policy here: https://www.portainer.io/products/community-edition/customer-success
Learn more about Portainers community support channels [here.](https://www.portainer.io/help_about)
- Issues: https://github.com/portainer/portainer/issues
- FAQ: https://documentation.portainer.io
- Slack (chat): https://portainer.io/slack/
You can join the Portainer Community by visiting community.portainer.io. This will give you advance notice of events, content and other related Portainer content.
## Reporting bugs and contributing
- Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new).

View File

@@ -0,0 +1,69 @@
package adminmonitor
import (
"context"
"log"
"time"
portainer "github.com/portainer/portainer/api"
)
var logFatalf = log.Fatalf
type Monitor struct {
timeout time.Duration
datastore portainer.DataStore
shutdownCtx context.Context
cancellationFunc context.CancelFunc
}
// New creates a monitor that when started will wait for the timeout duration and then shutdown the application unless it has been initialized.
func New(timeout time.Duration, datastore portainer.DataStore, shutdownCtx context.Context) *Monitor {
return &Monitor{
timeout: timeout,
datastore: datastore,
shutdownCtx: shutdownCtx,
}
}
// Starts starts the monitor. Active monitor could be stopped or shuttted down by cancelling the shutdown context.
func (m *Monitor) Start() {
cancellationCtx, cancellationFunc := context.WithCancel(context.Background())
m.cancellationFunc = cancellationFunc
go func() {
log.Println("[DEBUG] [internal,init] [message: start initialization monitor ]")
select {
case <-time.After(m.timeout):
initialized, err := m.WasInitialized()
if err != nil {
logFatalf("%s", err)
}
if !initialized {
logFatalf("[FATAL] [internal,init] No administrator account was created in %f mins. Shutting down the Portainer instance for security reasons", m.timeout.Minutes())
}
case <-cancellationCtx.Done():
log.Println("[DEBUG] [internal,init] [message: canceling initialization monitor]")
case <-m.shutdownCtx.Done():
log.Println("[DEBUG] [internal,init] [message: shutting down initialization monitor]")
}
}()
}
// Stop stops monitor. Safe to call even if monitor wasn't started.
func (m *Monitor) Stop() {
if m.cancellationFunc == nil {
return
}
m.cancellationFunc()
m.cancellationFunc = nil
}
// WasInitialized is a system initialization check
func (m *Monitor) WasInitialized() (bool, error) {
users, err := m.datastore.User().UsersByRole(portainer.AdministratorRole)
if err != nil {
return false, err
}
return len(users) > 0, nil
}

View File

@@ -0,0 +1,50 @@
package adminmonitor
import (
"context"
"testing"
"time"
portainer "github.com/portainer/portainer/api"
i "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert"
)
func Test_stopWithoutStarting(t *testing.T) {
monitor := New(1*time.Minute, nil, nil)
monitor.Stop()
}
func Test_stopCouldBeCalledMultipleTimes(t *testing.T) {
monitor := New(1*time.Minute, nil, nil)
monitor.Stop()
monitor.Stop()
}
func Test_canStopStartedMonitor(t *testing.T) {
monitor := New(1*time.Minute, nil, context.Background())
monitor.Start()
assert.NotNil(t, monitor.cancellationFunc, "cancellation function is missing in started monitor")
monitor.Stop()
assert.Nil(t, monitor.cancellationFunc, "cancellation function should absent in stopped monitor")
}
func Test_start_shouldFatalAfterTimeout_ifNotInitialized(t *testing.T) {
timeout := 10 * time.Millisecond
datastore := i.NewDatastore(i.WithUsers([]portainer.User{}))
var fataled bool
origLogFatalf := logFatalf
logFatalf = func(s string, v ...interface{}) { fataled = true }
defer func() {
logFatalf = origLogFatalf
}()
monitor := New(timeout, datastore, context.Background())
monitor.Start()
<-time.After(2 * timeout)
assert.True(t, fataled, "monitor should been timeout and fatal")
}

119
api/archive/targz.go Normal file
View File

@@ -0,0 +1,119 @@
package archive
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// TarGzDir creates a tar.gz archive and returns it's path.
// abosolutePath should be an absolute path to a directory.
// Archive name will be <directoryName>.tar.gz and will be placed next to the directory.
func TarGzDir(absolutePath string) (string, error) {
targzPath := filepath.Join(absolutePath, fmt.Sprintf("%s.tar.gz", filepath.Base(absolutePath)))
outFile, err := os.Create(targzPath)
if err != nil {
return "", err
}
defer outFile.Close()
zipWriter := gzip.NewWriter(outFile)
defer zipWriter.Close()
tarWriter := tar.NewWriter(zipWriter)
defer tarWriter.Close()
err = filepath.Walk(absolutePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if path == targzPath {
return nil // skip archive file
}
pathInArchive := filepath.Clean(strings.TrimPrefix(path, absolutePath))
if pathInArchive == "" {
return nil // skip root dir
}
return addToArchive(tarWriter, pathInArchive, path, info)
})
return targzPath, err
}
func addToArchive(tarWriter *tar.Writer, pathInArchive string, path string, info os.FileInfo) error {
header, err := tar.FileInfoHeader(info, info.Name())
if err != nil {
return err
}
header.Name = pathInArchive // use relative paths in archive
err = tarWriter.WriteHeader(header)
if err != nil {
return err
}
if info.IsDir() {
return nil
}
file, err := os.Open(path)
if err != nil {
return err
}
_, err = io.Copy(tarWriter, file)
return err
}
// ExtractTarGz reads a .tar.gz archive from the reader and extracts it into outputDirPath directory
func ExtractTarGz(r io.Reader, outputDirPath string) error {
zipReader, err := gzip.NewReader(r)
if err != nil {
return err
}
defer zipReader.Close()
tarReader := tar.NewReader(zipReader)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
switch header.Typeflag {
case tar.TypeDir:
// skip, dir will be created with a file
case tar.TypeReg:
p := filepath.Clean(filepath.Join(outputDirPath, header.Name))
if err := os.MkdirAll(filepath.Dir(p), 0744); err != nil {
return fmt.Errorf("Failed to extract dir %s", filepath.Dir(p))
}
outFile, err := os.Create(p)
if err != nil {
return fmt.Errorf("Failed to create file %s", header.Name)
}
if _, err := io.Copy(outFile, tarReader); err != nil {
return fmt.Errorf("Failed to extract file %s", header.Name)
}
outFile.Close()
default:
return fmt.Errorf("Tar: uknown type: %v in %s",
header.Typeflag,
header.Name)
}
}
return nil
}

99
api/archive/targz_test.go Normal file
View File

@@ -0,0 +1,99 @@
package archive
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"testing"
"github.com/docker/docker/pkg/ioutils"
"github.com/stretchr/testify/assert"
)
func listFiles(dir string) []string {
items := make([]string, 0)
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if path == dir {
return nil
}
items = append(items, path)
return nil
})
return items
}
func Test_shouldCreateArhive(t *testing.T) {
tmpdir, _ := ioutils.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
content := []byte("content")
ioutil.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
ioutil.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
ioutil.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
gzPath, err := TarGzDir(tmpdir)
assert.Nil(t, err)
assert.Equal(t, filepath.Join(tmpdir, fmt.Sprintf("%s.tar.gz", filepath.Base(tmpdir))), gzPath)
extractionDir, _ := ioutils.TempDir("", "extract")
defer os.RemoveAll(extractionDir)
cmd := exec.Command("tar", "-xzf", gzPath, "-C", extractionDir)
err = cmd.Run()
if err != nil {
t.Fatal("Failed to extract archive: ", err)
}
extractedFiles := listFiles(extractionDir)
wasExtracted := func(p string) {
fullpath := path.Join(extractionDir, p)
assert.Contains(t, extractedFiles, fullpath)
copyContent, _ := ioutil.ReadFile(fullpath)
assert.Equal(t, content, copyContent)
}
wasExtracted("outer")
wasExtracted("dir/inner")
wasExtracted("dir/.dotfile")
}
func Test_shouldCreateArhiveXXXXX(t *testing.T) {
tmpdir, _ := ioutils.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
content := []byte("content")
ioutil.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
ioutil.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
ioutil.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
gzPath, err := TarGzDir(tmpdir)
assert.Nil(t, err)
assert.Equal(t, filepath.Join(tmpdir, fmt.Sprintf("%s.tar.gz", filepath.Base(tmpdir))), gzPath)
extractionDir, _ := ioutils.TempDir("", "extract")
defer os.RemoveAll(extractionDir)
r, _ := os.Open(gzPath)
ExtractTarGz(r, extractionDir)
if err != nil {
t.Fatal("Failed to extract archive: ", err)
}
extractedFiles := listFiles(extractionDir)
wasExtracted := func(p string) {
fullpath := path.Join(extractionDir, p)
assert.Contains(t, extractedFiles, fullpath)
copyContent, _ := ioutil.ReadFile(fullpath)
assert.Equal(t, content, copyContent)
}
wasExtracted("outer")
wasExtracted("dir/inner")
wasExtracted("dir/.dotfile")
}

BIN
api/archive/testdata/sample_archive.zip vendored Normal file

Binary file not shown.

View File

@@ -3,10 +3,13 @@ package archive
import (
"archive/zip"
"bytes"
"fmt"
"github.com/pkg/errors"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
// UnzipArchive will unzip an archive from bytes into the dest destination folder on disk
@@ -52,3 +55,60 @@ func extractFileFromArchive(file *zip.File, dest string) error {
return outFile.Close()
}
// UnzipFile will decompress a zip archive, moving all files and folders
// within the zip file (parameter 1) to an output directory (parameter 2).
func UnzipFile(src string, dest string) error {
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer r.Close()
for _, f := range r.File {
p := filepath.Join(dest, f.Name)
// Check for ZipSlip. More Info: http://bit.ly/2MsjAWE
if !strings.HasPrefix(p, filepath.Clean(dest)+string(os.PathSeparator)) {
return fmt.Errorf("%s: illegal file path", p)
}
if f.FileInfo().IsDir() {
// Make Folder
os.MkdirAll(p, os.ModePerm)
continue
}
err = unzipFile(f, p)
if err != nil {
return err
}
}
return nil
}
func unzipFile(f *zip.File, p string) error {
// Make File
if err := os.MkdirAll(filepath.Dir(p), os.ModePerm); err != nil {
return errors.Wrapf(err, "unzipFile: can't make a path %s", p)
}
outFile, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return errors.Wrapf(err, "unzipFile: can't create file %s", p)
}
defer outFile.Close()
rc, err := f.Open()
if err != nil {
return errors.Wrapf(err, "unzipFile: can't open zip file %s in the archive", f.Name)
}
defer rc.Close()
_, err = io.Copy(outFile, rc)
if err != nil {
return errors.Wrapf(err, "unzipFile: can't copy an archived file content")
}
return nil
}

32
api/archive/zip_test.go Normal file
View File

@@ -0,0 +1,32 @@
package archive
import (
"github.com/stretchr/testify/assert"
"io/ioutil"
"os"
"path/filepath"
"testing"
)
func TestUnzipFile(t *testing.T) {
dir, err := ioutil.TempDir("", "unzip-test-")
assert.NoError(t, err)
defer os.RemoveAll(dir)
/*
Archive structure.
├── 0
│ ├── 1
│ │ └── 2.txt
│ └── 1.txt
└── 0.txt
*/
err = UnzipFile("./testdata/sample_archive.zip", dir)
assert.NoError(t, err)
archiveDir := dir + "/sample_archive"
assert.FileExists(t, filepath.Join(archiveDir, "0.txt"))
assert.FileExists(t, filepath.Join(archiveDir, "0", "1.txt"))
assert.FileExists(t, filepath.Join(archiveDir, "0", "1", "2.txt"))
}

83
api/backup/backup.go Normal file
View File

@@ -0,0 +1,83 @@
package backup
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/archive"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/offlinegate"
)
const rwxr__r__ os.FileMode = 0744
var filesToBackup = []string{"compose", "config.json", "custom_templates", "edge_jobs", "edge_stacks", "extensions", "portainer.key", "portainer.pub", "tls"}
// Creates a tar.gz system archive and encrypts it if password is not empty. Returns a path to the archive file.
func CreateBackupArchive(password string, gate *offlinegate.OfflineGate, datastore portainer.DataStore, filestorePath string) (string, error) {
unlock := gate.Lock()
defer unlock()
backupDirPath := filepath.Join(filestorePath, "backup", time.Now().Format("2006-01-02_15-04-05"))
if err := os.MkdirAll(backupDirPath, rwxr__r__); err != nil {
return "", errors.Wrap(err, "Failed to create backup dir")
}
if err := backupDb(backupDirPath, datastore); err != nil {
return "", errors.Wrap(err, "Failed to backup database")
}
for _, filename := range filesToBackup {
err := copyPath(filepath.Join(filestorePath, filename), backupDirPath)
if err != nil {
return "", errors.Wrap(err, "Failed to create backup file")
}
}
archivePath, err := archive.TarGzDir(backupDirPath)
if err != nil {
return "", errors.Wrap(err, "Failed to make an archive")
}
if password != "" {
archivePath, err = encrypt(archivePath, password)
if err != nil {
return "", errors.Wrap(err, "Failed to encrypt backup with the password")
}
}
return archivePath, nil
}
func backupDb(backupDirPath string, datastore portainer.DataStore) error {
backupWriter, err := os.Create(filepath.Join(backupDirPath, "portainer.db"))
if err != nil {
return err
}
if err = datastore.BackupTo(backupWriter); err != nil {
return err
}
return backupWriter.Close()
}
func encrypt(path string, passphrase string) (string, error) {
in, err := os.Open(path)
if err != nil {
return "", err
}
defer in.Close()
outFileName := fmt.Sprintf("%s.encrypted", path)
out, err := os.Create(outFileName)
if err != nil {
return "", err
}
err = crypto.AesEncrypt(in, out, []byte(passphrase))
return outFileName, err
}

68
api/backup/copy.go Normal file
View File

@@ -0,0 +1,68 @@
package backup
import (
"errors"
"io"
"os"
"path/filepath"
"strings"
)
func copyPath(path string, toDir string) error {
info, err := os.Stat(path)
if err != nil && errors.Is(err, os.ErrNotExist) {
// skip copy if file does not exist
return nil
}
if !info.IsDir() {
destination := filepath.Join(toDir, info.Name())
return copyFile(path, destination)
}
return copyDir(path, toDir)
}
func copyDir(fromDir, toDir string) error {
cleanedSourcePath := filepath.Clean(fromDir)
parentDirectory := filepath.Dir(cleanedSourcePath)
err := filepath.Walk(cleanedSourcePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
destination := filepath.Join(toDir, strings.TrimPrefix(path, parentDirectory))
if info.IsDir() {
return nil // skip directory creations
}
if info.Mode()&os.ModeSymlink != 0 { // entry is a symlink
return nil // don't copy symlinks
}
return copyFile(path, destination)
})
return err
}
// copies regular a file from src to dst
func copyFile(src, dst string) error {
from, err := os.Open(src)
if err != nil {
return err
}
defer from.Close()
// has to include 'execute' bit, otherwise fails. MkdirAll follows `mkdir -m` restrictions
if err := os.MkdirAll(filepath.Dir(dst), 0744); err != nil {
return err
}
to, err := os.Create(dst)
if err != nil {
return err
}
defer to.Close()
_, err = io.Copy(to, from)
return err
}

105
api/backup/copy_test.go Normal file
View File

@@ -0,0 +1,105 @@
package backup
import (
"io/ioutil"
"os"
"path"
"path/filepath"
"testing"
"github.com/docker/docker/pkg/ioutils"
"github.com/stretchr/testify/assert"
)
func listFiles(dir string) []string {
items := make([]string, 0)
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if path == dir {
return nil
}
items = append(items, path)
return nil
})
return items
}
func contains(t *testing.T, list []string, path string) {
assert.Contains(t, list, path)
copyContent, _ := ioutil.ReadFile(path)
assert.Equal(t, "content\n", string(copyContent))
}
func Test_copyFile_returnsError_whenSourceDoesNotExist(t *testing.T) {
tmpdir, _ := ioutils.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
err := copyFile("does-not-exist", tmpdir)
assert.NotNil(t, err)
}
func Test_copyFile_shouldMakeAbackup(t *testing.T) {
tmpdir, _ := ioutils.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
content := []byte("content")
ioutil.WriteFile(path.Join(tmpdir, "origin"), content, 0600)
err := copyFile(path.Join(tmpdir, "origin"), path.Join(tmpdir, "copy"))
assert.Nil(t, err)
copyContent, _ := ioutil.ReadFile(path.Join(tmpdir, "copy"))
assert.Equal(t, content, copyContent)
}
func Test_copyDir_shouldCopyAllFilesAndDirectories(t *testing.T) {
destination, _ := ioutils.TempDir("", "destination")
defer os.RemoveAll(destination)
err := copyDir("./test_assets/copy_test", destination)
assert.Nil(t, err)
createdFiles := listFiles(destination)
contains(t, createdFiles, filepath.Join(destination, "copy_test", "outer"))
contains(t, createdFiles, filepath.Join(destination, "copy_test", "dir", ".dotfile"))
contains(t, createdFiles, filepath.Join(destination, "copy_test", "dir", "inner"))
}
func Test_backupPath_shouldSkipWhenNotExist(t *testing.T) {
tmpdir, _ := ioutils.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
err := copyPath("does-not-exists", tmpdir)
assert.Nil(t, err)
assert.Empty(t, listFiles(tmpdir))
}
func Test_backupPath_shouldCopyFile(t *testing.T) {
tmpdir, _ := ioutils.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
content := []byte("content")
ioutil.WriteFile(path.Join(tmpdir, "file"), content, 0600)
os.MkdirAll(path.Join(tmpdir, "backup"), 0700)
err := copyPath(path.Join(tmpdir, "file"), path.Join(tmpdir, "backup"))
assert.Nil(t, err)
copyContent, err := ioutil.ReadFile(path.Join(tmpdir, "backup", "file"))
assert.Nil(t, err)
assert.Equal(t, content, copyContent)
}
func Test_backupPath_shouldCopyDir(t *testing.T) {
destination, _ := ioutils.TempDir("", "destination")
defer os.RemoveAll(destination)
err := copyPath("./test_assets/copy_test", destination)
assert.Nil(t, err)
createdFiles := listFiles(destination)
contains(t, createdFiles, filepath.Join(destination, "copy_test", "outer"))
contains(t, createdFiles, filepath.Join(destination, "copy_test", "dir", ".dotfile"))
contains(t, createdFiles, filepath.Join(destination, "copy_test", "dir", "inner"))
}

68
api/backup/restore.go Normal file
View File

@@ -0,0 +1,68 @@
package backup
import (
"context"
"io"
"os"
"path/filepath"
"time"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/archive"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/offlinegate"
)
var filesToRestore = append(filesToBackup, "portainer.db")
// Restores system state from backup archive, will trigger system shutdown, when finished.
func RestoreArchive(archive io.Reader, password string, filestorePath string, gate *offlinegate.OfflineGate, datastore portainer.DataStore, shutdownTrigger context.CancelFunc) error {
var err error
if password != "" {
archive, err = decrypt(archive, password)
if err != nil {
return errors.Wrap(err, "failed to decrypt the archive")
}
}
restorePath := filepath.Join(filestorePath, "restore", time.Now().Format("20060102150405"))
defer os.RemoveAll(filepath.Dir(restorePath))
err = extractArchive(archive, restorePath)
if err != nil {
return errors.Wrap(err, "cannot extract files from the archive. Please ensure the password is correct and try again")
}
unlock := gate.Lock()
defer unlock()
if err = datastore.Close(); err != nil {
return errors.Wrap(err, "Failed to stop db")
}
if err = restoreFiles(restorePath, filestorePath); err != nil {
return errors.Wrap(err, "failed to restore the system state")
}
shutdownTrigger()
return nil
}
func decrypt(r io.Reader, password string) (io.Reader, error) {
return crypto.AesDecrypt(r, []byte(password))
}
func extractArchive(r io.Reader, destinationDirPath string) error {
return archive.ExtractTarGz(r, destinationDirPath)
}
func restoreFiles(srcDir string, destinationDir string) error {
for _, filename := range filesToRestore {
err := copyPath(filepath.Join(srcDir, filename), destinationDir)
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1 @@
content

View File

@@ -0,0 +1 @@
content

View File

@@ -0,0 +1 @@
content

View File

@@ -0,0 +1,73 @@
package bolttest
import (
"io/ioutil"
"log"
"os"
"github.com/pkg/errors"
"github.com/portainer/portainer/api/bolt"
"github.com/portainer/portainer/api/filesystem"
)
var errTempDir = errors.New("can't create a temp dir")
func MustNewTestStore(init bool) (*bolt.Store, func()) {
store, teardown, err := NewTestStore(init)
if err != nil {
if !errors.Is(err, errTempDir) {
teardown()
}
log.Fatal(err)
}
return store, teardown
}
func NewTestStore(init bool) (*bolt.Store, func(), error) {
// Creates unique temp directory in a concurrency friendly manner.
dataStorePath, err := ioutil.TempDir("", "boltdb")
if err != nil {
return nil, nil, errors.Wrap(errTempDir, err.Error())
}
fileService, err := filesystem.NewService(dataStorePath, "")
if err != nil {
return nil, nil, err
}
store, err := bolt.NewStore(dataStorePath, fileService)
if err != nil {
return nil, nil, err
}
err = store.Open()
if err != nil {
return nil, nil, err
}
if init {
err = store.Init()
if err != nil {
return nil, nil, err
}
}
teardown := func() {
teardown(store, dataStorePath)
}
return store, teardown, nil
}
func teardown(store *bolt.Store, dataStorePath string) {
err := store.Close()
if err != nil {
log.Fatalln(err)
}
err = os.RemoveAll(dataStorePath)
if err != nil {
log.Fatalln(err)
}
}

View File

@@ -2,7 +2,7 @@ package customtemplate
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
@@ -13,18 +13,18 @@ const (
// Service represents a service for managing custom template data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -32,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) CustomTemplates() ([]portainer.CustomTemplate, error) {
var customTemplates = make([]portainer.CustomTemplate, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -56,7 +56,7 @@ func (service *Service) CustomTemplate(ID portainer.CustomTemplateID) (*portaine
var customTemplate portainer.CustomTemplate
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &customTemplate)
err := internal.GetObject(service.connection, BucketName, identifier, &customTemplate)
if err != nil {
return nil, err
}
@@ -67,18 +67,18 @@ func (service *Service) CustomTemplate(ID portainer.CustomTemplateID) (*portaine
// UpdateCustomTemplate updates an custom template.
func (service *Service) UpdateCustomTemplate(ID portainer.CustomTemplateID, customTemplate *portainer.CustomTemplate) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, customTemplate)
return internal.UpdateObject(service.connection, BucketName, identifier, customTemplate)
}
// DeleteCustomTemplate deletes an custom template.
func (service *Service) DeleteCustomTemplate(ID portainer.CustomTemplateID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}
// CreateCustomTemplate assign an ID to a new custom template and saves it.
func (service *Service) CreateCustomTemplate(customTemplate *portainer.CustomTemplate) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data, err := internal.MarshalObject(customTemplate)
@@ -92,5 +92,5 @@ func (service *Service) CreateCustomTemplate(customTemplate *portainer.CustomTem
// GetNextIdentifier returns the next identifier for a custom template.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
return internal.GetNextIdentifier(service.connection, BucketName)
}

View File

@@ -1,6 +1,7 @@
package bolt
import (
"io"
"log"
"path"
"time"
@@ -17,6 +18,7 @@ import (
"github.com/portainer/portainer/api/bolt/endpointrelation"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/extension"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/portainer/portainer/api/bolt/migrator"
"github.com/portainer/portainer/api/bolt/registry"
"github.com/portainer/portainer/api/bolt/resourcecontrol"
@@ -42,7 +44,7 @@ const (
// BoltDB as the storage system.
type Store struct {
path string
db *bolt.DB
connection *internal.DbConnection
isNew bool
fileService portainer.FileService
CustomTemplateService *customtemplate.Service
@@ -83,6 +85,7 @@ func NewStore(storePath string, fileService portainer.FileService) (*Store, erro
path: storePath,
fileService: fileService,
isNew: true,
connection: &internal.DbConnection{},
}
databasePath := path.Join(storePath, databaseFileName)
@@ -105,15 +108,15 @@ func (store *Store) Open() error {
if err != nil {
return err
}
store.db = db
store.connection.DB = db
return store.initServices()
}
// Close closes the BoltDB database.
func (store *Store) Close() error {
if store.db != nil {
return store.db.Close()
if store.connection.DB != nil {
return store.connection.Close()
}
return nil
}
@@ -134,8 +137,9 @@ func (store *Store) CheckCurrentEdition() error {
// MigrateData automatically migrate the data based on the DBVersion.
// This process is only triggered on an existing database, not if the database was just created.
func (store *Store) MigrateData() error {
if store.isNew {
// if force is true, then migrate regardless.
func (store *Store) MigrateData(force bool) error {
if store.isNew && !force {
return store.VersionService.StoreDBVersion(portainer.DBVersion)
}
@@ -148,7 +152,7 @@ func (store *Store) MigrateData() error {
if version < portainer.DBVersion {
migratorParams := &migrator.Parameters{
DB: store.db,
DB: store.connection.DB,
DatabaseVersion: version,
EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService,
@@ -165,6 +169,7 @@ func (store *Store) MigrateData() error {
UserService: store.UserService,
VersionService: store.VersionService,
FileService: store.fileService,
DockerhubService: store.DockerHubService,
AuthorizationService: authorization.NewService(store),
}
migrator := migrator.NewMigrator(migratorParams)
@@ -180,238 +185,11 @@ func (store *Store) MigrateData() error {
return nil
}
func (store *Store) initServices() error {
authorizationsetService, err := role.NewService(store.db)
if err != nil {
// BackupTo backs up db to a provided writer.
// It does hot backup and doesn't block other database reads and writes
func (store *Store) BackupTo(w io.Writer) error {
return store.connection.View(func(tx *bolt.Tx) error {
_, err := tx.WriteTo(w)
return err
}
store.RoleService = authorizationsetService
customTemplateService, err := customtemplate.NewService(store.db)
if err != nil {
return err
}
store.CustomTemplateService = customTemplateService
dockerhubService, err := dockerhub.NewService(store.db)
if err != nil {
return err
}
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
edgeJobService, err := edgejob.NewService(store.db)
if err != nil {
return err
}
store.EdgeJobService = edgeJobService
endpointgroupService, err := endpointgroup.NewService(store.db)
if err != nil {
return err
}
store.EndpointGroupService = endpointgroupService
endpointService, err := endpoint.NewService(store.db)
if err != nil {
return err
}
store.EndpointService = endpointService
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
}
store.ExtensionService = extensionService
registryService, err := registry.NewService(store.db)
if err != nil {
return err
}
store.RegistryService = registryService
resourcecontrolService, err := resourcecontrol.NewService(store.db)
if err != nil {
return err
}
store.ResourceControlService = resourcecontrolService
settingsService, err := settings.NewService(store.db)
if err != nil {
return err
}
store.SettingsService = settingsService
stackService, err := stack.NewService(store.db)
if err != nil {
return err
}
store.StackService = stackService
tagService, err := tag.NewService(store.db)
if err != nil {
return err
}
store.TagService = tagService
teammembershipService, err := teammembership.NewService(store.db)
if err != nil {
return err
}
store.TeamMembershipService = teammembershipService
teamService, err := team.NewService(store.db)
if err != nil {
return err
}
store.TeamService = teamService
tunnelServerService, err := tunnelserver.NewService(store.db)
if err != nil {
return err
}
store.TunnelServerService = tunnelServerService
userService, err := user.NewService(store.db)
if err != nil {
return err
}
store.UserService = userService
versionService, err := version.NewService(store.db)
if err != nil {
return err
}
store.VersionService = versionService
webhookService, err := webhook.NewService(store.db)
if err != nil {
return err
}
store.WebhookService = webhookService
scheduleService, err := schedule.NewService(store.db)
if err != nil {
return err
}
store.ScheduleService = scheduleService
return nil
}
// CustomTemplate gives access to the CustomTemplate data management layer
func (store *Store) CustomTemplate() portainer.CustomTemplateService {
return store.CustomTemplateService
}
// DockerHub gives access to the DockerHub data management layer
func (store *Store) DockerHub() portainer.DockerHubService {
return store.DockerHubService
}
// EdgeGroup gives access to the EdgeGroup data management layer
func (store *Store) EdgeGroup() portainer.EdgeGroupService {
return store.EdgeGroupService
}
// EdgeJob gives access to the EdgeJob data management layer
func (store *Store) EdgeJob() portainer.EdgeJobService {
return store.EdgeJobService
}
// EdgeStack gives access to the EdgeStack data management layer
func (store *Store) EdgeStack() portainer.EdgeStackService {
return store.EdgeStackService
}
// Endpoint gives access to the Endpoint data management layer
func (store *Store) Endpoint() portainer.EndpointService {
return store.EndpointService
}
// EndpointGroup gives access to the EndpointGroup data management layer
func (store *Store) EndpointGroup() portainer.EndpointGroupService {
return store.EndpointGroupService
}
// EndpointRelation gives access to the EndpointRelation data management layer
func (store *Store) EndpointRelation() portainer.EndpointRelationService {
return store.EndpointRelationService
}
// Registry gives access to the Registry data management layer
func (store *Store) Registry() portainer.RegistryService {
return store.RegistryService
}
// ResourceControl gives access to the ResourceControl data management layer
func (store *Store) ResourceControl() portainer.ResourceControlService {
return store.ResourceControlService
}
// Role gives access to the Role data management layer
func (store *Store) Role() portainer.RoleService {
return store.RoleService
}
// Settings gives access to the Settings data management layer
func (store *Store) Settings() portainer.SettingsService {
return store.SettingsService
}
// Stack gives access to the Stack data management layer
func (store *Store) Stack() portainer.StackService {
return store.StackService
}
// Tag gives access to the Tag data management layer
func (store *Store) Tag() portainer.TagService {
return store.TagService
}
// TeamMembership gives access to the TeamMembership data management layer
func (store *Store) TeamMembership() portainer.TeamMembershipService {
return store.TeamMembershipService
}
// Team gives access to the Team data management layer
func (store *Store) Team() portainer.TeamService {
return store.TeamService
}
// TunnelServer gives access to the TunnelServer data management layer
func (store *Store) TunnelServer() portainer.TunnelServerService {
return store.TunnelServerService
}
// User gives access to the User data management layer
func (store *Store) User() portainer.UserService {
return store.UserService
}
// Version gives access to the Version data management layer
func (store *Store) Version() portainer.VersionService {
return store.VersionService
}
// Webhook gives access to the Webhook data management layer
func (store *Store) Webhook() portainer.WebhookService {
return store.WebhookService
})
}

View File

@@ -1,10 +1,8 @@
package dockerhub
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
)
const (
@@ -15,18 +13,18 @@ const (
// Service represents a service for managing Dockerhub data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -34,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) DockerHub() (*portainer.DockerHub, error) {
var dockerhub portainer.DockerHub
err := internal.GetObject(service.db, BucketName, []byte(dockerHubKey), &dockerhub)
err := internal.GetObject(service.connection, BucketName, []byte(dockerHubKey), &dockerhub)
if err != nil {
return nil, err
}
@@ -44,5 +42,5 @@ func (service *Service) DockerHub() (*portainer.DockerHub, error) {
// UpdateDockerHub updates a DockerHub object.
func (service *Service) UpdateDockerHub(dockerhub *portainer.DockerHub) error {
return internal.UpdateObject(service.db, BucketName, []byte(dockerHubKey), dockerhub)
return internal.UpdateObject(service.connection, BucketName, []byte(dockerHubKey), dockerhub)
}

View File

@@ -2,7 +2,7 @@ package edgegroup
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
@@ -13,18 +13,18 @@ const (
// Service represents a service for managing Edge group data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -32,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) EdgeGroups() ([]portainer.EdgeGroup, error) {
var groups = make([]portainer.EdgeGroup, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -56,7 +56,7 @@ func (service *Service) EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGrou
var group portainer.EdgeGroup
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &group)
err := internal.GetObject(service.connection, BucketName, identifier, &group)
if err != nil {
return nil, err
}
@@ -67,18 +67,18 @@ func (service *Service) EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGrou
// 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)
return internal.UpdateObject(service.connection, 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)
return internal.DeleteObject(service.connection, 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 {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()

View File

@@ -2,7 +2,7 @@ package edgejob
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
@@ -13,18 +13,18 @@ const (
// Service represents a service for managing edge jobs data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -32,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) EdgeJobs() ([]portainer.EdgeJob, error) {
var edgeJobs = make([]portainer.EdgeJob, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -56,7 +56,7 @@ func (service *Service) EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, err
var edgeJob portainer.EdgeJob
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &edgeJob)
err := internal.GetObject(service.connection, BucketName, identifier, &edgeJob)
if err != nil {
return nil, err
}
@@ -66,7 +66,7 @@ func (service *Service) EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, err
// CreateEdgeJob creates a new Edge job
func (service *Service) CreateEdgeJob(edgeJob *portainer.EdgeJob) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
if edgeJob.ID == 0 {
@@ -86,16 +86,16 @@ func (service *Service) CreateEdgeJob(edgeJob *portainer.EdgeJob) error {
// UpdateEdgeJob updates an Edge job by ID
func (service *Service) UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, edgeJob)
return internal.UpdateObject(service.connection, BucketName, identifier, edgeJob)
}
// DeleteEdgeJob deletes an Edge job
func (service *Service) DeleteEdgeJob(ID portainer.EdgeJobID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}
// GetNextIdentifier returns the next identifier for an endpoint.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
return internal.GetNextIdentifier(service.connection, BucketName)
}

View File

@@ -2,7 +2,7 @@ package edgestack
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
@@ -13,18 +13,18 @@ const (
// Service represents a service for managing Edge stack data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -32,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) EdgeStacks() ([]portainer.EdgeStack, error) {
var stacks = make([]portainer.EdgeStack, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -56,7 +56,7 @@ func (service *Service) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStac
var stack portainer.EdgeStack
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &stack)
err := internal.GetObject(service.connection, BucketName, identifier, &stack)
if err != nil {
return nil, err
}
@@ -66,7 +66,7 @@ func (service *Service) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStac
// 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 {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
if edgeStack.ID == 0 {
@@ -86,16 +86,16 @@ func (service *Service) CreateEdgeStack(edgeStack *portainer.EdgeStack) error {
// 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)
return internal.UpdateObject(service.connection, 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)
return internal.DeleteObject(service.connection, BucketName, identifier)
}
// GetNextIdentifier returns the next identifier for an endpoint.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
return internal.GetNextIdentifier(service.connection, BucketName)
}

View File

@@ -2,7 +2,7 @@ package endpoint
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
@@ -13,18 +13,18 @@ const (
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -33,7 +33,7 @@ func (service *Service) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint,
var endpoint portainer.Endpoint
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &endpoint)
err := internal.GetObject(service.connection, BucketName, identifier, &endpoint)
if err != nil {
return nil, err
}
@@ -44,20 +44,20 @@ func (service *Service) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint,
// UpdateEndpoint updates an endpoint.
func (service *Service) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, endpoint)
return internal.UpdateObject(service.connection, BucketName, identifier, endpoint)
}
// DeleteEndpoint deletes an endpoint.
func (service *Service) DeleteEndpoint(ID portainer.EndpointID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}
// Endpoints return an array containing all the endpoints.
func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
var endpoints = make([]portainer.Endpoint, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -78,7 +78,7 @@ func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
// CreateEndpoint assign an ID to a new endpoint and saves it.
func (service *Service) CreateEndpoint(endpoint *portainer.Endpoint) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
// We manually manage sequences for endpoints
@@ -98,12 +98,12 @@ func (service *Service) CreateEndpoint(endpoint *portainer.Endpoint) error {
// GetNextIdentifier returns the next identifier for an endpoint.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
return internal.GetNextIdentifier(service.connection, BucketName)
}
// Synchronize creates, updates and deletes endpoints inside a single transaction.
func (service *Service) Synchronize(toCreate, toUpdate, toDelete []*portainer.Endpoint) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
for _, endpoint := range toCreate {

View File

@@ -1,7 +1,7 @@
package endpointgroup
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
@@ -14,18 +14,18 @@ const (
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -34,7 +34,7 @@ func (service *Service) EndpointGroup(ID portainer.EndpointGroupID) (*portainer.
var endpointGroup portainer.EndpointGroup
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &endpointGroup)
err := internal.GetObject(service.connection, BucketName, identifier, &endpointGroup)
if err != nil {
return nil, err
}
@@ -45,20 +45,20 @@ func (service *Service) EndpointGroup(ID portainer.EndpointGroupID) (*portainer.
// UpdateEndpointGroup updates an endpoint group.
func (service *Service) UpdateEndpointGroup(ID portainer.EndpointGroupID, endpointGroup *portainer.EndpointGroup) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, endpointGroup)
return internal.UpdateObject(service.connection, BucketName, identifier, endpointGroup)
}
// DeleteEndpointGroup deletes an endpoint group.
func (service *Service) DeleteEndpointGroup(ID portainer.EndpointGroupID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}
// EndpointGroups return an array containing all the endpoint groups.
func (service *Service) EndpointGroups() ([]portainer.EndpointGroup, error) {
var endpointGroups = make([]portainer.EndpointGroup, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -79,7 +79,7 @@ func (service *Service) EndpointGroups() ([]portainer.EndpointGroup, error) {
// CreateEndpointGroup assign an ID to a new endpoint group and saves it.
func (service *Service) CreateEndpointGroup(endpointGroup *portainer.EndpointGroup) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()

View File

@@ -13,18 +13,18 @@ const (
// Service represents a service for managing endpoint relation data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -33,7 +33,7 @@ func (service *Service) EndpointRelation(endpointID portainer.EndpointID) (*port
var endpointRelation portainer.EndpointRelation
identifier := internal.Itob(int(endpointID))
err := internal.GetObject(service.db, BucketName, identifier, &endpointRelation)
err := internal.GetObject(service.connection, BucketName, identifier, &endpointRelation)
if err != nil {
return nil, err
}
@@ -43,7 +43,7 @@ func (service *Service) EndpointRelation(endpointID portainer.EndpointID) (*port
// CreateEndpointRelation saves endpointRelation
func (service *Service) CreateEndpointRelation(endpointRelation *portainer.EndpointRelation) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data, err := internal.MarshalObject(endpointRelation)
@@ -58,11 +58,11 @@ func (service *Service) CreateEndpointRelation(endpointRelation *portainer.Endpo
// 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)
return internal.UpdateObject(service.connection, 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)
return internal.DeleteObject(service.connection, BucketName, identifier)
}

View File

@@ -4,5 +4,5 @@ import "errors"
var (
ErrObjectNotFound = errors.New("Object not found inside the database")
ErrWrongDBEdition = errors.New("The Portainer database is set for Portainer Business Edition, please follow the instructions in our documention to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/")
ErrWrongDBEdition = errors.New("The Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/")
)

View File

@@ -1,7 +1,7 @@
package extension
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
@@ -14,18 +14,18 @@ const (
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -34,7 +34,7 @@ func (service *Service) Extension(ID portainer.ExtensionID) (*portainer.Extensio
var extension portainer.Extension
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &extension)
err := internal.GetObject(service.connection, BucketName, identifier, &extension)
if err != nil {
return nil, err
}
@@ -46,7 +46,7 @@ func (service *Service) Extension(ID portainer.ExtensionID) (*portainer.Extensio
func (service *Service) Extensions() ([]portainer.Extension, error) {
var extensions = make([]portainer.Extension, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -67,7 +67,7 @@ func (service *Service) Extensions() ([]portainer.Extension, error) {
// Persist persists a extension inside the database.
func (service *Service) Persist(extension *portainer.Extension) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data, err := internal.MarshalObject(extension)
@@ -82,5 +82,5 @@ func (service *Service) Persist(extension *portainer.Extension) error {
// DeleteExtension deletes a Extension.
func (service *Service) DeleteExtension(ID portainer.ExtensionID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}

View File

@@ -55,22 +55,6 @@ func (store *Store) Init() error {
return err
}
_, err = store.DockerHubService.DockerHub()
if err == errors.ErrObjectNotFound {
defaultDockerHub := &portainer.DockerHub{
Authentication: false,
Username: "",
Password: "",
}
err := store.DockerHubService.UpdateDockerHub(defaultDockerHub)
if err != nil {
return err
}
} else if err != nil {
return err
}
groups, err := store.EndpointGroupService.EndpointGroups()
if err != nil {
return err

View File

@@ -7,6 +7,10 @@ import (
"github.com/portainer/portainer/api/bolt/errors"
)
type DbConnection struct {
*bolt.DB
}
// Itob returns an 8-byte big endian representation of v.
// This function is typically used for encoding integer IDs to byte slices
// so that they can be used as BoltDB keys.
@@ -17,8 +21,8 @@ func Itob(v int) []byte {
}
// CreateBucket is a generic function used to create a bucket inside a bolt database.
func CreateBucket(db *bolt.DB, bucketName string) error {
return db.Update(func(tx *bolt.Tx) error {
func CreateBucket(connection *DbConnection, bucketName string) error {
return connection.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(bucketName))
if err != nil {
return err
@@ -28,10 +32,10 @@ func CreateBucket(db *bolt.DB, bucketName string) error {
}
// GetObject is a generic function used to retrieve an unmarshalled object from a bolt database.
func GetObject(db *bolt.DB, bucketName string, key []byte, object interface{}) error {
func GetObject(connection *DbConnection, bucketName string, key []byte, object interface{}) error {
var data []byte
err := db.View(func(tx *bolt.Tx) error {
err := connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
value := bucket.Get(key)
@@ -52,8 +56,8 @@ func GetObject(db *bolt.DB, bucketName string, key []byte, object interface{}) e
}
// UpdateObject is a generic function used to update an object inside a bolt database.
func UpdateObject(db *bolt.DB, bucketName string, key []byte, object interface{}) error {
return db.Update(func(tx *bolt.Tx) error {
func UpdateObject(connection *DbConnection, bucketName string, key []byte, object interface{}) error {
return connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
data, err := MarshalObject(object)
@@ -71,18 +75,18 @@ func UpdateObject(db *bolt.DB, bucketName string, key []byte, object interface{}
}
// DeleteObject is a generic function used to delete an object inside a bolt database.
func DeleteObject(db *bolt.DB, bucketName string, key []byte) error {
return db.Update(func(tx *bolt.Tx) error {
func DeleteObject(connection *DbConnection, bucketName string, key []byte) error {
return connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
return bucket.Delete(key)
})
}
// GetNextIdentifier is a generic function that returns the specified bucket identifier incremented by 1.
func GetNextIdentifier(db *bolt.DB, bucketName string) int {
func GetNextIdentifier(connection *DbConnection, bucketName string) int {
var identifier int
db.Update(func(tx *bolt.Tx) error {
connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
id, err := bucket.NextSequence()
if err != nil {

41
api/bolt/log/log.go Normal file
View File

@@ -0,0 +1,41 @@
package log
import (
"fmt"
"log"
)
const (
INFO = "INFO"
ERROR = "ERROR"
DEBUG = "DEBUG"
FATAL = "FATAL"
)
type ScopedLog struct {
scope string
}
func NewScopedLog(scope string) *ScopedLog {
return &ScopedLog{scope: scope}
}
func (slog *ScopedLog) print(kind string, message string) {
log.Printf("[%s] [%s] %s", kind, slog.scope, message)
}
func (slog *ScopedLog) Debug(message string) {
slog.print(DEBUG, fmt.Sprintf("[message: %s]", message))
}
func (slog *ScopedLog) Info(message string) {
slog.print(INFO, fmt.Sprintf("[message: %s]", message))
}
func (slog *ScopedLog) Error(message string, err error) {
slog.print(ERROR, fmt.Sprintf("[message: %s] [error: %s]", message, err))
}
func (slog *ScopedLog) NotImplemented(method string) {
log.Fatalf("[%s] [%s] [%s]", FATAL, slog.scope, fmt.Sprintf("%s is not yet implemented", method))
}

1
api/bolt/log/log.test.go Normal file
View File

@@ -0,0 +1 @@
package log

View File

@@ -0,0 +1,19 @@
package migrator
func (m *Migrator) migrateDBVersionToDB30() error {
if err := m.migrateSettingsToDB30(); err != nil {
return err
}
return nil
}
func (m *Migrator) migrateSettingsToDB30() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.OAuthSettings.SSO = false
legacySettings.OAuthSettings.LogoutURI = ""
return m.settingsService.UpdateSettings(legacySettings)
}

View File

@@ -0,0 +1,95 @@
package migrator
import (
"os"
"path"
"testing"
"time"
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/portainer/portainer/api/bolt/settings"
)
var (
testingDBStorePath string
testingDBFileName string
dummyLogoURL string
dbConn *bolt.DB
settingsService *settings.Service
)
// initTestingDBConn creates a raw bolt DB connection
// for unit testing usage only since using NewStore will cause cycle import inside migrator pkg
func initTestingDBConn(storePath, fileName string) (*bolt.DB, error) {
databasePath := path.Join(storePath, fileName)
dbConn, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second})
if err != nil {
return nil, err
}
return dbConn, nil
}
// initTestingDBConn creates a settings service with raw bolt DB connection
// for unit testing usage only since using NewStore will cause cycle import inside migrator pkg
func initTestingSettingsService(dbConn *bolt.DB, preSetObj map[string]interface{}) (*settings.Service, error) {
internalDBConn := &internal.DbConnection{
DB: dbConn,
}
settingsService, err := settings.NewService(internalDBConn)
if err != nil {
return nil, err
}
//insert a obj
if err := internal.UpdateObject(internalDBConn, "settings", []byte("SETTINGS"), preSetObj); err != nil {
return nil, err
}
return settingsService, nil
}
func setup() error {
testingDBStorePath, _ = os.Getwd()
testingDBFileName = "portainer-ee-mig-30.db"
dummyLogoURL = "example.com"
var err error
dbConn, err = initTestingDBConn(testingDBStorePath, testingDBFileName)
if err != nil {
return err
}
dummySettingsObj := map[string]interface{}{
"LogoURL": dummyLogoURL,
}
settingsService, err = initTestingSettingsService(dbConn, dummySettingsObj)
if err != nil {
return err
}
return nil
}
func TestMigrateSettings(t *testing.T) {
if err := setup(); err != nil {
t.Errorf("failed to complete testing setups, err: %v", err)
}
defer dbConn.Close()
defer os.Remove(testingDBFileName)
m := &Migrator{
db: dbConn,
settingsService: settingsService,
}
if err := m.migrateSettingsToDB30(); err != nil {
t.Errorf("failed to update settings: %v", err)
}
updatedSettings, err := m.settingsService.Settings()
if err != nil {
t.Errorf("failed to retrieve the updated settings: %v", err)
}
if updatedSettings.LogoURL != dummyLogoURL {
t.Errorf("unexpected value changes in the updated settings, want LogoURL value: %s, got LogoURL value: %s", dummyLogoURL, updatedSettings.LogoURL)
}
if updatedSettings.OAuthSettings.SSO != false {
t.Errorf("unexpected default OAuth SSO setting, want: false, got: %t", updatedSettings.OAuthSettings.SSO)
}
if updatedSettings.OAuthSettings.LogoutURI != "" {
t.Errorf("unexpected default OAuth HideInternalAuth setting, want:, got: %s", updatedSettings.OAuthSettings.LogoutURI)
}
}

View File

@@ -0,0 +1,213 @@
package migrator
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
endpointutils "github.com/portainer/portainer/api/internal/endpoint"
snapshotutils "github.com/portainer/portainer/api/internal/snapshot"
)
func (m *Migrator) migrateDBVersionToDB32() error {
err := m.updateRegistriesToDB32()
if err != nil {
return err
}
err = m.updateDockerhubToDB32()
if err != nil {
return err
}
if err := m.updateVolumeResourceControlToDB32(); err != nil {
return err
}
return nil
}
func (m *Migrator) updateRegistriesToDB32() error {
registries, err := m.registryService.Registries()
if err != nil {
return err
}
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, registry := range registries {
registry.RegistryAccesses = portainer.RegistryAccesses{}
for _, endpoint := range endpoints {
filteredUserAccessPolicies := portainer.UserAccessPolicies{}
for userId, registryPolicy := range registry.UserAccessPolicies {
if _, found := endpoint.UserAccessPolicies[userId]; found {
filteredUserAccessPolicies[userId] = registryPolicy
}
}
filteredTeamAccessPolicies := portainer.TeamAccessPolicies{}
for teamId, registryPolicy := range registry.TeamAccessPolicies {
if _, found := endpoint.TeamAccessPolicies[teamId]; found {
filteredTeamAccessPolicies[teamId] = registryPolicy
}
}
registry.RegistryAccesses[endpoint.ID] = portainer.RegistryAccessPolicies{
UserAccessPolicies: filteredUserAccessPolicies,
TeamAccessPolicies: filteredTeamAccessPolicies,
Namespaces: []string{},
}
}
m.registryService.UpdateRegistry(registry.ID, &registry)
}
return nil
}
func (m *Migrator) updateDockerhubToDB32() error {
dockerhub, err := m.dockerhubService.DockerHub()
if err == errors.ErrObjectNotFound {
return nil
} else if err != nil {
return err
}
if !dockerhub.Authentication {
return nil
}
registry := &portainer.Registry{
Type: portainer.DockerHubRegistry,
Name: "Dockerhub (authenticated - migrated)",
URL: "docker.io",
Authentication: true,
Username: dockerhub.Username,
Password: dockerhub.Password,
RegistryAccesses: portainer.RegistryAccesses{},
}
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range endpoints {
if endpoint.Type != portainer.KubernetesLocalEnvironment &&
endpoint.Type != portainer.AgentOnKubernetesEnvironment &&
endpoint.Type != portainer.EdgeAgentOnKubernetesEnvironment {
userAccessPolicies := portainer.UserAccessPolicies{}
for userId := range endpoint.UserAccessPolicies {
if _, found := endpoint.UserAccessPolicies[userId]; found {
userAccessPolicies[userId] = portainer.AccessPolicy{
RoleID: 0,
}
}
}
teamAccessPolicies := portainer.TeamAccessPolicies{}
for teamId := range endpoint.TeamAccessPolicies {
if _, found := endpoint.TeamAccessPolicies[teamId]; found {
teamAccessPolicies[teamId] = portainer.AccessPolicy{
RoleID: 0,
}
}
}
registry.RegistryAccesses[endpoint.ID] = portainer.RegistryAccessPolicies{
UserAccessPolicies: userAccessPolicies,
TeamAccessPolicies: teamAccessPolicies,
Namespaces: []string{},
}
}
}
return m.registryService.CreateRegistry(registry)
}
func (m *Migrator) updateVolumeResourceControlToDB32() error {
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return fmt.Errorf("failed fetching endpoints: %w", err)
}
resourceControls, err := m.resourceControlService.ResourceControls()
if err != nil {
return fmt.Errorf("failed fetching resource controls: %w", err)
}
toUpdate := map[portainer.ResourceControlID]string{}
volumeResourceControls := map[string]*portainer.ResourceControl{}
for i := range resourceControls {
resourceControl := resourceControls[i]
if resourceControl.Type == portainer.VolumeResourceControl {
volumeResourceControls[resourceControl.ResourceID] = &resourceControl
}
}
for _, endpoint := range endpoints {
if !endpointutils.IsDockerEndpoint(&endpoint) {
continue
}
totalSnapshots := len(endpoint.Snapshots)
if totalSnapshots == 0 {
continue
}
snapshot := endpoint.Snapshots[totalSnapshots-1]
endpointDockerID, err := snapshotutils.FetchDockerID(snapshot)
if err != nil {
return fmt.Errorf("failed fetching endpoint docker id: %w", err)
}
if volumesData, done := snapshot.SnapshotRaw.Volumes.(map[string]interface{}); done {
if volumesData["Volumes"] == nil {
continue
}
findResourcesToUpdateForDB32(endpointDockerID, volumesData, toUpdate, volumeResourceControls)
}
}
for _, resourceControl := range volumeResourceControls {
if newResourceID, ok := toUpdate[resourceControl.ID]; ok {
resourceControl.ResourceID = newResourceID
err := m.resourceControlService.UpdateResourceControl(resourceControl.ID, resourceControl)
if err != nil {
return fmt.Errorf("failed updating resource control %d: %w", resourceControl.ID, err)
}
} else {
err := m.resourceControlService.DeleteResourceControl(resourceControl.ID)
if err != nil {
return fmt.Errorf("failed deleting resource control %d: %w", resourceControl.ID, err)
}
}
}
return nil
}
func findResourcesToUpdateForDB32(dockerID string, volumesData map[string]interface{}, toUpdate map[portainer.ResourceControlID]string, volumeResourceControls map[string]*portainer.ResourceControl) {
volumes := volumesData["Volumes"].([]interface{})
for _, volumeMeta := range volumes {
volume := volumeMeta.(map[string]interface{})
volumeName := volume["Name"].(string)
oldResourceID := fmt.Sprintf("%s%s", volumeName, volume["CreatedAt"].(string))
resourceControl, ok := volumeResourceControls[oldResourceID]
if ok {
toUpdate[resourceControl.ID] = fmt.Sprintf("%s_%s", volumeName, dockerID)
}
}
}

View File

@@ -3,10 +3,12 @@ package migrator
import (
"github.com/boltdb/bolt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/dockerhub"
"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"
plog "github.com/portainer/portainer/api/bolt/log"
"github.com/portainer/portainer/api/bolt/registry"
"github.com/portainer/portainer/api/bolt/resourcecontrol"
"github.com/portainer/portainer/api/bolt/role"
@@ -20,6 +22,8 @@ import (
"github.com/portainer/portainer/api/internal/authorization"
)
var migrateLog = plog.NewScopedLog("bolt, migrate")
type (
// Migrator defines a service to migrate data after a Portainer version update.
Migrator struct {
@@ -41,6 +45,7 @@ type (
versionService *version.Service
fileService portainer.FileService
authorizationService *authorization.Service
dockerhubService *dockerhub.Service
}
// Parameters represents the required parameters to create a new Migrator instance.
@@ -63,6 +68,7 @@ type (
VersionService *version.Service
FileService portainer.FileService
AuthorizationService *authorization.Service
DockerhubService *dockerhub.Service
}
)
@@ -87,6 +93,7 @@ func NewMigrator(parameters *Parameters) *Migrator {
versionService: parameters.VersionService,
fileService: parameters.FileService,
authorizationService: parameters.AuthorizationService,
dockerhubService: parameters.DockerhubService,
}
}
@@ -358,5 +365,21 @@ func (m *Migrator) Migrate() error {
}
}
// Portainer 2.6.0
if m.currentDBVersion < 30 {
err := m.migrateDBVersionToDB30()
if err != nil {
return err
}
}
// Portainer 2.9.0
if m.currentDBVersion < 32 {
err := m.migrateDBVersionToDB32()
if err != nil {
return err
}
}
return m.versionService.StoreDBVersion(portainer.DBVersion)
}

View File

@@ -1,7 +1,7 @@
package registry
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
@@ -14,18 +14,18 @@ const (
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -34,7 +34,7 @@ func (service *Service) Registry(ID portainer.RegistryID) (*portainer.Registry,
var registry portainer.Registry
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &registry)
err := internal.GetObject(service.connection, BucketName, identifier, &registry)
if err != nil {
return nil, err
}
@@ -46,7 +46,7 @@ func (service *Service) Registry(ID portainer.RegistryID) (*portainer.Registry,
func (service *Service) Registries() ([]portainer.Registry, error) {
var registries = make([]portainer.Registry, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -67,7 +67,7 @@ func (service *Service) Registries() ([]portainer.Registry, error) {
// CreateRegistry creates a new registry.
func (service *Service) CreateRegistry(registry *portainer.Registry) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
@@ -85,11 +85,11 @@ func (service *Service) CreateRegistry(registry *portainer.Registry) error {
// UpdateRegistry updates an registry.
func (service *Service) UpdateRegistry(ID portainer.RegistryID, registry *portainer.Registry) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, registry)
return internal.UpdateObject(service.connection, BucketName, identifier, registry)
}
// DeleteRegistry deletes an registry.
func (service *Service) DeleteRegistry(ID portainer.RegistryID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}

View File

@@ -1,7 +1,7 @@
package resourcecontrol
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
@@ -14,18 +14,18 @@ const (
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -34,7 +34,7 @@ func (service *Service) ResourceControl(ID portainer.ResourceControlID) (*portai
var resourceControl portainer.ResourceControl
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &resourceControl)
err := internal.GetObject(service.connection, BucketName, identifier, &resourceControl)
if err != nil {
return nil, err
}
@@ -48,7 +48,7 @@ func (service *Service) ResourceControl(ID portainer.ResourceControlID) (*portai
func (service *Service) ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
var resourceControl *portainer.ResourceControl
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -82,7 +82,7 @@ func (service *Service) ResourceControlByResourceIDAndType(resourceID string, re
func (service *Service) ResourceControls() ([]portainer.ResourceControl, error) {
var rcs = make([]portainer.ResourceControl, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -103,7 +103,7 @@ func (service *Service) ResourceControls() ([]portainer.ResourceControl, error)
// CreateResourceControl creates a new ResourceControl object
func (service *Service) CreateResourceControl(resourceControl *portainer.ResourceControl) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
@@ -121,11 +121,11 @@ func (service *Service) CreateResourceControl(resourceControl *portainer.Resourc
// UpdateResourceControl saves a ResourceControl object.
func (service *Service) UpdateResourceControl(ID portainer.ResourceControlID, resourceControl *portainer.ResourceControl) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, resourceControl)
return internal.UpdateObject(service.connection, BucketName, identifier, resourceControl)
}
// DeleteResourceControl deletes a ResourceControl object by ID
func (service *Service) DeleteResourceControl(ID portainer.ResourceControlID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}

View File

@@ -1,7 +1,7 @@
package role
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
@@ -14,18 +14,18 @@ const (
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -34,7 +34,7 @@ func (service *Service) Role(ID portainer.RoleID) (*portainer.Role, error) {
var set portainer.Role
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &set)
err := internal.GetObject(service.connection, BucketName, identifier, &set)
if err != nil {
return nil, err
}
@@ -46,7 +46,7 @@ func (service *Service) Role(ID portainer.RoleID) (*portainer.Role, error) {
func (service *Service) Roles() ([]portainer.Role, error) {
var sets = make([]portainer.Role, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -67,7 +67,7 @@ func (service *Service) Roles() ([]portainer.Role, error) {
// CreateRole creates a new Role.
func (service *Service) CreateRole(role *portainer.Role) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
@@ -85,5 +85,5 @@ func (service *Service) CreateRole(role *portainer.Role) error {
// UpdateRole updates a role.
func (service *Service) UpdateRole(ID portainer.RoleID, role *portainer.Role) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, role)
return internal.UpdateObject(service.connection, BucketName, identifier, role)
}

View File

@@ -1,7 +1,7 @@
package schedule
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
@@ -14,18 +14,18 @@ const (
// Service represents a service for managing schedule data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -34,7 +34,7 @@ func (service *Service) Schedule(ID portainer.ScheduleID) (*portainer.Schedule,
var schedule portainer.Schedule
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &schedule)
err := internal.GetObject(service.connection, BucketName, identifier, &schedule)
if err != nil {
return nil, err
}
@@ -45,20 +45,20 @@ func (service *Service) Schedule(ID portainer.ScheduleID) (*portainer.Schedule,
// UpdateSchedule updates a schedule.
func (service *Service) UpdateSchedule(ID portainer.ScheduleID, schedule *portainer.Schedule) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, schedule)
return internal.UpdateObject(service.connection, BucketName, identifier, schedule)
}
// DeleteSchedule deletes a schedule.
func (service *Service) DeleteSchedule(ID portainer.ScheduleID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}
// Schedules return a array containing all the schedules.
func (service *Service) Schedules() ([]portainer.Schedule, error) {
var schedules = make([]portainer.Schedule, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -82,7 +82,7 @@ func (service *Service) Schedules() ([]portainer.Schedule, error) {
func (service *Service) SchedulesByJobType(jobType portainer.JobType) ([]portainer.Schedule, error) {
var schedules = make([]portainer.Schedule, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -105,7 +105,7 @@ func (service *Service) SchedulesByJobType(jobType portainer.JobType) ([]portain
// CreateSchedule assign an ID to a new schedule and saves it.
func (service *Service) CreateSchedule(schedule *portainer.Schedule) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
// We manually manage sequences for schedules
@@ -125,5 +125,5 @@ func (service *Service) CreateSchedule(schedule *portainer.Schedule) error {
// GetNextIdentifier returns the next identifier for a schedule.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
return internal.GetNextIdentifier(service.connection, BucketName)
}

258
api/bolt/services.go Normal file
View File

@@ -0,0 +1,258 @@
package bolt
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/customtemplate"
"github.com/portainer/portainer/api/bolt/dockerhub"
"github.com/portainer/portainer/api/bolt/edgegroup"
"github.com/portainer/portainer/api/bolt/edgejob"
"github.com/portainer/portainer/api/bolt/edgestack"
"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"
"github.com/portainer/portainer/api/bolt/role"
"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/team"
"github.com/portainer/portainer/api/bolt/teammembership"
"github.com/portainer/portainer/api/bolt/tunnelserver"
"github.com/portainer/portainer/api/bolt/user"
"github.com/portainer/portainer/api/bolt/version"
"github.com/portainer/portainer/api/bolt/webhook"
)
func (store *Store) initServices() error {
authorizationsetService, err := role.NewService(store.connection)
if err != nil {
return err
}
store.RoleService = authorizationsetService
customTemplateService, err := customtemplate.NewService(store.connection)
if err != nil {
return err
}
store.CustomTemplateService = customTemplateService
dockerhubService, err := dockerhub.NewService(store.connection)
if err != nil {
return err
}
store.DockerHubService = dockerhubService
edgeStackService, err := edgestack.NewService(store.connection)
if err != nil {
return err
}
store.EdgeStackService = edgeStackService
edgeGroupService, err := edgegroup.NewService(store.connection)
if err != nil {
return err
}
store.EdgeGroupService = edgeGroupService
edgeJobService, err := edgejob.NewService(store.connection)
if err != nil {
return err
}
store.EdgeJobService = edgeJobService
endpointgroupService, err := endpointgroup.NewService(store.connection)
if err != nil {
return err
}
store.EndpointGroupService = endpointgroupService
endpointService, err := endpoint.NewService(store.connection)
if err != nil {
return err
}
store.EndpointService = endpointService
endpointRelationService, err := endpointrelation.NewService(store.connection)
if err != nil {
return err
}
store.EndpointRelationService = endpointRelationService
extensionService, err := extension.NewService(store.connection)
if err != nil {
return err
}
store.ExtensionService = extensionService
registryService, err := registry.NewService(store.connection)
if err != nil {
return err
}
store.RegistryService = registryService
resourcecontrolService, err := resourcecontrol.NewService(store.connection)
if err != nil {
return err
}
store.ResourceControlService = resourcecontrolService
settingsService, err := settings.NewService(store.connection)
if err != nil {
return err
}
store.SettingsService = settingsService
stackService, err := stack.NewService(store.connection)
if err != nil {
return err
}
store.StackService = stackService
tagService, err := tag.NewService(store.connection)
if err != nil {
return err
}
store.TagService = tagService
teammembershipService, err := teammembership.NewService(store.connection)
if err != nil {
return err
}
store.TeamMembershipService = teammembershipService
teamService, err := team.NewService(store.connection)
if err != nil {
return err
}
store.TeamService = teamService
tunnelServerService, err := tunnelserver.NewService(store.connection)
if err != nil {
return err
}
store.TunnelServerService = tunnelServerService
userService, err := user.NewService(store.connection)
if err != nil {
return err
}
store.UserService = userService
versionService, err := version.NewService(store.connection)
if err != nil {
return err
}
store.VersionService = versionService
webhookService, err := webhook.NewService(store.connection)
if err != nil {
return err
}
store.WebhookService = webhookService
scheduleService, err := schedule.NewService(store.connection)
if err != nil {
return err
}
store.ScheduleService = scheduleService
return nil
}
// CustomTemplate gives access to the CustomTemplate data management layer
func (store *Store) CustomTemplate() portainer.CustomTemplateService {
return store.CustomTemplateService
}
// EdgeGroup gives access to the EdgeGroup data management layer
func (store *Store) EdgeGroup() portainer.EdgeGroupService {
return store.EdgeGroupService
}
// EdgeJob gives access to the EdgeJob data management layer
func (store *Store) EdgeJob() portainer.EdgeJobService {
return store.EdgeJobService
}
// EdgeStack gives access to the EdgeStack data management layer
func (store *Store) EdgeStack() portainer.EdgeStackService {
return store.EdgeStackService
}
// Endpoint gives access to the Endpoint data management layer
func (store *Store) Endpoint() portainer.EndpointService {
return store.EndpointService
}
// EndpointGroup gives access to the EndpointGroup data management layer
func (store *Store) EndpointGroup() portainer.EndpointGroupService {
return store.EndpointGroupService
}
// EndpointRelation gives access to the EndpointRelation data management layer
func (store *Store) EndpointRelation() portainer.EndpointRelationService {
return store.EndpointRelationService
}
// Registry gives access to the Registry data management layer
func (store *Store) Registry() portainer.RegistryService {
return store.RegistryService
}
// ResourceControl gives access to the ResourceControl data management layer
func (store *Store) ResourceControl() portainer.ResourceControlService {
return store.ResourceControlService
}
// Role gives access to the Role data management layer
func (store *Store) Role() portainer.RoleService {
return store.RoleService
}
// Settings gives access to the Settings data management layer
func (store *Store) Settings() portainer.SettingsService {
return store.SettingsService
}
// Stack gives access to the Stack data management layer
func (store *Store) Stack() portainer.StackService {
return store.StackService
}
// Tag gives access to the Tag data management layer
func (store *Store) Tag() portainer.TagService {
return store.TagService
}
// TeamMembership gives access to the TeamMembership data management layer
func (store *Store) TeamMembership() portainer.TeamMembershipService {
return store.TeamMembershipService
}
// Team gives access to the Team data management layer
func (store *Store) Team() portainer.TeamService {
return store.TeamService
}
// TunnelServer gives access to the TunnelServer data management layer
func (store *Store) TunnelServer() portainer.TunnelServerService {
return store.TunnelServerService
}
// User gives access to the User data management layer
func (store *Store) User() portainer.UserService {
return store.UserService
}
// Version gives access to the Version data management layer
func (store *Store) Version() portainer.VersionService {
return store.VersionService
}
// Webhook gives access to the Webhook data management layer
func (store *Store) Webhook() portainer.WebhookService {
return store.WebhookService
}

View File

@@ -1,10 +1,8 @@
package settings
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
)
const (
@@ -15,18 +13,18 @@ const (
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -34,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) Settings() (*portainer.Settings, error) {
var settings portainer.Settings
err := internal.GetObject(service.db, BucketName, []byte(settingsKey), &settings)
err := internal.GetObject(service.connection, BucketName, []byte(settingsKey), &settings)
if err != nil {
return nil, err
}
@@ -44,5 +42,5 @@ func (service *Service) Settings() (*portainer.Settings, error) {
// UpdateSettings persists a Settings object.
func (service *Service) UpdateSettings(settings *portainer.Settings) error {
return internal.UpdateObject(service.db, BucketName, []byte(settingsKey), settings)
return internal.UpdateObject(service.connection, BucketName, []byte(settingsKey), settings)
}

View File

@@ -1,7 +1,7 @@
package stack
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/internal"
@@ -15,18 +15,18 @@ const (
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -35,7 +35,7 @@ func (service *Service) Stack(ID portainer.StackID) (*portainer.Stack, error) {
var stack portainer.Stack
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &stack)
err := internal.GetObject(service.connection, BucketName, identifier, &stack)
if err != nil {
return nil, err
}
@@ -47,7 +47,7 @@ func (service *Service) Stack(ID portainer.StackID) (*portainer.Stack, error) {
func (service *Service) StackByName(name string) (*portainer.Stack, error) {
var stack *portainer.Stack
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -78,7 +78,7 @@ func (service *Service) StackByName(name string) (*portainer.Stack, error) {
func (service *Service) Stacks() ([]portainer.Stack, error) {
var stacks = make([]portainer.Stack, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -99,12 +99,12 @@ func (service *Service) Stacks() ([]portainer.Stack, error) {
// GetNextIdentifier returns the next identifier for a stack.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
return internal.GetNextIdentifier(service.connection, BucketName)
}
// CreateStack creates a new stack.
func (service *Service) CreateStack(stack *portainer.Stack) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
// We manually manage sequences for stacks
@@ -125,11 +125,11 @@ func (service *Service) CreateStack(stack *portainer.Stack) error {
// UpdateStack updates a stack.
func (service *Service) UpdateStack(ID portainer.StackID, stack *portainer.Stack) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, stack)
return internal.UpdateObject(service.connection, BucketName, identifier, stack)
}
// DeleteStack deletes a stack.
func (service *Service) DeleteStack(ID portainer.StackID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}

View File

@@ -1,7 +1,7 @@
package tag
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
@@ -14,18 +14,18 @@ const (
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -33,7 +33,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) Tags() ([]portainer.Tag, error) {
var tags = make([]portainer.Tag, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -57,7 +57,7 @@ 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)
err := internal.GetObject(service.connection, BucketName, identifier, &tag)
if err != nil {
return nil, err
}
@@ -67,7 +67,7 @@ func (service *Service) Tag(ID portainer.TagID) (*portainer.Tag, error) {
// CreateTag creates a new tag.
func (service *Service) CreateTag(tag *portainer.Tag) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
@@ -85,11 +85,11 @@ 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)
return internal.UpdateObject(service.connection, BucketName, identifier, tag)
}
// DeleteTag deletes a tag.
func (service *Service) DeleteTag(ID portainer.TagID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}

View File

@@ -17,18 +17,18 @@ const (
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -37,7 +37,7 @@ func (service *Service) Team(ID portainer.TeamID) (*portainer.Team, error) {
var team portainer.Team
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &team)
err := internal.GetObject(service.connection, BucketName, identifier, &team)
if err != nil {
return nil, err
}
@@ -49,7 +49,7 @@ func (service *Service) Team(ID portainer.TeamID) (*portainer.Team, error) {
func (service *Service) TeamByName(name string) (*portainer.Team, error) {
var team *portainer.Team
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -80,7 +80,7 @@ func (service *Service) TeamByName(name string) (*portainer.Team, error) {
func (service *Service) Teams() ([]portainer.Team, error) {
var teams = make([]portainer.Team, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -102,12 +102,12 @@ func (service *Service) Teams() ([]portainer.Team, error) {
// UpdateTeam saves a Team.
func (service *Service) UpdateTeam(ID portainer.TeamID, team *portainer.Team) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, team)
return internal.UpdateObject(service.connection, BucketName, identifier, team)
}
// CreateTeam creates a new Team.
func (service *Service) CreateTeam(team *portainer.Team) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
@@ -125,5 +125,5 @@ func (service *Service) CreateTeam(team *portainer.Team) error {
// DeleteTeam deletes a Team.
func (service *Service) DeleteTeam(ID portainer.TeamID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}

View File

@@ -1,7 +1,7 @@
package teammembership
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
@@ -14,18 +14,18 @@ const (
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -34,7 +34,7 @@ func (service *Service) TeamMembership(ID portainer.TeamMembershipID) (*portaine
var membership portainer.TeamMembership
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &membership)
err := internal.GetObject(service.connection, BucketName, identifier, &membership)
if err != nil {
return nil, err
}
@@ -46,7 +46,7 @@ func (service *Service) TeamMembership(ID portainer.TeamMembershipID) (*portaine
func (service *Service) TeamMemberships() ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -69,7 +69,7 @@ func (service *Service) TeamMemberships() ([]portainer.TeamMembership, error) {
func (service *Service) TeamMembershipsByUserID(userID portainer.UserID) ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -95,7 +95,7 @@ func (service *Service) TeamMembershipsByUserID(userID portainer.UserID) ([]port
func (service *Service) TeamMembershipsByTeamID(teamID portainer.TeamID) ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -120,12 +120,12 @@ func (service *Service) TeamMembershipsByTeamID(teamID portainer.TeamID) ([]port
// UpdateTeamMembership saves a TeamMembership object.
func (service *Service) UpdateTeamMembership(ID portainer.TeamMembershipID, membership *portainer.TeamMembership) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, membership)
return internal.UpdateObject(service.connection, BucketName, identifier, membership)
}
// CreateTeamMembership creates a new TeamMembership object.
func (service *Service) CreateTeamMembership(membership *portainer.TeamMembership) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
@@ -143,12 +143,12 @@ func (service *Service) CreateTeamMembership(membership *portainer.TeamMembershi
// DeleteTeamMembership deletes a TeamMembership object.
func (service *Service) DeleteTeamMembership(ID portainer.TeamMembershipID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}
// DeleteTeamMembershipByUserID deletes all the TeamMembership object associated to a UserID.
func (service *Service) DeleteTeamMembershipByUserID(userID portainer.UserID) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -173,7 +173,7 @@ func (service *Service) DeleteTeamMembershipByUserID(userID portainer.UserID) er
// DeleteTeamMembershipByTeamID deletes all the TeamMembership object associated to a TeamID.
func (service *Service) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()

View File

@@ -1,10 +1,8 @@
package tunnelserver
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
)
const (
@@ -15,18 +13,18 @@ const (
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -34,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) Info() (*portainer.TunnelServerInfo, error) {
var info portainer.TunnelServerInfo
err := internal.GetObject(service.db, BucketName, []byte(infoKey), &info)
err := internal.GetObject(service.connection, BucketName, []byte(infoKey), &info)
if err != nil {
return nil, err
}
@@ -44,5 +42,5 @@ func (service *Service) Info() (*portainer.TunnelServerInfo, error) {
// UpdateInfo persists a TunnelServerInfo object.
func (service *Service) UpdateInfo(settings *portainer.TunnelServerInfo) error {
return internal.UpdateObject(service.db, BucketName, []byte(infoKey), settings)
return internal.UpdateObject(service.connection, BucketName, []byte(infoKey), settings)
}

View File

@@ -17,18 +17,18 @@ const (
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -37,7 +37,7 @@ func (service *Service) User(ID portainer.UserID) (*portainer.User, error) {
var user portainer.User
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &user)
err := internal.GetObject(service.connection, BucketName, identifier, &user)
if err != nil {
return nil, err
}
@@ -51,7 +51,7 @@ func (service *Service) UserByUsername(username string) (*portainer.User, error)
username = strings.ToLower(username)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -81,7 +81,7 @@ func (service *Service) UserByUsername(username string) (*portainer.User, error)
func (service *Service) Users() ([]portainer.User, error) {
var users = make([]portainer.User, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -103,7 +103,7 @@ func (service *Service) Users() ([]portainer.User, error) {
// UsersByRole return an array containing all the users with the specified role.
func (service *Service) UsersByRole(role portainer.UserRole) ([]portainer.User, error) {
var users = make([]portainer.User, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -128,12 +128,12 @@ func (service *Service) UsersByRole(role portainer.UserRole) ([]portainer.User,
func (service *Service) UpdateUser(ID portainer.UserID, user *portainer.User) error {
identifier := internal.Itob(int(ID))
user.Username = strings.ToLower(user.Username)
return internal.UpdateObject(service.db, BucketName, identifier, user)
return internal.UpdateObject(service.connection, BucketName, identifier, user)
}
// CreateUser creates a new user.
func (service *Service) CreateUser(user *portainer.User) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
@@ -152,5 +152,5 @@ func (service *Service) CreateUser(user *portainer.User) error {
// DeleteUser deletes a user.
func (service *Service) DeleteUser(ID portainer.UserID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}

View File

@@ -19,18 +19,18 @@ const (
// Service represents a service to manage stored versions.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -38,7 +38,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) DBVersion() (int, error) {
var data []byte
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
value := bucket.Get([]byte(versionKey))
@@ -75,7 +75,7 @@ func (service *Service) Edition() (portainer.SoftwareEdition, error) {
// StoreDBVersion store the database version.
func (service *Service) StoreDBVersion(version int) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data := []byte(strconv.Itoa(version))
@@ -87,7 +87,7 @@ func (service *Service) StoreDBVersion(version int) error {
func (service *Service) InstanceID() (string, error) {
var data []byte
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
value := bucket.Get([]byte(instanceKey))
@@ -109,7 +109,7 @@ func (service *Service) InstanceID() (string, error) {
// StoreInstanceID store the instance ID.
func (service *Service) StoreInstanceID(ID string) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data := []byte(ID)
@@ -120,7 +120,7 @@ func (service *Service) StoreInstanceID(ID string) error {
func (service *Service) getKey(key string) ([]byte, error) {
var data []byte
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
value := bucket.Get([]byte(key))
@@ -142,7 +142,7 @@ func (service *Service) getKey(key string) ([]byte, error) {
}
func (service *Service) setKey(key string, value string) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data := []byte(value)

View File

@@ -1,7 +1,7 @@
package webhook
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/internal"
@@ -15,18 +15,18 @@ const (
// Service represents a service for managing webhook data.
type Service struct {
db *bolt.DB
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
connection: connection,
}, nil
}
@@ -34,7 +34,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) Webhooks() ([]portainer.Webhook, error) {
var webhooks = make([]portainer.Webhook, 0)
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -58,7 +58,7 @@ func (service *Service) Webhook(ID portainer.WebhookID) (*portainer.Webhook, err
var webhook portainer.Webhook
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &webhook)
err := internal.GetObject(service.connection, BucketName, identifier, &webhook)
if err != nil {
return nil, err
}
@@ -70,7 +70,7 @@ func (service *Service) Webhook(ID portainer.WebhookID) (*portainer.Webhook, err
func (service *Service) WebhookByResourceID(ID string) (*portainer.Webhook, error) {
var webhook *portainer.Webhook
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -101,7 +101,7 @@ func (service *Service) WebhookByResourceID(ID string) (*portainer.Webhook, erro
func (service *Service) WebhookByToken(token string) (*portainer.Webhook, error) {
var webhook *portainer.Webhook
err := service.db.View(func(tx *bolt.Tx) error {
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -131,12 +131,12 @@ func (service *Service) WebhookByToken(token string) (*portainer.Webhook, error)
// DeleteWebhook deletes a webhook.
func (service *Service) DeleteWebhook(ID portainer.WebhookID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
return internal.DeleteObject(service.connection, BucketName, identifier)
}
// CreateWebhook assign an ID to a new webhook and saves it.
func (service *Service) CreateWebhook(webhook *portainer.Webhook) error {
return service.db.Update(func(tx *bolt.Tx) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()

View File

@@ -1,6 +1,7 @@
package chisel
import (
"context"
"fmt"
"log"
"strconv"
@@ -9,7 +10,7 @@ import (
"github.com/dchest/uniuri"
chserver "github.com/jpillora/chisel/server"
cmap "github.com/orcaman/concurrent-map"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
)
@@ -29,13 +30,15 @@ type Service struct {
dataStore portainer.DataStore
snapshotService portainer.SnapshotService
chiselServer *chserver.Server
shutdownCtx context.Context
}
// NewService returns a pointer to a new instance of Service
func NewService(dataStore portainer.DataStore) *Service {
func NewService(dataStore portainer.DataStore, shutdownCtx context.Context) *Service {
return &Service{
tunnelDetailsMap: cmap.New(),
dataStore: dataStore,
shutdownCtx: shutdownCtx,
}
}
@@ -83,6 +86,11 @@ func (service *Service) StartTunnelServer(addr, port string, snapshotService por
return nil
}
// StopTunnelServer stops tunnel http server
func (service *Service) StopTunnelServer() error {
return service.chiselServer.Close()
}
func (service *Service) retrievePrivateKeySeed() (string, error) {
var serverInfo *portainer.TunnelServerInfo
@@ -108,13 +116,16 @@ func (service *Service) retrievePrivateKeySeed() (string, error) {
func (service *Service) startTunnelVerificationLoop() {
log.Printf("[DEBUG] [chisel, monitoring] [check_interval_seconds: %f] [message: starting tunnel management process]", tunnelCleanupInterval.Seconds())
ticker := time.NewTicker(tunnelCleanupInterval)
stopSignal := make(chan struct{})
for {
select {
case <-ticker.C:
service.checkTunnels()
case <-stopSignal:
case <-service.shutdownCtx.Done():
log.Println("[DEBUG] Shutting down tunnel service")
if err := service.StopTunnelServer(); err != nil {
log.Printf("Stopped tunnel service: %s", err)
}
ticker.Stop()
return
}

View File

@@ -5,7 +5,7 @@ import (
"log"
"time"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"os"
"path/filepath"

19
api/cmd/portainer/log.go Normal file
View File

@@ -0,0 +1,19 @@
package main
import (
"log"
"github.com/sirupsen/logrus"
)
func configureLogger() {
logger := logrus.New() // logger is to implicitly substitute stdlib's log
log.SetOutput(logger.Writer())
formatter := &logrus.TextFormatter{DisableTimestamp: true, DisableLevelTruncation: true}
logger.SetFormatter(formatter)
logrus.SetFormatter(formatter)
logger.SetLevel(logrus.DebugLevel)
logrus.SetLevel(logrus.DebugLevel)
}

View File

@@ -1,11 +1,12 @@
package main
import (
"context"
"log"
"os"
"strings"
"time"
wrapper "github.com/portainer/docker-compose-wrapper"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt"
"github.com/portainer/portainer/api/chisel"
@@ -20,6 +21,8 @@ import (
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/portainer/api/http/proxy"
kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/edge"
"github.com/portainer/portainer/api/internal/snapshot"
"github.com/portainer/portainer/api/jwt"
"github.com/portainer/portainer/api/kubernetes"
@@ -67,7 +70,7 @@ func initDataStore(dataStorePath string, fileService portainer.FileService) port
log.Fatalf("failed initializing data store: %v", err)
}
err = store.MigrateData()
err = store.MigrateData(false)
if err != nil {
log.Fatalf("failed migration: %v", err)
}
@@ -75,20 +78,25 @@ func initDataStore(dataStorePath string, fileService portainer.FileService) port
}
func initComposeStackManager(assetsPath string, dataStorePath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
composeWrapper := exec.NewComposeWrapper(assetsPath, proxyManager)
if composeWrapper != nil {
return composeWrapper
composeWrapper, err := exec.NewComposeStackManager(assetsPath, dataStorePath, proxyManager)
if err != nil {
if err == wrapper.ErrBinaryNotFound {
log.Printf("[INFO] [message: docker-compose binary not found, falling back to libcompose]")
return libcompose.NewComposeStackManager(dataStorePath, reverseTunnelService)
}
log.Fatalf("failed initalizing compose stack manager; err=%s", err)
}
return libcompose.NewComposeStackManager(dataStorePath, reverseTunnelService)
return composeWrapper
}
func initSwarmStackManager(assetsPath string, dataStorePath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) (portainer.SwarmStackManager, error) {
return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService, reverseTunnelService)
}
func initKubernetesDeployer(assetsPath string) portainer.KubernetesDeployer {
return exec.NewKubernetesDeployer(assetsPath)
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, assetsPath string) portainer.KubernetesDeployer {
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, assetsPath)
}
func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error) {
@@ -132,15 +140,15 @@ func initDockerClientFactory(signatureService portainer.DigitalSignatureService,
return docker.NewClientFactory(signatureService, reverseTunnelService)
}
func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, instanceID string) *kubecli.ClientFactory {
return kubecli.NewClientFactory(signatureService, reverseTunnelService, instanceID)
func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, instanceID string, dataStore portainer.DataStore) *kubecli.ClientFactory {
return kubecli.NewClientFactory(signatureService, reverseTunnelService, instanceID, dataStore)
}
func initSnapshotService(snapshotInterval string, dataStore portainer.DataStore, dockerClientFactory *docker.ClientFactory, kubernetesClientFactory *kubecli.ClientFactory) (portainer.SnapshotService, error) {
func initSnapshotService(snapshotInterval string, dataStore portainer.DataStore, dockerClientFactory *docker.ClientFactory, kubernetesClientFactory *kubecli.ClientFactory, shutdownCtx context.Context) (portainer.SnapshotService, error) {
dockerSnapshotter := docker.NewSnapshotter(dockerClientFactory)
kubernetesSnapshotter := kubernetes.NewSnapshotter(kubernetesClientFactory)
snapshotService, err := snapshot.NewService(snapshotInterval, dataStore, dockerSnapshotter, kubernetesSnapshotter)
snapshotService, err := snapshot.NewService(snapshotInterval, dataStore, dockerSnapshotter, kubernetesSnapshotter, shutdownCtx)
if err != nil {
return nil, err
}
@@ -148,21 +156,6 @@ func initSnapshotService(snapshotInterval string, dataStore portainer.DataStore,
return snapshotService, nil
}
func loadEdgeJobsFromDatabase(dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService) error {
edgeJobs, err := dataStore.EdgeJob().EdgeJobs()
if err != nil {
return err
}
for _, edgeJob := range edgeJobs {
for endpointID := range edgeJob.Endpoints {
reverseTunnelService.AddEdgeJob(endpointID, &edgeJob)
}
}
return nil
}
func initStatus(flags *portainer.CLIFlags) *portainer.Status {
return &portainer.Status{
Version: portainer.APIVersion,
@@ -179,6 +172,7 @@ func updateSettingsFromFlags(dataStore portainer.DataStore, flags *portainer.CLI
settings.SnapshotInterval = *flags.SnapshotInterval
settings.EnableEdgeComputeFeatures = *flags.EnableEdgeComputeFeatures
settings.EnableTelemetry = true
settings.OAuthSettings.SSO = true
if *flags.Templates != "" {
settings.TemplatesURL = *flags.Templates
@@ -254,6 +248,7 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore portainer.Dat
AllowVolumeBrowserForRegularUsers: false,
EnableHostManagementFeatures: false,
AllowSysctlSettingForRegularUsers: true,
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowHostNamespaceForRegularUsers: true,
@@ -315,6 +310,7 @@ func createUnsecuredEndpoint(endpointURL string, dataStore portainer.DataStore,
AllowVolumeBrowserForRegularUsers: false,
EnableHostManagementFeatures: false,
AllowSysctlSettingForRegularUsers: true,
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowHostNamespaceForRegularUsers: true,
@@ -353,28 +349,12 @@ func initEndpoint(flags *portainer.CLIFlags, dataStore portainer.DataStore, snap
return createUnsecuredEndpoint(*flags.EndpointURL, dataStore, snapshotService)
}
func terminateIfNoAdminCreated(dataStore portainer.DataStore) {
timer1 := time.NewTimer(5 * time.Minute)
<-timer1.C
users, err := dataStore.User().UsersByRole(portainer.AdministratorRole)
if err != nil {
log.Fatalf("failed getting admin user: %v", err)
}
if len(users) == 0 {
log.Fatal("No administrator account was created after 5 min. Shutting down the Portainer instance for security reasons.")
return
}
}
func main() {
flags := initCLI()
func buildServer(flags *portainer.CLIFlags) portainer.Server {
shutdownCtx, shutdownTrigger := context.WithCancel(context.Background())
fileService := initFileService(*flags.Data)
dataStore := initDataStore(*flags.Data, fileService)
defer dataStore.Close()
if err := dataStore.CheckCurrentEdition(); err != nil {
log.Fatal(err)
@@ -400,7 +380,7 @@ func main() {
log.Fatalf("failed initializing key pai: %v", err)
}
reverseTunnelService := chisel.NewService(dataStore)
reverseTunnelService := chisel.NewService(dataStore, shutdownCtx)
instanceID, err := dataStore.Version().InstanceID()
if err != nil {
@@ -408,14 +388,17 @@ func main() {
}
dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
kubernetesClientFactory := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, instanceID)
kubernetesClientFactory := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, instanceID, dataStore)
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory)
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx)
if err != nil {
log.Fatalf("failed initializing snapshot service: %v", err)
}
snapshotService.Start()
authorizationService := authorization.NewService(dataStore)
authorizationService.K8sClientFactory = kubernetesClientFactory
swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService, reverseTunnelService)
if err != nil {
log.Fatalf("failed initializing swarm stack manager: %v", err)
@@ -425,7 +408,7 @@ func main() {
composeStackManager := initComposeStackManager(*flags.Assets, *flags.Data, reverseTunnelService, proxyManager)
kubernetesDeployer := initKubernetesDeployer(*flags.Assets)
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, *flags.Assets)
if dataStore.IsNew() {
err = updateSettingsFromFlags(dataStore, flags)
@@ -434,7 +417,7 @@ func main() {
}
}
err = loadEdgeJobsFromDatabase(dataStore, reverseTunnelService)
err = edge.LoadEdgeJobs(dataStore, reverseTunnelService)
if err != nil {
log.Fatalf("failed loading edge jobs from database: %v", err)
}
@@ -482,14 +465,13 @@ func main() {
}
}
go terminateIfNoAdminCreated(dataStore)
err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService)
if err != nil {
log.Fatalf("failed starting tunnel server: %v", err)
log.Fatalf("failed starting license service: %s", err)
}
var server portainer.Server = &http.Server{
return &http.Server{
AuthorizationService: authorizationService,
ReverseTunnelService: reverseTunnelService,
Status: applicationStatus,
BindAddress: *flags.Addr,
@@ -513,11 +495,20 @@ func main() {
SSLKey: *flags.SSLKey,
DockerClientFactory: dockerClientFactory,
KubernetesClientFactory: kubernetesClientFactory,
}
log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr)
err = server.Start()
if err != nil {
log.Fatalf("failed starting server: %v", err)
ShutdownCtx: shutdownCtx,
ShutdownTrigger: shutdownTrigger,
}
}
func main() {
flags := initCLI()
configureLogger()
for {
server := buildServer(flags)
log.Printf("Starting Portainer %s on %s\n", portainer.APIVersion, *flags.Addr)
err := server.Start()
log.Printf("Http server exited: %s\n", err)
}
}

70
api/crypto/aes.go Normal file
View File

@@ -0,0 +1,70 @@
package crypto
import (
"crypto/aes"
"crypto/cipher"
"io"
"golang.org/x/crypto/scrypt"
)
// NOTE: has to go with what is considered to be a simplistic in that it omits any
// authentication of the encrypted data.
// Person with better knowledge is welcomed to improve it.
// sourced from https://golang.org/src/crypto/cipher/example_test.go
var emptySalt []byte = make([]byte, 0, 0)
// AesEncrypt reads from input, encrypts with AES-256 and writes to the output.
// passphrase is used to generate an encryption key.
func AesEncrypt(input io.Reader, output io.Writer, passphrase []byte) error {
// making a 32 bytes key that would correspond to AES-256
// don't necessarily need a salt, so just kept in empty
key, err := scrypt.Key(passphrase, emptySalt, 32768, 8, 1, 32)
if err != nil {
return err
}
block, err := aes.NewCipher(key)
if err != nil {
return err
}
// If the key is unique for each ciphertext, then it's ok to use a zero
// IV.
var iv [aes.BlockSize]byte
stream := cipher.NewOFB(block, iv[:])
writer := &cipher.StreamWriter{S: stream, W: output}
// Copy the input to the output, encrypting as we go.
if _, err := io.Copy(writer, input); err != nil {
return err
}
return nil
}
// AesDecrypt reads from input, decrypts with AES-256 and returns the reader to a read decrypted content from.
// passphrase is used to generate an encryption key.
func AesDecrypt(input io.Reader, passphrase []byte) (io.Reader, error) {
// making a 32 bytes key that would correspond to AES-256
// don't necessarily need a salt, so just kept in empty
key, err := scrypt.Key(passphrase, emptySalt, 32768, 8, 1, 32)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
// If the key is unique for each ciphertext, then it's ok to use a zero
// IV.
var iv [aes.BlockSize]byte
stream := cipher.NewOFB(block, iv[:])
reader := &cipher.StreamReader{S: stream, R: input}
return reader, nil
}

132
api/crypto/aes_test.go Normal file
View File

@@ -0,0 +1,132 @@
package crypto
import (
"io"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/docker/docker/pkg/ioutils"
"github.com/stretchr/testify/assert"
)
func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
tmpdir, _ := ioutils.TempDir("", "encrypt")
defer os.RemoveAll(tmpdir)
var (
originFilePath = filepath.Join(tmpdir, "origin")
encryptedFilePath = filepath.Join(tmpdir, "encrypted")
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
)
content := []byte("content")
ioutil.WriteFile(originFilePath, content, 0600)
originFile, _ := os.Open(originFilePath)
defer originFile.Close()
encryptedFileWriter, _ := os.Create(encryptedFilePath)
defer encryptedFileWriter.Close()
err := AesEncrypt(originFile, encryptedFileWriter, []byte("passphrase"))
assert.Nil(t, err, "Failed to encrypt a file")
encryptedContent, err := ioutil.ReadFile(encryptedFilePath)
assert.Nil(t, err, "Couldn't read encrypted file")
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
encryptedFileReader, _ := os.Open(encryptedFilePath)
defer encryptedFileReader.Close()
decryptedFileWriter, _ := os.Create(decryptedFilePath)
defer decryptedFileWriter.Close()
decryptedReader, err := AesDecrypt(encryptedFileReader, []byte("passphrase"))
assert.Nil(t, err, "Failed to decrypt file")
io.Copy(decryptedFileWriter, decryptedReader)
decryptedContent, _ := ioutil.ReadFile(decryptedFilePath)
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
}
func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
tmpdir, _ := ioutils.TempDir("", "encrypt")
defer os.RemoveAll(tmpdir)
var (
originFilePath = filepath.Join(tmpdir, "origin")
encryptedFilePath = filepath.Join(tmpdir, "encrypted")
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
)
content := []byte("content")
ioutil.WriteFile(originFilePath, content, 0600)
originFile, _ := os.Open(originFilePath)
defer originFile.Close()
encryptedFileWriter, _ := os.Create(encryptedFilePath)
defer encryptedFileWriter.Close()
err := AesEncrypt(originFile, encryptedFileWriter, []byte(""))
assert.Nil(t, err, "Failed to encrypt a file")
encryptedContent, err := ioutil.ReadFile(encryptedFilePath)
assert.Nil(t, err, "Couldn't read encrypted file")
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
encryptedFileReader, _ := os.Open(encryptedFilePath)
defer encryptedFileReader.Close()
decryptedFileWriter, _ := os.Create(decryptedFilePath)
defer decryptedFileWriter.Close()
decryptedReader, err := AesDecrypt(encryptedFileReader, []byte(""))
assert.Nil(t, err, "Failed to decrypt file")
io.Copy(decryptedFileWriter, decryptedReader)
decryptedContent, _ := ioutil.ReadFile(decryptedFilePath)
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
}
func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T) {
tmpdir, _ := ioutils.TempDir("", "encrypt")
defer os.RemoveAll(tmpdir)
var (
originFilePath = filepath.Join(tmpdir, "origin")
encryptedFilePath = filepath.Join(tmpdir, "encrypted")
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
)
content := []byte("content")
ioutil.WriteFile(originFilePath, content, 0600)
originFile, _ := os.Open(originFilePath)
defer originFile.Close()
encryptedFileWriter, _ := os.Create(encryptedFilePath)
defer encryptedFileWriter.Close()
err := AesEncrypt(originFile, encryptedFileWriter, []byte("passphrase"))
assert.Nil(t, err, "Failed to encrypt a file")
encryptedContent, err := ioutil.ReadFile(encryptedFilePath)
assert.Nil(t, err, "Couldn't read encrypted file")
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
encryptedFileReader, _ := os.Open(encryptedFilePath)
defer encryptedFileReader.Close()
decryptedFileWriter, _ := os.Create(decryptedFilePath)
defer decryptedFileWriter.Close()
decryptedReader, err := AesDecrypt(encryptedFileReader, []byte("garbage"))
assert.Nil(t, err, "Should allow to decrypt with wrong passphrase")
io.Copy(decryptedFileWriter, decryptedReader)
decryptedContent, _ := ioutil.ReadFile(decryptedFilePath)
assert.NotEqual(t, content, decryptedContent, "Original and decrypted content should NOT match")
}

122
api/exec/compose_stack.go Normal file
View File

@@ -0,0 +1,122 @@
package exec
import (
"fmt"
"os"
"path"
"regexp"
"strings"
wrapper "github.com/portainer/docker-compose-wrapper"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/proxy/factory"
)
// ComposeStackManager is a wrapper for docker-compose binary
type ComposeStackManager struct {
wrapper *wrapper.ComposeWrapper
configPath string
proxyManager *proxy.Manager
}
// NewComposeStackManager returns a docker-compose wrapper if corresponding binary present, otherwise nil
func NewComposeStackManager(binaryPath string, configPath string, proxyManager *proxy.Manager) (*ComposeStackManager, error) {
wrap, err := wrapper.NewComposeWrapper(binaryPath)
if err != nil {
return nil, err
}
return &ComposeStackManager{
wrapper: wrap,
proxyManager: proxyManager,
configPath: configPath,
}, nil
}
// NormalizeStackName returns a new stack name with unsupported characters replaced
func (w *ComposeStackManager) NormalizeStackName(name string) string {
r := regexp.MustCompile("[^a-z0-9]+")
return r.ReplaceAllString(strings.ToLower(name), "")
}
// ComposeSyntaxMaxVersion returns the maximum supported version of the docker compose syntax
func (w *ComposeStackManager) ComposeSyntaxMaxVersion() string {
return portainer.ComposeSyntaxMaxVersion
}
// Up builds, (re)creates and starts containers in the background. Wraps `docker-compose up -d` command
func (w *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
url, proxy, err := w.fetchEndpointProxy(endpoint)
if err != nil {
return err
}
if proxy != nil {
defer proxy.Close()
}
envFilePath, err := createEnvFile(stack)
if err != nil {
return err
}
filePath := stackFilePath(stack)
_, err = w.wrapper.Up([]string{filePath}, url, stack.Name, envFilePath, w.configPath)
return err
}
// Down stops and removes containers, networks, images, and volumes. Wraps `docker-compose down --remove-orphans` command
func (w *ComposeStackManager) Down(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
url, proxy, err := w.fetchEndpointProxy(endpoint)
if err != nil {
return err
}
if proxy != nil {
defer proxy.Close()
}
filePath := stackFilePath(stack)
_, err = w.wrapper.Down([]string{filePath}, url, stack.Name)
return err
}
func stackFilePath(stack *portainer.Stack) string {
return path.Join(stack.ProjectPath, stack.EntryPoint)
}
func (w *ComposeStackManager) fetchEndpointProxy(endpoint *portainer.Endpoint) (string, *factory.ProxyServer, error) {
if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") {
return "", nil, nil
}
proxy, err := w.proxyManager.CreateComposeProxyServer(endpoint)
if err != nil {
return "", nil, err
}
return fmt.Sprintf("http://127.0.0.1:%d", proxy.Port), proxy, nil
}
func createEnvFile(stack *portainer.Stack) (string, error) {
if stack.Env == nil || len(stack.Env) == 0 {
return "", nil
}
envFilePath := path.Join(stack.ProjectPath, "stack.env")
envfile, err := os.OpenFile(envFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return "", err
}
for _, v := range stack.Env {
envfile.WriteString(fmt.Sprintf("%s=%s\n", v.Name, v.Value))
}
envfile.Close()
return envFilePath, nil
}

View File

@@ -33,7 +33,9 @@ func setup(t *testing.T) (*portainer.Stack, *portainer.Endpoint) {
Name: "project-name",
}
endpoint := &portainer.Endpoint{}
endpoint := &portainer.Endpoint{
URL: "unix://",
}
return stack, endpoint
}
@@ -42,14 +44,17 @@ func Test_UpAndDown(t *testing.T) {
stack, endpoint := setup(t)
w := NewComposeWrapper("", nil)
w, err := NewComposeStackManager("", "", nil)
if err != nil {
t.Fatalf("Failed creating manager: %s", err)
}
err := w.Up(stack, endpoint)
err = w.Up(stack, endpoint)
if err != nil {
t.Fatalf("Error calling docker-compose up: %s", err)
}
if containerExists(composedContainerName) == false {
if !containerExists(composedContainerName) {
t.Fatal("container should exist")
}
@@ -63,13 +68,13 @@ func Test_UpAndDown(t *testing.T) {
}
}
func containerExists(contaierName string) bool {
cmd := exec.Command(osProgram("docker"), "ps", "-a", "-f", fmt.Sprintf("name=%s", contaierName))
func containerExists(containerName string) bool {
cmd := exec.Command("docker", "ps", "-a", "-f", fmt.Sprintf("name=%s", containerName))
out, err := cmd.Output()
if err != nil {
log.Fatalf("failed to list containers: %s", err)
}
return strings.Contains(string(out), contaierName)
return strings.Contains(string(out), containerName)
}

View File

@@ -0,0 +1,112 @@
package exec
import (
"io/ioutil"
"os"
"path"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert"
)
func Test_stackFilePath(t *testing.T) {
tests := []struct {
name string
stack *portainer.Stack
expected string
}{
// {
// name: "should return empty result if stack is missing",
// stack: nil,
// expected: "",
// },
// {
// name: "should return empty result if stack don't have entrypoint",
// stack: &portainer.Stack{},
// expected: "",
// },
{
name: "should allow file name and dir",
stack: &portainer.Stack{
ProjectPath: "dir",
EntryPoint: "file",
},
expected: path.Join("dir", "file"),
},
{
name: "should allow file name only",
stack: &portainer.Stack{
EntryPoint: "file",
},
expected: "file",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := stackFilePath(tt.stack)
assert.Equal(t, tt.expected, result)
})
}
}
func Test_createEnvFile(t *testing.T) {
dir := t.TempDir()
tests := []struct {
name string
stack *portainer.Stack
expected string
expectedFile bool
}{
// {
// name: "should not add env file option if stack is missing",
// stack: nil,
// expected: "",
// },
{
name: "should not add env file option if stack doesn't have env variables",
stack: &portainer.Stack{
ProjectPath: dir,
},
expected: "",
},
{
name: "should not add env file option if stack's env variables are empty",
stack: &portainer.Stack{
ProjectPath: dir,
Env: []portainer.Pair{},
},
expected: "",
},
{
name: "should add env file option if stack has env variables",
stack: &portainer.Stack{
ProjectPath: dir,
Env: []portainer.Pair{
{Name: "var1", Value: "value1"},
{Name: "var2", Value: "value2"},
},
},
expected: "var1=value1\nvar2=value2\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, _ := createEnvFile(tt.stack)
if tt.expected != "" {
assert.Equal(t, path.Join(tt.stack.ProjectPath, "stack.env"), result)
f, _ := os.Open(path.Join(dir, "stack.env"))
content, _ := ioutil.ReadAll(f)
assert.Equal(t, tt.expected, string(content))
} else {
assert.Equal(t, "", result)
}
})
}
}

View File

@@ -1,137 +0,0 @@
package exec
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"path"
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy"
)
// ComposeWrapper is a wrapper for docker-compose binary
type ComposeWrapper struct {
binaryPath string
proxyManager *proxy.Manager
}
// NewComposeWrapper returns a docker-compose wrapper if corresponding binary present, otherwise nil
func NewComposeWrapper(binaryPath string, proxyManager *proxy.Manager) *ComposeWrapper {
if !IsBinaryPresent(programPath(binaryPath, "docker-compose")) {
return nil
}
return &ComposeWrapper{
binaryPath: binaryPath,
proxyManager: proxyManager,
}
}
// ComposeSyntaxMaxVersion returns the maximum supported version of the docker compose syntax
func (w *ComposeWrapper) ComposeSyntaxMaxVersion() string {
return portainer.ComposeSyntaxMaxVersion
}
// NormalizeStackName returns a new stack name with unsupported characters replaced
func (w *ComposeWrapper) NormalizeStackName(name string) string {
return name
}
// Up builds, (re)creates and starts containers in the background. Wraps `docker-compose up -d` command
func (w *ComposeWrapper) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
_, err := w.command([]string{"up", "-d"}, stack, endpoint)
return err
}
// Down stops and removes containers, networks, images, and volumes. Wraps `docker-compose down --remove-orphans` command
func (w *ComposeWrapper) Down(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
_, err := w.command([]string{"down", "--remove-orphans"}, stack, endpoint)
return err
}
func (w *ComposeWrapper) command(command []string, stack *portainer.Stack, endpoint *portainer.Endpoint) ([]byte, error) {
if endpoint == nil {
return nil, errors.New("cannot call a compose command on an empty endpoint")
}
program := programPath(w.binaryPath, "docker-compose")
options := setComposeFile(stack)
options = addProjectNameOption(options, stack)
options, err := addEnvFileOption(options, stack)
if err != nil {
return nil, err
}
if !(endpoint.URL == "" || strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://")) {
proxy, err := w.proxyManager.CreateComposeProxyServer(endpoint)
if err != nil {
return nil, err
}
defer proxy.Close()
options = append(options, "-H", fmt.Sprintf("http://127.0.0.1:%d", proxy.Port))
}
args := append(options, command...)
var stderr bytes.Buffer
cmd := exec.Command(program, args...)
cmd.Stderr = &stderr
out, err := cmd.Output()
if err != nil {
return out, errors.New(stderr.String())
}
return out, nil
}
func setComposeFile(stack *portainer.Stack) []string {
options := make([]string, 0)
if stack == nil || stack.EntryPoint == "" {
return options
}
composeFilePath := path.Join(stack.ProjectPath, stack.EntryPoint)
options = append(options, "-f", composeFilePath)
return options
}
func addProjectNameOption(options []string, stack *portainer.Stack) []string {
if stack == nil || stack.Name == "" {
return options
}
options = append(options, "-p", stack.Name)
return options
}
func addEnvFileOption(options []string, stack *portainer.Stack) ([]string, error) {
if stack == nil || stack.Env == nil || len(stack.Env) == 0 {
return options, nil
}
envFilePath := path.Join(stack.ProjectPath, "stack.env")
envfile, err := os.OpenFile(envFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return options, err
}
for _, v := range stack.Env {
envfile.WriteString(fmt.Sprintf("%s=%s\n", v.Name, v.Value))
}
envfile.Close()
options = append(options, "--env-file", envFilePath)
return options, nil
}

View File

@@ -1,143 +0,0 @@
package exec
import (
"io/ioutil"
"os"
"path"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert"
)
func Test_setComposeFile(t *testing.T) {
tests := []struct {
name string
stack *portainer.Stack
expected []string
}{
{
name: "should return empty result if stack is missing",
stack: nil,
expected: []string{},
},
{
name: "should return empty result if stack don't have entrypoint",
stack: &portainer.Stack{},
expected: []string{},
},
{
name: "should allow file name and dir",
stack: &portainer.Stack{
ProjectPath: "dir",
EntryPoint: "file",
},
expected: []string{"-f", path.Join("dir", "file")},
},
{
name: "should allow file name only",
stack: &portainer.Stack{
EntryPoint: "file",
},
expected: []string{"-f", "file"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := setComposeFile(tt.stack)
assert.ElementsMatch(t, tt.expected, result)
})
}
}
func Test_addProjectNameOption(t *testing.T) {
tests := []struct {
name string
stack *portainer.Stack
expected []string
}{
{
name: "should not add project option if stack is missing",
stack: nil,
expected: []string{},
},
{
name: "should not add project option if stack doesn't have name",
stack: &portainer.Stack{},
expected: []string{},
},
{
name: "should add project name option if stack has a name",
stack: &portainer.Stack{
Name: "project-name",
},
expected: []string{"-p", "project-name"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
options := []string{"-a", "b"}
result := addProjectNameOption(options, tt.stack)
assert.ElementsMatch(t, append(options, tt.expected...), result)
})
}
}
func Test_addEnvFileOption(t *testing.T) {
dir := t.TempDir()
tests := []struct {
name string
stack *portainer.Stack
expected []string
expectedContent string
}{
{
name: "should not add env file option if stack is missing",
stack: nil,
expected: []string{},
},
{
name: "should not add env file option if stack doesn't have env variables",
stack: &portainer.Stack{},
expected: []string{},
},
{
name: "should not add env file option if stack's env variables are empty",
stack: &portainer.Stack{
ProjectPath: dir,
Env: []portainer.Pair{},
},
expected: []string{},
},
{
name: "should add env file option if stack has env variables",
stack: &portainer.Stack{
ProjectPath: dir,
Env: []portainer.Pair{
{Name: "var1", Value: "value1"},
{Name: "var2", Value: "value2"},
},
},
expected: []string{"--env-file", path.Join(dir, "stack.env")},
expectedContent: "var1=value1\nvar2=value2\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
options := []string{"-a", "b"}
result, _ := addEnvFileOption(options, tt.stack)
assert.ElementsMatch(t, append(options, tt.expected...), result)
if tt.expectedContent != "" {
f, _ := os.Open(path.Join(dir, "stack.env"))
content, _ := ioutil.ReadAll(f)
assert.Equal(t, tt.expectedContent, string(content))
}
})
}
}

View File

@@ -2,71 +2,234 @@ package exec
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/kubernetes/cli"
"io/ioutil"
"net/http"
"net/url"
"os/exec"
"path"
"runtime"
"strings"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
)
// KubernetesDeployer represents a service to deploy resources inside a Kubernetes environment.
type KubernetesDeployer struct {
binaryPath string
binaryPath string
dataStore portainer.DataStore
reverseTunnelService portainer.ReverseTunnelService
signatureService portainer.DigitalSignatureService
kubernetesClientFactory *cli.ClientFactory
kubernetesTokenCacheManager *kubernetes.TokenCacheManager
}
// NewKubernetesDeployer initializes a new KubernetesDeployer service.
func NewKubernetesDeployer(binaryPath string) *KubernetesDeployer {
func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, kubernetesClientFactory *cli.ClientFactory, datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, binaryPath string) *KubernetesDeployer {
return &KubernetesDeployer{
binaryPath: binaryPath,
binaryPath: binaryPath,
dataStore: datastore,
reverseTunnelService: reverseTunnelService,
signatureService: signatureService,
kubernetesClientFactory: kubernetesClientFactory,
kubernetesTokenCacheManager: kubernetesTokenCacheManager,
}
}
func (deployer *KubernetesDeployer) getToken(request *http.Request, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, error) {
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return "", err
}
kubecli, err := deployer.kubernetesClientFactory.GetKubeClient(endpoint)
if err != nil {
return "", err
}
tokenCache := deployer.kubernetesTokenCacheManager.GetOrCreateTokenCache(int(endpoint.ID))
tokenManager, err := kubernetes.NewTokenManager(kubecli, deployer.dataStore, tokenCache, setLocalAdminToken)
if err != nil {
return "", err
}
if tokenData.Role == portainer.AdministratorRole {
return tokenManager.GetAdminServiceAccountToken(), nil
}
token, err := tokenManager.GetUserServiceAccountToken(int(tokenData.ID), endpoint.ID)
if err != nil {
return "", err
}
if token == "" {
return "", fmt.Errorf("can not get a valid user service account token")
}
return token, nil
}
// Deploy will deploy a Kubernetes manifest inside a specific namespace in a Kubernetes endpoint.
// If composeFormat is set to true, it will leverage the kompose binary to deploy a compose compliant manifest.
// Otherwise it will use kubectl to deploy the manifest.
func (deployer *KubernetesDeployer) Deploy(endpoint *portainer.Endpoint, data string, composeFormat bool, namespace string) ([]byte, error) {
if composeFormat {
convertedData, err := deployer.convertComposeData(data)
func (deployer *KubernetesDeployer) Deploy(request *http.Request, endpoint *portainer.Endpoint, stackConfig string, namespace string) (string, error) {
if endpoint.Type == portainer.KubernetesLocalEnvironment {
token, err := deployer.getToken(request, endpoint, true);
if err != nil {
return nil, err
return "", err
}
data = string(convertedData)
command := path.Join(deployer.binaryPath, "kubectl")
if runtime.GOOS == "windows" {
command = path.Join(deployer.binaryPath, "kubectl.exe")
}
args := make([]string, 0)
args = append(args, "--server", endpoint.URL)
args = append(args, "--insecure-skip-tls-verify")
args = append(args, "--token", token)
args = append(args, "--namespace", namespace)
args = append(args, "apply", "-f", "-")
var stderr bytes.Buffer
cmd := exec.Command(command, args...)
cmd.Stderr = &stderr
cmd.Stdin = strings.NewReader(stackConfig)
output, err := cmd.Output()
if err != nil {
return "", errors.New(stderr.String())
}
return string(output), nil
}
token, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
// agent
endpointURL := endpoint.URL
if endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
tunnel := deployer.reverseTunnelService.GetTunnelDetails(endpoint.ID)
if tunnel.Status == portainer.EdgeAgentIdle {
err := deployer.reverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
if err != nil {
return "", err
}
settings, err := deployer.dataStore.Settings().Settings()
if err != nil {
return "", err
}
waitForAgentToConnect := time.Duration(settings.EdgeAgentCheckinInterval) * time.Second
time.Sleep(waitForAgentToConnect * 2)
}
endpointURL = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
}
transport := &http.Transport{}
if endpoint.TLSConfig.TLS {
tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
if err != nil {
return "", err
}
transport.TLSClientConfig = tlsConfig
}
httpCli := &http.Client{
Transport: transport,
}
if !strings.HasPrefix(endpointURL, "http") {
endpointURL = fmt.Sprintf("https://%s", endpointURL)
}
url, err := url.Parse(fmt.Sprintf("%s/v2/kubernetes/stack", endpointURL))
if err != nil {
return nil, err
return "", err
}
command := path.Join(deployer.binaryPath, "kubectl")
if runtime.GOOS == "windows" {
command = path.Join(deployer.binaryPath, "kubectl.exe")
}
args := make([]string, 0)
args = append(args, "--server", endpoint.URL)
args = append(args, "--insecure-skip-tls-verify")
args = append(args, "--token", string(token))
args = append(args, "--namespace", namespace)
args = append(args, "apply", "-f", "-")
var stderr bytes.Buffer
cmd := exec.Command(command, args...)
cmd.Stderr = &stderr
cmd.Stdin = strings.NewReader(data)
output, err := cmd.Output()
reqPayload, err := json.Marshal(
struct {
StackConfig string
Namespace string
}{
StackConfig: stackConfig,
Namespace: namespace,
})
if err != nil {
return nil, errors.New(stderr.String())
return "", err
}
return output, nil
req, err := http.NewRequest(http.MethodPost, url.String(), bytes.NewReader(reqPayload))
if err != nil {
return "", err
}
signature, err := deployer.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
if err != nil {
return "", err
}
token, err := deployer.getToken(request, endpoint, false);
if err != nil {
return "", err
}
req.Header.Set(portainer.PortainerAgentPublicKeyHeader, deployer.signatureService.EncodedPublicKey())
req.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
req.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
resp, err := httpCli.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var errorResponseData struct {
Message string
Details string
}
err = json.NewDecoder(resp.Body).Decode(&errorResponseData)
if err != nil {
output, parseStringErr := ioutil.ReadAll(resp.Body)
if parseStringErr != nil {
return "", parseStringErr
}
return "", fmt.Errorf("Failed parsing, body: %s, error: %w", output, err)
}
return "", fmt.Errorf("Deployment to agent failed: %s", errorResponseData.Details)
}
var responseData struct{ Output string }
err = json.NewDecoder(resp.Body).Decode(&responseData)
if err != nil {
parsedOutput, parseStringErr := ioutil.ReadAll(resp.Body)
if parseStringErr != nil {
return "", parseStringErr
}
return "", fmt.Errorf("Failed decoding, body: %s, err: %w", parsedOutput, err)
}
return responseData.Output, nil
}
func (deployer *KubernetesDeployer) convertComposeData(data string) ([]byte, error) {
// ConvertCompose leverages the kompose binary to deploy a compose compliant manifest.
func (deployer *KubernetesDeployer) ConvertCompose(data string) ([]byte, error) {
command := path.Join(deployer.binaryPath, "kompose")
if runtime.GOOS == "windows" {
command = path.Join(deployer.binaryPath, "kompose.exe")

View File

@@ -8,9 +8,11 @@ import (
"os"
"os/exec"
"path"
"regexp"
"runtime"
"strings"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
)
// SwarmStackManager represents a service for managing stacks.
@@ -42,7 +44,7 @@ func NewSwarmStackManager(binaryPath, dataPath string, signatureService portaine
}
// Login executes the docker login command against a list of registries (including DockerHub).
func (manager *SwarmStackManager) Login(dockerhub *portainer.DockerHub, registries []portainer.Registry, endpoint *portainer.Endpoint) {
func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoint *portainer.Endpoint) {
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
for _, registry := range registries {
if registry.Authentication {
@@ -50,11 +52,6 @@ func (manager *SwarmStackManager) Login(dockerhub *portainer.DockerHub, registri
runCommandAndCaptureStdErr(command, registryArgs, nil, "")
}
}
if dockerhub.Authentication {
dockerhubArgs := append(args, "login", "--username", dockerhub.Username, "--password", dockerhub.Password)
runCommandAndCaptureStdErr(command, dockerhubArgs, nil, "")
}
}
// Logout executes the docker logout command.
@@ -189,3 +186,8 @@ func (manager *SwarmStackManager) retrieveConfigurationFromDisk(path string) (ma
return config, nil
}
func (manager *SwarmStackManager) NormalizeStackName(name string) string {
r := regexp.MustCompile("[^a-z0-9]+")
return r.ReplaceAllString(strings.ToLower(name), "")
}

View File

@@ -1,24 +0,0 @@
package exec
import (
"os/exec"
"path/filepath"
"runtime"
)
func osProgram(program string) string {
if runtime.GOOS == "windows" {
program += ".exe"
}
return program
}
func programPath(rootPath, program string) string {
return filepath.Join(rootPath, osProgram(program))
}
// IsBinaryPresent returns true if corresponding program exists on PATH
func IsBinaryPresent(program string) bool {
_, err := exec.LookPath(program)
return err == nil
}

View File

@@ -1,16 +0,0 @@
package exec
import (
"testing"
)
func Test_isBinaryPresent(t *testing.T) {
if !IsBinaryPresent("docker") {
t.Error("expect docker binary to exist on the path")
}
if IsBinaryPresent("executable-with-this-name-should-not-exist") {
t.Error("expect binary with a random name to be missing on the path")
}
}

View File

@@ -9,7 +9,7 @@ import (
"io/ioutil"
"github.com/gofrs/uuid"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"io"
"os"
@@ -31,6 +31,8 @@ const (
ComposeStorePath = "compose"
// ComposeFileDefaultName represents the default name of a compose file.
ComposeFileDefaultName = "docker-compose.yml"
// ManifestFileDefaultName represents the default name of a k8s manifest file.
ManifestFileDefaultName = "k8s-deployment.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.
@@ -279,13 +281,7 @@ func (service *Service) WriteJSONToFile(path string, content interface{}) error
// FileExists checks for the existence of the specified file.
func (service *Service) FileExists(filePath string) (bool, error) {
if _, err := os.Stat(filePath); err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
return FileExists(filePath)
}
// KeyPairFilesExist checks for the existence of the key files.
@@ -505,3 +501,36 @@ func (service *Service) GetTemporaryPath() (string, error) {
return path.Join(service.fileStorePath, TempPath, uid.String()), nil
}
// GetDataStorePath returns path to data folder
func (service *Service) GetDatastorePath() string {
return service.dataStorePath
}
// FileExists checks for the existence of the specified file.
func FileExists(filePath string) (bool, error) {
if _, err := os.Stat(filePath); err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
}
func MoveDirectory(originalPath, newPath string) error {
if _, err := os.Stat(originalPath); err != nil {
return err
}
alreadyExists, err := FileExists(newPath)
if err != nil {
return err
}
if alreadyExists {
return errors.New("Target path already exists")
}
return os.Rename(originalPath, newPath)
}

View File

@@ -0,0 +1,55 @@
package filesystem
import (
"fmt"
"math/rand"
"os"
"path"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_fileSystemService_FileExists_whenFileExistsShouldReturnTrue(t *testing.T) {
service := createService(t)
testHelperFileExists_fileExists(t, service.FileExists)
}
func Test_fileSystemService_FileExists_whenFileNotExistsShouldReturnFalse(t *testing.T) {
service := createService(t)
testHelperFileExists_fileNotExists(t, service.FileExists)
}
func Test_FileExists_whenFileExistsShouldReturnTrue(t *testing.T) {
testHelperFileExists_fileExists(t, FileExists)
}
func Test_FileExists_whenFileNotExistsShouldReturnFalse(t *testing.T) {
testHelperFileExists_fileNotExists(t, FileExists)
}
func testHelperFileExists_fileExists(t *testing.T, checker func(path string) (bool, error)) {
file, err := os.CreateTemp("", t.Name())
assert.NoError(t, err, "CreateTemp should not fail")
t.Cleanup(func() {
os.RemoveAll(file.Name())
})
exists, err := checker(file.Name())
assert.NoError(t, err, "FileExists should not fail")
assert.True(t, exists)
}
func testHelperFileExists_fileNotExists(t *testing.T, checker func(path string) (bool, error)) {
filePath := path.Join(os.TempDir(), fmt.Sprintf("%s%d", t.Name(), rand.Int()))
err := os.RemoveAll(filePath)
assert.NoError(t, err, "RemoveAll should not fail")
exists, err := checker(filePath)
assert.NoError(t, err, "FileExists should not fail")
assert.False(t, exists)
}

View File

@@ -0,0 +1,49 @@
package filesystem
import (
"fmt"
"os"
"path"
"testing"
"github.com/stretchr/testify/assert"
)
// temporary function until upgrade to 1.16
func tempDir(t *testing.T) string {
tmpDir, err := os.MkdirTemp("", "dir")
assert.NoError(t, err, "MkdirTemp should not fail")
return tmpDir
}
func Test_movePath_shouldFailIfOriginalPathDoesntExist(t *testing.T) {
tmpDir := tempDir(t)
missingPath := path.Join(tmpDir, "missing")
targetPath := path.Join(tmpDir, "target")
defer os.RemoveAll(tmpDir)
err := MoveDirectory(missingPath, targetPath)
assert.Error(t, err, "move directory should fail when target path exists")
}
func Test_movePath_shouldFailIfTargetPathDoesExist(t *testing.T) {
originalPath := tempDir(t)
missingPath := tempDir(t)
defer os.RemoveAll(originalPath)
defer os.RemoveAll(missingPath)
err := MoveDirectory(originalPath, missingPath)
assert.Error(t, err, "move directory should fail when target path exists")
}
func Test_movePath_success(t *testing.T) {
originalPath := tempDir(t)
defer os.RemoveAll(originalPath)
err := MoveDirectory(originalPath, fmt.Sprintf("%s-old", originalPath))
assert.NoError(t, err)
}

View File

@@ -0,0 +1,22 @@
package filesystem
import (
"os"
"path"
"testing"
"github.com/stretchr/testify/assert"
)
func createService(t *testing.T) *Service {
dataStorePath := path.Join(os.TempDir(), t.Name())
service, err := NewService(dataStorePath, "")
assert.NoError(t, err, "NewService should not fail")
t.Cleanup(func() {
os.RemoveAll(dataStorePath)
})
return service
}

219
api/git/azure.go Normal file
View File

@@ -0,0 +1,219 @@
package git
import (
"context"
"fmt"
"github.com/pkg/errors"
"github.com/portainer/portainer/api/archive"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
)
const (
azureDevOpsHost = "dev.azure.com"
visualStudioHostSuffix = ".visualstudio.com"
)
func isAzureUrl(s string) bool {
return strings.Contains(s, azureDevOpsHost) ||
strings.Contains(s, visualStudioHostSuffix)
}
type azureOptions struct {
organisation, project, repository string
// a user may pass credentials in a repository URL,
// for example https://<username>:<password>@<domain>/<path>
username, password string
}
type azureDownloader struct {
client *http.Client
baseUrl string
}
func NewAzureDownloader(client *http.Client) *azureDownloader {
return &azureDownloader{
client: client,
baseUrl: "https://dev.azure.com",
}
}
func (a *azureDownloader) download(ctx context.Context, destination string, options cloneOptions) error {
zipFilepath, err := a.downloadZipFromAzureDevOps(ctx, options)
if err != nil {
return errors.Wrap(err, "failed to download a zip file from Azure DevOps")
}
defer os.Remove(zipFilepath)
err = archive.UnzipFile(zipFilepath, destination)
if err != nil {
return errors.Wrap(err, "failed to unzip file")
}
return nil
}
func (a *azureDownloader) downloadZipFromAzureDevOps(ctx context.Context, options cloneOptions) (string, error) {
config, err := parseUrl(options.repositoryUrl)
if err != nil {
return "", errors.WithMessage(err, "failed to parse url")
}
downloadUrl, err := a.buildDownloadUrl(config, options.referenceName)
if err != nil {
return "", errors.WithMessage(err, "failed to build download url")
}
zipFile, err := ioutil.TempFile("", "azure-git-repo-*.zip")
if err != nil {
return "", errors.WithMessage(err, "failed to create temp file")
}
defer zipFile.Close()
req, err := http.NewRequestWithContext(ctx, "GET", downloadUrl, nil)
if options.username != "" || options.password != "" {
req.SetBasicAuth(options.username, options.password)
} else if config.username != "" || config.password != "" {
req.SetBasicAuth(config.username, config.password)
}
if err != nil {
return "", errors.WithMessage(err, "failed to create a new HTTP request")
}
res, err := a.client.Do(req)
if err != nil {
return "", errors.WithMessage(err, "failed to make an HTTP request")
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return "", fmt.Errorf("failed to download zip with a status \"%v\"", res.Status)
}
_, err = io.Copy(zipFile, res.Body)
if err != nil {
return "", errors.WithMessage(err, "failed to save HTTP response to a file")
}
return zipFile.Name(), nil
}
func parseUrl(rawUrl string) (*azureOptions, error) {
if strings.HasPrefix(rawUrl, "https://") || strings.HasPrefix(rawUrl, "http://") {
return parseHttpUrl(rawUrl)
}
if strings.HasPrefix(rawUrl, "git@ssh") {
return parseSshUrl(rawUrl)
}
if strings.HasPrefix(rawUrl, "ssh://") {
r := []rune(rawUrl)
return parseSshUrl(string(r[6:])) // remove the prefix
}
return nil, errors.Errorf("supported url schemes are https and ssh; recevied URL %s rawUrl", rawUrl)
}
var expectedSshUrl = "git@ssh.dev.azure.com:v3/Organisation/Project/Repository"
func parseSshUrl(rawUrl string) (*azureOptions, error) {
path := strings.Split(rawUrl, "/")
unexpectedUrlErr := errors.Errorf("want url %s, got %s", expectedSshUrl, rawUrl)
if len(path) != 4 {
return nil, unexpectedUrlErr
}
return &azureOptions{
organisation: path[1],
project: path[2],
repository: path[3],
}, nil
}
const expectedAzureDevOpsHttpUrl = "https://Organisation@dev.azure.com/Organisation/Project/_git/Repository"
const expectedVisualStudioHttpUrl = "https://organisation.visualstudio.com/project/_git/repository"
func parseHttpUrl(rawUrl string) (*azureOptions, error) {
u, err := url.Parse(rawUrl)
if err != nil {
return nil, errors.Wrap(err, "failed to parse HTTP url")
}
opt := azureOptions{}
switch {
case u.Host == azureDevOpsHost:
path := strings.Split(u.Path, "/")
if len(path) != 5 {
return nil, errors.Errorf("want url %s, got %s", expectedAzureDevOpsHttpUrl, u)
}
opt.organisation = path[1]
opt.project = path[2]
opt.repository = path[4]
case strings.HasSuffix(u.Host, visualStudioHostSuffix):
path := strings.Split(u.Path, "/")
if len(path) != 4 {
return nil, errors.Errorf("want url %s, got %s", expectedVisualStudioHttpUrl, u)
}
opt.organisation = strings.TrimSuffix(u.Host, visualStudioHostSuffix)
opt.project = path[1]
opt.repository = path[3]
default:
return nil, errors.Errorf("unknown azure host in url \"%s\"", rawUrl)
}
opt.username = u.User.Username()
opt.password, _ = u.User.Password()
return &opt, nil
}
func (a *azureDownloader) buildDownloadUrl(config *azureOptions, referenceName string) (string, error) {
rawUrl := fmt.Sprintf("%s/%s/%s/_apis/git/repositories/%s/items",
a.baseUrl,
url.PathEscape(config.organisation),
url.PathEscape(config.project),
url.PathEscape(config.repository))
u, err := url.Parse(rawUrl)
if err != nil {
return "", errors.Wrapf(err, "failed to parse download url path %s", rawUrl)
}
q := u.Query()
// scopePath=/&download=true&versionDescriptor.version=main&$format=zip&recursionLevel=full&api-version=6.0
q.Set("scopePath", "/")
q.Set("download", "true")
q.Set("versionDescriptor.versionType", getVersionType(referenceName))
q.Set("versionDescriptor.version", formatReferenceName(referenceName))
q.Set("$format", "zip")
q.Set("recursionLevel", "full")
q.Set("api-version", "6.0")
u.RawQuery = q.Encode()
return u.String(), nil
}
const (
branchPrefix = "refs/heads/"
tagPrefix = "refs/tags/"
)
func formatReferenceName(name string) string {
if strings.HasPrefix(name, branchPrefix) {
return strings.TrimPrefix(name, branchPrefix)
}
if strings.HasPrefix(name, tagPrefix) {
return strings.TrimPrefix(name, tagPrefix)
}
return name
}
func getVersionType(name string) string {
if strings.HasPrefix(name, branchPrefix) {
return "branch"
}
if strings.HasPrefix(name, tagPrefix) {
return "tag"
}
return "commit"
}

View File

@@ -0,0 +1,93 @@
package git
import (
"fmt"
"os"
"path/filepath"
"testing"
"github.com/docker/docker/pkg/ioutils"
_ "github.com/joho/godotenv/autoload"
"github.com/stretchr/testify/assert"
)
func TestService_ClonePublicRepository_Azure(t *testing.T) {
ensureIntegrationTest(t)
pat := getRequiredValue(t, "AZURE_DEVOPS_PAT")
service := NewService()
type args struct {
repositoryURLFormat string
referenceName string
username string
password string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "Clone Azure DevOps repo branch",
args: args{
repositoryURLFormat: "https://:%s@portainer.visualstudio.com/Playground/_git/dev_integration",
referenceName: "refs/heads/main",
username: "",
password: pat,
},
wantErr: false,
},
{
name: "Clone Azure DevOps repo tag",
args: args{
repositoryURLFormat: "https://:%s@portainer.visualstudio.com/Playground/_git/dev_integration",
referenceName: "refs/tags/v1.1",
username: "",
password: pat,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dst, err := ioutils.TempDir("", "clone")
assert.NoError(t, err)
defer os.RemoveAll(dst)
repositoryUrl := fmt.Sprintf(tt.args.repositoryURLFormat, tt.args.password)
err = service.CloneRepository(dst, repositoryUrl, tt.args.referenceName, "", "")
assert.NoError(t, err)
assert.FileExists(t, filepath.Join(dst, "README.md"))
})
}
}
func TestService_ClonePrivateRepository_Azure(t *testing.T) {
ensureIntegrationTest(t)
pat := getRequiredValue(t, "AZURE_DEVOPS_PAT")
service := NewService()
dst, err := ioutils.TempDir("", "clone")
assert.NoError(t, err)
defer os.RemoveAll(dst)
repositoryUrl := "https://portainer.visualstudio.com/Playground/_git/dev_integration"
err = service.CloneRepository(dst, repositoryUrl, "refs/heads/main", "", pat)
assert.NoError(t, err)
assert.FileExists(t, filepath.Join(dst, "README.md"))
}
func getRequiredValue(t *testing.T, name string) string {
value, ok := os.LookupEnv(name)
if !ok {
t.Fatalf("can't find required env var \"%s\"", name)
}
return value
}
func ensureIntegrationTest(t *testing.T) {
if _, ok := os.LookupEnv("INTEGRATION_TEST"); !ok {
t.Skip("skip an integration test")
}
}

250
api/git/azure_test.go Normal file
View File

@@ -0,0 +1,250 @@
package git
import (
"context"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"net/url"
"testing"
)
func Test_buildDownloadUrl(t *testing.T) {
a := NewAzureDownloader(nil)
u, err := a.buildDownloadUrl(&azureOptions{
organisation: "organisation",
project: "project",
repository: "repository",
}, "refs/heads/main")
expectedUrl, _ := url.Parse("https://dev.azure.com/organisation/project/_apis/git/repositories/repository/items?scopePath=/&download=true&versionDescriptor.version=main&$format=zip&recursionLevel=full&api-version=6.0&versionDescriptor.versionType=branch")
actualUrl, _ := url.Parse(u)
if assert.NoError(t, err) {
assert.Equal(t, expectedUrl.Host, actualUrl.Host)
assert.Equal(t, expectedUrl.Scheme, actualUrl.Scheme)
assert.Equal(t, expectedUrl.Path, actualUrl.Path)
assert.Equal(t, expectedUrl.Query(), actualUrl.Query())
}
}
func Test_parseAzureUrl(t *testing.T) {
type args struct {
url string
}
tests := []struct {
name string
args args
want *azureOptions
wantErr bool
}{
{
name: "Expected SSH URL format starting with ssh://",
args: args{
url: "ssh://git@ssh.dev.azure.com:v3/Organisation/Project/Repository",
},
want: &azureOptions{
organisation: "Organisation",
project: "Project",
repository: "Repository",
},
wantErr: false,
},
{
name: "Expected SSH URL format starting with git@ssh",
args: args{
url: "git@ssh.dev.azure.com:v3/Organisation/Project/Repository",
},
want: &azureOptions{
organisation: "Organisation",
project: "Project",
repository: "Repository",
},
wantErr: false,
},
{
name: "Unexpected SSH URL format",
args: args{
url: "git@ssh.dev.azure.com:v3/Organisation/Repository",
},
wantErr: true,
},
{
name: "Expected HTTPS URL format",
args: args{
url: "https://Organisation@dev.azure.com/Organisation/Project/_git/Repository",
},
want: &azureOptions{
organisation: "Organisation",
project: "Project",
repository: "Repository",
username: "Organisation",
},
wantErr: false,
},
{
name: "HTTPS URL with credentials",
args: args{
url: "https://username:password@dev.azure.com/Organisation/Project/_git/Repository",
},
want: &azureOptions{
organisation: "Organisation",
project: "Project",
repository: "Repository",
username: "username",
password: "password",
},
wantErr: false,
},
{
name: "HTTPS URL with password",
args: args{
url: "https://:password@dev.azure.com/Organisation/Project/_git/Repository",
},
want: &azureOptions{
organisation: "Organisation",
project: "Project",
repository: "Repository",
password: "password",
},
wantErr: false,
},
{
name: "Visual Studio HTTPS URL with credentials",
args: args{
url: "https://username:password@organisation.visualstudio.com/project/_git/repository",
},
want: &azureOptions{
organisation: "organisation",
project: "project",
repository: "repository",
username: "username",
password: "password",
},
wantErr: false,
},
{
name: "Unexpected HTTPS URL format",
args: args{
url: "https://Organisation@dev.azure.com/Project/_git/Repository",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseUrl(tt.args.url)
if (err != nil) != tt.wantErr {
t.Errorf("parseUrl() error = %v, wantErr %v", err, tt.wantErr)
return
}
assert.Equal(t, tt.want, got)
})
}
}
func Test_isAzureUrl(t *testing.T) {
type args struct {
s string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "Is Azure url",
args: args{
s: "https://Organisation@dev.azure.com/Organisation/Project/_git/Repository",
},
want: true,
},
{
name: "Is Azure url",
args: args{
s: "https://portainer.visualstudio.com/project/_git/repository",
},
want: true,
},
{
name: "Is NOT Azure url",
args: args{
s: "https://github.com/Organisation/Repository",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, isAzureUrl(tt.args.s))
})
}
}
func Test_azureDownloader_downloadZipFromAzureDevOps(t *testing.T) {
type args struct {
options cloneOptions
}
type basicAuth struct {
username, password string
}
tests := []struct {
name string
args args
want *basicAuth
}{
{
name: "username, password embedded",
args: args{
options: cloneOptions{
repositoryUrl: "https://username:password@dev.azure.com/Organisation/Project/_git/Repository",
},
},
want: &basicAuth{
username: "username",
password: "password",
},
},
{
name: "username, password embedded, clone options take precedence",
args: args{
options: cloneOptions{
repositoryUrl: "https://username:password@dev.azure.com/Organisation/Project/_git/Repository",
username: "u",
password: "p",
},
},
want: &basicAuth{
username: "u",
password: "p",
},
},
{
name: "no credentials",
args: args{
options: cloneOptions{
repositoryUrl: "https://dev.azure.com/Organisation/Project/_git/Repository",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var zipRequestAuth *basicAuth
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if username, password, ok := r.BasicAuth(); ok {
zipRequestAuth = &basicAuth{username, password}
}
w.WriteHeader(http.StatusNotFound) // this makes function under test to return an error
}))
defer server.Close()
a := &azureDownloader{
client: server.Client(),
baseUrl: server.URL,
}
_, err := a.downloadZipFromAzureDevOps(context.Background(), tt.args.options)
assert.Error(t, err)
assert.Equal(t, tt.want, zipRequestAuth)
})
}
}

View File

@@ -1,21 +1,72 @@
package git
import (
"context"
"crypto/tls"
"net/http"
"net/url"
"strings"
"os"
"path/filepath"
"time"
"gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/transport/client"
githttp "gopkg.in/src-d/go-git.v4/plumbing/transport/http"
"github.com/pkg/errors"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/transport/client"
githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
)
type cloneOptions struct {
repositoryUrl string
username string
password string
referenceName string
depth int
}
type downloader interface {
download(ctx context.Context, dst string, opt cloneOptions) error
}
type gitClient struct {
preserveGitDirectory bool
}
func (c gitClient) download(ctx context.Context, dst string, opt cloneOptions) error {
gitOptions := git.CloneOptions{
URL: opt.repositoryUrl,
Depth: opt.depth,
}
if opt.password != "" || opt.username != "" {
gitOptions.Auth = &githttp.BasicAuth{
Username: opt.username,
Password: opt.password,
}
}
if opt.referenceName != "" {
gitOptions.ReferenceName = plumbing.ReferenceName(opt.referenceName)
}
_, err := git.PlainCloneContext(ctx, dst, false, &gitOptions)
if err != nil {
return errors.Wrap(err, "failed to clone git repository")
}
if !c.preserveGitDirectory {
os.RemoveAll(filepath.Join(dst, ".git"))
}
return nil
}
// Service represents a service for managing Git.
type Service struct {
httpsCli *http.Client
azure downloader
git downloader
}
// NewService initializes a new service.
@@ -31,32 +82,29 @@ func NewService() *Service {
return &Service{
httpsCli: httpsCli,
azure: NewAzureDownloader(httpsCli),
git: gitClient{},
}
}
// ClonePublicRepository clones a public git repository using the specified URL in the specified
// CloneRepository clones a git repository using the specified URL in the specified
// destination folder.
func (service *Service) ClonePublicRepository(repositoryURL, referenceName string, destination string) error {
return cloneRepository(repositoryURL, referenceName, destination)
}
// ClonePrivateRepositoryWithBasicAuth clones a private git repository using the specified URL in the specified
// destination folder. It will use the specified username and password for basic HTTP authentication.
func (service *Service) ClonePrivateRepositoryWithBasicAuth(repositoryURL, referenceName string, destination, username, password string) error {
credentials := username + ":" + url.PathEscape(password)
repositoryURL = strings.Replace(repositoryURL, "://", "://"+credentials+"@", 1)
return cloneRepository(repositoryURL, referenceName, destination)
}
func cloneRepository(repositoryURL, referenceName, destination string) error {
options := &git.CloneOptions{
URL: repositoryURL,
func (service *Service) CloneRepository(destination, repositoryURL, referenceName, username, password string) error {
options := cloneOptions{
repositoryUrl: repositoryURL,
username: username,
password: password,
referenceName: referenceName,
depth: 1,
}
if referenceName != "" {
options.ReferenceName = plumbing.ReferenceName(referenceName)
return service.cloneRepository(destination, options)
}
func (service *Service) cloneRepository(destination string, options cloneOptions) error {
if isAzureUrl(options.repositoryUrl) {
return service.azure.download(context.TODO(), destination, options)
}
_, err := git.PlainClone(destination, false, options)
return err
return service.git.download(context.TODO(), destination, options)
}

View File

@@ -0,0 +1,27 @@
package git
import (
"os"
"path/filepath"
"testing"
"github.com/docker/docker/pkg/ioutils"
"github.com/stretchr/testify/assert"
)
func TestService_ClonePrivateRepository_GitHub(t *testing.T) {
ensureIntegrationTest(t)
pat := getRequiredValue(t, "GITHUB_PAT")
username := getRequiredValue(t, "GITHUB_USERNAME")
service := NewService()
dst, err := ioutils.TempDir("", "clone")
assert.NoError(t, err)
defer os.RemoveAll(dst)
repositoryUrl := "https://github.com/portainer/private-test-repository.git"
err = service.CloneRepository(dst, repositoryUrl, "refs/heads/main", username, pat)
assert.NoError(t, err)
assert.FileExists(t, filepath.Join(dst, "README.md"))
}

176
api/git/git_test.go Normal file
View File

@@ -0,0 +1,176 @@
package git
import (
"context"
"io/ioutil"
"log"
"os"
"path/filepath"
"testing"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/pkg/errors"
"github.com/portainer/portainer/api/archive"
"github.com/stretchr/testify/assert"
)
var bareRepoDir string
func TestMain(m *testing.M) {
if err := testMain(m); err != nil {
log.Fatal(err)
}
}
// testMain does extra setup/teardown before/after testing.
// The function is separated from TestMain due to necessity to call os.Exit/log.Fatal in the latter.
func testMain(m *testing.M) error {
dir, err := ioutil.TempDir("", "git-repo-")
if err != nil {
return errors.Wrap(err, "failed to create a temp dir")
}
defer os.RemoveAll(dir)
bareRepoDir = filepath.Join(dir, "test-clone.git")
file, err := os.OpenFile("./testdata/test-clone-git-repo.tar.gz", os.O_RDONLY, 0755)
if err != nil {
return errors.Wrap(err, "failed to open an archive")
}
err = archive.ExtractTarGz(file, dir)
if err != nil {
return errors.Wrapf(err, "failed to extract file from the archive to a folder %s\n", dir)
}
m.Run()
return nil
}
func Test_ClonePublicRepository_Shallow(t *testing.T) {
service := Service{git: gitClient{preserveGitDirectory: true}} // no need for http client since the test access the repo via file system.
repositoryURL := bareRepoDir
referenceName := "refs/heads/main"
destination := "shallow"
dir, err := ioutil.TempDir("", destination)
if err != nil {
t.Fatalf("failed to create a temp dir")
}
defer os.RemoveAll(dir)
t.Logf("Cloning into %s", dir)
err = service.CloneRepository(dir, repositoryURL, referenceName, "", "")
assert.NoError(t, err)
assert.Equal(t, 1, getCommitHistoryLength(t, err, dir), "cloned repo has incorrect depth")
}
func Test_ClonePublicRepository_NoGitDirectory(t *testing.T) {
service := Service{git: gitClient{preserveGitDirectory: false}} // no need for http client since the test access the repo via file system.
repositoryURL := bareRepoDir
referenceName := "refs/heads/main"
destination := "shallow"
dir, err := ioutil.TempDir("", destination)
if err != nil {
t.Fatalf("failed to create a temp dir")
}
defer os.RemoveAll(dir)
t.Logf("Cloning into %s", dir)
err = service.CloneRepository(dir, repositoryURL, referenceName, "", "")
assert.NoError(t, err)
assert.NoDirExists(t, filepath.Join(dir, ".git"))
}
func Test_cloneRepository(t *testing.T) {
service := Service{git: gitClient{preserveGitDirectory: true}} // no need for http client since the test access the repo via file system.
repositoryURL := bareRepoDir
referenceName := "refs/heads/main"
destination := "shallow"
dir, err := ioutil.TempDir("", destination)
if err != nil {
t.Fatalf("failed to create a temp dir")
}
defer os.RemoveAll(dir)
t.Logf("Cloning into %s", dir)
err = service.cloneRepository(dir, cloneOptions{
repositoryUrl: repositoryURL,
referenceName: referenceName,
depth: 10,
})
assert.NoError(t, err)
assert.Equal(t, 3, getCommitHistoryLength(t, err, dir), "cloned repo has incorrect depth")
}
func getCommitHistoryLength(t *testing.T, err error, dir string) int {
repo, err := git.PlainOpen(dir)
if err != nil {
t.Fatalf("can't open a git repo at %s with error %v", dir, err)
}
iter, err := repo.Log(&git.LogOptions{All: true})
if err != nil {
t.Fatalf("can't get a commit history iterator with error %v", err)
}
count := 0
err = iter.ForEach(func(_ *object.Commit) error {
count++
return nil
})
if err != nil {
t.Fatalf("can't iterate over the commit history with error %v", err)
}
return count
}
type testDownloader struct {
called bool
}
func (t *testDownloader) download(_ context.Context, _ string, _ cloneOptions) error {
t.called = true
return nil
}
func Test_cloneRepository_azure(t *testing.T) {
tests := []struct {
name string
url string
called bool
}{
{
name: "Azure HTTP URL",
url: "https://Organisation@dev.azure.com/Organisation/Project/_git/Repository",
called: true,
},
{
name: "Azure SSH URL",
url: "git@ssh.dev.azure.com:v3/Organisation/Project/Repository",
called: true,
},
{
name: "Something else",
url: "https://example.com",
called: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
azure := &testDownloader{}
git := &testDownloader{}
s := &Service{azure: azure, git: git}
s.cloneRepository("", cloneOptions{repositoryUrl: tt.url, depth: 1})
// if azure API is called, git isn't and vice versa
assert.Equal(t, tt.called, azure.called)
assert.Equal(t, tt.called, !git.called)
})
}
}

BIN
api/git/testdata/azure-repo.zip vendored Normal file

Binary file not shown.

Binary file not shown.

10
api/git/types/types.go Normal file
View File

@@ -0,0 +1,10 @@
package gittypes
type RepoConfig struct {
// The repo url
URL string `example:"https://github.com/portainer/portainer-ee.git"`
// The reference name
ReferenceName string `example:"refs/heads/branch_name"`
// Path to where the config file is in this url/refName
ConfigFilePath string `example:"docker-compose.yml"`
}

View File

@@ -1,9 +1,9 @@
module github.com/portainer/portainer/api
go 1.13
go 1.16
require (
github.com/Microsoft/go-winio v0.4.14
github.com/Microsoft/go-winio v0.4.16
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a
github.com/boltdb/bolt v1.3.1
github.com/containerd/containerd v1.3.1 // indirect
@@ -13,30 +13,35 @@ require (
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/go-git/go-git/v5 v5.3.0
github.com/go-ldap/ldap/v3 v3.1.8
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/joho/godotenv v1.3.0
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/pkg/errors v0.9.1
github.com/portainer/docker-compose-wrapper v0.0.0-20210527221011-0a1418224b92
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/stretchr/testify v1.6.1
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 // indirect
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.7.0
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/src-d/go-git.v4 v4.13.1
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
k8s.io/api v0.17.2
k8s.io/apimachinery v0.17.2
k8s.io/client-go v0.17.2
)
replace github.com/docker/docker => github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203
replace golang.org/x/sys => golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456

View File

@@ -11,10 +11,10 @@ github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxB
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
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/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk=
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
github.com/Microsoft/hcsshim v0.8.6 h1:ZfF0+zZeYdzMIVMZHKtDKJvLHj76XCuVae/jNkjj0IA=
github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
@@ -47,7 +47,7 @@ github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc h1:TP+534wVl
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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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=
@@ -80,6 +80,7 @@ github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkg
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
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/evanphx/json-patch v4.2.0+incompatible h1:fUDGZCv/7iAN7u0puUVhvKCcsR6vRfwrJatElLBEf0I=
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
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=
@@ -92,6 +93,15 @@ 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-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-billy/v5 v5.1.0 h1:4pl5BV4o7ZG/lterP4S6WzJ6xr49Ba5ET9ygheTYahk=
github.com/go-git/go-billy/v5 v5.1.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12 h1:PbKy9zOy4aAKrJ5pibIRpVO2BXnK1Tlcg+caKI7Ox5M=
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
github.com/go-git/go-git/v5 v5.3.0 h1:8WKMtJR2j8RntEXR/uvTKagfEt4GYlwQ7mntE4+0GWc=
github.com/go-git/go-git/v5 v5.3.0/go.mod h1:xdX4bWJ48aOrdhnl2XqHYstHbbp6+LFS4r4X+lNVprw=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-ldap/ldap/v3 v3.1.8 h1:5vU/2jOh9HqprwXp8aF915s9p6Z8wmbSEVF7/gdTFhM=
github.com/go-ldap/ldap/v3 v3.1.8/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
@@ -105,7 +115,6 @@ github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dp
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/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I=
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
@@ -145,13 +154,16 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
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/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
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=
@@ -168,22 +180,22 @@ github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46O
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
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/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck=
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
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/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-shellwords v1.0.6 h1:9Jok5pILi5S1MnDirGVTufYGtksUs/V2BWUP3ZkeUUI=
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
@@ -205,10 +217,13 @@ github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
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=
@@ -218,14 +233,16 @@ github.com/opencontainers/runc v0.0.0-20161109192122-51371867a01c h1:iOMba/KmaXg
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/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/portainer/docker-compose-wrapper v0.0.0-20210527221011-0a1418224b92 h1:Hh7SHCf3SJblVywU0TTn5lpTKsH5W23LAKH5sqWggig=
github.com/portainer/docker-compose-wrapper v0.0.0-20210527221011-0a1418224b92/go.mod h1:PF2O2O4UNYWdtPcp6n/mIKpKk+f1jhFTezS8txbf+XM=
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=
@@ -246,34 +263,29 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
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/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/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
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/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
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 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
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/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
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=
@@ -286,10 +298,9 @@ golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/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-20190820162420-60c769a6c586/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/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -306,12 +317,10 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r
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/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 h1:e6HwijUxhDe+hPNjZQQn9bA5PW3vNmnN64U2ZW759Lk=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210326060303-6b1517762897 h1:KrsHThm5nFk34YtATK1LsThyGhGbGe1olrte/HInHvs=
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
@@ -322,27 +331,16 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/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-20190209173611-3b5209105503/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-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
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=
@@ -354,13 +352,11 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/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/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
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/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7 h1:ZUjXAXmrAyrmmCPHgCA/vChHcpsX27MZ3yBonD/z1KE=
@@ -371,25 +367,24 @@ google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
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/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
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=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
@@ -408,6 +403,7 @@ k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUc
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU=
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo=
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=

View File

@@ -5,6 +5,7 @@ import (
"log"
"net/http"
"strings"
"time"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
@@ -130,19 +131,21 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use
}
func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError {
tokenData := &portainer.TokenData{
ID: user.ID,
Username: user.Username,
Role: user.Role,
}
return handler.persistAndWriteToken(w, composeTokenData(user))
}
return handler.persistAndWriteToken(w, tokenData)
func (handler *Handler) writeTokenForOAuth(w http.ResponseWriter, user *portainer.User, expiryTime *time.Time) *httperror.HandlerError {
token, err := handler.JWTService.GenerateTokenForOAuth(composeTokenData(user), expiryTime)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to generate JWT token", Err: err}
}
return response.JSON(w, &authenticateResponse{JWT: token})
}
func (handler *Handler) persistAndWriteToken(w http.ResponseWriter, tokenData *portainer.TokenData) *httperror.HandlerError {
token, err := handler.JWTService.GenerateToken(tokenData)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to generate JWT token", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to generate JWT token", Err: err}
}
return response.JSON(w, &authenticateResponse{JWT: token})
@@ -204,3 +207,11 @@ func teamMembershipExists(teamID portainer.TeamID, memberships []portainer.TeamM
}
return false
}
func composeTokenData(user *portainer.User) *portainer.TokenData {
return &portainer.TokenData{
ID: user.ID,
Username: user.Username,
Role: user.Role,
}
}

View File

@@ -4,6 +4,7 @@ import (
"errors"
"log"
"net/http"
"time"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
@@ -25,7 +26,24 @@ func (payload *oauthPayload) Validate(r *http.Request) error {
return nil
}
// @id AuthenticateOauth
func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuthSettings) (string, *time.Time, error) {
if code == "" {
return "", nil, errors.New("Invalid OAuth authorization code")
}
if settings == nil {
return "", nil, errors.New("Invalid OAuth configuration")
}
username, expiryTime, err := handler.OAuthService.Authenticate(code, settings)
if err != nil {
return "", nil, err
}
return username, expiryTime, nil
}
// @id ValidateOAuth
// @summary Authenticate with OAuth
// @tags auth
// @accept json
@@ -36,52 +54,35 @@ func (payload *oauthPayload) Validate(r *http.Request) error {
// @failure 422 "Invalid Credentials"
// @failure 500 "Server error"
// @router /auth/oauth/validate [post]
func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuthSettings) (string, error) {
if code == "" {
return "", errors.New("Invalid OAuth authorization code")
}
if settings == nil {
return "", errors.New("Invalid OAuth configuration")
}
username, err := handler.OAuthService.Authenticate(code, settings)
if err != nil {
return "", err
}
return username, nil
}
func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload oauthPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve settings from the database", Err: err}
}
if settings.AuthenticationMethod != 3 {
return &httperror.HandlerError{http.StatusForbidden, "OAuth authentication is not enabled", errors.New("OAuth authentication is not enabled")}
if settings.AuthenticationMethod != portainer.AuthenticationOAuth {
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "OAuth authentication is not enabled", Err: errors.New("OAuth authentication is not enabled")}
}
username, err := handler.authenticateOAuth(payload.Code, &settings.OAuthSettings)
username, expiryTime, err := handler.authenticateOAuth(payload.Code, &settings.OAuthSettings)
if err != nil {
log.Printf("[DEBUG] - OAuth authentication error: %s", err)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate through OAuth", httperrors.ErrUnauthorized}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to authenticate through OAuth", Err: httperrors.ErrUnauthorized}
}
user, err := handler.DataStore.User().UserByUsername(username)
if err != nil && err != bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a user with the specified username from the database", Err: err}
}
if user == nil && !settings.OAuthSettings.OAuthAutoCreateUsers {
return &httperror.HandlerError{http.StatusForbidden, "Account not created beforehand in Portainer and automatic user provisioning not enabled", httperrors.ErrUnauthorized}
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Account not created beforehand in Portainer and automatic user provisioning not enabled", Err: httperrors.ErrUnauthorized}
}
if user == nil {
@@ -92,7 +93,7 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
err = handler.DataStore.User().CreateUser(user)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist user inside the database", Err: err}
}
if settings.OAuthSettings.DefaultTeamID != 0 {
@@ -104,11 +105,11 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
err = handler.DataStore.TeamMembership().CreateTeamMembership(membership)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist team membership inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist team membership inside the database", Err: err}
}
}
}
return handler.writeToken(w, user)
return handler.writeTokenForOAuth(w, user, expiryTime)
}

View File

@@ -0,0 +1,53 @@
package backup
import (
"fmt"
"net/http"
"os"
"path/filepath"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
operations "github.com/portainer/portainer/api/backup"
)
type (
backupPayload struct {
Password string
}
)
func (p *backupPayload) Validate(r *http.Request) error {
return nil
}
// @id Backup
// @summary Creates an archive with a system data snapshot that could be used to restore the system.
// @description Creates an archive with a system data snapshot that could be used to restore the system.
// @description **Access policy**: admin
// @tags backup
// @security jwt
// @produce octet-stream
// @param Password body string false "Password to encrypt the backup with"
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /backup [post]
func (h *Handler) backup(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload backupPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
archivePath, err := operations.CreateBackupArchive(payload.Password, h.gate, h.dataStore, h.filestorePath)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to create backup", Err: err}
}
defer os.RemoveAll(filepath.Dir(archivePath))
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fmt.Sprintf("portainer-backup_%s", filepath.Base(archivePath))))
http.ServeFile(w, r, archivePath)
return nil
}

View File

@@ -0,0 +1,122 @@
package backup
import (
"bytes"
"context"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"testing"
"time"
"github.com/docker/docker/pkg/ioutils"
"github.com/portainer/portainer/api/adminmonitor"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/offlinegate"
i "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert"
)
func listFiles(dir string) []string {
items := make([]string, 0)
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if path == dir {
return nil
}
items = append(items, path)
return nil
})
return items
}
func contains(t *testing.T, list []string, path string) {
assert.Contains(t, list, path)
copyContent, _ := ioutil.ReadFile(path)
assert.Equal(t, "content\n", string(copyContent))
}
func Test_backupHandlerWithoutPassword_shouldCreateATarballArchive(t *testing.T) {
r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"password":""}`))
w := httptest.NewRecorder()
gate := offlinegate.NewOfflineGate()
adminMonitor := adminmonitor.New(time.Hour, nil, context.Background())
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", func() {}, adminMonitor).backup(w, r)
assert.Nil(t, handlerErr, "Handler should not fail")
response := w.Result()
body, _ := io.ReadAll(response.Body)
tmpdir, _ := ioutils.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
archivePath := filepath.Join(tmpdir, "archive.tar.gz")
err := ioutil.WriteFile(archivePath, body, 0600)
if err != nil {
t.Fatal("Failed to save downloaded .tar.gz archive: ", err)
}
cmd := exec.Command("tar", "-xzf", archivePath, "-C", tmpdir)
err = cmd.Run()
if err != nil {
t.Fatal("Failed to extract archive: ", err)
}
createdFiles := listFiles(tmpdir)
contains(t, createdFiles, path.Join(tmpdir, "portainer.key"))
contains(t, createdFiles, path.Join(tmpdir, "portainer.pub"))
contains(t, createdFiles, path.Join(tmpdir, "tls", "file1"))
contains(t, createdFiles, path.Join(tmpdir, "tls", "file2"))
assert.NotContains(t, createdFiles, path.Join(tmpdir, "extra_file"))
assert.NotContains(t, createdFiles, path.Join(tmpdir, "extra_folder", "file1"))
}
func Test_backupHandlerWithPassword_shouldCreateEncryptedATarballArchive(t *testing.T) {
r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"password":"secret"}`))
w := httptest.NewRecorder()
gate := offlinegate.NewOfflineGate()
adminMonitor := adminmonitor.New(time.Hour, nil, nil)
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", func() {}, adminMonitor).backup(w, r)
assert.Nil(t, handlerErr, "Handler should not fail")
response := w.Result()
body, _ := io.ReadAll(response.Body)
tmpdir, _ := ioutils.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
dr, err := crypto.AesDecrypt(bytes.NewReader(body), []byte("secret"))
if err != nil {
t.Fatal("Failed to decrypt archive")
}
archivePath := filepath.Join(tmpdir, "archive.tag.gz")
archive, _ := os.Create(archivePath)
defer archive.Close()
io.Copy(archive, dr)
cmd := exec.Command("tar", "-xzf", archivePath, "-C", tmpdir)
err = cmd.Run()
if err != nil {
t.Fatal("Failed to extract archive: ", err)
}
createdFiles := listFiles(tmpdir)
contains(t, createdFiles, path.Join(tmpdir, "portainer.key"))
contains(t, createdFiles, path.Join(tmpdir, "portainer.pub"))
contains(t, createdFiles, path.Join(tmpdir, "tls", "file1"))
contains(t, createdFiles, path.Join(tmpdir, "tls", "file2"))
assert.NotContains(t, createdFiles, path.Join(tmpdir, "extra_file"))
assert.NotContains(t, createdFiles, path.Join(tmpdir, "extra_folder", "file1"))
}

View File

@@ -0,0 +1,65 @@
package backup
import (
"context"
"net/http"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/adminmonitor"
"github.com/portainer/portainer/api/http/offlinegate"
"github.com/portainer/portainer/api/http/security"
)
// Handler is an http handler responsible for backup and restore portainer state
type Handler struct {
*mux.Router
bouncer *security.RequestBouncer
dataStore portainer.DataStore
gate *offlinegate.OfflineGate
filestorePath string
shutdownTrigger context.CancelFunc
adminMonitor *adminmonitor.Monitor
}
// NewHandler creates an new instance of backup handler
func NewHandler(bouncer *security.RequestBouncer, dataStore portainer.DataStore, gate *offlinegate.OfflineGate, filestorePath string, shutdownTrigger context.CancelFunc, adminMonitor *adminmonitor.Monitor) *Handler {
h := &Handler{
Router: mux.NewRouter(),
bouncer: bouncer,
dataStore: dataStore,
gate: gate,
filestorePath: filestorePath,
shutdownTrigger: shutdownTrigger,
adminMonitor: adminMonitor,
}
h.Handle("/backup", bouncer.RestrictedAccess(adminAccess(httperror.LoggerHandler(h.backup)))).Methods(http.MethodPost)
h.Handle("/restore", bouncer.PublicAccess(httperror.LoggerHandler(h.restore))).Methods(http.MethodPost)
return h
}
func adminAccess(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
httperror.WriteError(w, http.StatusInternalServerError, "Unable to retrieve user info from request context", err)
}
if !securityContext.IsAdmin {
httperror.WriteError(w, http.StatusUnauthorized, "User is not authorized to perfom the action", nil)
}
next.ServeHTTP(w, r)
})
}
func systemWasInitialized(dataStore portainer.DataStore) (bool, error) {
users, err := dataStore.User().UsersByRole(portainer.AdministratorRole)
if err != nil {
return false, err
}
return len(users) > 0, nil
}

View File

@@ -0,0 +1,69 @@
package backup
import (
"bytes"
"io"
"net/http"
"github.com/pkg/errors"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
operations "github.com/portainer/portainer/api/backup"
)
type restorePayload struct {
FileContent []byte
FileName string
Password string
}
// @id Restore
// @summary Triggers a system restore using provided backup file
// @description Triggers a system restore using provided backup file
// @description **Access policy**: public
// @tags backup
// @param FileContent body []byte true "Content of the backup"
// @param FileName body string true "File name"
// @param Password body string false "Password to decrypt the backup with"
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /restore [post]
func (h *Handler) restore(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
initialized, err := h.adminMonitor.WasInitialized()
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to check system initialization", Err: err}
}
if initialized {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Cannot restore already initialized instance", Err: errors.New("system already initialized")}
}
h.adminMonitor.Stop()
defer h.adminMonitor.Start()
var payload restorePayload
err = decodeForm(r, &payload)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
var archiveReader io.Reader = bytes.NewReader(payload.FileContent)
err = operations.RestoreArchive(archiveReader, payload.Password, h.filestorePath, h.gate, h.dataStore, h.shutdownTrigger)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to restore the backup", Err: err}
}
return nil
}
func decodeForm(r *http.Request, p *restorePayload) error {
content, name, err := request.RetrieveMultiPartFormFile(r, "file")
if err != nil {
return err
}
p.FileContent = content
p.FileName = name
password, _ := request.RetrieveMultiPartFormValue(r, "password", true)
p.Password = password
return nil
}

View File

@@ -0,0 +1,123 @@
package backup
import (
"bytes"
"context"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/adminmonitor"
"github.com/portainer/portainer/api/http/offlinegate"
i "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert"
)
func Test_restoreArchive_usingCombinationOfPasswords(t *testing.T) {
tests := []struct {
name string
backupPassword string
restorePassword string
fails bool
}{
{
name: "empty password to both encrypt and decrypt",
backupPassword: "",
restorePassword: "",
fails: false,
},
{
name: "same password to encrypt and decrypt",
backupPassword: "secret",
restorePassword: "secret",
fails: false,
},
{
name: "different passwords to encrypt and decrypt",
backupPassword: "secret",
restorePassword: "terces",
fails: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
datastore := i.NewDatastore(i.WithUsers([]portainer.User{}), i.WithEdgeJobs([]portainer.EdgeJob{}))
adminMonitor := adminmonitor.New(time.Hour, datastore, context.Background())
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor)
//backup
archive := backup(t, h, test.backupPassword)
//restore
w := httptest.NewRecorder()
r, err := prepareMultipartRequest(test.restorePassword, archive)
assert.Nil(t, err, "Shouldn't fail to write multipart form")
restoreErr := h.restore(w, r)
assert.Equal(t, test.fails, restoreErr != nil, "Didn't meet expectation of failing restore handler")
})
}
}
func Test_restoreArchive_shouldFailIfSystemWasAlreadyInitialized(t *testing.T) {
admin := portainer.User{
Role: portainer.AdministratorRole,
}
datastore := i.NewDatastore(i.WithUsers([]portainer.User{admin}), i.WithEdgeJobs([]portainer.EdgeJob{}))
adminMonitor := adminmonitor.New(time.Hour, datastore, context.Background())
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor)
//backup
archive := backup(t, h, "password")
//restore
w := httptest.NewRecorder()
r, err := prepareMultipartRequest("password", archive)
assert.Nil(t, err, "Shouldn't fail to write multipart form")
restoreErr := h.restore(w, r)
assert.NotNil(t, restoreErr, "Should fail, because system it already initialized")
assert.Equal(t, "Cannot restore already initialized instance", restoreErr.Message, "Should fail with certain error")
}
func backup(t *testing.T, h *Handler, password string) []byte {
r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(fmt.Sprintf(`{"password":"%s"}`, password)))
w := httptest.NewRecorder()
backupErr := h.backup(w, r)
assert.Nil(t, backupErr, "Backup should not fail")
response := w.Result()
archive, _ := io.ReadAll(response.Body)
return archive
}
func prepareMultipartRequest(password string, file []byte) (*http.Request, error) {
var body bytes.Buffer
w := multipart.NewWriter(&body)
err := w.WriteField("password", password)
if err != nil {
return nil, err
}
fw, err := w.CreateFormFile("file", "filename")
if err != nil {
return nil, err
}
io.Copy(fw, bytes.NewReader(file))
r := httptest.NewRequest(http.MethodPost, "http://localhost/", &body)
r.Header.Set("Content-Type", w.FormDataContentType())
w.Close()
return r, nil
}

View File

@@ -0,0 +1 @@
content

View File

@@ -0,0 +1 @@
content

View File

@@ -0,0 +1 @@
content

View File

@@ -0,0 +1 @@
content

View File

@@ -0,0 +1 @@
content

View File

@@ -0,0 +1 @@
content

View File

@@ -236,16 +236,14 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
projectPath := handler.FileService.GetCustomTemplateProjectPath(strconv.Itoa(customTemplateID))
customTemplate.ProjectPath = projectPath
gitCloneParams := &cloneRepositoryParameters{
url: payload.RepositoryURL,
referenceName: payload.RepositoryReferenceName,
path: projectPath,
authentication: payload.RepositoryAuthentication,
username: payload.RepositoryUsername,
password: payload.RepositoryPassword,
repositoryUsername := payload.RepositoryUsername
repositoryPassword := payload.RepositoryPassword
if !payload.RepositoryAuthentication {
repositoryUsername = ""
repositoryPassword = ""
}
err = handler.cloneGitRepository(gitCloneParams)
err = handler.GitService.CloneRepository(projectPath, payload.RepositoryURL, payload.RepositoryReferenceName, repositoryUsername, repositoryPassword)
if err != nil {
return nil, err
}

View File

@@ -1,17 +0,0 @@
package customtemplates
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)
}

View File

@@ -1,28 +0,0 @@
package dockerhub
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
)
// @id DockerHubInspect
// @summary Retrieve DockerHub information
// @description Use this endpoint to retrieve the information used to connect to the DockerHub
// @description **Access policy**: authenticated
// @tags dockerhub
// @security jwt
// @produce json
// @success 200 {object} portainer.DockerHub
// @failure 500 "Server error"
// @router /dockerhub [get]
func (handler *Handler) dockerhubInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
dockerhub, err := handler.DataStore.DockerHub().DockerHub()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve DockerHub details from the database", err}
}
hideFields(dockerhub)
return response.JSON(w, dockerhub)
}

View File

@@ -1,68 +0,0 @@
package dockerhub
import (
"errors"
"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 dockerhubUpdatePayload struct {
// Enable authentication against DockerHub
Authentication bool `validate:"required" example:"false"`
// Username used to authenticate against the DockerHub
Username string `validate:"required" example:"hub_user"`
// Password used to authenticate against the DockerHub
Password string `validate:"required" example:"hub_password"`
}
func (payload *dockerhubUpdatePayload) Validate(r *http.Request) error {
if payload.Authentication && (govalidator.IsNull(payload.Username) || govalidator.IsNull(payload.Password)) {
return errors.New("Invalid credentials. Username and password must be specified when authentication is enabled")
}
return nil
}
// @id DockerHubUpdate
// @summary Update DockerHub information
// @description Use this endpoint to update the information used to connect to the DockerHub
// @description **Access policy**: administrator
// @tags dockerhub
// @security jwt
// @accept json
// @produce json
// @param body body dockerhubUpdatePayload true "DockerHub information"
// @success 204 "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /dockerhub [put]
func (handler *Handler) dockerhubUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload dockerhubUpdatePayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
dockerhub := &portainer.DockerHub{
Authentication: false,
Username: "",
Password: "",
}
if payload.Authentication {
dockerhub.Authentication = true
dockerhub.Username = payload.Username
dockerhub.Password = payload.Password
}
err = handler.DataStore.DockerHub().UpdateDockerHub(dockerhub)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the Dockerhub changes inside the database", err}
}
return response.Empty(w)
}

View File

@@ -1,33 +0,0 @@
package dockerhub
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"
)
func hideFields(dockerHub *portainer.DockerHub) {
dockerHub.Password = ""
}
// Handler is the HTTP handler used to handle DockerHub operations.
type Handler struct {
*mux.Router
DataStore portainer.DataStore
}
// NewHandler creates a handler to manage Dockerhub operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
}
h.Handle("/dockerhub",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.dockerhubInspect))).Methods(http.MethodGet)
h.Handle("/dockerhub",
bouncer.AdminAccess(httperror.LoggerHandler(h.dockerhubUpdate))).Methods(http.MethodPut)
return h
}

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