Compare commits
224 Commits
feat/EE-97
...
feat/EE-82
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b311f6b5a | ||
|
|
08529ac0f2 | ||
|
|
ce7d52dda3 | ||
|
|
93ce0bd8f6 | ||
|
|
6f1ef44def | ||
|
|
90bbc18aab | ||
|
|
3d47258069 | ||
|
|
b5ea45f81b | ||
|
|
fff4f6819d | ||
|
|
45b477aa45 | ||
|
|
ea6df891c3 | ||
|
|
230f8fddc3 | ||
|
|
6734f0ab74 | ||
|
|
3e60167aeb | ||
|
|
8a4902f15a | ||
|
|
dde0467b89 | ||
|
|
a2a197b14b | ||
|
|
ee403ca32a | ||
|
|
d7fcfee2a2 | ||
|
|
3018801fc0 | ||
|
|
6bfbf58cdb | ||
|
|
3568fe9e52 | ||
|
|
2270de73ee | ||
|
|
819faa3948 | ||
|
|
ef8794c2b9 | ||
|
|
5618794927 | ||
|
|
47d462f085 | ||
|
|
0114766d50 | ||
|
|
2b94aa5aa6 | ||
|
|
746e738f1d | ||
|
|
29f5008c5f | ||
|
|
e54d99fd3d | ||
|
|
b3784792fe | ||
|
|
87e7d8ada8 | ||
|
|
af03d91e39 | ||
|
|
71635834c7 | ||
|
|
43702c2516 | ||
|
|
a21798f518 | ||
|
|
3641158daf | ||
|
|
0ac6274712 | ||
|
|
886d6764be | ||
|
|
39e24ec93f | ||
|
|
b7980f1b60 | ||
|
|
ce04944ce6 | ||
|
|
564bea7575 | ||
|
|
dcc77e50e5 | ||
|
|
317ebe2bfc | ||
|
|
daabce2b8f | ||
|
|
7e2ce3ffc2 | ||
|
|
d99358ea8e | ||
|
|
befccacc27 | ||
|
|
ca849e31a1 | ||
|
|
335bfb81ba | ||
|
|
ba2e1d1f60 | ||
|
|
a7fc7816d1 | ||
|
|
5b26ef2036 | ||
|
|
effb0f6272 | ||
|
|
2f95b449aa | ||
|
|
12cf4a00f0 | ||
|
|
d09ae22ba8 | ||
|
|
ac7d819620 | ||
|
|
0aec8fd423 | ||
|
|
8bf662c13a | ||
|
|
fc9511dc97 | ||
|
|
6d8f5e7479 | ||
|
|
a3ec2f8e85 | ||
|
|
c04bbb5775 | ||
|
|
20cbeb698d | ||
|
|
e75678dd11 | ||
|
|
e3e7e84821 | ||
|
|
ad2910f3f0 | ||
|
|
f5aa6c4dc2 | ||
|
|
d1a21ef6c1 | ||
|
|
c542964073 | ||
|
|
572b64b68e | ||
|
|
239e434522 | ||
|
|
9f4fe3af9e | ||
|
|
014ba40081 | ||
|
|
bca32b02c7 | ||
|
|
a7ed6222b0 | ||
|
|
d0d38990c7 | ||
|
|
32a9a2e46b | ||
|
|
660bc2dadf | ||
|
|
4cbd231a5f | ||
|
|
6d5877ca1c | ||
|
|
dbb9a21384 | ||
|
|
f4dd3067ed | ||
|
|
3dccc59048 | ||
|
|
52d4296c08 | ||
|
|
36fcbb9e18 | ||
|
|
f03cf2a6e4 | ||
|
|
6c8276c65c | ||
|
|
c705c04d65 | ||
|
|
56344ca7d9 | ||
|
|
91ff7e4143 | ||
|
|
f2faccdb10 | ||
|
|
ccf6babc02 | ||
|
|
158bdae10e | ||
|
|
59faec45ce | ||
|
|
c72d07441d | ||
|
|
7e7127831d | ||
|
|
3746542c69 | ||
|
|
ebe448b602 | ||
|
|
d84a5b9c67 | ||
|
|
86ad1c6af1 | ||
|
|
a62e0496de | ||
|
|
05ba00a8f7 | ||
|
|
7846fdd801 | ||
|
|
50b57614cf | ||
|
|
90f5a6cd0d | ||
|
|
3fc021826c | ||
|
|
25c010ec3e | ||
|
|
20f8d03366 | ||
|
|
c84da11a91 | ||
|
|
44b6aaedc8 | ||
|
|
b9cad8a7ea | ||
|
|
cc9dd55b5c | ||
|
|
93eaccc878 | ||
|
|
0a65204b0f | ||
|
|
c99b412e11 | ||
|
|
3b4afe838c | ||
|
|
3339ed9509 | ||
|
|
4a1a46c8c1 | ||
|
|
387bbeceba | ||
|
|
86335a4357 | ||
|
|
590b6f69bf | ||
|
|
45afe76bc7 | ||
|
|
739dda1318 | ||
|
|
9bef81eef6 | ||
|
|
aa25eac951 | ||
|
|
d5864d78fc | ||
|
|
0ac8a45825 | ||
|
|
48dbb308ec | ||
|
|
5c1888bfc6 | ||
|
|
bc459b55ae | ||
|
|
f2ec7605c2 | ||
|
|
81b4672076 | ||
|
|
0cfa912d77 | ||
|
|
fc0de913c3 | ||
|
|
f7e6ba544e | ||
|
|
24b1894a84 | ||
|
|
46dec01fe3 | ||
|
|
e401724d43 | ||
|
|
d2d7f6fdb9 | ||
|
|
b747f5f81e | ||
|
|
afbd353808 | ||
|
|
51d584bb50 | ||
|
|
36fbaa9026 | ||
|
|
a71e71f481 | ||
|
|
83f4c5ec0b | ||
|
|
41308d570d | ||
|
|
46ff8a01bc | ||
|
|
2b257d2785 | ||
|
|
da41dbb79a | ||
|
|
68d42617f2 | ||
|
|
8323e22309 | ||
|
|
20d4341170 | ||
|
|
832cafc933 | ||
|
|
f3c537ac2c | ||
|
|
958baf6283 | ||
|
|
08e392378e | ||
|
|
a2d9734b8b | ||
|
|
15aed9fc6f | ||
|
|
121d33538d | ||
|
|
7a03351df8 | ||
|
|
0c2987893d | ||
|
|
d1eddaa188 | ||
|
|
d336ada3c2 | ||
|
|
839198fbff | ||
|
|
486ffa5bbd | ||
|
|
4cd468ce21 | ||
|
|
cbd7fdc62e | ||
|
|
b9fe8009dd | ||
|
|
6a504e7134 | ||
|
|
51ba0876a5 | ||
|
|
769e6a4c6c | ||
|
|
105d1ae519 | ||
|
|
cf508065ec | ||
|
|
eab828279e | ||
|
|
d5763a970b | ||
|
|
c9f68a4d8f | ||
|
|
7848bcf2f4 | ||
|
|
b924347c5b | ||
|
|
9fbda9fb99 | ||
|
|
82f8062784 | ||
|
|
49982eb98a | ||
|
|
4be3ac470f | ||
|
|
a50ab51bef | ||
|
|
7975ef796d | ||
|
|
f8b226a1ef | ||
|
|
342a0d6d22 | ||
|
|
58bf76a58f | ||
|
|
bd98b8956a | ||
|
|
4bc958f865 | ||
|
|
b67c0e870c | ||
|
|
067257df2b | ||
|
|
5f2f7a87ab | ||
|
|
f656ad7124 | ||
|
|
f681e2d532 | ||
|
|
fdb9bf09de | ||
|
|
92ad3e788d | ||
|
|
bc2f5a3260 | ||
|
|
487123491e | ||
|
|
380f106571 | ||
|
|
341378e783 | ||
|
|
b360936454 | ||
|
|
8204d32538 | ||
|
|
60c5ab3eec | ||
|
|
20cf948e53 | ||
|
|
45fcb1ad26 | ||
|
|
7398d54ed0 | ||
|
|
faded67deb | ||
|
|
eadd8b36d6 | ||
|
|
1ad4623b08 | ||
|
|
890bbf4058 | ||
|
|
865c8d899b | ||
|
|
aa5277de2e | ||
|
|
9136ba30eb | ||
|
|
3d9c10adf1 | ||
|
|
3f9ff8460f | ||
|
|
ae3809cefd | ||
|
|
174e28b850 | ||
|
|
3da9751c82 | ||
|
|
8e246c203c |
9
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
9
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
@@ -1,6 +1,10 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report
|
||||
title: ''
|
||||
labels: bug/need-confirmation, kind/bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
@@ -9,7 +13,7 @@ Thanks for reporting a bug for Portainer !
|
||||
|
||||
You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
|
||||
|
||||
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/.
|
||||
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
|
||||
|
||||
Before opening a new issue, make sure that we do not have any duplicates
|
||||
already open. You can ensure this by searching the issue list for this
|
||||
@@ -40,9 +44,12 @@ You can see how [here](https://documentation.portainer.io/archive/1.23.2/faq/#ho
|
||||
|
||||
- Portainer version:
|
||||
- Docker version (managed by Portainer):
|
||||
- Kubernetes version (managed by Portainer):
|
||||
- Platform (windows/linux):
|
||||
- Command used to start Portainer (`docker run -p 9000:9000 portainer/portainer`):
|
||||
- Browser:
|
||||
- Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commerical setup.
|
||||
- Have you reviewed our technical documentation and knowledge base? Yes/No
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
42
.github/ISSUE_TEMPLATE/Custom.md
vendored
42
.github/ISSUE_TEMPLATE/Custom.md
vendored
@@ -1,17 +1,25 @@
|
||||
---
|
||||
name: Question
|
||||
about: Ask us a question about Portainer usage or deployment
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
|
||||
|
||||
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/
|
||||
|
||||
Also, be sure to check our FAQ and documentation first: https://portainer.readthedocs.io
|
||||
-->
|
||||
|
||||
**Question**:
|
||||
How can I deploy Portainer on... ?
|
||||
---
|
||||
name: Question
|
||||
about: Ask us a question about Portainer usage or deployment
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
Before you start, we need a little bit more information from you:
|
||||
|
||||
Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commerical setup.
|
||||
|
||||
Have you reviewed our technical documentation and knowledge base? Yes/No
|
||||
|
||||
<!--
|
||||
|
||||
You can find more information about Portainer support framework policy here: https://old.portainer.io/2019/04/portainer-support-policy/
|
||||
|
||||
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
|
||||
|
||||
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
|
||||
-->
|
||||
|
||||
**Question**:
|
||||
How can I deploy Portainer on... ?
|
||||
|
||||
65
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
65
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
@@ -1,31 +1,34 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest a feature/enhancement that should be added in Portainer
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
Thanks for opening a feature request for Portainer !
|
||||
|
||||
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/
|
||||
|
||||
Before opening a new issue, make sure that we do not have any duplicates
|
||||
already open. You can ensure this by searching the issue list for this
|
||||
repository. If there is a duplicate, please close your issue and add a comment
|
||||
to the existing issue instead.
|
||||
|
||||
Also, be sure to check our FAQ and documentation first: https://portainer.readthedocs.io
|
||||
-->
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest a feature/enhancement that should be added in Portainer
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
Thanks for opening a feature request for Portainer !
|
||||
|
||||
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
|
||||
|
||||
Before opening a new issue, make sure that we do not have any duplicates
|
||||
already open. You can ensure this by searching the issue list for this
|
||||
repository. If there is a duplicate, please close your issue and add a comment
|
||||
to the existing issue instead.
|
||||
|
||||
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
|
||||
-->
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
||||
1
.github/stale.yml
vendored
1
.github/stale.yml
vendored
@@ -15,6 +15,7 @@ issues:
|
||||
- kind/question
|
||||
- kind/style
|
||||
- kind/workaround
|
||||
- kind/refactor
|
||||
- bug/need-confirmation
|
||||
- bug/confirmed
|
||||
- status/discuss
|
||||
|
||||
19
.github/workflows/rebase.yml
vendored
Normal file
19
.github/workflows/rebase.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Automatic Rebase
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
jobs:
|
||||
rebase:
|
||||
name: Rebase
|
||||
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0 # otherwise, you will fail to push refs to dest repo
|
||||
- name: Automatic Rebase
|
||||
uses: cirrus-actions/rebase@1.4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -8,10 +8,8 @@ api/cmd/portainer/portainer*
|
||||
**/.vscode/tasks.json
|
||||
|
||||
.eslintcache
|
||||
.idea
|
||||
test/e2e/cypress/screenshots
|
||||
*.db
|
||||
*.log
|
||||
__debug_bin
|
||||
|
||||
api/docs
|
||||
.idea
|
||||
.env
|
||||
|
||||
32
ATTRIBUTIONS.md
Normal file
32
ATTRIBUTIONS.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Open Source License Attribution
|
||||
|
||||
This application uses Open Source components. You can find the source
|
||||
code of their open source projects along with license information below.
|
||||
We acknowledge and are grateful to these developers for their contributions
|
||||
to open source.
|
||||
|
||||
### [angular-json-tree](https://github.com/awendland/angular-json-tree)
|
||||
|
||||
by [Alex Wendland](https://github.com/awendland) is licensed under [CC BY 4.0 License](https://creativecommons.org/licenses/by/4.0/)
|
||||
|
||||
### [caniuse-db](https://github.com/Fyrd/caniuse)
|
||||
|
||||
by [caniuse.com](caniuse.com) is licensed under [CC BY 4.0 License](https://creativecommons.org/licenses/by/4.0/)
|
||||
|
||||
### [caniuse-lite](https://github.com/ben-eb/caniuse-lite)
|
||||
|
||||
by [caniuse.com](caniuse.com) is licensed under [CC BY 4.0 License](https://creativecommons.org/licenses/by/4.0/)
|
||||
|
||||
### [spdx-exceptions](https://github.com/jslicense/spdx-exceptions.json)
|
||||
|
||||
by Kyle Mitchell using [SPDX](https://spdx.dev/) from Linux Foundation licensed under [CC BY 3.0 License](https://creativecommons.org/licenses/by/3.0/)
|
||||
|
||||
### [fontawesome-free](https://github.com/FortAwesome/Font-Awesome) Icons
|
||||
|
||||
by [Fort Awesome](https://fortawesome.com/) is licensed under [CC BY 4.0 License](https://creativecommons.org/licenses/by/4.0/)
|
||||
|
||||
Portainer also contains the following code, which is licensed under the [MIT license](https://opensource.org/licenses/MIT):
|
||||
|
||||
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (portainer.io)
|
||||
|
||||
rdash-angular: Copyright (c) [2014][elliot hesp]
|
||||
@@ -127,3 +127,9 @@ When adding a new route to an existing handler use the following as a template (
|
||||
```
|
||||
|
||||
explanation about each line can be found (here)[https://github.com/swaggo/swag#api-operation]
|
||||
|
||||
## Licensing
|
||||
|
||||
See the [LICENSE](https://github.com/portainer/portainer/blob/develop/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.
|
||||
|
||||
We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes.
|
||||
|
||||
15
README.md
15
README.md
@@ -28,14 +28,15 @@ Unlike the public demo, the playground sessions are deleted after 4 hours. Apart
|
||||
|
||||
## Getting started
|
||||
|
||||
- [Deploy Portainer](https://www.portainer.io/installation/)
|
||||
- [Deploy Portainer](https://documentation.portainer.io/quickstart/)
|
||||
- [Documentation](https://documentation.portainer.io)
|
||||
- [Building Portainer](https://documentation.portainer.io/contributing/instructions/)
|
||||
|
||||
## Getting help
|
||||
|
||||
For FORMAL Support, please purchase a support subscription from here: https://www.portainer.io/products-services/portainer-business-support/
|
||||
For FORMAL Support, please purchase a support subscription from here: https://www.portainer.io/products/portainer-business
|
||||
|
||||
For community support: You can find more information about Portainer's community support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
|
||||
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
|
||||
|
||||
- Issues: https://github.com/portainer/portainer/issues
|
||||
- FAQ: https://documentation.portainer.io
|
||||
@@ -44,7 +45,7 @@ For community support: You can find more information about Portainer's community
|
||||
## Reporting bugs and contributing
|
||||
|
||||
- Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new).
|
||||
- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://www.portainer.io/documentation/how-to-contribute/) to build it locally and make a pull request. We need all the help we can get!
|
||||
- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://documentation.portainer.io/contributing/instructions/) to build it locally and make a pull request. We need all the help we can get!
|
||||
|
||||
## Security
|
||||
|
||||
@@ -64,8 +65,4 @@ Portainer supports "Current - 2 docker versions only. Prior versions may operate
|
||||
|
||||
Portainer is licensed under the zlib license. See [LICENSE](./LICENSE) for reference.
|
||||
|
||||
Portainer also contains the following code, which is licensed under the [MIT license](https://opensource.org/licenses/MIT):
|
||||
|
||||
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (portainer.io)
|
||||
|
||||
rdash-angular: Copyright (c) [2014][elliot hesp]
|
||||
Portainer also contains code from open source projects. See [ATTRIBUTIONS.md](./ATTRIBUTIONS.md) for a list.
|
||||
|
||||
@@ -37,7 +37,7 @@ func (m *Monitor) Start() {
|
||||
case <-time.After(m.timeout):
|
||||
initialized, err := m.WasInitialized()
|
||||
if err != nil {
|
||||
logFatalf("failed getting admin user: %s", err)
|
||||
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())
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -26,7 +27,7 @@ func listFiles(dir string) []string {
|
||||
}
|
||||
|
||||
func Test_shouldCreateArhive(t *testing.T) {
|
||||
tmpdir, _ := ioutil.TempDir("", "backup")
|
||||
tmpdir, _ := ioutils.TempDir("", "backup")
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
content := []byte("content")
|
||||
@@ -39,7 +40,7 @@ func Test_shouldCreateArhive(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, filepath.Join(tmpdir, fmt.Sprintf("%s.tar.gz", filepath.Base(tmpdir))), gzPath)
|
||||
|
||||
extractionDir, _ := ioutil.TempDir("", "extract")
|
||||
extractionDir, _ := ioutils.TempDir("", "extract")
|
||||
defer os.RemoveAll(extractionDir)
|
||||
|
||||
cmd := exec.Command("tar", "-xzf", gzPath, "-C", extractionDir)
|
||||
@@ -62,7 +63,7 @@ func Test_shouldCreateArhive(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_shouldCreateArhiveXXXXX(t *testing.T) {
|
||||
tmpdir, _ := ioutil.TempDir("", "backup")
|
||||
tmpdir, _ := ioutils.TempDir("", "backup")
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
content := []byte("content")
|
||||
@@ -75,7 +76,7 @@ func Test_shouldCreateArhiveXXXXX(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, filepath.Join(tmpdir, fmt.Sprintf("%s.tar.gz", filepath.Base(tmpdir))), gzPath)
|
||||
|
||||
extractionDir, _ := ioutil.TempDir("", "extract")
|
||||
extractionDir, _ := ioutils.TempDir("", "extract")
|
||||
defer os.RemoveAll(extractionDir)
|
||||
|
||||
r, _ := os.Open(gzPath)
|
||||
|
||||
@@ -16,8 +16,8 @@ func TestUnzipFile(t *testing.T) {
|
||||
Archive structure.
|
||||
├── 0
|
||||
│ ├── 1
|
||||
│ │ └── 2.txt
|
||||
│ └── 1.txt
|
||||
│ │ └── 2.txt
|
||||
│ └── 1.txt
|
||||
└── 0.txt
|
||||
*/
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package backup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
@@ -12,40 +11,12 @@ import (
|
||||
"github.com/portainer/portainer/api/archive"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/http/offlinegate"
|
||||
"github.com/portainer/portainer/api/s3"
|
||||
)
|
||||
|
||||
const rwxr__r__ os.FileMode = 0744
|
||||
|
||||
var filesToBackup = []string{"compose", "config.json", "custom_templates", "edge_jobs", "edge_stacks", "extensions", "portainer.key", "portainer.pub", "tls"}
|
||||
|
||||
func BackupToS3(settings portainer.S3BackupSettings, gate *offlinegate.OfflineGate, datastore portainer.DataStore, filestorePath string) error {
|
||||
archivePath, err := CreateBackupArchive(settings.Password, gate, datastore, filestorePath)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] failed to backup: %s \n", err)
|
||||
return err
|
||||
}
|
||||
archiveReader, err := os.Open(archivePath)
|
||||
if err != nil {
|
||||
log.Println("[ERROR] failed to open backup file")
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(filepath.Dir(archivePath))
|
||||
|
||||
archiveName := fmt.Sprintf("portainer-backup_%s", filepath.Base(archivePath))
|
||||
|
||||
s3session, err := s3.NewSession(settings.Region, settings.AccessKeyID, settings.SecretAccessKey)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] %s \n", err)
|
||||
return err
|
||||
}
|
||||
if err := s3.Upload(s3session, archiveReader, settings.BucketName, archiveName); err != nil {
|
||||
log.Printf("[ERROR] failed to upload backup to S3: %s \n", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/offlinegate"
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
// BackupScheduler orchestrates S3 settings and active backup cron jobs
|
||||
type BackupScheduler struct {
|
||||
cronmanager *cron.Cron
|
||||
s3backupService portainer.S3BackupService
|
||||
gate *offlinegate.OfflineGate
|
||||
datastore portainer.DataStore
|
||||
filestorePath string
|
||||
}
|
||||
|
||||
func NewBackupScheduler(offlineGate *offlinegate.OfflineGate, datastore portainer.DataStore, filestorePath string) *BackupScheduler {
|
||||
crontab := cron.New(cron.WithChain(cron.Recover(cron.DefaultLogger)))
|
||||
s3backupService := datastore.S3Backup()
|
||||
|
||||
return &BackupScheduler{
|
||||
cronmanager: crontab,
|
||||
s3backupService: s3backupService,
|
||||
gate: offlineGate,
|
||||
datastore: datastore,
|
||||
filestorePath: filestorePath,
|
||||
}
|
||||
}
|
||||
|
||||
// Start fetches latest backup settings and starts cron job if configured
|
||||
func (s *BackupScheduler) Start() error {
|
||||
s.cronmanager.Start()
|
||||
|
||||
settings, err := s.s3backupService.GetSettings()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to fetch settings")
|
||||
}
|
||||
|
||||
if canBeScheduled(settings) {
|
||||
return s.startJob(settings)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the scheduler if it is running; otherwise it does nothing.
|
||||
// A context is returned so the caller can wait for running jobs to complete.
|
||||
func (s *BackupScheduler) Stop() context.Context {
|
||||
if s.cronmanager != nil {
|
||||
log.Println("[DEBUG] Stopping backup scheduler")
|
||||
return s.cronmanager.Stop()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update updates stored S3 backup settings and orchestrates cron jobs.
|
||||
// When scheduler has an active cron job, then it shuts it down.
|
||||
// When a provided settings has a cron, then starts a new cron job.
|
||||
// When ever current cron is being shut down, last cron error going to be dropped.
|
||||
func (s *BackupScheduler) Update(settings portainer.S3BackupSettings) error {
|
||||
|
||||
if err := s.s3backupService.UpdateSettings(settings); err != nil {
|
||||
return errors.Wrap(err, "failed to update settings")
|
||||
}
|
||||
|
||||
if err := s.stopJobs(); err != nil {
|
||||
return errors.Wrap(err, "failed to stop current cronjob")
|
||||
}
|
||||
|
||||
if canBeScheduled(settings) {
|
||||
return s.startJob(settings)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// stops current backup cron job and drops last cron error if any
|
||||
func (s *BackupScheduler) stopJobs() error {
|
||||
// stopping all cron jobs as there should be only one (c)
|
||||
for _, job := range s.cronmanager.Entries() {
|
||||
s.cronmanager.Remove(job.ID)
|
||||
}
|
||||
|
||||
return s.s3backupService.DropStatus()
|
||||
}
|
||||
|
||||
func (s *BackupScheduler) startJob(settings portainer.S3BackupSettings) error {
|
||||
_, err := s.cronmanager.AddFunc(settings.CronRule, s.backup(settings))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to start a new backup cron job")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func canBeScheduled(s portainer.S3BackupSettings) bool {
|
||||
return s.AccessKeyID != "" && s.SecretAccessKey != "" && s.Region != "" && s.BucketName != "" && s.CronRule != ""
|
||||
}
|
||||
|
||||
func (s *BackupScheduler) backup(settings portainer.S3BackupSettings) func() {
|
||||
return func() {
|
||||
err := BackupToS3(settings, s.gate, s.datastore, s.filestorePath)
|
||||
status := portainer.S3BackupStatus{
|
||||
Failed: err != nil,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
if err = s.s3backupService.UpdateStatus(status); err != nil {
|
||||
log.Printf("[ERROR] failed to update status of last scheduled backup. Status: %+v . Err: %s \n", status, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
i "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func newScheduler(status *portainer.S3BackupStatus, settings *portainer.S3BackupSettings) *BackupScheduler {
|
||||
scheduler := NewBackupScheduler(nil, i.NewDatastore(i.WithS3BackupService(status, settings)), "")
|
||||
scheduler.Start()
|
||||
|
||||
return scheduler
|
||||
}
|
||||
|
||||
func settings(cronRule string,
|
||||
accessKeyID string,
|
||||
secretAccessKey string,
|
||||
region string,
|
||||
bucketName string) *portainer.S3BackupSettings {
|
||||
return &portainer.S3BackupSettings{
|
||||
CronRule: cronRule,
|
||||
AccessKeyID: accessKeyID,
|
||||
SecretAccessKey: secretAccessKey,
|
||||
Region: region,
|
||||
BucketName: bucketName,
|
||||
}
|
||||
}
|
||||
|
||||
func Test_startWithoutCron_shouldNotStartAJob(t *testing.T) {
|
||||
scheduler := newScheduler(&portainer.S3BackupStatus{}, &portainer.S3BackupSettings{})
|
||||
defer scheduler.Stop()
|
||||
|
||||
jobs := scheduler.cronmanager.Entries()
|
||||
assert.Len(t, jobs, 0, "should have empty job list")
|
||||
}
|
||||
|
||||
func Test_startWitACron_shouldAlsoStartAJob(t *testing.T) {
|
||||
scheduler := newScheduler(&portainer.S3BackupStatus{}, settings("*/10 * * * *", "id", "key", "region", "bucket"))
|
||||
defer scheduler.Stop()
|
||||
|
||||
jobs := scheduler.cronmanager.Entries()
|
||||
assert.Len(t, jobs, 1, "should have 1 active job")
|
||||
}
|
||||
|
||||
func Test_update_shouldDropStatus(t *testing.T) {
|
||||
storedStatus := &portainer.S3BackupStatus{Failed: true, Timestamp: time.Now().Add(-time.Hour)}
|
||||
scheduler := newScheduler(storedStatus, &portainer.S3BackupSettings{})
|
||||
defer scheduler.Stop()
|
||||
|
||||
scheduler.Update(*settings("*/10 * * * *", "id", "key", "region", "bucket"))
|
||||
assert.Equal(t, portainer.S3BackupStatus{}, *storedStatus, "stasus should be dropped")
|
||||
}
|
||||
|
||||
func Test_update_shouldUpdateSettings(t *testing.T) {
|
||||
storedSettings := &portainer.S3BackupSettings{}
|
||||
scheduler := newScheduler(&portainer.S3BackupStatus{}, storedSettings)
|
||||
defer scheduler.Stop()
|
||||
|
||||
newSettings := settings("", "id2", "key2", "region2", "bucket2")
|
||||
scheduler.Update(*newSettings)
|
||||
|
||||
assert.EqualValues(t, *storedSettings, *newSettings, "updated settings should match stored settings")
|
||||
}
|
||||
|
||||
func Test_updateWithCron_shouldStartAJob(t *testing.T) {
|
||||
scheduler := newScheduler(&portainer.S3BackupStatus{}, &portainer.S3BackupSettings{})
|
||||
defer scheduler.Stop()
|
||||
|
||||
jobs := scheduler.cronmanager.Entries()
|
||||
assert.Len(t, jobs, 0, "should have empty job list upon startup")
|
||||
|
||||
scheduler.Update(*settings("*/10 * * * *", "id", "key", "region", "bucket"))
|
||||
|
||||
jobs = scheduler.cronmanager.Entries()
|
||||
assert.Len(t, jobs, 1, "should have 1 active job")
|
||||
}
|
||||
|
||||
func Test_updateWithoutCron_shouldStopActiveJob(t *testing.T) {
|
||||
scheduler := newScheduler(&portainer.S3BackupStatus{}, &portainer.S3BackupSettings{})
|
||||
defer scheduler.Stop()
|
||||
|
||||
scheduler.Update(*settings("*/10 * * * *", "id", "key", "region", "bucket"))
|
||||
|
||||
jobs := scheduler.cronmanager.Entries()
|
||||
assert.Len(t, jobs, 1, "should have 1 active job")
|
||||
|
||||
scheduler.Update(*settings("", "id2", "key2", "region2", "bucket2"))
|
||||
|
||||
jobs = scheduler.cronmanager.Entries()
|
||||
assert.Len(t, jobs, 0, "should have no active jobs")
|
||||
}
|
||||
|
||||
func Test_updateWithACron_shouldStopActiveJob_andStartNewJob(t *testing.T) {
|
||||
scheduler := newScheduler(&portainer.S3BackupStatus{}, &portainer.S3BackupSettings{})
|
||||
defer scheduler.Stop()
|
||||
|
||||
scheduler.Update(*settings("*/10 * * * *", "id", "key", "region", "bucket"))
|
||||
|
||||
jobs := scheduler.cronmanager.Entries()
|
||||
assert.Len(t, jobs, 1, "should have 1 active job")
|
||||
initJobId := jobs[0].ID
|
||||
|
||||
scheduler.Update(*settings("*/10 * * * *", "id", "key", "region", "bucket"))
|
||||
|
||||
jobs = scheduler.cronmanager.Entries()
|
||||
assert.Len(t, jobs, 1, "should have 1 active job")
|
||||
assert.NotEqual(t, initJobId, jobs[0].ID, "new job should have a diffent id")
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -30,7 +31,7 @@ func contains(t *testing.T, list []string, path string) {
|
||||
}
|
||||
|
||||
func Test_copyFile_returnsError_whenSourceDoesNotExist(t *testing.T) {
|
||||
tmpdir, _ := ioutil.TempDir("", "backup")
|
||||
tmpdir, _ := ioutils.TempDir("", "backup")
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
err := copyFile("does-not-exist", tmpdir)
|
||||
@@ -38,7 +39,7 @@ func Test_copyFile_returnsError_whenSourceDoesNotExist(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_copyFile_shouldMakeAbackup(t *testing.T) {
|
||||
tmpdir, _ := ioutil.TempDir("", "backup")
|
||||
tmpdir, _ := ioutils.TempDir("", "backup")
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
content := []byte("content")
|
||||
@@ -52,7 +53,7 @@ func Test_copyFile_shouldMakeAbackup(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_copyDir_shouldCopyAllFilesAndDirectories(t *testing.T) {
|
||||
destination, _ := ioutil.TempDir("", "destination")
|
||||
destination, _ := ioutils.TempDir("", "destination")
|
||||
defer os.RemoveAll(destination)
|
||||
err := copyDir("./test_assets/copy_test", destination)
|
||||
assert.Nil(t, err)
|
||||
@@ -65,7 +66,7 @@ func Test_copyDir_shouldCopyAllFilesAndDirectories(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_backupPath_shouldSkipWhenNotExist(t *testing.T) {
|
||||
tmpdir, _ := ioutil.TempDir("", "backup")
|
||||
tmpdir, _ := ioutils.TempDir("", "backup")
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
err := copyPath("does-not-exists", tmpdir)
|
||||
@@ -75,7 +76,7 @@ func Test_backupPath_shouldSkipWhenNotExist(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_backupPath_shouldCopyFile(t *testing.T) {
|
||||
tmpdir, _ := ioutil.TempDir("", "backup")
|
||||
tmpdir, _ := ioutils.TempDir("", "backup")
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
content := []byte("content")
|
||||
@@ -91,7 +92,7 @@ func Test_backupPath_shouldCopyFile(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_backupPath_shouldCopyDir(t *testing.T) {
|
||||
destination, _ := ioutil.TempDir("", "destination")
|
||||
destination, _ := ioutils.TempDir("", "destination")
|
||||
defer os.RemoveAll(destination)
|
||||
err := copyPath("./test_assets/copy_test", destination)
|
||||
assert.Nil(t, err)
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
plog "github.com/portainer/portainer/api/bolt/log"
|
||||
)
|
||||
|
||||
var backupDefaults = struct {
|
||||
backupDir string
|
||||
editions []string
|
||||
databaseFileName string
|
||||
}{
|
||||
"backups",
|
||||
[]string{"CE", "BE", "EE"},
|
||||
databaseFileName,
|
||||
}
|
||||
|
||||
var backupLog = plog.NewScopedLog("bolt, backup")
|
||||
|
||||
//
|
||||
// Backup Helpers
|
||||
//
|
||||
|
||||
// createBackupFolders create initial folders for backups
|
||||
func (store *Store) createBackupFolders() {
|
||||
for _, e := range backupDefaults.editions {
|
||||
|
||||
p := path.Join(store.path, backupDefaults.backupDir, e)
|
||||
|
||||
if exists, _ := store.fileService.FileExists(p); !exists {
|
||||
err := os.MkdirAll(p, 0700)
|
||||
if err != nil {
|
||||
backupLog.Error("Error while creating backup folders", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (store *Store) databasePath() string {
|
||||
return path.Join(store.path, databaseFileName)
|
||||
}
|
||||
|
||||
func (store *Store) editionBackupDir(edition portainer.SoftwareEdition) string {
|
||||
return path.Join(store.path, backupDefaults.backupDir, edition.GetEditionLabel())
|
||||
}
|
||||
|
||||
func (store *Store) copyDBFile(from string, to string) error {
|
||||
backupLog.Info(fmt.Sprintf("Copying db file from %s to %s", from, to))
|
||||
err := store.fileService.Copy(from, to, true)
|
||||
if err != nil {
|
||||
backupLog.Error("Failed", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// BackupOptions provide a helper to inject backup options
|
||||
type BackupOptions struct {
|
||||
Edition portainer.SoftwareEdition
|
||||
Version int
|
||||
BackupDir string
|
||||
BackupFileName string
|
||||
BackupPath string
|
||||
}
|
||||
|
||||
func (store *Store) setupOptions(options *BackupOptions) *BackupOptions {
|
||||
if options == nil {
|
||||
options = &BackupOptions{}
|
||||
}
|
||||
if options.Edition == 0 {
|
||||
options.Edition = store.edition()
|
||||
}
|
||||
if options.Version == 0 {
|
||||
options.Version, _ = store.version()
|
||||
}
|
||||
if options.BackupDir == "" {
|
||||
options.BackupDir = store.editionBackupDir(options.Edition)
|
||||
}
|
||||
if options.BackupFileName == "" {
|
||||
options.BackupFileName = fmt.Sprintf("%s.%s.%s", backupDefaults.databaseFileName, fmt.Sprintf("%03d", options.Version), time.Now().Format("20060102150405"))
|
||||
}
|
||||
if options.BackupPath == "" {
|
||||
options.BackupPath = path.Join(options.BackupDir, options.BackupFileName)
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func (store *Store) listEditionBackups(edition portainer.SoftwareEdition) ([]string, error) {
|
||||
var fileNames = []string{}
|
||||
|
||||
files, err := ioutil.ReadDir(store.editionBackupDir(edition))
|
||||
|
||||
if err != nil {
|
||||
backupLog.Error("Error while retrieving backup files", err)
|
||||
return fileNames, err
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
fileNames = append(fileNames, f.Name())
|
||||
}
|
||||
|
||||
return fileNames, nil
|
||||
}
|
||||
|
||||
func (store *Store) lastestEditionBackup() (string, error) {
|
||||
edition := store.edition()
|
||||
|
||||
files, err := store.listEditionBackups(edition)
|
||||
if err != nil {
|
||||
backupLog.Error("Error while retrieving backup files", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return files[len(files)-1], nil
|
||||
}
|
||||
|
||||
// BackupWithOptions backup current database with options
|
||||
func (store *Store) BackupWithOptions(options *BackupOptions) (string, error) {
|
||||
backupLog.Info("creating db backup")
|
||||
store.createBackupFolders()
|
||||
|
||||
options = store.setupOptions(options)
|
||||
|
||||
return options.BackupPath, store.copyDBFile(store.databasePath(), options.BackupPath)
|
||||
}
|
||||
|
||||
// Backup current database with default options
|
||||
func (store *Store) Backup() (string, error) {
|
||||
return store.BackupWithOptions(nil)
|
||||
}
|
||||
|
||||
// RestoreWithOptions previously saved backup for the current Edition with options
|
||||
// Restore strategies:
|
||||
// - default: restore latest from current edition
|
||||
// - restore a specific
|
||||
func (store *Store) RestoreWithOptions(options *BackupOptions) error {
|
||||
|
||||
// Check if backup file exist before restoring
|
||||
|
||||
options = store.setupOptions(options)
|
||||
|
||||
_, err := os.Stat(options.BackupPath)
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
backupLog.Error(fmt.Sprintf("Backup file to restore does not exist %s", options.BackupPath), err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = store.Close()
|
||||
if err != nil {
|
||||
backupLog.Error("Error while closing store before restore", err)
|
||||
return err
|
||||
}
|
||||
backupLog.Info("Restoring db backup")
|
||||
err = store.copyDBFile(options.BackupPath, store.databasePath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return store.Open()
|
||||
}
|
||||
|
||||
// Restore previously saved backup for the current Edition with default options
|
||||
func (store *Store) Restore() error {
|
||||
var options = &BackupOptions{}
|
||||
var err error
|
||||
options.BackupFileName, err = store.lastestEditionBackup()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return store.RestoreWithOptions(options)
|
||||
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func TestCreateBackupFolders(t *testing.T) {
|
||||
store := NewTestStore(portainer.PortainerEE, portainer.DBVersionEE, false)
|
||||
if exists, _ := store.fileService.FileExists("tmp/backups"); exists {
|
||||
t.Error("Expect backups folder to not exist")
|
||||
}
|
||||
store.createBackupFolders()
|
||||
if exists, _ := store.fileService.FileExists("tmp/backups"); !exists {
|
||||
t.Error("Expect backups folder to exist")
|
||||
}
|
||||
store.createBackupFolders()
|
||||
store.Close()
|
||||
teardown()
|
||||
}
|
||||
|
||||
func TestStoreCreation(t *testing.T) {
|
||||
store := NewTestStore(portainer.PortainerEE, portainer.DBVersionEE, false)
|
||||
if store == nil {
|
||||
t.Error("Expect to create a store")
|
||||
}
|
||||
|
||||
if store.edition() != portainer.PortainerEE {
|
||||
t.Error("Expect to get EE Edition")
|
||||
}
|
||||
|
||||
version, err := store.version()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if version != portainer.DBVersionEE {
|
||||
t.Error("Expect to get EE DBVersion")
|
||||
}
|
||||
|
||||
store.Close()
|
||||
teardown()
|
||||
}
|
||||
|
||||
func TestBackup(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
edition portainer.SoftwareEdition
|
||||
version int
|
||||
}{
|
||||
{edition: portainer.PortainerCE, version: portainer.DBVersion},
|
||||
{edition: portainer.PortainerEE, version: portainer.DBVersionEE},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
backupFileName := fmt.Sprintf("tmp/backups/%s/portainer.db.%03d.*", tc.edition.GetEditionLabel(), tc.version)
|
||||
t.Run(fmt.Sprintf("Backup should create %s", backupFileName), func(t *testing.T) {
|
||||
store := NewTestStore(tc.edition, tc.version, false)
|
||||
store.Backup()
|
||||
|
||||
if !isFileExist(backupFileName) {
|
||||
t.Errorf("Expect backup file to be created %s", backupFileName)
|
||||
}
|
||||
store.Close()
|
||||
})
|
||||
}
|
||||
t.Run("BackupWithOption should create a name specific backup", func(t *testing.T) {
|
||||
edition := portainer.PortainerCE
|
||||
version := portainer.DBVersion
|
||||
store := NewTestStore(edition, version, false)
|
||||
store.BackupWithOptions(&BackupOptions{
|
||||
BackupFileName: beforePortainerUpgradeToEEBackup,
|
||||
Edition: portainer.PortainerCE,
|
||||
})
|
||||
backupFileName := fmt.Sprintf("tmp/backups/%s/%s", edition.GetEditionLabel(), beforePortainerUpgradeToEEBackup)
|
||||
if !isFileExist(backupFileName) {
|
||||
t.Errorf("Expect backup file to be created %s", backupFileName)
|
||||
}
|
||||
store.Close()
|
||||
})
|
||||
|
||||
teardown()
|
||||
}
|
||||
|
||||
// TODO restore / backup failed test cases
|
||||
func TestRestore(t *testing.T) {
|
||||
|
||||
editions := []portainer.SoftwareEdition{portainer.PortainerCE, portainer.PortainerEE}
|
||||
var currentVersion = 0
|
||||
|
||||
for i, e := range editions {
|
||||
editionLabel := e.GetEditionLabel()
|
||||
currentVersion = 10 ^ i + 1
|
||||
store := NewTestStore(e, currentVersion, false)
|
||||
t.Run(fmt.Sprintf("Basic Restore for %s", editionLabel), func(t *testing.T) {
|
||||
store.Backup()
|
||||
updateVersion(store, currentVersion+1)
|
||||
testVersion(store, currentVersion+1, t)
|
||||
store.Restore()
|
||||
testVersion(store, currentVersion, t)
|
||||
})
|
||||
t.Run(fmt.Sprintf("Basic Restore After Multiple Backup for %s", editionLabel), func(t *testing.T) {
|
||||
currentVersion = currentVersion + 5
|
||||
updateVersion(store, currentVersion)
|
||||
store.Backup()
|
||||
updateVersion(store, currentVersion+2)
|
||||
testVersion(store, currentVersion+2, t)
|
||||
store.Restore()
|
||||
testVersion(store, currentVersion, t)
|
||||
})
|
||||
store.Close()
|
||||
}
|
||||
|
||||
teardown()
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,10 @@ package bolt
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
"github.com/portainer/portainer/api/bolt/license"
|
||||
"github.com/portainer/portainer/api/bolt/s3backup"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/customtemplate"
|
||||
@@ -20,7 +16,10 @@ import (
|
||||
"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/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"
|
||||
"github.com/portainer/portainer/api/bolt/role"
|
||||
@@ -34,9 +33,10 @@ import (
|
||||
"github.com/portainer/portainer/api/bolt/user"
|
||||
"github.com/portainer/portainer/api/bolt/version"
|
||||
"github.com/portainer/portainer/api/bolt/webhook"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
var (
|
||||
const (
|
||||
databaseFileName = "portainer.db"
|
||||
)
|
||||
|
||||
@@ -56,11 +56,9 @@ type Store struct {
|
||||
EndpointService *endpoint.Service
|
||||
EndpointRelationService *endpointrelation.Service
|
||||
ExtensionService *extension.Service
|
||||
LicenseService *license.Service
|
||||
RegistryService *registry.Service
|
||||
ResourceControlService *resourcecontrol.Service
|
||||
RoleService *role.Service
|
||||
S3BackupService *s3backup.Service
|
||||
ScheduleService *schedule.Service
|
||||
SettingsService *settings.Service
|
||||
StackService *stack.Service
|
||||
@@ -73,14 +71,6 @@ type Store struct {
|
||||
WebhookService *webhook.Service
|
||||
}
|
||||
|
||||
func (store *Store) version() (int, error) {
|
||||
version, err := store.VersionService.DBVersion()
|
||||
if err == errors.ErrObjectNotFound {
|
||||
version = 0
|
||||
}
|
||||
return version, err
|
||||
}
|
||||
|
||||
func (store *Store) edition() portainer.SoftwareEdition {
|
||||
edition, err := store.VersionService.Edition()
|
||||
if err == errors.ErrObjectNotFound {
|
||||
@@ -137,6 +127,63 @@ func (store *Store) IsNew() bool {
|
||||
return store.isNew
|
||||
}
|
||||
|
||||
// CheckCurrentEdition checks if current edition is community edition
|
||||
func (store *Store) CheckCurrentEdition() error {
|
||||
if store.edition() != portainer.PortainerCE {
|
||||
return errors.ErrWrongDBEdition
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
// if force is true, then migrate regardless.
|
||||
func (store *Store) MigrateData(force bool) error {
|
||||
if store.isNew && !force {
|
||||
return store.VersionService.StoreDBVersion(portainer.DBVersion)
|
||||
}
|
||||
|
||||
version, err := store.VersionService.DBVersion()
|
||||
if err == errors.ErrObjectNotFound {
|
||||
version = 0
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if version < portainer.DBVersion {
|
||||
migratorParams := &migrator.Parameters{
|
||||
DB: store.connection.DB,
|
||||
DatabaseVersion: version,
|
||||
EndpointGroupService: store.EndpointGroupService,
|
||||
EndpointService: store.EndpointService,
|
||||
EndpointRelationService: store.EndpointRelationService,
|
||||
ExtensionService: store.ExtensionService,
|
||||
RegistryService: store.RegistryService,
|
||||
ResourceControlService: store.ResourceControlService,
|
||||
RoleService: store.RoleService,
|
||||
ScheduleService: store.ScheduleService,
|
||||
SettingsService: store.SettingsService,
|
||||
StackService: store.StackService,
|
||||
TagService: store.TagService,
|
||||
TeamMembershipService: store.TeamMembershipService,
|
||||
UserService: store.UserService,
|
||||
VersionService: store.VersionService,
|
||||
FileService: store.fileService,
|
||||
AuthorizationService: authorization.NewService(store),
|
||||
}
|
||||
migrator := migrator.NewMigrator(migratorParams)
|
||||
|
||||
log.Printf("Migrating database from version %v to %v.\n", version, portainer.DBVersion)
|
||||
err = migrator.Migrate()
|
||||
if err != nil {
|
||||
log.Printf("An error occurred during database migration: %s\n", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
||||
@@ -4,5 +4,5 @@ import "errors"
|
||||
|
||||
var (
|
||||
ErrObjectNotFound = errors.New("Object not found inside the database")
|
||||
ErrMigrationToCE = errors.New("DB is already on CE edition")
|
||||
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/")
|
||||
)
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
)
|
||||
|
||||
var (
|
||||
dataStorePath string
|
||||
testBackupPath string
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
databaseFileName = fmt.Sprintf("portainer-%08d.db", rand.Intn(100000000))
|
||||
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
dataStorePath = path.Join(pwd, "tmp")
|
||||
testBackupPath = path.Join(dataStorePath, "backups")
|
||||
|
||||
teardown()
|
||||
}
|
||||
|
||||
func NewTestStore(edition portainer.SoftwareEdition, version int, init bool) *Store {
|
||||
fileService, err := filesystem.NewService(dataStorePath, "")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
store, err := NewStore(dataStorePath, fileService)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = store.Open()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if init {
|
||||
err = store.Init()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
err = store.VersionService.StoreEdition(edition)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = store.VersionService.StoreDBVersion(version)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
func teardown() {
|
||||
err := os.RemoveAll(testBackupPath)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
files, err := filepath.Glob(path.Join(dataStorePath, "portainer-*.*"))
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
for _, f := range files {
|
||||
if err := os.Remove(f); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isFileExist(path string) bool {
|
||||
matches, err := filepath.Glob(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return len(matches) > 0
|
||||
}
|
||||
|
||||
func updateVersion(store *Store, v int) {
|
||||
err := store.VersionService.StoreDBVersion(v)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func testVersion(store *Store, versionWant int, t *testing.T) {
|
||||
if v, _ := store.version(); v != versionWant {
|
||||
t.Errorf("Expect store version to be %d but was %d", versionWant, v)
|
||||
}
|
||||
}
|
||||
|
||||
func testEdition(store *Store, editionWant portainer.SoftwareEdition, t *testing.T) {
|
||||
if e := store.edition(); e != editionWant {
|
||||
t.Errorf("Expect store edition to be %s but was %s", editionWant.GetEditionLabel(), e.GetEditionLabel())
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,6 @@ func (store *Store) Init() error {
|
||||
AnonymousMode: true,
|
||||
AutoCreateUsers: true,
|
||||
TLSConfig: portainer.TLSConfiguration{},
|
||||
URLs: []string{},
|
||||
SearchSettings: []portainer.LDAPSearchSettings{
|
||||
portainer.LDAPSearchSettings{},
|
||||
},
|
||||
@@ -41,11 +40,8 @@ func (store *Store) Init() error {
|
||||
portainer.LDAPGroupSearchSettings{},
|
||||
},
|
||||
},
|
||||
OAuthSettings: portainer.OAuthSettings{
|
||||
TeamMemberships: portainer.TeamMemberships{
|
||||
OAuthClaimMappings: make([]portainer.OAuthClaimMappings, 0),
|
||||
},
|
||||
},
|
||||
OAuthSettings: portainer.OAuthSettings{},
|
||||
|
||||
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
|
||||
TemplatesURL: portainer.DefaultTemplatesURL,
|
||||
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
|
||||
@@ -96,17 +92,5 @@ func (store *Store) Init() error {
|
||||
}
|
||||
}
|
||||
|
||||
roles, err := store.RoleService.Roles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(roles) == 0 {
|
||||
err := store.RoleService.CreateOrUpdatePredefinedRoles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
package license
|
||||
|
||||
import (
|
||||
"github.com/portainer/liblicense"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "license"
|
||||
)
|
||||
|
||||
// Service represents a service for managing endpoint data.
|
||||
type Service struct {
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// License returns a license by licenseKey
|
||||
func (service *Service) License(licenseKey string) (*liblicense.PortainerLicense, error) {
|
||||
var license liblicense.PortainerLicense
|
||||
identifier := []byte(licenseKey)
|
||||
|
||||
err := internal.GetObject(service.connection, BucketName, identifier, &license)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &license, nil
|
||||
}
|
||||
|
||||
// Licenses return an array containing all the licenses.
|
||||
func (service *Service) Licenses() ([]liblicense.PortainerLicense, error) {
|
||||
var licenses = make([]liblicense.PortainerLicense, 0)
|
||||
|
||||
err := service.connection.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var license liblicense.PortainerLicense
|
||||
err := internal.UnmarshalObject(v, &license)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
licenses = append(licenses, license)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return licenses, err
|
||||
}
|
||||
|
||||
// AddLicense persists a license inside the database.
|
||||
func (service *Service) AddLicense(licenseKey string, license *liblicense.PortainerLicense) error {
|
||||
return service.connection.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
data, err := internal.MarshalObject(license)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put([]byte(licenseKey), data)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateLicense updates a license.
|
||||
func (service *Service) UpdateLicense(licenseKey string, license *liblicense.PortainerLicense) error {
|
||||
identifier := []byte(licenseKey)
|
||||
return internal.UpdateObject(service.connection, BucketName, identifier, license)
|
||||
}
|
||||
|
||||
// DeleteLicense deletes a License.
|
||||
func (service *Service) DeleteLicense(licenseKey string) error {
|
||||
identifier := []byte(licenseKey)
|
||||
return internal.DeleteObject(service.connection, BucketName, identifier)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
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 +0,0 @@
|
||||
package log
|
||||
@@ -1,207 +0,0 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
errors "github.com/portainer/portainer/api/bolt/errors"
|
||||
plog "github.com/portainer/portainer/api/bolt/log"
|
||||
"github.com/portainer/portainer/api/bolt/migrator"
|
||||
"github.com/portainer/portainer/api/cli"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
const beforePortainerUpgradeToEEBackup = "portainer.db.before-EE-upgrade"
|
||||
|
||||
var migrateLog = plog.NewScopedLog("bolt, migrate")
|
||||
|
||||
// FailSafeMigrate backup and restore DB if migration fail
|
||||
func (store *Store) FailSafeMigrate(migrator *migrator.Migrator, version int) error {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
migrateLog.Info(fmt.Sprintf("Error during migration, recovering [%v]", err))
|
||||
store.Restore()
|
||||
}
|
||||
}()
|
||||
return migrator.Migrate(version)
|
||||
}
|
||||
|
||||
// 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.
|
||||
// if force is true, then migrate regardless.
|
||||
func (store *Store) MigrateData(force bool) error {
|
||||
// 0 – if DB is new then we don't need to migrate any data and just set version and edition to latest EE
|
||||
if store.isNew && !force {
|
||||
err := store.VersionService.StoreDBVersion(portainer.DBVersionEE)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = store.VersionService.StoreEdition(portainer.PortainerEE)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
migrator, err := store.newMigrator()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if migrator.Edition() == portainer.PortainerCE {
|
||||
// backup before migrating
|
||||
store.BackupWithOptions(&BackupOptions{
|
||||
BackupFileName: beforePortainerUpgradeToEEBackup,
|
||||
Edition: portainer.PortainerCE,
|
||||
})
|
||||
|
||||
store.VersionService.StorePreviousDBVersion(migrator.Version())
|
||||
|
||||
// 1 – We need to migrate DB to latest CE version
|
||||
|
||||
if migrator.Version() < portainer.DBVersion {
|
||||
store.Backup()
|
||||
err = store.FailSafeMigrate(migrator, portainer.DBVersion)
|
||||
if err != nil {
|
||||
store.Restore()
|
||||
migrateLog.Error("An error occurred while migrating CE database to latest version", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if portainer.Edition == portainer.PortainerEE {
|
||||
// 2 – if DB is CE Edition we need to upgrade settings to EE
|
||||
if migrator.Edition() < portainer.PortainerEE {
|
||||
err = migrator.UpgradeToEE()
|
||||
if err != nil {
|
||||
migrateLog.Error("An error occurred while upgrading database to EE", err)
|
||||
store.RollbackFailedUpgradeToEE()
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 3 – if DB is EE Edition we need to migrate to latest version of EE
|
||||
if migrator.Edition() == portainer.PortainerEE && migrator.Version() < portainer.DBVersionEE {
|
||||
store.Backup()
|
||||
err = store.FailSafeMigrate(migrator, portainer.DBVersionEE)
|
||||
if err != nil {
|
||||
migrateLog.Error("An error occurred while migrating EE database to latest version", err)
|
||||
store.Restore()
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RollbackFailedUpgradeToEE down migrate to previous version
|
||||
func (store *Store) RollbackFailedUpgradeToEE() error {
|
||||
return store.RestoreWithOptions(&BackupOptions{
|
||||
BackupFileName: beforePortainerUpgradeToEEBackup,
|
||||
Edition: portainer.PortainerCE,
|
||||
})
|
||||
}
|
||||
|
||||
// RollbackToCE rollbacks the store to the current ce version
|
||||
func (store *Store) RollbackToCE() error {
|
||||
|
||||
migrator, err := store.newMigrator()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
migrateLog.Info(fmt.Sprintf("Current Software Edition: %s", migrator.Edition().GetEditionLabel()))
|
||||
migrateLog.Info(fmt.Sprintf("Current DB Version: %d", migrator.Version()))
|
||||
|
||||
if migrator.Edition() == portainer.PortainerCE {
|
||||
return errors.ErrMigrationToCE
|
||||
}
|
||||
|
||||
previousVersion, err := store.VersionService.PreviousDBVersion()
|
||||
if err != nil {
|
||||
migrateLog.Error("An Error occurred with retrieving previous DB version", err)
|
||||
return err
|
||||
}
|
||||
|
||||
confirmed, err := cli.Confirm(fmt.Sprintf("Are you sure you want to rollback your database to %d?", previousVersion))
|
||||
if err != nil || !confirmed {
|
||||
return err
|
||||
}
|
||||
|
||||
if previousVersion < 25 {
|
||||
migrator.DowngradeSettingsFrom25()
|
||||
}
|
||||
|
||||
err = store.VersionService.StoreDBVersion(previousVersion)
|
||||
if err != nil {
|
||||
migrateLog.Error(fmt.Sprintf("An Error occurred with rolling back to CE Edition, DB Version %d", previousVersion), err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = store.VersionService.StoreEdition(portainer.PortainerCE)
|
||||
if err != nil {
|
||||
migrateLog.Error(fmt.Sprintf("An Error occurred with rolling back to CE Edition, DB Version %d", previousVersion), err)
|
||||
return err
|
||||
}
|
||||
|
||||
migrateLog.Info(fmt.Sprintf("Rolled back to CE Edition, DB Version %d", previousVersion))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *Store) newMigrator() (*migrator.Migrator, error) {
|
||||
|
||||
version, err := store.version()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
edition := store.edition()
|
||||
|
||||
params := &migrator.Parameters{
|
||||
DB: store.connection.DB,
|
||||
DatabaseVersion: version,
|
||||
CurrentEdition: edition,
|
||||
|
||||
EndpointGroupService: store.EndpointGroupService,
|
||||
EndpointService: store.EndpointService,
|
||||
EndpointRelationService: store.EndpointRelationService,
|
||||
ExtensionService: store.ExtensionService,
|
||||
RegistryService: store.RegistryService,
|
||||
ResourceControlService: store.ResourceControlService,
|
||||
RoleService: store.RoleService,
|
||||
ScheduleService: store.ScheduleService,
|
||||
SettingsService: store.SettingsService,
|
||||
StackService: store.StackService,
|
||||
TagService: store.TagService,
|
||||
TeamMembershipService: store.TeamMembershipService,
|
||||
UserService: store.UserService,
|
||||
VersionService: store.VersionService,
|
||||
FileService: store.fileService,
|
||||
AuthorizationService: authorization.NewService(store),
|
||||
}
|
||||
|
||||
return migrator.NewMigrator(params), nil
|
||||
}
|
||||
|
||||
// RollbackVersion down migrate to previous version
|
||||
func (store *Store) RollbackVersion(version int) error {
|
||||
// TODO
|
||||
backupLog.NotImplemented("RollbackVersion")
|
||||
return nil
|
||||
}
|
||||
|
||||
// RollbackEdition downgrade to previous edition
|
||||
func (store *Store) RollbackEdition(edition portainer.SoftwareEdition) error {
|
||||
// TODO
|
||||
backupLog.NotImplemented("RollbackEdition")
|
||||
// Change Edition
|
||||
// Migrate Services
|
||||
// Restore Latest
|
||||
return nil
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
)
|
||||
|
||||
// New Database should be EE and DBVersion
|
||||
//
|
||||
|
||||
func TestMigrateData(t *testing.T) {
|
||||
var store *Store
|
||||
|
||||
t.Run("MigrateData for New Store", func(t *testing.T) {
|
||||
fileService, err := filesystem.NewService(dataStorePath, "")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
store, err := NewStore(dataStorePath, fileService)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = store.Open()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = store.Init()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
store.MigrateData(false)
|
||||
|
||||
testVersion(store, portainer.DBVersionEE, t)
|
||||
testEdition(store, portainer.PortainerEE, t)
|
||||
|
||||
store.Close()
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
edition portainer.SoftwareEdition
|
||||
version int
|
||||
expectedVersion int
|
||||
}{
|
||||
{edition: portainer.PortainerCE, version: 5, expectedVersion: portainer.DBVersionEE},
|
||||
{edition: portainer.PortainerCE, version: 21, expectedVersion: portainer.DBVersionEE},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
store = NewTestStore(tc.edition, tc.version, true)
|
||||
t.Run(fmt.Sprintf("MigrateData for %s version %d", tc.edition.GetEditionLabel(), tc.version), func(t *testing.T) {
|
||||
store.MigrateData(false)
|
||||
testVersion(store, tc.expectedVersion, t)
|
||||
testEdition(store, portainer.PortainerEE, t)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("Restoring DB after migrateData for %s version %d", tc.edition.GetEditionLabel(), tc.version), func(t *testing.T) {
|
||||
store.RollbackToCE()
|
||||
testVersion(store, tc.version, t)
|
||||
testEdition(store, tc.edition, t)
|
||||
})
|
||||
|
||||
store.Close()
|
||||
}
|
||||
|
||||
t.Run("Error in MigrateData should restore backup before MigrateData", func(t *testing.T) {
|
||||
version := 21
|
||||
store = NewTestStore(portainer.PortainerCE, version, true)
|
||||
|
||||
deleteBucket(store.connection.DB, "settings")
|
||||
store.MigrateData(false)
|
||||
|
||||
testVersion(store, version, t)
|
||||
testEdition(store, portainer.PortainerCE, t)
|
||||
|
||||
store.Close()
|
||||
})
|
||||
|
||||
teardown()
|
||||
}
|
||||
|
||||
func deleteBucket(db *bolt.DB, bucketName string) {
|
||||
db.Update(func(tx *bolt.Tx) error {
|
||||
log.Printf("Delete bucket %s\n", bucketName)
|
||||
err := tx.DeleteBucket([]byte(bucketName))
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package migrator
|
||||
|
||||
// DowngradeSettingsFrom25 downgrade template settings for portainer v1.2
|
||||
func (migrator *Migrator) DowngradeSettingsFrom25() error {
|
||||
legacySettings, err := migrator.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
legacySettings.TemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates-1.20.0.json"
|
||||
|
||||
err = migrator.settingsService.UpdateSettings(legacySettings)
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -1,308 +0,0 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// MigrateCE checks the database version and migrate the existing data to the most recent data model.
|
||||
func (m *Migrator) MigrateCE() error {
|
||||
// Portainer < 1.12
|
||||
if m.currentDBVersion < 1 {
|
||||
err := m.updateAdminUserToDBVersion1()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.12.x
|
||||
if m.currentDBVersion < 2 {
|
||||
err := m.updateResourceControlsToDBVersion2()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = m.updateEndpointsToDBVersion2()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.13.x
|
||||
if m.currentDBVersion < 3 {
|
||||
err := m.updateSettingsToDBVersion3()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.14.0
|
||||
if m.currentDBVersion < 4 {
|
||||
err := m.updateEndpointsToDBVersion4()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1235
|
||||
if m.currentDBVersion < 5 {
|
||||
err := m.updateSettingsToVersion5()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1236
|
||||
if m.currentDBVersion < 6 {
|
||||
err := m.updateSettingsToVersion6()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1449
|
||||
if m.currentDBVersion < 7 {
|
||||
err := m.updateSettingsToVersion7()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if m.currentDBVersion < 8 {
|
||||
err := m.updateEndpointsToVersion8()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// https: //github.com/portainer/portainer/issues/1396
|
||||
if m.currentDBVersion < 9 {
|
||||
err := m.updateEndpointsToVersion9()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/461
|
||||
if m.currentDBVersion < 10 {
|
||||
err := m.updateEndpointsToVersion10()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1906
|
||||
if m.currentDBVersion < 11 {
|
||||
err := m.updateEndpointsToVersion11()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.18.0
|
||||
if m.currentDBVersion < 12 {
|
||||
err := m.updateEndpointsToVersion12()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateEndpointGroupsToVersion12()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateStacksToVersion12()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.19.0
|
||||
if m.currentDBVersion < 13 {
|
||||
err := m.updateSettingsToVersion13()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.19.2
|
||||
if m.currentDBVersion < 14 {
|
||||
err := m.updateResourceControlsToDBVersion14()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.20.0
|
||||
if m.currentDBVersion < 15 {
|
||||
err := m.updateSettingsToDBVersion15()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateTemplatesToVersion15()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if m.currentDBVersion < 16 {
|
||||
err := m.updateSettingsToDBVersion16()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.20.1
|
||||
if m.currentDBVersion < 17 {
|
||||
err := m.updateExtensionsToDBVersion17()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.21.0
|
||||
if m.currentDBVersion < 18 {
|
||||
err := m.updateUsersToDBVersion18()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateEndpointsToDBVersion18()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateEndpointGroupsToDBVersion18()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateRegistriesToDBVersion18()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.22.0
|
||||
if m.currentDBVersion < 19 {
|
||||
err := m.updateSettingsToDBVersion19()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.22.1
|
||||
if m.currentDBVersion < 20 {
|
||||
err := m.updateUsersToDBVersion20()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateSettingsToDBVersion20()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateSchedulesToDBVersion20()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.23.0
|
||||
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
|
||||
if m.currentDBVersion < 22 {
|
||||
err := m.updateResourceControlsToDBVersion22()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateUsersAndRolesToDBVersion22()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.24.0
|
||||
if m.currentDBVersion < 23 {
|
||||
err := m.updateTagsToDBVersion23()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateEndpointsAndEndpointGroupsToDBVersion23()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.24.1
|
||||
if m.currentDBVersion < 24 {
|
||||
err := m.updateSettingsToDB24()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.0.0
|
||||
if m.currentDBVersion < 25 {
|
||||
err := m.updateSettingsToDB25()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateStacksToDB24()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.1.0
|
||||
if m.currentDBVersion < 26 {
|
||||
err := m.updateEndpointSettingsToDB26()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateRbacRolesToDB26()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.2.0
|
||||
if m.currentDBVersion < 27 {
|
||||
err := m.updateStackResourceControlToDB27()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer EE-2.4.0
|
||||
if m.currentDBVersion < 28 {
|
||||
err := m.updateUsersAndRolesToDBVersion28()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer EE-2.4.0
|
||||
if m.currentDBVersion < 29 {
|
||||
err := m.updateRbacRolesToDB29()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer EE-2.7.0
|
||||
if m.currentDBVersion < 31 {
|
||||
err := m.updateSettingsToDB31()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Update DB version to ", portainer.DBVersion)
|
||||
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||
}
|
||||
@@ -29,6 +29,11 @@ func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
|
||||
return err
|
||||
}
|
||||
|
||||
settings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, user := range legacyUsers {
|
||||
user.PortainerAuthorizations = authorization.DefaultPortainerAuthorizations()
|
||||
err = m.userService.UpdateUser(user.ID, &user)
|
||||
@@ -51,7 +56,7 @@ func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
|
||||
return err
|
||||
}
|
||||
helpDeskRole.Priority = 2
|
||||
helpDeskRole.Authorizations = authorization.DefaultEndpointAuthorizationsForHelpDeskRole()
|
||||
helpDeskRole.Authorizations = authorization.DefaultEndpointAuthorizationsForHelpDeskRole(settings.AllowVolumeBrowserForRegularUsers)
|
||||
|
||||
err = m.roleService.UpdateRole(helpDeskRole.ID, helpDeskRole)
|
||||
|
||||
@@ -60,7 +65,7 @@ func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
|
||||
return err
|
||||
}
|
||||
standardUserRole.Priority = 3
|
||||
standardUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForStandardUserRole()
|
||||
standardUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForStandardUserRole(settings.AllowVolumeBrowserForRegularUsers)
|
||||
|
||||
err = m.roleService.UpdateRole(standardUserRole.ID, standardUserRole)
|
||||
|
||||
@@ -69,7 +74,7 @@ func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
|
||||
return err
|
||||
}
|
||||
readOnlyUserRole.Priority = 4
|
||||
readOnlyUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForReadOnlyUserRole()
|
||||
readOnlyUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForReadOnlyUserRole(settings.AllowVolumeBrowserForRegularUsers)
|
||||
|
||||
err = m.roleService.UpdateRole(readOnlyUserRole.ID, readOnlyUserRole)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,10 +2,9 @@ package migrator
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
func (m *Migrator) updateEndpointSettingsToDB26() error {
|
||||
func (m *Migrator) updateEndpointSettingsToDB25() error {
|
||||
settings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -50,27 +49,3 @@ func (m *Migrator) updateEndpointSettingsToDB26() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateRbacRolesToDB26() error {
|
||||
defaultAuthorizationsOfRoles := map[portainer.RoleID]portainer.Authorizations{
|
||||
portainer.RoleIDEndpointAdmin: authorization.DefaultEndpointAuthorizationsForEndpointAdministratorRole(),
|
||||
portainer.RoleIDHelpdesk: authorization.DefaultEndpointAuthorizationsForHelpDeskRole(),
|
||||
portainer.RoleIDStandardUser: authorization.DefaultEndpointAuthorizationsForStandardUserRole(),
|
||||
portainer.RoleIDReadonly: authorization.DefaultEndpointAuthorizationsForReadOnlyUserRole(),
|
||||
}
|
||||
|
||||
for roleID, defaultAuthorizations := range defaultAuthorizationsOfRoles {
|
||||
role, err := m.roleService.Role(roleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
role.Authorizations = defaultAuthorizations
|
||||
|
||||
err = m.roleService.UpdateRole(role.ID, role)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return m.authorizationService.UpdateUsersAuthorizations()
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
)
|
||||
|
||||
func (m *Migrator) updateStackResourceControlToDB27() error {
|
||||
@@ -18,9 +18,6 @@ func (m *Migrator) updateStackResourceControlToDB27() error {
|
||||
}
|
||||
|
||||
stackName := resource.ResourceID
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stack, err := m.stackService.StackByName(stackName)
|
||||
if err != nil {
|
||||
@@ -31,7 +28,7 @@ func (m *Migrator) updateStackResourceControlToDB27() error {
|
||||
return err
|
||||
}
|
||||
|
||||
resource.ResourceID = fmt.Sprintf("%d_%s", stack.EndpointID, stack.Name)
|
||||
resource.ResourceID = stackutils.ResourceControlID(stack.EndpointID, stack.Name)
|
||||
|
||||
err = m.resourceControlService.UpdateResourceControl(resource.ID, &resource)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package migrator
|
||||
|
||||
func (m *Migrator) updateUsersAndRolesToDBVersion28() error {
|
||||
err := m.roleService.CreateOrUpdatePredefinedRoles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.authorizationService.UpdateUsersAuthorizations()
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
func (m *Migrator) updateRbacRolesToDB29() error {
|
||||
defaultAuthorizationsOfRoles := map[portainer.RoleID]portainer.Authorizations{
|
||||
portainer.RoleIDEndpointAdmin: authorization.DefaultEndpointAuthorizationsForEndpointAdministratorRole(),
|
||||
portainer.RoleIDHelpdesk: authorization.DefaultEndpointAuthorizationsForHelpDeskRole(),
|
||||
portainer.RoleIDOperator: authorization.DefaultEndpointAuthorizationsForOperatorRole(),
|
||||
portainer.RoleIDStandardUser: authorization.DefaultEndpointAuthorizationsForStandardUserRole(),
|
||||
portainer.RoleIDReadonly: authorization.DefaultEndpointAuthorizationsForReadOnlyUserRole(),
|
||||
}
|
||||
|
||||
for roleID, defaultAuthorizations := range defaultAuthorizationsOfRoles {
|
||||
role, err := m.roleService.Role(roleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
role.Authorizations = defaultAuthorizations
|
||||
|
||||
err = m.roleService.UpdateRole(role.ID, role)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return m.authorizationService.UpdateUsersAuthorizations()
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package migrator
|
||||
|
||||
func (m *Migrator) updateSettingsToDB31() error {
|
||||
legacySettings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
legacySettings.OAuthSettings.SSO = false
|
||||
legacySettings.OAuthSettings.HideInternalAuth = false
|
||||
legacySettings.OAuthSettings.LogoutURI = ""
|
||||
return m.settingsService.UpdateSettings(legacySettings)
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/portainer/portainer/api/bolt/settings"
|
||||
)
|
||||
|
||||
var (
|
||||
testingDBStorePath string
|
||||
testingDBFileName string
|
||||
dummyLogoURL string
|
||||
dbConn *bolt.DB
|
||||
settingsService *settings.Service
|
||||
)
|
||||
|
||||
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 TestUpdateSettingsToDB31(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.updateSettingsToDB31(); 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.HideInternalAuth != false {
|
||||
t.Errorf("unexpected default OAuth HideInternalAuth setting, want: false, got: %t", updatedSettings.OAuthSettings.HideInternalAuth)
|
||||
}
|
||||
if updatedSettings.OAuthSettings.LogoutURI != "" {
|
||||
t.Errorf("unexpected default OAuth HideInternalAuth setting, want:, got: %s", updatedSettings.OAuthSettings.LogoutURI)
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
"github.com/portainer/portainer/api/bolt/settings"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -1,15 +1,12 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/endpoint"
|
||||
"github.com/portainer/portainer/api/bolt/endpointgroup"
|
||||
"github.com/portainer/portainer/api/bolt/endpointrelation"
|
||||
"github.com/portainer/portainer/api/bolt/extension"
|
||||
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"
|
||||
@@ -23,15 +20,11 @@ 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 {
|
||||
db *bolt.DB
|
||||
currentDBVersion int
|
||||
currentEdition portainer.SoftwareEdition
|
||||
|
||||
currentDBVersion int
|
||||
db *bolt.DB
|
||||
endpointGroupService *endpointgroup.Service
|
||||
endpointService *endpoint.Service
|
||||
endpointRelationService *endpointrelation.Service
|
||||
@@ -52,10 +45,8 @@ type (
|
||||
|
||||
// Parameters represents the required parameters to create a new Migrator instance.
|
||||
Parameters struct {
|
||||
DB *bolt.DB
|
||||
DatabaseVersion int
|
||||
CurrentEdition portainer.SoftwareEdition
|
||||
|
||||
DB *bolt.DB
|
||||
DatabaseVersion int
|
||||
EndpointGroupService *endpointgroup.Service
|
||||
EndpointService *endpoint.Service
|
||||
EndpointRelationService *endpointrelation.Service
|
||||
@@ -80,7 +71,6 @@ func NewMigrator(parameters *Parameters) *Migrator {
|
||||
return &Migrator{
|
||||
db: parameters.DB,
|
||||
currentDBVersion: parameters.DatabaseVersion,
|
||||
currentEdition: parameters.CurrentEdition,
|
||||
endpointGroupService: parameters.EndpointGroupService,
|
||||
endpointService: parameters.EndpointService,
|
||||
endpointRelationService: parameters.EndpointRelationService,
|
||||
@@ -100,43 +90,273 @@ func NewMigrator(parameters *Parameters) *Migrator {
|
||||
}
|
||||
}
|
||||
|
||||
// Version exposes version of database
|
||||
func (migrator *Migrator) Version() int {
|
||||
return migrator.currentDBVersion
|
||||
}
|
||||
|
||||
// Edition exposes edition of portainer
|
||||
func (migrator *Migrator) Edition() portainer.SoftwareEdition {
|
||||
return migrator.currentEdition
|
||||
}
|
||||
|
||||
// Migrate helper to upgrade DB
|
||||
func (migrator *Migrator) Migrate(version int) error {
|
||||
|
||||
migrateLog.Info(fmt.Sprintf("Migrating %s database from version %d to %d.", migrator.Edition().GetEditionLabel(), migrator.currentDBVersion, version))
|
||||
// TODO : run backup before migration and restore if failed
|
||||
err := migrator.MigrateCE() //CE
|
||||
if err != nil {
|
||||
migrateLog.Error("An error occurred during database migration", err)
|
||||
return err
|
||||
// Migrate checks the database version and migrate the existing data to the most recent data model.
|
||||
func (m *Migrator) Migrate() error {
|
||||
// Portainer < 1.12
|
||||
if m.currentDBVersion < 1 {
|
||||
err := m.updateAdminUserToDBVersion1()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
migrator.versionService.StoreDBVersion(version)
|
||||
migrator.currentDBVersion = version
|
||||
// Portainer 1.12.x
|
||||
if m.currentDBVersion < 2 {
|
||||
err := m.updateResourceControlsToDBVersion2()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = m.updateEndpointsToDBVersion2()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RollbackVersion rolls back the db to version
|
||||
func (migrator *Migrator) RollbackVersion(version int) error {
|
||||
|
||||
err := migrator.versionService.StoreDBVersion(version) // portainer.DBVersion
|
||||
return err
|
||||
}
|
||||
|
||||
// RollbackEdition rolls back the db to portainer CE
|
||||
func (migrator *Migrator) RollbackEdition(edition portainer.SoftwareEdition) error {
|
||||
|
||||
err := migrator.versionService.StoreEdition(portainer.PortainerCE)
|
||||
return err
|
||||
// Portainer 1.13.x
|
||||
if m.currentDBVersion < 3 {
|
||||
err := m.updateSettingsToDBVersion3()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.14.0
|
||||
if m.currentDBVersion < 4 {
|
||||
err := m.updateEndpointsToDBVersion4()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1235
|
||||
if m.currentDBVersion < 5 {
|
||||
err := m.updateSettingsToVersion5()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1236
|
||||
if m.currentDBVersion < 6 {
|
||||
err := m.updateSettingsToVersion6()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1449
|
||||
if m.currentDBVersion < 7 {
|
||||
err := m.updateSettingsToVersion7()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if m.currentDBVersion < 8 {
|
||||
err := m.updateEndpointsToVersion8()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// https: //github.com/portainer/portainer/issues/1396
|
||||
if m.currentDBVersion < 9 {
|
||||
err := m.updateEndpointsToVersion9()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/461
|
||||
if m.currentDBVersion < 10 {
|
||||
err := m.updateEndpointsToVersion10()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1906
|
||||
if m.currentDBVersion < 11 {
|
||||
err := m.updateEndpointsToVersion11()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.18.0
|
||||
if m.currentDBVersion < 12 {
|
||||
err := m.updateEndpointsToVersion12()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateEndpointGroupsToVersion12()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateStacksToVersion12()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.19.0
|
||||
if m.currentDBVersion < 13 {
|
||||
err := m.updateSettingsToVersion13()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.19.2
|
||||
if m.currentDBVersion < 14 {
|
||||
err := m.updateResourceControlsToDBVersion14()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.20.0
|
||||
if m.currentDBVersion < 15 {
|
||||
err := m.updateSettingsToDBVersion15()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateTemplatesToVersion15()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if m.currentDBVersion < 16 {
|
||||
err := m.updateSettingsToDBVersion16()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.20.1
|
||||
if m.currentDBVersion < 17 {
|
||||
err := m.updateExtensionsToDBVersion17()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.21.0
|
||||
if m.currentDBVersion < 18 {
|
||||
err := m.updateUsersToDBVersion18()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateEndpointsToDBVersion18()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateEndpointGroupsToDBVersion18()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateRegistriesToDBVersion18()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.22.0
|
||||
if m.currentDBVersion < 19 {
|
||||
err := m.updateSettingsToDBVersion19()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.22.1
|
||||
if m.currentDBVersion < 20 {
|
||||
err := m.updateUsersToDBVersion20()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateSettingsToDBVersion20()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateSchedulesToDBVersion20()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.23.0
|
||||
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
|
||||
if m.currentDBVersion < 22 {
|
||||
err := m.updateResourceControlsToDBVersion22()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateUsersAndRolesToDBVersion22()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.24.0
|
||||
if m.currentDBVersion < 23 {
|
||||
err := m.updateTagsToDBVersion23()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateEndpointsAndEndpointGroupsToDBVersion23()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.24.1
|
||||
if m.currentDBVersion < 24 {
|
||||
err := m.updateSettingsToDB24()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.0.0
|
||||
if m.currentDBVersion < 25 {
|
||||
err := m.updateSettingsToDB25()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateStacksToDB24()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.1.0
|
||||
if m.currentDBVersion < 26 {
|
||||
err := m.updateEndpointSettingsToDB25()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.2.0
|
||||
if m.currentDBVersion < 27 {
|
||||
err := m.updateStackResourceControlToDB27()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
package migrator
|
||||
|
||||
// test CE version is always upgraded to latest version of CE
|
||||
// test EE version is always upgraded to latest version of EE
|
||||
@@ -1,228 +0,0 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
// UpgradeToEE will migrate the db from latest ce version to latest ee version
|
||||
// Latest version is v25 on 06/11/2020
|
||||
func (m *Migrator) UpgradeToEE() error {
|
||||
|
||||
migrateLog.Info(fmt.Sprintf("Migrating CE database version %d to EE database version %d.", m.Version(), portainer.DBVersion))
|
||||
|
||||
migrateLog.Info("Updating LDAP settings to EE")
|
||||
err := m.updateSettingsToEE()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
migrateLog.Info("Updating user roles to EE")
|
||||
err = m.updateUserRolesToEE()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
migrateLog.Info("Updating role authorizations to EE")
|
||||
err = m.updateRoleAuthorizationsToEE()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
migrateLog.Info("Updating user authorizations")
|
||||
err = m.authorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
migrateLog.Info(fmt.Sprintf("Setting db version to %d", portainer.DBVersionEE))
|
||||
err = m.versionService.StoreDBVersion(portainer.DBVersionEE)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
migrateLog.Info(fmt.Sprintf("Setting edition to %s", portainer.PortainerEE.GetEditionLabel()))
|
||||
err = m.versionService.StoreEdition(portainer.PortainerEE)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.currentDBVersion = portainer.DBVersionEE
|
||||
m.currentEdition = portainer.PortainerEE
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateSettingsToEE() error {
|
||||
legacySettings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
legacySettings.LDAPSettings.URLs = []string{}
|
||||
url := legacySettings.LDAPSettings.URL
|
||||
if url != "" {
|
||||
legacySettings.LDAPSettings.URLs = append(legacySettings.LDAPSettings.URLs, url)
|
||||
}
|
||||
|
||||
legacySettings.LDAPSettings.ServerType = portainer.LDAPServerCustom
|
||||
|
||||
return m.settingsService.UpdateSettings(legacySettings)
|
||||
}
|
||||
|
||||
// Updating role authorizations because of the new policies in Kube RBAC
|
||||
func (m *Migrator) updateRoleAuthorizationsToEE() error {
|
||||
migrateLog.Debug("Retriving settings")
|
||||
|
||||
migrateLog.Debug("Updating Endpoint Admin Role")
|
||||
endpointAdministratorRole, err := m.roleService.Role(portainer.RoleID(1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
endpointAdministratorRole.Priority = 1
|
||||
endpointAdministratorRole.Authorizations = authorization.DefaultEndpointAuthorizationsForEndpointAdministratorRole()
|
||||
|
||||
err = m.roleService.UpdateRole(endpointAdministratorRole.ID, endpointAdministratorRole)
|
||||
|
||||
migrateLog.Debug("Updating Help Desk Role")
|
||||
helpDeskRole, err := m.roleService.Role(portainer.RoleID(2))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
helpDeskRole.Priority = 2
|
||||
helpDeskRole.Authorizations = authorization.DefaultEndpointAuthorizationsForHelpDeskRole()
|
||||
|
||||
err = m.roleService.UpdateRole(helpDeskRole.ID, helpDeskRole)
|
||||
|
||||
migrateLog.Debug("Updating Standard User Role")
|
||||
standardUserRole, err := m.roleService.Role(portainer.RoleID(3))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
standardUserRole.Priority = 3
|
||||
standardUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForStandardUserRole()
|
||||
|
||||
err = m.roleService.UpdateRole(standardUserRole.ID, standardUserRole)
|
||||
|
||||
migrateLog.Debug("Updating Read Only User Role")
|
||||
readOnlyUserRole, err := m.roleService.Role(portainer.RoleID(4))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
readOnlyUserRole.Priority = 4
|
||||
readOnlyUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForReadOnlyUserRole()
|
||||
|
||||
err = m.roleService.UpdateRole(readOnlyUserRole.ID, readOnlyUserRole)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// If RBAC extension wasn't installed before, update all users in endpoints and
|
||||
// endpoint groups to have read only access.
|
||||
func (m *Migrator) updateUserRolesToEE() error {
|
||||
err := m.updateUserAuthorizationToEE()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
migrateLog.Debug("Retriving extension info")
|
||||
extensions, err := m.extensionService.Extensions()
|
||||
for _, extension := range extensions {
|
||||
if extension.ID == 3 && extension.Enabled {
|
||||
migrateLog.Info("RBAC extensions were enabled before; Skip updating User Roles")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
migrateLog.Debug("Retriving endpoint groups")
|
||||
endpointGroups, err := m.endpointGroupService.EndpointGroups()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpointGroup := range endpointGroups {
|
||||
migrateLog.Debug(fmt.Sprintf("Updating user policies for endpoint group %v", endpointGroup.ID))
|
||||
for key := range endpointGroup.UserAccessPolicies {
|
||||
updateUserAccessPolicyToReadOnlyRole(endpointGroup.UserAccessPolicies, key)
|
||||
}
|
||||
|
||||
for key := range endpointGroup.TeamAccessPolicies {
|
||||
updateTeamAccessPolicyToReadOnlyRole(endpointGroup.TeamAccessPolicies, key)
|
||||
}
|
||||
|
||||
err := m.endpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
migrateLog.Debug("Retriving endpoints")
|
||||
endpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
migrateLog.Debug(fmt.Sprintf("Updating user policies for endpoint %v", endpoint.ID))
|
||||
for key := range endpoint.UserAccessPolicies {
|
||||
updateUserAccessPolicyToReadOnlyRole(endpoint.UserAccessPolicies, key)
|
||||
}
|
||||
|
||||
for key := range endpoint.TeamAccessPolicies {
|
||||
updateTeamAccessPolicyToReadOnlyRole(endpoint.TeamAccessPolicies, key)
|
||||
}
|
||||
|
||||
err := m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateUserAuthorizationToEE() error {
|
||||
legacyUsers, err := m.userService.Users()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, user := range legacyUsers {
|
||||
user.PortainerAuthorizations = authorization.DefaultPortainerAuthorizations()
|
||||
|
||||
err = m.userService.UpdateUser(user.ID, &user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateUserAccessPolicyToNoRole(policies portainer.UserAccessPolicies, key portainer.UserID) {
|
||||
tmp := policies[key]
|
||||
tmp.RoleID = 0
|
||||
policies[key] = tmp
|
||||
}
|
||||
|
||||
func updateTeamAccessPolicyToNoRole(policies portainer.TeamAccessPolicies, key portainer.TeamID) {
|
||||
tmp := policies[key]
|
||||
tmp.RoleID = 0
|
||||
policies[key] = tmp
|
||||
}
|
||||
|
||||
func updateUserAccessPolicyToReadOnlyRole(policies portainer.UserAccessPolicies, key portainer.UserID) {
|
||||
tmp := policies[key]
|
||||
tmp.RoleID = 4
|
||||
policies[key] = tmp
|
||||
}
|
||||
|
||||
func updateTeamAccessPolicyToReadOnlyRole(policies portainer.TeamAccessPolicies, key portainer.TeamID) {
|
||||
tmp := policies[key]
|
||||
tmp.RoleID = 4
|
||||
policies[key] = tmp
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package role
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
// CreateOrUpdatePredefinedRoles update the predefined roles. Create one if it does not exist yet.
|
||||
func (service *Service) CreateOrUpdatePredefinedRoles() error {
|
||||
predefinedRoles := map[portainer.RoleID]*portainer.Role{
|
||||
portainer.RoleIDEndpointAdmin: &portainer.Role{
|
||||
Name: "Endpoint administrator",
|
||||
Description: "Full control of all resources in an endpoint",
|
||||
ID: portainer.RoleIDEndpointAdmin,
|
||||
Priority: 1,
|
||||
Authorizations: authorization.DefaultEndpointAuthorizationsForEndpointAdministratorRole(),
|
||||
},
|
||||
portainer.RoleIDOperator: &portainer.Role{
|
||||
Name: "Operator",
|
||||
Description: "Operational control of all existing resources in an endpoint",
|
||||
ID: portainer.RoleIDOperator,
|
||||
Priority: 2,
|
||||
Authorizations: authorization.DefaultEndpointAuthorizationsForOperatorRole(),
|
||||
},
|
||||
portainer.RoleIDHelpdesk: &portainer.Role{
|
||||
Name: "Helpdesk",
|
||||
Description: "Read-only access of all resources in an endpoint",
|
||||
ID: portainer.RoleIDHelpdesk,
|
||||
Priority: 3,
|
||||
Authorizations: authorization.DefaultEndpointAuthorizationsForHelpDeskRole(),
|
||||
},
|
||||
portainer.RoleIDStandardUser: &portainer.Role{
|
||||
Name: "Standard user",
|
||||
Description: "Full control of assigned resources in an endpoint",
|
||||
ID: portainer.RoleIDStandardUser,
|
||||
Priority: 4,
|
||||
Authorizations: authorization.DefaultEndpointAuthorizationsForStandardUserRole(),
|
||||
},
|
||||
portainer.RoleIDReadonly: &portainer.Role{
|
||||
Name: "Read-only user",
|
||||
Description: "Read-only access of assigned resources in an endpoint",
|
||||
ID: portainer.RoleIDReadonly,
|
||||
Priority: 5,
|
||||
Authorizations: authorization.DefaultEndpointAuthorizationsForReadOnlyUserRole(),
|
||||
},
|
||||
}
|
||||
|
||||
for roleID, predefinedRole := range predefinedRoles {
|
||||
_, err := service.Role(roleID)
|
||||
|
||||
if err == errors.ErrObjectNotFound {
|
||||
err := service.CreateRole(predefinedRole)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
} else {
|
||||
err = service.UpdateRole(predefinedRole.ID, predefinedRole)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -71,9 +71,7 @@ func (service *Service) CreateRole(role *portainer.Role) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
if role.ID == 0 {
|
||||
role.ID = portainer.RoleID(id)
|
||||
}
|
||||
role.ID = portainer.RoleID(id)
|
||||
|
||||
data, err := internal.MarshalObject(role)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
package s3backup
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
bucketName = "s3backup"
|
||||
statusKey = "lastRunStatus"
|
||||
settingsKey = "settings"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new service and ensures corresponding bucket exist
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, bucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetStatus returns the status of the last scheduled backup run
|
||||
func (s *Service) GetStatus() (portainer.S3BackupStatus, error) {
|
||||
var status portainer.S3BackupStatus
|
||||
err := internal.GetObject(s.connection, bucketName, []byte(statusKey), &status)
|
||||
if err == errors.ErrObjectNotFound {
|
||||
return status, nil
|
||||
}
|
||||
|
||||
return status, err
|
||||
}
|
||||
|
||||
// DropStatus deletes the status of the last sheduled backup run
|
||||
func (s *Service) DropStatus() error {
|
||||
return internal.DeleteObject(s.connection, bucketName, []byte(statusKey))
|
||||
}
|
||||
|
||||
// UpdateStatus upserts a status of the last scheduled backup run
|
||||
func (s *Service) UpdateStatus(status portainer.S3BackupStatus) error {
|
||||
return internal.UpdateObject(s.connection, bucketName, []byte(statusKey), status)
|
||||
}
|
||||
|
||||
// UpdateSettings updates stored s3 backup settings
|
||||
func (s *Service) UpdateSettings(settings portainer.S3BackupSettings) error {
|
||||
return internal.UpdateObject(s.connection, bucketName, []byte(settingsKey), settings)
|
||||
}
|
||||
|
||||
// GetSettings returns stored s3 backup settings
|
||||
func (s *Service) GetSettings() (portainer.S3BackupSettings, error) {
|
||||
var settings portainer.S3BackupSettings
|
||||
err := internal.GetObject(s.connection, bucketName, []byte(settingsKey), &settings)
|
||||
if err == errors.ErrObjectNotFound {
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
return settings, err
|
||||
}
|
||||
@@ -11,11 +11,9 @@ import (
|
||||
"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/license"
|
||||
"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/s3backup"
|
||||
"github.com/portainer/portainer/api/bolt/schedule"
|
||||
"github.com/portainer/portainer/api/bolt/settings"
|
||||
"github.com/portainer/portainer/api/bolt/stack"
|
||||
@@ -89,12 +87,6 @@ func (store *Store) initServices() error {
|
||||
}
|
||||
store.ExtensionService = extensionService
|
||||
|
||||
licenseService, err := license.NewService(store.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.LicenseService = licenseService
|
||||
|
||||
registryService, err := registry.NewService(store.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -107,12 +99,6 @@ func (store *Store) initServices() error {
|
||||
}
|
||||
store.ResourceControlService = resourcecontrolService
|
||||
|
||||
s3backupService, err := s3backup.NewService(store.connection)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
store.S3BackupService = s3backupService
|
||||
|
||||
settingsService, err := settings.NewService(store.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -216,11 +202,6 @@ func (store *Store) EndpointRelation() portainer.EndpointRelationService {
|
||||
return store.EndpointRelationService
|
||||
}
|
||||
|
||||
// License provides access to the License data management layer
|
||||
func (store *Store) License() portainer.LicenseRepository {
|
||||
return store.LicenseService
|
||||
}
|
||||
|
||||
// Registry gives access to the Registry data management layer
|
||||
func (store *Store) Registry() portainer.RegistryService {
|
||||
return store.RegistryService
|
||||
@@ -236,11 +217,6 @@ func (store *Store) Role() portainer.RoleService {
|
||||
return store.RoleService
|
||||
}
|
||||
|
||||
// S3Backup gives access to S3 backup settings and status
|
||||
func (store *Store) S3Backup() portainer.S3BackupService {
|
||||
return store.S3BackupService
|
||||
}
|
||||
|
||||
// Settings gives access to the Settings data management layer
|
||||
func (store *Store) Settings() portainer.SettingsService {
|
||||
return store.SettingsService
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package team
|
||||
|
||||
import (
|
||||
"github.com/boltdb/bolt"
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
"strings"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/boltdb/bolt"
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
"strings"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -11,11 +11,10 @@ import (
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "version"
|
||||
versionKey = "DB_VERSION"
|
||||
previousVersionKey = "PREVIOUS_DB_VERSION"
|
||||
instanceKey = "INSTANCE_ID"
|
||||
editionKey = "EDITION"
|
||||
BucketName = "version"
|
||||
versionKey = "DB_VERSION"
|
||||
instanceKey = "INSTANCE_ID"
|
||||
editionKey = "EDITION"
|
||||
)
|
||||
|
||||
// Service represents a service to manage stored versions.
|
||||
@@ -35,6 +34,30 @@ func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DBVersion retrieves the stored database version.
|
||||
func (service *Service) DBVersion() (int, error) {
|
||||
var data []byte
|
||||
|
||||
err := service.connection.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
value := bucket.Get([]byte(versionKey))
|
||||
if value == nil {
|
||||
return errors.ErrObjectNotFound
|
||||
}
|
||||
|
||||
data = make([]byte, len(value))
|
||||
copy(data, value)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return strconv.Atoi(string(data))
|
||||
}
|
||||
|
||||
// Edition retrieves the stored portainer edition.
|
||||
func (service *Service) Edition() (portainer.SoftwareEdition, error) {
|
||||
editionData, err := service.getKey(editionKey)
|
||||
@@ -50,54 +73,48 @@ func (service *Service) Edition() (portainer.SoftwareEdition, error) {
|
||||
return portainer.SoftwareEdition(edition), nil
|
||||
}
|
||||
|
||||
// StoreEdition store the portainer edition.
|
||||
func (service *Service) StoreEdition(edition portainer.SoftwareEdition) error {
|
||||
return service.setKey(editionKey, strconv.Itoa(int(edition)))
|
||||
}
|
||||
|
||||
// PreviousDBVersion retrieves the stored database version.
|
||||
func (service *Service) PreviousDBVersion() (int, error) {
|
||||
version, err := service.getKey(previousVersionKey)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return strconv.Atoi(string(version))
|
||||
}
|
||||
|
||||
// DBVersion retrieves the stored database version.
|
||||
func (service *Service) DBVersion() (int, error) {
|
||||
version, err := service.getKey(versionKey)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return strconv.Atoi(string(version))
|
||||
}
|
||||
|
||||
// StorePreviousDBVersion store the database version.
|
||||
func (service *Service) StorePreviousDBVersion(version int) error {
|
||||
return service.setKey(previousVersionKey, strconv.Itoa(version))
|
||||
}
|
||||
|
||||
// StoreDBVersion store the database version.
|
||||
func (service *Service) StoreDBVersion(version int) error {
|
||||
return service.setKey(versionKey, strconv.Itoa(version))
|
||||
return service.connection.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
data := []byte(strconv.Itoa(version))
|
||||
return bucket.Put([]byte(versionKey), data)
|
||||
})
|
||||
}
|
||||
|
||||
// InstanceID retrieves the stored instance ID.
|
||||
func (service *Service) InstanceID() (string, error) {
|
||||
instanceID, err := service.getKey(instanceKey)
|
||||
var data []byte
|
||||
|
||||
err := service.connection.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
value := bucket.Get([]byte(instanceKey))
|
||||
if value == nil {
|
||||
return errors.ErrObjectNotFound
|
||||
}
|
||||
|
||||
data = make([]byte, len(value))
|
||||
copy(data, value)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(instanceID), nil
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// StoreInstanceID store the instance ID.
|
||||
func (service *Service) StoreInstanceID(ID string) error {
|
||||
return service.setKey(instanceKey, ID)
|
||||
return service.connection.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
data := []byte(ID)
|
||||
return bucket.Put([]byte(instanceKey), data)
|
||||
})
|
||||
}
|
||||
|
||||
func (service *Service) getKey(key string) ([]byte, error) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api"
|
||||
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -42,7 +42,6 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
|
||||
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(),
|
||||
TLSKey: kingpin.Flag("tlskey", "Path to the TLS key").Default(defaultTLSKeyPath).String(),
|
||||
RollbackToCE: kingpin.Flag("rollback-to-ce", "Rollback the database store to CE").Bool(),
|
||||
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL").Default(defaultSSL).Bool(),
|
||||
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").Default(defaultSSLCertPath).String(),
|
||||
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").Default(defaultSSLKeyPath).String(),
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Confirm starts a rollback db cli application
|
||||
func Confirm(message string) (bool, error) {
|
||||
log.Printf("%s [y/N]", message)
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
answer, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
answer = strings.Replace(answer, "\n", "", -1)
|
||||
answer = strings.ToLower(answer)
|
||||
|
||||
return answer == "y" || answer == "yes", nil
|
||||
|
||||
}
|
||||
@@ -20,7 +20,6 @@ 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"
|
||||
@@ -28,92 +27,69 @@ import (
|
||||
kubecli "github.com/portainer/portainer/api/kubernetes/cli"
|
||||
"github.com/portainer/portainer/api/ldap"
|
||||
"github.com/portainer/portainer/api/libcompose"
|
||||
"github.com/portainer/portainer/api/license"
|
||||
"github.com/portainer/portainer/api/oauth"
|
||||
"github.com/portainer/portainer/api/useractivity"
|
||||
)
|
||||
|
||||
func initCLI() *portainer.CLIFlags {
|
||||
var cliService portainer.CLIService = &cli.Service{}
|
||||
flags, err := cliService.ParseFlags(portainer.APIVersion)
|
||||
if err != nil {
|
||||
log.Fatalf("failed parsing flags: %s", err)
|
||||
log.Fatalf("failed parsing flags: %v", err)
|
||||
}
|
||||
|
||||
err = cliService.ValidateFlags(flags)
|
||||
if err != nil {
|
||||
log.Fatalf("failed validating flags:%s", err)
|
||||
log.Fatalf("failed validating flags:%v", err)
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
func initUserActivityStore(dataStorePath string) portainer.UserActivityStore {
|
||||
store, err := useractivity.NewUserActivityStore(dataStorePath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed initalizing user activity store: %s", err)
|
||||
}
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
func initFileService(dataStorePath string) portainer.FileService {
|
||||
fileService, err := filesystem.NewService(dataStorePath, "")
|
||||
if err != nil {
|
||||
log.Fatalf("failed creating file service: %s", err)
|
||||
log.Fatalf("failed creating file service: %v", err)
|
||||
}
|
||||
return fileService
|
||||
}
|
||||
|
||||
func initDataStore(dataStorePath string, rollback bool, fileService portainer.FileService) portainer.DataStore {
|
||||
func initDataStore(dataStorePath string, fileService portainer.FileService) portainer.DataStore {
|
||||
store, err := bolt.NewStore(dataStorePath, fileService)
|
||||
if err != nil {
|
||||
log.Fatalf("failed creating data store: %s", err)
|
||||
log.Fatalf("failed creating data store: %v", err)
|
||||
}
|
||||
|
||||
err = store.Open()
|
||||
if err != nil {
|
||||
log.Fatalf("failed opening store: %s", err)
|
||||
log.Fatalf("failed opening store: %v", err)
|
||||
}
|
||||
|
||||
err = store.Init()
|
||||
if err != nil {
|
||||
log.Fatalf("failed initializing data store: %s", err)
|
||||
}
|
||||
|
||||
if rollback {
|
||||
err := store.RollbackToCE()
|
||||
if err != nil {
|
||||
log.Fatalf("failed rolling back to CE: %s", err)
|
||||
}
|
||||
|
||||
log.Println("Exiting rollback")
|
||||
os.Exit(0)
|
||||
return nil
|
||||
log.Fatalf("failed initializing data store: %v", err)
|
||||
}
|
||||
|
||||
err = store.MigrateData(false)
|
||||
if err != nil {
|
||||
log.Fatalf("failed migration: %s", err)
|
||||
log.Fatalf("failed migration: %v", err)
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
func initComposeStackManager(assetsPath string, dataStorePath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
|
||||
composeWrapper, err := exec.NewComposeStackManager(assetsPath, dataStorePath, proxyManager)
|
||||
if err != nil {
|
||||
log.Printf("[INFO] [main,compose] [message: falling-back to libcompose] [error: %s]", err)
|
||||
return libcompose.NewComposeStackManager(dataStorePath, reverseTunnelService)
|
||||
composeWrapper := exec.NewComposeWrapper(assetsPath, dataStorePath, proxyManager)
|
||||
if composeWrapper != nil {
|
||||
return composeWrapper
|
||||
}
|
||||
|
||||
return composeWrapper
|
||||
return libcompose.NewComposeStackManager(dataStorePath, reverseTunnelService)
|
||||
}
|
||||
|
||||
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(dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, assetsPath string) portainer.KubernetesDeployer {
|
||||
return exec.NewKubernetesDeployer(dataStore, reverseTunnelService, signatureService, assetsPath)
|
||||
func initKubernetesDeployer(assetsPath string) portainer.KubernetesDeployer {
|
||||
return exec.NewKubernetesDeployer(assetsPath)
|
||||
}
|
||||
|
||||
func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error) {
|
||||
@@ -122,11 +98,11 @@ func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userSessionTimeout := settings.UserSessionTimeout
|
||||
if userSessionTimeout == "" {
|
||||
userSessionTimeout = portainer.DefaultUserSessionTimeout
|
||||
if settings.UserSessionTimeout == "" {
|
||||
settings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
|
||||
dataStore.Settings().UpdateSettings(settings)
|
||||
}
|
||||
jwtService, err := jwt.NewService(userSessionTimeout)
|
||||
jwtService, err := jwt.NewService(settings.UserSessionTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -189,8 +165,6 @@ func updateSettingsFromFlags(dataStore portainer.DataStore, flags *portainer.CLI
|
||||
settings.SnapshotInterval = *flags.SnapshotInterval
|
||||
settings.EnableEdgeComputeFeatures = *flags.EnableEdgeComputeFeatures
|
||||
settings.EnableTelemetry = true
|
||||
settings.OAuthSettings.SSO = true
|
||||
settings.OAuthSettings.HideInternalAuth = true
|
||||
|
||||
if *flags.Templates != "" {
|
||||
settings.TemplatesURL = *flags.Templates
|
||||
@@ -223,7 +197,7 @@ func generateAndStoreKeyPair(fileService portainer.FileService, signatureService
|
||||
func initKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
|
||||
existingKeyPair, err := fileService.KeyPairFilesExist()
|
||||
if err != nil {
|
||||
log.Fatalf("failed checking for existing key pair: %s", err)
|
||||
log.Fatalf("failed checking for existing key pair: %v", err)
|
||||
}
|
||||
|
||||
if existingKeyPair {
|
||||
@@ -266,7 +240,6 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore portainer.Dat
|
||||
AllowVolumeBrowserForRegularUsers: false,
|
||||
EnableHostManagementFeatures: false,
|
||||
|
||||
AllowSysctlSettingForRegularUsers: true,
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowHostNamespaceForRegularUsers: true,
|
||||
@@ -328,7 +301,6 @@ func createUnsecuredEndpoint(endpointURL string, dataStore portainer.DataStore,
|
||||
AllowVolumeBrowserForRegularUsers: false,
|
||||
EnableHostManagementFeatures: false,
|
||||
|
||||
AllowSysctlSettingForRegularUsers: true,
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowHostNamespaceForRegularUsers: true,
|
||||
@@ -372,16 +344,15 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
fileService := initFileService(*flags.Data)
|
||||
|
||||
dataStore := initDataStore(*flags.Data, *flags.RollbackToCE, fileService)
|
||||
dataStore := initDataStore(*flags.Data, fileService)
|
||||
|
||||
if err := dataStore.CheckCurrentEdition(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
jwtService, err := initJWTService(dataStore)
|
||||
if err != nil {
|
||||
log.Fatalf("failed initializing JWT service: %s", err)
|
||||
}
|
||||
|
||||
licenseService := license.NewService(dataStore.License(), shutdownCtx)
|
||||
if err = licenseService.Init(); err != nil {
|
||||
log.Fatalf("failed initializing license service: %s", err)
|
||||
log.Fatalf("failed initializing JWT service: %v", err)
|
||||
}
|
||||
|
||||
ldapService := initLDAPService()
|
||||
@@ -396,14 +367,14 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
err = initKeyPair(fileService, digitalSignatureService)
|
||||
if err != nil {
|
||||
log.Fatalf("failed initializing key pair: %s", err)
|
||||
log.Fatalf("failed initializing key pai: %v", err)
|
||||
}
|
||||
|
||||
reverseTunnelService := chisel.NewService(dataStore, shutdownCtx)
|
||||
|
||||
instanceID, err := dataStore.Version().InstanceID()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get datastore version: %s", err)
|
||||
log.Fatalf("failed getting instance id: %v", err)
|
||||
}
|
||||
|
||||
dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
|
||||
@@ -411,55 +382,49 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx)
|
||||
if err != nil {
|
||||
log.Fatalf("failed initializing snapshot service: %s", err)
|
||||
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: %s", err)
|
||||
log.Fatalf("failed initializing swarm stack manager: %v", err)
|
||||
}
|
||||
kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()
|
||||
|
||||
userActivityStore := initUserActivityStore(*flags.Data)
|
||||
|
||||
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, authorizationService, userActivityStore)
|
||||
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager)
|
||||
|
||||
composeStackManager := initComposeStackManager(*flags.Assets, *flags.Data, reverseTunnelService, proxyManager)
|
||||
|
||||
kubernetesDeployer := initKubernetesDeployer(dataStore, reverseTunnelService, digitalSignatureService, *flags.Assets)
|
||||
kubernetesDeployer := initKubernetesDeployer(*flags.Assets)
|
||||
|
||||
if dataStore.IsNew() {
|
||||
err = updateSettingsFromFlags(dataStore, flags)
|
||||
if err != nil {
|
||||
log.Fatalf("failed updating settings from flags: %s", err)
|
||||
log.Fatalf("failed updating settings from flags: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = edge.LoadEdgeJobs(dataStore, reverseTunnelService)
|
||||
if err != nil {
|
||||
log.Fatalf("failed loading edge jobs from database: %s", err)
|
||||
log.Fatalf("failed loading edge jobs from database: %v", err)
|
||||
}
|
||||
|
||||
applicationStatus := initStatus(flags)
|
||||
|
||||
err = initEndpoint(flags, dataStore, snapshotService)
|
||||
if err != nil {
|
||||
log.Fatalf("failed initializing endpoint: %s", err)
|
||||
log.Fatalf("failed initializing endpoint: %v", err)
|
||||
}
|
||||
|
||||
adminPasswordHash := ""
|
||||
if *flags.AdminPasswordFile != "" {
|
||||
content, err := fileService.GetFileContent(*flags.AdminPasswordFile)
|
||||
if err != nil {
|
||||
log.Fatalf("failed getting admin password file: %s", err)
|
||||
log.Fatalf("failed getting admin password file: %v", err)
|
||||
}
|
||||
adminPasswordHash, err = cryptoService.Hash(strings.TrimSuffix(string(content), "\n"))
|
||||
if err != nil {
|
||||
log.Fatalf("failed hashing admin password: %s", err)
|
||||
log.Fatalf("failed hashing admin password: %v", err)
|
||||
}
|
||||
} else if *flags.AdminPassword != "" {
|
||||
adminPasswordHash = *flags.AdminPassword
|
||||
@@ -468,20 +433,19 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
if adminPasswordHash != "" {
|
||||
users, err := dataStore.User().UsersByRole(portainer.AdministratorRole)
|
||||
if err != nil {
|
||||
log.Fatalf("failed getting admin user: %s", err)
|
||||
log.Fatalf("failed getting admin user: %v", err)
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
log.Println("Created admin user with the given password.")
|
||||
user := &portainer.User{
|
||||
Username: "admin",
|
||||
Role: portainer.AdministratorRole,
|
||||
Password: adminPasswordHash,
|
||||
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
|
||||
Username: "admin",
|
||||
Role: portainer.AdministratorRole,
|
||||
Password: adminPasswordHash,
|
||||
}
|
||||
err := dataStore.User().CreateUser(user)
|
||||
if err != nil {
|
||||
log.Fatalf("failed creating admin user: %s", err)
|
||||
log.Fatalf("failed creating admin user: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Println("Instance already has an administrator user defined. Skipping admin password related flags.")
|
||||
@@ -489,23 +453,16 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
}
|
||||
|
||||
err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService)
|
||||
if err != nil {
|
||||
log.Fatalf("failed starting tunnel server: %s", err)
|
||||
}
|
||||
|
||||
err = licenseService.Start()
|
||||
if err != nil {
|
||||
log.Fatalf("failed starting license service: %s", err)
|
||||
}
|
||||
|
||||
return &http.Server{
|
||||
AuthorizationService: authorizationService,
|
||||
ReverseTunnelService: reverseTunnelService,
|
||||
Status: applicationStatus,
|
||||
BindAddress: *flags.Addr,
|
||||
AssetsPath: *flags.Assets,
|
||||
DataStore: dataStore,
|
||||
LicenseService: licenseService,
|
||||
SwarmStackManager: swarmStackManager,
|
||||
ComposeStackManager: composeStackManager,
|
||||
KubernetesDeployer: kubernetesDeployer,
|
||||
@@ -523,7 +480,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
SSLCert: *flags.SSLCert,
|
||||
SSLKey: *flags.SSLKey,
|
||||
DockerClientFactory: dockerClientFactory,
|
||||
UserActivityStore: userActivityStore,
|
||||
KubernetesClientFactory: kubernetesClientFactory,
|
||||
ShutdownCtx: shutdownCtx,
|
||||
ShutdownTrigger: shutdownTrigger,
|
||||
|
||||
@@ -7,11 +7,12 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
|
||||
tmpdir, _ := ioutil.TempDir("", "encrypt")
|
||||
tmpdir, _ := ioutils.TempDir("", "encrypt")
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
var (
|
||||
@@ -51,7 +52,7 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
|
||||
tmpdir, _ := ioutil.TempDir("", "encrypt")
|
||||
tmpdir, _ := ioutils.TempDir("", "encrypt")
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
var (
|
||||
@@ -91,7 +92,7 @@ func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T) {
|
||||
tmpdir, _ := ioutil.TempDir("", "encrypt")
|
||||
tmpdir, _ := ioutils.TempDir("", "encrypt")
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
var (
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
#! /bin/sh
|
||||
|
||||
go run -v -ldflags="-X github.com/portainer/liblicense.LicenseServerBaseURL=http://localhost:8080" cmd/portainer/main.go --data=./tmp/data
|
||||
@@ -1,120 +0,0 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"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
|
||||
dataPath string
|
||||
proxyManager *proxy.Manager
|
||||
}
|
||||
|
||||
// NewComposeStackManager returns a docker-compose wrapper if corresponding binary present, otherwise nil
|
||||
func NewComposeStackManager(binaryPath string, dataPath string, proxyManager *proxy.Manager) (*ComposeStackManager, error) {
|
||||
wrap, err := wrapper.NewComposeWrapper(binaryPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ComposeStackManager{
|
||||
wrapper: wrap,
|
||||
proxyManager: proxyManager,
|
||||
dataPath: dataPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 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(filePath, url, stack.Name, envFilePath, w.dataPath)
|
||||
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(filePath, url, stack.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
// NormalizeStackName returns the passed stack name, for interface implementation only
|
||||
func (w *ComposeStackManager) NormalizeStackName(name string) string {
|
||||
return name
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
141
api/exec/compose_wrapper.go
Normal file
141
api/exec/compose_wrapper.go
Normal file
@@ -0,0 +1,141 @@
|
||||
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
|
||||
dataPath string
|
||||
proxyManager *proxy.Manager
|
||||
}
|
||||
|
||||
// NewComposeWrapper returns a docker-compose wrapper if corresponding binary present, otherwise nil
|
||||
func NewComposeWrapper(binaryPath, dataPath string, proxyManager *proxy.Manager) *ComposeWrapper {
|
||||
if !IsBinaryPresent(programPath(binaryPath, "docker-compose")) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &ComposeWrapper{
|
||||
binaryPath: binaryPath,
|
||||
dataPath: dataPath,
|
||||
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.Env = os.Environ()
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("DOCKER_CONFIG=%s", w.dataPath))
|
||||
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
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
// +build integration
|
||||
|
||||
package exec
|
||||
|
||||
import (
|
||||
@@ -32,9 +33,7 @@ func setup(t *testing.T) (*portainer.Stack, *portainer.Endpoint) {
|
||||
Name: "project-name",
|
||||
}
|
||||
|
||||
endpoint := &portainer.Endpoint{
|
||||
URL: "unix://",
|
||||
}
|
||||
endpoint := &portainer.Endpoint{}
|
||||
|
||||
return stack, endpoint
|
||||
}
|
||||
@@ -43,17 +42,14 @@ func Test_UpAndDown(t *testing.T) {
|
||||
|
||||
stack, endpoint := setup(t)
|
||||
|
||||
w, err := NewComposeStackManager("", "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed creating manager: %s", err)
|
||||
}
|
||||
w := NewComposeWrapper("", "", nil)
|
||||
|
||||
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) {
|
||||
if containerExists(composedContainerName) == false {
|
||||
t.Fatal("container should exist")
|
||||
}
|
||||
|
||||
@@ -67,13 +63,13 @@ func Test_UpAndDown(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func containerExists(containerName string) bool {
|
||||
cmd := exec.Command("docker", "ps", "-a", "-f", fmt.Sprintf("name=%s", containerName))
|
||||
func containerExists(contaierName string) bool {
|
||||
cmd := exec.Command(osProgram("docker"), "ps", "-a", "-f", fmt.Sprintf("name=%s", contaierName))
|
||||
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to list containers: %s", err)
|
||||
}
|
||||
|
||||
return strings.Contains(string(out), containerName)
|
||||
return strings.Contains(string(out), contaierName)
|
||||
}
|
||||
143
api/exec/compose_wrapper_test.go
Normal file
143
api/exec/compose_wrapper_test.go
Normal file
@@ -0,0 +1,143 @@
|
||||
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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,188 +2,71 @@ package exec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"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
|
||||
dataStore portainer.DataStore
|
||||
reverseTunnelService portainer.ReverseTunnelService
|
||||
signatureService portainer.DigitalSignatureService
|
||||
binaryPath string
|
||||
}
|
||||
|
||||
// NewKubernetesDeployer initializes a new KubernetesDeployer service.
|
||||
func NewKubernetesDeployer(datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, binaryPath string) *KubernetesDeployer {
|
||||
func NewKubernetesDeployer(binaryPath string) *KubernetesDeployer {
|
||||
return &KubernetesDeployer{
|
||||
binaryPath: binaryPath,
|
||||
dataStore: datastore,
|
||||
reverseTunnelService: reverseTunnelService,
|
||||
signatureService: signatureService,
|
||||
binaryPath: binaryPath,
|
||||
}
|
||||
}
|
||||
|
||||
// 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, stackConfig string, namespace string) (string, error) {
|
||||
if endpoint.Type == portainer.KubernetesLocalEnvironment {
|
||||
token, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
|
||||
func (deployer *KubernetesDeployer) Deploy(endpoint *portainer.Endpoint, data string, composeFormat bool, namespace string) ([]byte, error) {
|
||||
if composeFormat {
|
||||
convertedData, err := deployer.convertComposeData(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, 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(stackConfig)
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", errors.New(stderr.String())
|
||||
}
|
||||
|
||||
return string(output), nil
|
||||
data = string(convertedData)
|
||||
}
|
||||
|
||||
// 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))
|
||||
token, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reqPayload, err := json.Marshal(
|
||||
struct {
|
||||
StackConfig string
|
||||
Namespace string
|
||||
}{
|
||||
StackConfig: stackConfig,
|
||||
Namespace: namespace,
|
||||
})
|
||||
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()
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, errors.New(stderr.String())
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
req.Header.Set(portainer.PortainerAgentPublicKeyHeader, deployer.signatureService.EncodedPublicKey())
|
||||
req.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
|
||||
|
||||
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
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// ConvertCompose leverages the kompose binary to deploy a compose compliant manifest.
|
||||
func (deployer *KubernetesDeployer) ConvertCompose(data string) ([]byte, error) {
|
||||
func (deployer *KubernetesDeployer) convertComposeData(data string) ([]byte, error) {
|
||||
command := path.Join(deployer.binaryPath, "kompose")
|
||||
if runtime.GOOS == "windows" {
|
||||
command = path.Join(deployer.binaryPath, "kompose.exe")
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"path"
|
||||
"runtime"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// SwarmStackManager represents a service for managing stacks.
|
||||
|
||||
24
api/exec/utils.go
Normal file
24
api/exec/utils.go
Normal file
@@ -0,0 +1,24 @@
|
||||
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
|
||||
}
|
||||
16
api/exec/utils_test.go
Normal file
16
api/exec/utils_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -106,66 +106,6 @@ func (service *Service) GetStackProjectPath(stackIdentifier string) string {
|
||||
return path.Join(service.fileStorePath, ComposeStorePath, stackIdentifier)
|
||||
}
|
||||
|
||||
// Copy copies the file on fromFilePath to toFilePath
|
||||
// if toFilePath exists func will fail unless deleteIfExists is true
|
||||
func (service *Service) Copy(fromFilePath string, toFilePath string, deleteIfExists bool) error {
|
||||
exists, err := service.FileExists(fromFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return errors.New("File doesn't exist")
|
||||
}
|
||||
|
||||
finput, err := os.Open(fromFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer finput.Close()
|
||||
|
||||
exists, err = service.FileExists(toFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if exists {
|
||||
if !deleteIfExists {
|
||||
return errors.New("Destination file exists")
|
||||
}
|
||||
|
||||
err := os.Remove(toFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
foutput, err := os.Create(toFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer foutput.Close()
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
n, err := finput.Read(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if _, err := foutput.Write(buf[:n]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StoreStackFileFromBytes creates a subfolder in the ComposeStorePath and stores a new file from bytes.
|
||||
// It returns the path to the folder where the file is stored.
|
||||
func (service *Service) StoreStackFileFromBytes(stackIdentifier, fileName string, data []byte) (string, error) {
|
||||
|
||||
@@ -2,16 +2,17 @@ package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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"
|
||||
"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
|
||||
|
||||
19
api/git/types/types.go
Normal file
19
api/git/types/types.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package gittypes
|
||||
|
||||
// RepoConfig represents a configuration for a repo
|
||||
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"`
|
||||
// Git credentials
|
||||
Authentication *GitAuthentication
|
||||
ConfigHash []byte
|
||||
}
|
||||
|
||||
type GitAuthentication struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
12
api/go.mod
12
api/go.mod
@@ -5,15 +5,13 @@ go 1.13
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.4.16
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a
|
||||
github.com/asdine/storm/v3 v3.2.1
|
||||
github.com/aws/aws-sdk-go v1.38.3
|
||||
github.com/boltdb/bolt v1.3.1
|
||||
github.com/containerd/containerd v1.3.1 // indirect
|
||||
github.com/coreos/go-semver v0.3.0
|
||||
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/docker/cli v0.0.0-20191126203649-54d085b857e9
|
||||
github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0
|
||||
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
|
||||
@@ -23,20 +21,16 @@ require (
|
||||
github.com/gorilla/websocket v1.4.1
|
||||
github.com/joho/godotenv v1.3.0
|
||||
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389
|
||||
github.com/json-iterator/go v1.1.10
|
||||
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-20210415235931-d457f9aba1cc
|
||||
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/portainer/liblicense v0.0.0-20210409011001-c758dd044fbb
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
go.etcd.io/bbolt v1.3.5 // indirect
|
||||
github.com/stretchr/testify v1.6.1
|
||||
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
|
||||
|
||||
53
api/go.sum
53
api/go.sum
@@ -11,8 +11,6 @@ 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/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
|
||||
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||
github.com/Microsoft/go-winio v0.3.8/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||
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=
|
||||
@@ -22,8 +20,6 @@ github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXn
|
||||
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
|
||||
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
|
||||
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
|
||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
|
||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
|
||||
@@ -38,10 +34,6 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||
github.com/asdine/storm/v3 v3.2.1 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac=
|
||||
github.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0=
|
||||
github.com/aws/aws-sdk-go v1.38.3 h1:QCL/le04oAz2jELMRSuJVjGT7H+4hhoQc66eMPCfU/k=
|
||||
github.com/aws/aws-sdk-go v1.38.3/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -88,7 +80,6 @@ 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=
|
||||
@@ -135,8 +126,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
@@ -164,7 +153,6 @@ 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.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
||||
@@ -172,10 +160,6 @@ github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH
|
||||
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.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
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=
|
||||
@@ -190,9 +174,8 @@ github.com/jpillora/sizestr v0.0.0-20160130011556-e2ea2fa42fb9/go.mod h1:1ffp+CR
|
||||
github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok=
|
||||
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
|
||||
github.com/json-iterator/go v1.1.10/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-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck=
|
||||
@@ -236,10 +219,8 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+
|
||||
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=
|
||||
@@ -257,16 +238,12 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
||||
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-20210415235931-d457f9aba1cc h1:MvSEkOvhW3m2D3L0/Ymrjgg0t3CpHlHwpZfpgpIGNiw=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20210415235931-d457f9aba1cc/go.mod h1:No8p8iZt9N2HOtDS9aWkh1ILxmQVoOTZZjHGlOij/ec=
|
||||
github.com/portainer/libcompose v0.5.3 h1:tE4WcPuGvo+NKeDkDWpwNavNLZ5GHIJ4RvuZXsI9uI8=
|
||||
github.com/portainer/libcompose v0.5.3/go.mod h1:7SKd/ho69rRKHDFSDUwkbMcol2TMKU5OslDsajr8Ro8=
|
||||
github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2 h1:0PfgGLys9yHr4rtnirg0W0Cjvv6/DzxBIZk5sV59208=
|
||||
github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2/go.mod h1:/wIeGwJOMYc1JplE/OvYMO5korce39HddIfI8VKGyAM=
|
||||
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 h1:H8HR2dHdBf8HANSkUyVw4o8+4tegGcd+zyKZ3e599II=
|
||||
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33/go.mod h1:Y2TfgviWI4rT2qaOTHr+hq6MdKIE5YjgQAu7qwptTV0=
|
||||
github.com/portainer/liblicense v0.0.0-20210409011001-c758dd044fbb h1:H1UHoRATMJWQOzvvxYDBcPxVMctpsSzsKdHwPTCFyEU=
|
||||
github.com/portainer/liblicense v0.0.0-20210409011001-c758dd044fbb/go.mod h1:6OSugUz07QF1WwrMmRNIMhgiq+drVMIZ98UTu5LVZP8=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8=
|
||||
@@ -281,8 +258,6 @@ 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/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
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=
|
||||
@@ -298,13 +273,11 @@ github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRci
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
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.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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/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/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
|
||||
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
|
||||
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=
|
||||
@@ -313,9 +286,6 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.1.0 h1:ngVtJC9TY/lg0AA/1k48FYhBrhRoFlEmWzsehpNAaZg=
|
||||
github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
|
||||
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
|
||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@@ -323,7 +293,6 @@ golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnf
|
||||
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-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/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=
|
||||
@@ -341,11 +310,8 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/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-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
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=
|
||||
@@ -383,9 +349,8 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
|
||||
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/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
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=
|
||||
@@ -401,24 +366,15 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/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=
|
||||
@@ -439,7 +395,6 @@ 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=
|
||||
|
||||
@@ -5,8 +5,6 @@ import "errors"
|
||||
var (
|
||||
// ErrEndpointAccessDenied Access denied to endpoint error
|
||||
ErrEndpointAccessDenied = errors.New("Access denied to endpoint")
|
||||
// ErrNoValidLicense Unauthorized error
|
||||
ErrNoValidLicense = errors.New("No valid Portainer License found")
|
||||
// ErrUnauthorized Unauthorized error
|
||||
ErrUnauthorized = errors.New("Unauthorized")
|
||||
// ErrResourceAccessDenied Access denied to resource error
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
@@ -14,7 +13,6 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
type authenticatePayload struct {
|
||||
@@ -51,75 +49,40 @@ func (payload *authenticatePayload) Validate(r *http.Request) error {
|
||||
// @failure 422 "Invalid Credentials"
|
||||
// @failure 500 "Server error"
|
||||
// @router /auth [post]
|
||||
func (handler *Handler) authenticate(rw http.ResponseWriter, r *http.Request) (*authMiddlewareResponse, *httperror.HandlerError) {
|
||||
resp := &authMiddlewareResponse{
|
||||
Method: portainer.AuthenticationInternal,
|
||||
}
|
||||
|
||||
func (handler *Handler) authenticate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload authenticatePayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return resp,
|
||||
&httperror.HandlerError{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Message: "Invalid request payload",
|
||||
Err: err,
|
||||
}
|
||||
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
resp.Username = payload.Username
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return resp, &httperror.HandlerError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "Unable to retrieve settings from the database",
|
||||
Err: err,
|
||||
}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
|
||||
u, err := handler.DataStore.User().UserByUsername(payload.Username)
|
||||
if err != nil && err != bolterrors.ErrObjectNotFound {
|
||||
return resp, &httperror.HandlerError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "Unable to retrieve a user with the specified username from the database",
|
||||
Err: err,
|
||||
}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err}
|
||||
}
|
||||
|
||||
if err == bolterrors.ErrObjectNotFound && (settings.AuthenticationMethod == portainer.AuthenticationInternal || settings.AuthenticationMethod == portainer.AuthenticationOAuth) {
|
||||
return resp, &httperror.HandlerError{
|
||||
StatusCode: http.StatusUnprocessableEntity,
|
||||
Message: "Invalid credentials",
|
||||
Err: httperrors.ErrUnauthorized,
|
||||
}
|
||||
|
||||
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized}
|
||||
}
|
||||
|
||||
if settings.AuthenticationMethod == portainer.AuthenticationLDAP {
|
||||
if u == nil && settings.LDAPSettings.AutoCreateUsers {
|
||||
return handler.authenticateLDAPAndCreateUser(rw, payload.Username, payload.Password, &settings.LDAPSettings)
|
||||
return handler.authenticateLDAPAndCreateUser(w, payload.Username, payload.Password, &settings.LDAPSettings)
|
||||
} else if u == nil && !settings.LDAPSettings.AutoCreateUsers {
|
||||
return resp,
|
||||
&httperror.HandlerError{
|
||||
StatusCode: http.StatusUnprocessableEntity,
|
||||
Message: "Invalid credentials",
|
||||
Err: httperrors.ErrUnauthorized,
|
||||
}
|
||||
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized}
|
||||
}
|
||||
return handler.authenticateLDAP(rw, u, payload.Password, &settings.LDAPSettings)
|
||||
return handler.authenticateLDAP(w, u, payload.Password, &settings.LDAPSettings)
|
||||
}
|
||||
|
||||
return handler.authenticateInternal(rw, u, payload.Password)
|
||||
return handler.authenticateInternal(w, u, payload.Password)
|
||||
}
|
||||
|
||||
func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.User, password string, ldapSettings *portainer.LDAPSettings) (*authMiddlewareResponse, *httperror.HandlerError) {
|
||||
resp := &authMiddlewareResponse{
|
||||
Method: portainer.AuthenticationLDAP,
|
||||
Username: user.Username,
|
||||
}
|
||||
|
||||
func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.User, password string, ldapSettings *portainer.LDAPSettings) *httperror.HandlerError {
|
||||
err := handler.LDAPService.AuthenticateUser(user.Username, password, ldapSettings)
|
||||
if err != nil {
|
||||
return handler.authenticateInternal(w, user, password)
|
||||
@@ -130,96 +93,32 @@ func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.
|
||||
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
|
||||
}
|
||||
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return resp,
|
||||
&httperror.HandlerError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "Unable to update user authorizations",
|
||||
Err: err,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
info := handler.LicenseService.Info()
|
||||
|
||||
if user.Role != portainer.AdministratorRole && !info.Valid {
|
||||
return resp,
|
||||
&httperror.HandlerError{
|
||||
StatusCode: http.StatusForbidden,
|
||||
Message: "License is not valid",
|
||||
Err: httperrors.ErrNoValidLicense,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return handler.writeToken(w, user, resp.Method)
|
||||
return handler.writeToken(w, user)
|
||||
}
|
||||
|
||||
func (handler *Handler) authenticateInternal(w http.ResponseWriter, user *portainer.User, password string) (*authMiddlewareResponse, *httperror.HandlerError) {
|
||||
resp := &authMiddlewareResponse{
|
||||
Method: portainer.AuthenticationInternal,
|
||||
Username: user.Username,
|
||||
}
|
||||
|
||||
func (handler *Handler) authenticateInternal(w http.ResponseWriter, user *portainer.User, password string) *httperror.HandlerError {
|
||||
err := handler.CryptoService.CompareHashAndData(user.Password, password)
|
||||
if err != nil {
|
||||
return resp,
|
||||
&httperror.HandlerError{
|
||||
StatusCode: http.StatusUnprocessableEntity,
|
||||
Message: "Invalid credentials",
|
||||
Err: httperrors.ErrUnauthorized,
|
||||
}
|
||||
|
||||
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized}
|
||||
}
|
||||
|
||||
info := handler.LicenseService.Info()
|
||||
|
||||
if user.Role != portainer.AdministratorRole && !info.Valid {
|
||||
return resp,
|
||||
&httperror.HandlerError{
|
||||
StatusCode: http.StatusForbidden,
|
||||
Message: "License is not valid",
|
||||
Err: httperrors.ErrNoValidLicense,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return handler.writeToken(w, user, resp.Method)
|
||||
return handler.writeToken(w, user)
|
||||
}
|
||||
|
||||
func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, username, password string, ldapSettings *portainer.LDAPSettings) (*authMiddlewareResponse, *httperror.HandlerError) {
|
||||
resp := &authMiddlewareResponse{
|
||||
Method: portainer.AuthenticationLDAP,
|
||||
Username: username,
|
||||
}
|
||||
|
||||
func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, username, password string, ldapSettings *portainer.LDAPSettings) *httperror.HandlerError {
|
||||
err := handler.LDAPService.AuthenticateUser(username, password, ldapSettings)
|
||||
if err != nil {
|
||||
return resp,
|
||||
&httperror.HandlerError{
|
||||
StatusCode: http.StatusUnprocessableEntity,
|
||||
Message: "Invalid credentials",
|
||||
Err: err,
|
||||
}
|
||||
|
||||
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", err}
|
||||
}
|
||||
|
||||
user := &portainer.User{
|
||||
Username: username,
|
||||
Role: portainer.StandardUserRole,
|
||||
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
|
||||
Username: username,
|
||||
Role: portainer.StandardUserRole,
|
||||
}
|
||||
|
||||
err = handler.DataStore.User().CreateUser(user)
|
||||
if err != nil {
|
||||
return resp,
|
||||
&httperror.HandlerError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "Unable to persist user inside the database",
|
||||
Err: err,
|
||||
}
|
||||
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.addUserIntoTeams(user, ldapSettings)
|
||||
@@ -227,78 +126,26 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use
|
||||
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
|
||||
}
|
||||
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
return handler.writeToken(w, user)
|
||||
}
|
||||
|
||||
func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError {
|
||||
tokenData := &portainer.TokenData{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
}
|
||||
|
||||
return handler.persistAndWriteToken(w, tokenData)
|
||||
}
|
||||
|
||||
func (handler *Handler) persistAndWriteToken(w http.ResponseWriter, tokenData *portainer.TokenData) *httperror.HandlerError {
|
||||
token, err := handler.JWTService.GenerateToken(tokenData)
|
||||
if err != nil {
|
||||
return resp,
|
||||
&httperror.HandlerError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "Unable to update user authorizations",
|
||||
Err: err,
|
||||
}
|
||||
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to generate JWT token", err}
|
||||
}
|
||||
|
||||
info := handler.LicenseService.Info()
|
||||
|
||||
if !info.Valid {
|
||||
return resp,
|
||||
&httperror.HandlerError{
|
||||
StatusCode: http.StatusForbidden,
|
||||
Message: "License is not valid",
|
||||
Err: httperrors.ErrNoValidLicense,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return handler.writeToken(w, user, resp.Method)
|
||||
}
|
||||
|
||||
func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User, method portainer.AuthenticationMethod) (*authMiddlewareResponse, *httperror.HandlerError) {
|
||||
tokenData := composeTokenData(user)
|
||||
|
||||
return handler.persistAndWriteToken(w, tokenData, nil, method)
|
||||
}
|
||||
|
||||
func (handler *Handler) writeTokenForOAuth(w http.ResponseWriter, user *portainer.User, expiryTime *time.Time, method portainer.AuthenticationMethod) (*authMiddlewareResponse, *httperror.HandlerError) {
|
||||
tokenData := composeTokenData(user)
|
||||
|
||||
return handler.persistAndWriteToken(w, tokenData, expiryTime, method)
|
||||
}
|
||||
|
||||
func (handler *Handler) persistAndWriteToken(w http.ResponseWriter, tokenData *portainer.TokenData, expiryTime *time.Time, method portainer.AuthenticationMethod) (*authMiddlewareResponse, *httperror.HandlerError) {
|
||||
resp := &authMiddlewareResponse{
|
||||
Username: tokenData.Username,
|
||||
Method: method,
|
||||
}
|
||||
|
||||
var token string
|
||||
var err error
|
||||
|
||||
if method == portainer.AuthenticationOAuth {
|
||||
token, err = handler.JWTService.GenerateTokenForOAuth(tokenData, expiryTime)
|
||||
if err != nil {
|
||||
return resp,
|
||||
&httperror.HandlerError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "Unable to generate JWT token for OAuth",
|
||||
Err: err,
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
token, err = handler.JWTService.GenerateToken(tokenData)
|
||||
if err != nil {
|
||||
return resp,
|
||||
&httperror.HandlerError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "Unable to generate JWT token",
|
||||
Err: err,
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return resp, response.JSON(w, &authenticateResponse{JWT: token})
|
||||
|
||||
return response.JSON(w, &authenticateResponse{JWT: token})
|
||||
}
|
||||
|
||||
func (handler *Handler) addUserIntoTeams(user *portainer.User, settings *portainer.LDAPSettings) error {
|
||||
@@ -332,7 +179,6 @@ func (handler *Handler) addUserIntoTeams(user *portainer.User, settings *portain
|
||||
|
||||
err := handler.DataStore.TeamMembership().CreateTeamMembership(membership)
|
||||
if err != nil {
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -358,11 +204,3 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
type oauthPayload struct {
|
||||
@@ -26,24 +25,7 @@ func (payload *oauthPayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuthSettings) (*portainer.OAuthInfo, error) {
|
||||
if code == "" {
|
||||
return nil, errors.New("Invalid OAuth authorization code")
|
||||
}
|
||||
|
||||
if settings == nil {
|
||||
return nil, errors.New("Invalid OAuth configuration")
|
||||
}
|
||||
|
||||
authInfo, err := handler.OAuthService.Authenticate(code, settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return authInfo, nil
|
||||
}
|
||||
|
||||
// @id ValidateOAuth
|
||||
// @id AuthenticateOauth
|
||||
// @summary Authenticate with OAuth
|
||||
// @tags auth
|
||||
// @accept json
|
||||
@@ -54,81 +36,63 @@ func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuth
|
||||
// @failure 422 "Invalid Credentials"
|
||||
// @failure 500 "Server error"
|
||||
// @router /auth/oauth/validate [post]
|
||||
func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) (*authMiddlewareResponse, *httperror.HandlerError) {
|
||||
resp := &authMiddlewareResponse{
|
||||
Method: portainer.AuthenticationOAuth,
|
||||
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 resp, &httperror.HandlerError{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Message: "Invalid request payload",
|
||||
Err: err,
|
||||
}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return resp, &httperror.HandlerError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "Unable to retrieve settings from the database",
|
||||
Err: err,
|
||||
}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
|
||||
if settings.AuthenticationMethod != 3 {
|
||||
return resp, &httperror.HandlerError{
|
||||
StatusCode: http.StatusForbidden,
|
||||
Message: "OAuth authentication is not enabled",
|
||||
Err: errors.New("OAuth authentication is not enabled"),
|
||||
}
|
||||
return &httperror.HandlerError{http.StatusForbidden, "OAuth authentication is not enabled", errors.New("OAuth authentication is not enabled")}
|
||||
}
|
||||
|
||||
authInfo, err := handler.authenticateOAuth(payload.Code, &settings.OAuthSettings)
|
||||
username, err := handler.authenticateOAuth(payload.Code, &settings.OAuthSettings)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] - OAuth authentication error: %s", err)
|
||||
return resp, &httperror.HandlerError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "Unable to authenticate through OAuth",
|
||||
Err: httperrors.ErrUnauthorized,
|
||||
}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate through OAuth", httperrors.ErrUnauthorized}
|
||||
}
|
||||
|
||||
resp.Username = authInfo.Username
|
||||
|
||||
user, err := handler.DataStore.User().UserByUsername(authInfo.Username)
|
||||
user, err := handler.DataStore.User().UserByUsername(username)
|
||||
if err != nil && err != bolterrors.ErrObjectNotFound {
|
||||
return resp, &httperror.HandlerError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "Unable to retrieve a user with the specified username from the database",
|
||||
Err: err,
|
||||
}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err}
|
||||
}
|
||||
|
||||
if user == nil && !settings.OAuthSettings.OAuthAutoMapTeamMemberships && !settings.OAuthSettings.OAuthAutoCreateUsers {
|
||||
return resp, &httperror.HandlerError{
|
||||
StatusCode: http.StatusForbidden,
|
||||
Message: "Account not created beforehand in Portainer and automatic user provisioning not enabled",
|
||||
Err: httperrors.ErrUnauthorized,
|
||||
}
|
||||
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}
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
user = &portainer.User{
|
||||
Username: authInfo.Username,
|
||||
Role: portainer.StandardUserRole,
|
||||
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
|
||||
Username: username,
|
||||
Role: portainer.StandardUserRole,
|
||||
}
|
||||
|
||||
err = handler.DataStore.User().CreateUser(user)
|
||||
if err != nil {
|
||||
return resp, &httperror.HandlerError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "Unable to persist user inside the database",
|
||||
Err: err,
|
||||
}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err}
|
||||
}
|
||||
|
||||
if settings.OAuthSettings.DefaultTeamID != 0 {
|
||||
@@ -140,54 +104,11 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) (*
|
||||
|
||||
err = handler.DataStore.TeamMembership().CreateTeamMembership(membership)
|
||||
if err != nil {
|
||||
return &authMiddlewareResponse{
|
||||
Method: portainer.AuthenticationOAuth,
|
||||
}, &httperror.HandlerError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "Unable to persist team membership inside the database",
|
||||
Err: err,
|
||||
}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist team membership inside the database", err}
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return resp, &httperror.HandlerError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "Unable to update user authorizations",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if settings.OAuthSettings.OAuthAutoMapTeamMemberships {
|
||||
if settings.OAuthSettings.TeamMemberships.OAuthClaimName == "" {
|
||||
return resp, &httperror.HandlerError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "Unable to process user oauth team memberships",
|
||||
Err: errors.New("empty value set for oauth team membership Claim name"),
|
||||
}
|
||||
}
|
||||
|
||||
err = updateOAuthTeamMemberships(handler.DataStore, settings.OAuthSettings.TeamMemberships.OAuthClaimMappings, *user, authInfo.Teams)
|
||||
if err != nil {
|
||||
return resp, &httperror.HandlerError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "Unable to update user oauth team memberships",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info := handler.LicenseService.Info()
|
||||
|
||||
if user.Role != portainer.AdministratorRole && !info.Valid {
|
||||
return resp, &httperror.HandlerError{
|
||||
StatusCode: http.StatusForbidden,
|
||||
Message: "License is not valid",
|
||||
Err: httperrors.ErrNoValidLicense,
|
||||
}
|
||||
}
|
||||
|
||||
return handler.writeTokenForOAuth(w, user, authInfo.ExpiryTime, portainer.AuthenticationOAuth)
|
||||
return handler.writeToken(w, user)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle authentication operations.
|
||||
@@ -21,12 +18,9 @@ type Handler struct {
|
||||
CryptoService portainer.CryptoService
|
||||
JWTService portainer.JWTService
|
||||
LDAPService portainer.LDAPService
|
||||
LicenseService portainer.LicenseService
|
||||
OAuthService portainer.OAuthService
|
||||
ProxyManager *proxy.Manager
|
||||
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
|
||||
AuthorizationService *authorization.Service
|
||||
UserActivityStore portainer.UserActivityStore
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage authentication operations.
|
||||
@@ -36,53 +30,11 @@ func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimi
|
||||
}
|
||||
|
||||
h.Handle("/auth/oauth/validate",
|
||||
rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.authActivityMiddleware(h.validateOAuth, portainer.AuthenticationActivitySuccess))))).Methods(http.MethodPost)
|
||||
rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.validateOAuth)))).Methods(http.MethodPost)
|
||||
h.Handle("/auth",
|
||||
rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.authActivityMiddleware(h.authenticate, portainer.AuthenticationActivitySuccess))))).Methods(http.MethodPost)
|
||||
rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.authenticate)))).Methods(http.MethodPost)
|
||||
h.Handle("/auth/logout",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.authActivityMiddleware(h.logout, portainer.AuthenticationActivityLogOut)))).Methods(http.MethodPost)
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.logout))).Methods(http.MethodPost)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
type authMiddlewareHandler func(http.ResponseWriter, *http.Request) (*authMiddlewareResponse, *httperror.HandlerError)
|
||||
|
||||
type authMiddlewareResponse struct {
|
||||
Username string
|
||||
Method portainer.AuthenticationMethod
|
||||
}
|
||||
|
||||
func (handler *Handler) authActivityMiddleware(prev authMiddlewareHandler, defaultActivityType portainer.AuthenticationActivityType) httperror.LoggerHandler {
|
||||
return func(rw http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
resp, respErr := prev(rw, r)
|
||||
|
||||
method := resp.Method
|
||||
if int(method) == 0 {
|
||||
method = portainer.AuthenticationInternal
|
||||
}
|
||||
|
||||
activityType := defaultActivityType
|
||||
if respErr != nil && activityType == portainer.AuthenticationActivitySuccess {
|
||||
activityType = portainer.AuthenticationActivityFailure
|
||||
}
|
||||
|
||||
origin := getOrigin(r.RemoteAddr)
|
||||
|
||||
_, err := handler.UserActivityStore.LogAuthActivity(resp.Username, origin, method, activityType)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] [msg: Failed logging auth activity] [error: %s]", err)
|
||||
}
|
||||
|
||||
return respErr
|
||||
}
|
||||
}
|
||||
|
||||
func getOrigin(addr string) string {
|
||||
ipRegex := regexp.MustCompile(`:\d+$`)
|
||||
ipSplit := ipRegex.Split(addr, -1)
|
||||
if len(ipSplit) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return ipSplit[0]
|
||||
}
|
||||
|
||||
@@ -15,23 +15,13 @@ import (
|
||||
// @success 204 "Success"
|
||||
// @failure 500 "Server error"
|
||||
// @router /auth/logout [post]
|
||||
func (handler *Handler) logout(w http.ResponseWriter, r *http.Request) (*authMiddlewareResponse, *httperror.HandlerError) {
|
||||
func (handler *Handler) logout(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
resp := &authMiddlewareResponse{
|
||||
Username: tokenData.Username,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return resp, &httperror.HandlerError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "Unable to retrieve user details from authentication token",
|
||||
Err: err,
|
||||
}
|
||||
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user details from authentication token", err}
|
||||
}
|
||||
|
||||
handler.KubernetesTokenCacheManager.RemoveUserFromCache(int(tokenData.ID))
|
||||
|
||||
return resp, response.Empty(w)
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// removeMemberships removes a user's team membership(s) if user does not belong to it/them anymore
|
||||
func removeMemberships(tms portainer.TeamMembershipService, user portainer.User, teams []portainer.Team) error {
|
||||
log.Println("[DEBUG] [internal,oauth] [message: removing user team memberships which no longer exist]")
|
||||
memberships, err := tms.TeamMembershipsByUserID(user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, membership := range memberships {
|
||||
teamsContainsTeamID := false
|
||||
for _, team := range teams {
|
||||
if team.ID == membership.TeamID {
|
||||
teamsContainsTeamID = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !teamsContainsTeamID {
|
||||
err := tms.DeleteTeamMembership(membership.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createOrUpdateMembership creates a membership if it does not exist or updates a memberships role (if already existent)
|
||||
func createOrUpdateMembership(tms portainer.TeamMembershipService, user portainer.User, team portainer.Team) error {
|
||||
memberships, err := tms.TeamMembershipsByTeamID(team.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("[DEBUG] [internal,oauth] [message: memberships: %v]", memberships)
|
||||
|
||||
var membership *portainer.TeamMembership
|
||||
for _, m := range memberships {
|
||||
if m.UserID == user.ID {
|
||||
membership = &m
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if membership == nil {
|
||||
membership = &portainer.TeamMembership{
|
||||
UserID: user.ID,
|
||||
TeamID: team.ID,
|
||||
Role: portainer.MembershipRole(user.Role),
|
||||
}
|
||||
log.Printf("[DEBUG] [internal,oauth] [message: creating oauth user team membership: %v]", membership)
|
||||
err = tms.CreateTeamMembership(membership)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Printf("[DEBUG] [internal,oauth] [message: membership found %v]", membership)
|
||||
if updatedRole := portainer.MembershipRole(user.Role); membership.Role != updatedRole {
|
||||
log.Printf("[DEBUG] [internal,oauth] [message: updating membership role %d]", updatedRole)
|
||||
membership.Role = updatedRole
|
||||
err = tms.UpdateTeamMembership(membership.ID, membership)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// mapAllClaimValuesToTeams maps claim values to teams if no explicit mapping exists.
|
||||
// Mapping oauth teams (claim values) to portainer teams by case-insensitive team name
|
||||
func mapAllClaimValuesToTeams(ts portainer.TeamService, user portainer.User, oAuthTeams []string) ([]portainer.Team, error) {
|
||||
teams := make([]portainer.Team, 0)
|
||||
|
||||
log.Println("[DEBUG] [internal,oauth] [message: mapping oauth claim values automatically to existing portainer teams]")
|
||||
dsTeams, err := ts.Teams()
|
||||
if err != nil {
|
||||
return []portainer.Team{}, err
|
||||
}
|
||||
|
||||
for _, oAuthTeam := range oAuthTeams {
|
||||
for _, team := range dsTeams {
|
||||
if strings.EqualFold(team.Name, oAuthTeam) {
|
||||
teams = append(teams, team)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return teams, nil
|
||||
}
|
||||
|
||||
// mapClaimValRegexToTeams maps oauth ClaimValRegex values (stored in settings) to oauth provider teams.
|
||||
// The `ClaimValRegex` is a regexp string that is matched against the oauth team value(s) extracted from oauth user response.
|
||||
// A successful match entails extraction of the respective portainer team (for the mapping).
|
||||
func mapClaimValRegexToTeams(ts portainer.TeamService, claimMappings []portainer.OAuthClaimMappings, oAuthTeams []string) ([]portainer.Team, error) {
|
||||
teams := make([]portainer.Team, 0)
|
||||
|
||||
log.Println("[DEBUG] [internal,oauth] [message: using oauth claim mappings to map groups to portainer teams]")
|
||||
for _, oAuthTeam := range oAuthTeams {
|
||||
for _, mapping := range claimMappings {
|
||||
match, err := regexp.MatchString(mapping.ClaimValRegex, oAuthTeam)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if match {
|
||||
log.Printf("[DEBUG] [internal,oauth] [message: oauth mapping claim matched; claim: %s, team: %s]\n", mapping.ClaimValRegex, oAuthTeam)
|
||||
|
||||
team, err := ts.Team(portainer.TeamID(mapping.Team))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
teams = append(teams, *team)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return teams, nil
|
||||
}
|
||||
|
||||
// updateOAuthTeamMemberships will create, update and delete an oauth user's team memberships.
|
||||
// The mappings of oauth groups to portainer teams is based on the length of `OAuthClaimMappings`; use them if they exist (len > 0),
|
||||
// otherwise map the **values** of the oauth `Claim name` (`OAuthClaimName`) to already existent portainer teams (case-insensitive).
|
||||
func updateOAuthTeamMemberships(dataStore portainer.DataStore, oAuthClaimMappings []portainer.OAuthClaimMappings, user portainer.User, oAuthTeams []string) error {
|
||||
var teams []portainer.Team
|
||||
var err error
|
||||
|
||||
if len(oAuthClaimMappings) > 0 {
|
||||
teams, err = mapClaimValRegexToTeams(dataStore.Team(), oAuthClaimMappings, oAuthTeams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to map claim value regex(s) to teams, mappings: %v, err: %w", oAuthClaimMappings, err)
|
||||
}
|
||||
} else {
|
||||
teams, err = mapAllClaimValuesToTeams(dataStore.Team(), user, oAuthTeams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to map claim value(s) to portainer teams, err: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, team := range teams {
|
||||
err := createOrUpdateMembership(dataStore.TeamMembership(), user, team)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create or update oauth memberships, user: %v, team: %v, err: %w", user, team, err)
|
||||
}
|
||||
}
|
||||
|
||||
err = removeMemberships(dataStore.TeamMembership(), user, teams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove oauth memberships, user: %v, err: %w", user, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,314 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/bolttest"
|
||||
)
|
||||
|
||||
func Test_mapClaimValRegexToTeams(t *testing.T) {
|
||||
store, teardown, err := bolttest.NewTestStore(false)
|
||||
if err != nil {
|
||||
t.Errorf("failed to initialise test store")
|
||||
}
|
||||
defer teardown()
|
||||
|
||||
t.Run("returns no portainer teams if no oauth teams are present", func(t *testing.T) {
|
||||
mappings := []portainer.OAuthClaimMappings{}
|
||||
oAuthTeams := []string{}
|
||||
teams, _ := mapClaimValRegexToTeams(store.TeamService, mappings, oAuthTeams)
|
||||
if len(teams) > 0 {
|
||||
t.Errorf("mapClaimValRegexToTeams return no teams; teams returned %d", len(teams))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns no portainer teams if no regex match occurs", func(t *testing.T) {
|
||||
store.TeamService.CreateTeam(&portainer.Team{ID: 1, Name: "testing"})
|
||||
|
||||
mappings := []portainer.OAuthClaimMappings{
|
||||
{ClaimValRegex: "@", Team: 1},
|
||||
}
|
||||
oAuthTeams := []string{"portainer"}
|
||||
|
||||
teams, _ := mapClaimValRegexToTeams(store.TeamService, mappings, oAuthTeams)
|
||||
if len(teams) > 0 {
|
||||
t.Errorf("mapClaimValRegexToTeams return no teams upon no regex match; teams returned %d", len(teams))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns team upon regex match", func(t *testing.T) {
|
||||
store.TeamService.CreateTeam(&portainer.Team{ID: 1, Name: "testing"})
|
||||
|
||||
mappings := []portainer.OAuthClaimMappings{
|
||||
{ClaimValRegex: "@", Team: 1},
|
||||
}
|
||||
oAuthTeams := []string{"@portainer"}
|
||||
|
||||
got, _ := mapClaimValRegexToTeams(store.TeamService, mappings, oAuthTeams)
|
||||
want := []portainer.Team{{ID: 1, Name: "testing"}}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("mapClaimValRegexToTeams failed to return team; got=%v, want=%v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("succcessfully fails to return non-existent team upon regex match", func(t *testing.T) {
|
||||
mappings := []portainer.OAuthClaimMappings{
|
||||
{ClaimValRegex: "@", Team: 1337},
|
||||
}
|
||||
oAuthTeams := []string{"@portainer"}
|
||||
_, err := mapClaimValRegexToTeams(store.TeamService, mappings, oAuthTeams)
|
||||
if err == nil {
|
||||
t.Errorf("mapClaimValRegexToTeams should fail to return non-existent team")
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func Test_mapAllClaimValuesToTeams(t *testing.T) {
|
||||
store, teardown, err := bolttest.NewTestStore(false)
|
||||
if err != nil {
|
||||
t.Errorf("failed to initialise test store")
|
||||
}
|
||||
defer teardown()
|
||||
|
||||
store.TeamService.CreateTeam(&portainer.Team{ID: 1, Name: "team-x"})
|
||||
|
||||
t.Run("returns no portainer teams if no oauth teams are present", func(t *testing.T) {
|
||||
oAuthTeams := []string{}
|
||||
user := portainer.User{ID: 1, Role: 1}
|
||||
|
||||
teams, _ := mapAllClaimValuesToTeams(store.TeamService, user, oAuthTeams)
|
||||
if len(teams) > 0 {
|
||||
t.Errorf("mapAllClaimValuesToTeams return no teams; teams returned %d", len(teams))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns no portainer teams if no regex match occurs", func(t *testing.T) {
|
||||
oAuthTeams := []string{"team-1"}
|
||||
user := portainer.User{ID: 1, Role: 1}
|
||||
|
||||
teams, _ := mapAllClaimValuesToTeams(store.TeamService, user, oAuthTeams)
|
||||
if len(teams) > 0 {
|
||||
t.Errorf("mapAllClaimValuesToTeams return no teams upon no regex match; teams returned %d", len(teams))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns team upon regex match", func(t *testing.T) {
|
||||
oAuthTeams := []string{"team-x"}
|
||||
user := portainer.User{ID: 1, Role: 1}
|
||||
|
||||
got, _ := mapAllClaimValuesToTeams(store.TeamService, user, oAuthTeams)
|
||||
want := []portainer.Team{{ID: 1, Name: "team-x"}}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("mapAllClaimValuesToTeams failed to return team; got=%v, want=%v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func Test_createOrUpdateMembership(t *testing.T) {
|
||||
store, teardown, err := bolttest.NewTestStore(false)
|
||||
if err != nil {
|
||||
t.Errorf("failed to initialise test store")
|
||||
}
|
||||
defer teardown()
|
||||
|
||||
t.Run("creates membership for new user-team", func(t *testing.T) {
|
||||
user := portainer.User{ID: 1, Role: 1}
|
||||
team := portainer.Team{ID: 1, Name: "team-1"}
|
||||
|
||||
err := createOrUpdateMembership(store.TeamMembershipService, user, team)
|
||||
if err != nil {
|
||||
t.Errorf("createOrUpdateMembership should not throw error when creating new team membership")
|
||||
}
|
||||
|
||||
got, _ := store.TeamMembershipService.TeamMemberships()
|
||||
want := []portainer.TeamMembership{{ID: 1, UserID: 1, TeamID: 1, Role: 1}}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("createOrUpdateMembership should succeed in creating new team membership; got=%v, want=%v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("updates membership for existing user-team", func(t *testing.T) {
|
||||
user := portainer.User{ID: 1, Role: 3}
|
||||
team := portainer.Team{ID: 2, Name: "team-2"}
|
||||
store.TeamMembershipService.CreateTeamMembership(&portainer.TeamMembership{ID: 1, UserID: user.ID, TeamID: team.ID, Role: 1})
|
||||
|
||||
err := createOrUpdateMembership(store.TeamMembershipService, user, team)
|
||||
if err != nil {
|
||||
t.Errorf("createOrUpdateMembership should not throw error when updating existing team membership")
|
||||
}
|
||||
|
||||
got, _ := store.TeamMembershipService.TeamMembership(2)
|
||||
want := &portainer.TeamMembership{ID: 2, UserID: 1, TeamID: 2, Role: 3}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("createOrUpdateMembership should succeed in creating new team membership; got=%v, want=%v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func Test_removeMemberships(t *testing.T) {
|
||||
store, teardown, err := bolttest.NewTestStore(false)
|
||||
if err != nil {
|
||||
t.Errorf("failed to initialise test store")
|
||||
}
|
||||
defer teardown()
|
||||
|
||||
t.Run("removes nothing if no user team memberships exist", func(t *testing.T) {
|
||||
user := portainer.User{ID: 1, Role: 1}
|
||||
teams := []portainer.Team{{ID: 1, Name: "team-remove"}}
|
||||
|
||||
before, _ := store.TeamMembershipService.TeamMemberships()
|
||||
|
||||
removeMemberships(store.TeamMembershipService, user, teams)
|
||||
|
||||
after, _ := store.TeamMembershipService.TeamMemberships()
|
||||
|
||||
if !reflect.DeepEqual(before, after) {
|
||||
t.Errorf("removeMemberships should not have removed any memberships; before=%v, after=%v", before, after)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("does not remove user team membership if it does belong to team whitelist", func(t *testing.T) {
|
||||
user := portainer.User{ID: 1, Role: 1}
|
||||
teams := []portainer.Team{{ID: 1, Name: "team-remove"}}
|
||||
store.TeamMembershipService.CreateTeamMembership(&portainer.TeamMembership{ID: 1, UserID: user.ID, TeamID: teams[0].ID, Role: 1})
|
||||
|
||||
before, _ := store.TeamMembershipService.TeamMembership(1)
|
||||
|
||||
removeMemberships(store.TeamMembershipService, user, teams)
|
||||
|
||||
after, _ := store.TeamMembershipService.TeamMembership(1)
|
||||
|
||||
if !reflect.DeepEqual(before, after) {
|
||||
t.Errorf("removeMemberships should not have removed any memberships; before=%v, after=%v", before, after)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("removes memberships if user team membership does not belong to team whitelist", func(t *testing.T) {
|
||||
user := portainer.User{ID: 1, Role: 1}
|
||||
teams := []portainer.Team{{ID: 1, Name: "team-xyz"}}
|
||||
store.TeamMembershipService.CreateTeamMembership(&portainer.TeamMembership{ID: 2, UserID: user.ID, TeamID: 100, Role: 1})
|
||||
store.TeamMembershipService.CreateTeamMembership(&portainer.TeamMembership{ID: 3, UserID: user.ID, TeamID: 50, Role: 1})
|
||||
|
||||
removeMemberships(store.TeamMembershipService, user, teams)
|
||||
|
||||
memberships, _ := store.TeamMembershipService.TeamMembershipsByTeamID(100)
|
||||
if len(memberships) > 0 {
|
||||
t.Errorf("removeMemberships should have successfully removed team membership; team-membership=%v", memberships)
|
||||
}
|
||||
|
||||
memberships, _ = store.TeamMembershipService.TeamMembershipsByTeamID(50)
|
||||
if len(memberships) > 0 {
|
||||
t.Errorf("removeMemberships should have successfully removed team membership; team-membership=%v", memberships)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func Test_updateOAuthTeamMemberships(t *testing.T) {
|
||||
store, teardown, err := bolttest.NewTestStore(false)
|
||||
if err != nil {
|
||||
t.Errorf("failed to initialise test store")
|
||||
}
|
||||
defer teardown()
|
||||
|
||||
t.Run("creates new team memberships based on claim val regex", func(t *testing.T) {
|
||||
store.Team().CreateTeam(&portainer.Team{ID: 1, Name: "testing"})
|
||||
|
||||
claimMappings := []portainer.OAuthClaimMappings{
|
||||
{ClaimValRegex: "@portainer", Team: 1},
|
||||
}
|
||||
user := portainer.User{ID: 1, Role: 1}
|
||||
oAuthTeams := []string{"@portainer"}
|
||||
|
||||
before, _ := store.TeamMembershipService.TeamMembershipsByTeamID(1)
|
||||
if len(before) > 0 {
|
||||
t.Errorf("updateOAuthTeamMemberships should not have a team membership with team id 1")
|
||||
}
|
||||
|
||||
updateOAuthTeamMemberships(store, claimMappings, user, oAuthTeams)
|
||||
|
||||
after, _ := store.TeamMembershipService.TeamMembershipsByTeamID(1)
|
||||
|
||||
if reflect.DeepEqual(before, after) {
|
||||
t.Errorf("updateOAuthTeamMemberships should have created new team membership based on claim value regex")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fallsback to creating team memberships by mapping oauth teams directly to portainer teams", func(t *testing.T) {
|
||||
store.Team().CreateTeam(&portainer.Team{ID: 2, Name: "testing"})
|
||||
|
||||
claimMappings := []portainer.OAuthClaimMappings{}
|
||||
user := portainer.User{ID: 1, Role: 1}
|
||||
oAuthTeams := []string{"testing"}
|
||||
|
||||
before, _ := store.TeamMembershipService.TeamMembershipsByTeamID(2)
|
||||
if len(before) > 0 {
|
||||
t.Errorf("updateOAuthTeamMemberships should not have a team membership with team id 2")
|
||||
}
|
||||
|
||||
updateOAuthTeamMemberships(store, claimMappings, user, oAuthTeams)
|
||||
|
||||
after, _ := store.TeamMembershipService.TeamMembershipsByTeamID(2)
|
||||
|
||||
if reflect.DeepEqual(before, after) {
|
||||
t.Errorf("updateOAuthTeamMemberships should have created new team membership based on existing portainer teams matching oauth team")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("updates existing team membership based on claim val regex", func(t *testing.T) {
|
||||
store.Team().CreateTeam(&portainer.Team{ID: 1, Name: "testing"})
|
||||
|
||||
claimMappings := []portainer.OAuthClaimMappings{
|
||||
{ClaimValRegex: "@portainer", Team: 1},
|
||||
}
|
||||
user := portainer.User{ID: 1, Role: 2}
|
||||
oAuthTeams := []string{"@portainer"}
|
||||
|
||||
got, _ := store.TeamMembershipService.TeamMembershipsByTeamID(1)
|
||||
want := []portainer.TeamMembership{{ID: 1, UserID: 1, TeamID: 1, Role: 1}}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("updateOAuthTeamMemberships should have initial role of 1, got=%v, want=%v", got, want)
|
||||
}
|
||||
|
||||
updateOAuthTeamMemberships(store, claimMappings, user, oAuthTeams)
|
||||
|
||||
got, _ = store.TeamMembershipService.TeamMembershipsByTeamID(1)
|
||||
want = []portainer.TeamMembership{{ID: 1, UserID: 1, TeamID: 1, Role: 2}}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("updateOAuthTeamMemberships should have updated existing team membership role, got=%v, want=%v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("removes an outdated oauth team membership", func(t *testing.T) {
|
||||
store.TeamMembershipService.CreateTeamMembership(&portainer.TeamMembership{
|
||||
ID: 1, UserID: 1, TeamID: 1, Role: 1,
|
||||
})
|
||||
|
||||
claimMappings := []portainer.OAuthClaimMappings{}
|
||||
user := portainer.User{ID: 1, Role: 1}
|
||||
oAuthTeams := []string{}
|
||||
|
||||
got, _ := store.TeamMembershipService.TeamMembershipsByTeamID(1)
|
||||
if len(got) == 0 {
|
||||
t.Errorf("updateOAuthTeamMemberships should have initial team membership")
|
||||
}
|
||||
|
||||
updateOAuthTeamMemberships(store, claimMappings, user, oAuthTeams)
|
||||
|
||||
got, _ = store.TeamMembershipService.TeamMembershipsByTeamID(1)
|
||||
if len(got) > 0 {
|
||||
t.Errorf("updateOAuthTeamMemberships should have removed existing, non-mapped team membership")
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
@@ -6,10 +6,8 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
operations "github.com/portainer/portainer/api/backup"
|
||||
)
|
||||
|
||||
@@ -17,30 +15,12 @@ type (
|
||||
backupPayload struct {
|
||||
Password string
|
||||
}
|
||||
s3BackupPayload struct {
|
||||
portainer.S3BackupSettings
|
||||
}
|
||||
)
|
||||
|
||||
func (p *backupPayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (payload *s3BackupPayload) Validate(r *http.Request) error {
|
||||
switch {
|
||||
case payload.AccessKeyID == "":
|
||||
return errors.New("missing AccessKeyID")
|
||||
case payload.SecretAccessKey == "":
|
||||
return errors.New("missing SecretAccessKey")
|
||||
case payload.Region == "":
|
||||
return errors.New("missing Region")
|
||||
case payload.BucketName == "":
|
||||
return errors.New("missing BucketName")
|
||||
default:
|
||||
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.
|
||||
@@ -71,26 +51,3 @@ func (h *Handler) backup(w http.ResponseWriter, r *http.Request) *httperror.Hand
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id BackupToS3
|
||||
// @summary Execute backup to AWS S3 Bucket
|
||||
// @description Creates an archive with a system data snapshot and upload it to the target S3 bucket
|
||||
// @description **Access policy**: admin
|
||||
// @tags backup
|
||||
// @security jwt
|
||||
// @param body body s3BackupPayload true "S3 backup settings"
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
// @router /backup/s3/execute [post]
|
||||
func (h *Handler) backupToS3(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload s3BackupPayload
|
||||
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
|
||||
}
|
||||
if err := operations.BackupToS3(payload.S3BackupSettings, h.gate, h.dataStore, h.filestorePath); err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to execute S3 backup", Err: err}
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
)
|
||||
|
||||
// @id BackupSettingsFetch
|
||||
// @summary Fetch s3 backup settings/configurations
|
||||
// @description **Access policy**: admin
|
||||
// @tags backup
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 {object} portainer.S3BackupSettings "Success"
|
||||
// @failure 401 "Unauthorized"
|
||||
// @failure 500 "Server error"
|
||||
// @router /backup/s3/settings [get]
|
||||
func (h *Handler) backupSettingsFetch(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
settings, err := h.dataStore.S3Backup().GetSettings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve backup settings from the database", Err: err}
|
||||
}
|
||||
return response.JSON(w, settings)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
)
|
||||
|
||||
type backupStatus struct {
|
||||
Failed bool
|
||||
TimestampUTC string
|
||||
}
|
||||
|
||||
// @id BackupStatusFetch
|
||||
// @summary Fetch the status of the last scheduled backup run
|
||||
// @description **Access policy**: public
|
||||
// @tags backup
|
||||
// @produce json
|
||||
// @success 200 {object} backupStatus "Success"
|
||||
// @failure 500 "Server error"
|
||||
// @router /backup/s3/status [get]
|
||||
func (h *Handler) backupStatusFetch(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
status, err := h.dataStore.S3Backup().GetStatus()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve last backup run status from the database", Err: err}
|
||||
}
|
||||
return response.JSON(w, backupStatus{Failed: status.Failed, TimestampUTC: status.Timestamp.UTC().Format(time.RFC3339)})
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"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"
|
||||
@@ -48,13 +49,13 @@ func Test_backupHandlerWithoutPassword_shouldCreateATarballArchive(t *testing.T)
|
||||
gate := offlinegate.NewOfflineGate()
|
||||
adminMonitor := adminmonitor.New(time.Hour, nil, context.Background())
|
||||
|
||||
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", nil, func() {}, adminMonitor).backup(w, r)
|
||||
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, _ := ioutil.TempDir("", "backup")
|
||||
tmpdir, _ := ioutils.TempDir("", "backup")
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
archivePath := filepath.Join(tmpdir, "archive.tar.gz")
|
||||
@@ -85,13 +86,13 @@ func Test_backupHandlerWithPassword_shouldCreateEncryptedATarballArchive(t *test
|
||||
gate := offlinegate.NewOfflineGate()
|
||||
adminMonitor := adminmonitor.New(time.Hour, nil, nil)
|
||||
|
||||
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", nil, func() {}, adminMonitor).backup(w, r)
|
||||
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, _ := ioutil.TempDir("", "backup")
|
||||
tmpdir, _ := ioutils.TempDir("", "backup")
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
dr, err := crypto.AesDecrypt(bytes.NewReader(body), []byte("secret"))
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/adminmonitor"
|
||||
operations "github.com/portainer/portainer/api/backup"
|
||||
"github.com/portainer/portainer/api/http/offlinegate"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
@@ -16,7 +15,6 @@ import (
|
||||
// Handler is an http handler responsible for backup and restore portainer state
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
backupScheduler *operations.BackupScheduler
|
||||
bouncer *security.RequestBouncer
|
||||
dataStore portainer.DataStore
|
||||
gate *offlinegate.OfflineGate
|
||||
@@ -26,11 +24,10 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// NewHandler creates an new instance of backup handler
|
||||
func NewHandler(bouncer *security.RequestBouncer, dataStore portainer.DataStore, gate *offlinegate.OfflineGate, filestorePath string, backupScheduler *operations.BackupScheduler, shutdownTrigger context.CancelFunc, adminMonitor *adminmonitor.Monitor) *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,
|
||||
backupScheduler: backupScheduler,
|
||||
dataStore: dataStore,
|
||||
gate: gate,
|
||||
filestorePath: filestorePath,
|
||||
@@ -38,11 +35,6 @@ func NewHandler(bouncer *security.RequestBouncer, dataStore portainer.DataStore,
|
||||
adminMonitor: adminMonitor,
|
||||
}
|
||||
|
||||
h.Handle("/backup/s3/settings", bouncer.RestrictedAccess(adminAccess(httperror.LoggerHandler(h.backupSettingsFetch)))).Methods(http.MethodGet)
|
||||
h.Handle("/backup/s3/settings", bouncer.RestrictedAccess(adminAccess(httperror.LoggerHandler(h.updateSettings)))).Methods(http.MethodPost)
|
||||
h.Handle("/backup/s3/status", bouncer.PublicAccess(httperror.LoggerHandler(h.backupStatusFetch))).Methods(http.MethodGet)
|
||||
h.Handle("/backup/s3/execute", bouncer.RestrictedAccess(adminAccess(httperror.LoggerHandler(h.backupToS3)))).Methods(http.MethodPost)
|
||||
h.Handle("/backup/s3/restore", bouncer.PublicAccess(httperror.LoggerHandler(h.restoreFromS3))).Methods(http.MethodPost)
|
||||
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)
|
||||
|
||||
@@ -63,3 +55,11 @@ func adminAccess(next http.Handler) http.Handler {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ type restorePayload struct {
|
||||
// @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"
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
// @router /restore [post]
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
operations "github.com/portainer/portainer/api/backup"
|
||||
s3client "github.com/portainer/portainer/api/s3"
|
||||
)
|
||||
|
||||
type restoreS3Settings struct {
|
||||
portainer.S3Location
|
||||
Password string
|
||||
}
|
||||
|
||||
func (p *restoreS3Settings) Validate(r *http.Request) error {
|
||||
if p.AccessKeyID == "" {
|
||||
return errors.New("missing AccessKeyID field")
|
||||
}
|
||||
if p.SecretAccessKey == "" {
|
||||
return errors.New("missing SecretAccessKe field")
|
||||
}
|
||||
if p.Region == "" {
|
||||
return errors.New("missing Region field")
|
||||
}
|
||||
if p.BucketName == "" {
|
||||
return errors.New("missing BucketName field")
|
||||
}
|
||||
if p.Filename == "" {
|
||||
return errors.New("missing Filename field")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id RestoreFromS3
|
||||
// @summary Triggers a system restore using details of s3 backup
|
||||
// @description Triggers a system restore using details of s3 backup
|
||||
// @description **Access policy**: public
|
||||
// @tags backup
|
||||
// @param AccessKeyID body string false "AWS access key id"
|
||||
// @param SecretAccessKey body string false "AWS secret access key"
|
||||
// @param Region body string false "AWS S3 region"
|
||||
// @param BucketName body string false "AWS S3 bucket name"
|
||||
// @param Filename body string false "AWS S3 filename in the bucket"
|
||||
// @param Password body string false "Password to decrypt the backup with"
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
// @router /backup/s3/restore [post]
|
||||
func (h *Handler) restoreFromS3(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: fmt.Errorf("system already initialized")}
|
||||
}
|
||||
|
||||
h.adminMonitor.Stop()
|
||||
defer h.adminMonitor.Start()
|
||||
|
||||
var payload restoreS3Settings
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
|
||||
}
|
||||
|
||||
backupFile, err := createTmpBackupLocation(h.filestorePath)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to restore", Err: err}
|
||||
}
|
||||
defer func() {
|
||||
backupFile.Close()
|
||||
os.RemoveAll(filepath.Dir(backupFile.Name()))
|
||||
}()
|
||||
|
||||
s3session, err := s3client.NewSession(payload.Region, payload.AccessKeyID, payload.SecretAccessKey)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] %s \n", err)
|
||||
}
|
||||
if err = s3client.Download(s3session, backupFile, payload.S3Location); err != nil {
|
||||
log.Printf("[ERROR] %s \n", errors.Wrap(err, "failed downloading file from S3"))
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to download file from S3", Err: err}
|
||||
}
|
||||
|
||||
if err = operations.RestoreArchive(backupFile, payload.Password, h.filestorePath, h.gate, h.dataStore, h.shutdownTrigger); err != nil {
|
||||
log.Printf("[ERROR] %s", errors.Wrap(err, "faild to restore system from backup"))
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to restore backup", Err: err}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createTmpBackupLocation(filestorePath string) (*os.File, error) {
|
||||
restoreDir, err := ioutil.TempDir(filestorePath, fmt.Sprintf("restore_%s", time.Now().Format("2006-01-02_15-04-05")))
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to create tmp download dir")
|
||||
}
|
||||
|
||||
f, err := os.Create(filepath.Join(restoreDir, "backup_file"))
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to create tmp download file")
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
@@ -51,7 +51,7 @@ func Test_restoreArchive_usingCombinationOfPasswords(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", nil, func() {}, adminMonitor)
|
||||
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor)
|
||||
|
||||
//backup
|
||||
archive := backup(t, h, test.backupPassword)
|
||||
@@ -74,7 +74,7 @@ func Test_restoreArchive_shouldFailIfSystemWasAlreadyInitialized(t *testing.T) {
|
||||
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", nil, func() {}, adminMonitor)
|
||||
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor)
|
||||
|
||||
//backup
|
||||
archive := backup(t, h, "password")
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type backupSettings struct {
|
||||
portainer.S3BackupSettings
|
||||
}
|
||||
|
||||
func (p *backupSettings) Validate(r *http.Request) error {
|
||||
if p.CronRule == "" {
|
||||
return nil
|
||||
}
|
||||
if _, err := cron.ParseStandard(p.CronRule); err != nil {
|
||||
return errors.New("invalid cron rule")
|
||||
}
|
||||
if p.AccessKeyID == "" {
|
||||
return errors.New("missing AccessKeyID")
|
||||
}
|
||||
if p.SecretAccessKey == "" {
|
||||
return errors.New("missing SecretAccessKey")
|
||||
}
|
||||
if p.Region == "" {
|
||||
return errors.New("missing Region")
|
||||
}
|
||||
if p.BucketName == "" {
|
||||
return errors.New("missing BucketName")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id UpdateS3Settings
|
||||
// @summary Updates stored s3 backup settings and updates running cron jobs as needed
|
||||
// @description Updates stored s3 backup settings and updates running cron jobs as needed
|
||||
// @description **Access policy**: admin
|
||||
// @tags backup
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param CronRule body string false "Crontab rule to make periodical backups"
|
||||
// @param AccessKeyID body string false "AWS access key id"
|
||||
// @param SecretAccessKey body string false "AWS secret access key"
|
||||
// @param Region body string false "AWS S3 region"
|
||||
// @param BucketName body string false "AWS S3 bucket name"
|
||||
// @param Password body string false "Password to encrypt the backup with"
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
// @router /backup/s3/settings [post]
|
||||
func (h *Handler) updateSettings(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload backupSettings
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
|
||||
}
|
||||
|
||||
if err := h.backupScheduler.Update(payload.S3BackupSettings); err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Couldn't update backup settings", Err: err}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_ValidateCronRules(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
rule string
|
||||
isErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty cron rule",
|
||||
rule: "",
|
||||
isErr: false,
|
||||
},
|
||||
{
|
||||
name: "incorrect cron rule",
|
||||
rule: "* wrong *",
|
||||
isErr: true,
|
||||
},
|
||||
{
|
||||
name: "standart cron rule",
|
||||
rule: "* * * * 1",
|
||||
isErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
emtpyRequest := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
s := &backupSettings{
|
||||
S3BackupSettings: portainer.S3BackupSettings{
|
||||
CronRule: test.rule,
|
||||
},
|
||||
}
|
||||
|
||||
err := s.Validate(emtpyRequest)
|
||||
assert.Equal(t, err != nil, test.isErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/http/useractivity"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
@@ -45,7 +44,7 @@ func (handler *Handler) customTemplateCreate(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "unable to retrieve user details from authentication token", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user details from authentication token", err}
|
||||
}
|
||||
|
||||
customTemplate, err := handler.createCustomTemplate(method, r)
|
||||
@@ -158,8 +157,6 @@ func (handler *Handler) createCustomTemplateFromFileContent(r *http.Request) (*p
|
||||
}
|
||||
customTemplate.ProjectPath = projectPath
|
||||
|
||||
useractivity.LogHttpActivity(handler.UserActivityStore, handlerActivityContext, r, payload)
|
||||
|
||||
return customTemplate, nil
|
||||
}
|
||||
|
||||
@@ -253,8 +250,6 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
|
||||
return nil, err
|
||||
}
|
||||
|
||||
useractivity.LogHttpActivity(handler.UserActivityStore, handlerActivityContext, r, payload)
|
||||
|
||||
return customTemplate, nil
|
||||
}
|
||||
|
||||
@@ -334,7 +329,5 @@ func (handler *Handler) createCustomTemplateFromFileUpload(r *http.Request) (*po
|
||||
}
|
||||
customTemplate.ProjectPath = projectPath
|
||||
|
||||
useractivity.LogHttpActivity(handler.UserActivityStore, handlerActivityContext, r, payload)
|
||||
|
||||
return customTemplate, nil
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/http/useractivity"
|
||||
)
|
||||
|
||||
// @id CustomTemplateDelete
|
||||
@@ -72,8 +71,6 @@ func (handler *Handler) customTemplateDelete(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
}
|
||||
|
||||
useractivity.LogHttpActivity(handler.UserActivityStore, handlerActivityContext, r, nil)
|
||||
|
||||
return response.Empty(w)
|
||||
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/http/useractivity"
|
||||
)
|
||||
|
||||
type customTemplateUpdatePayload struct {
|
||||
@@ -128,7 +127,5 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist custom template changes inside the database", err}
|
||||
}
|
||||
|
||||
useractivity.LogHttpActivity(handler.UserActivityStore, handlerActivityContext, r, payload)
|
||||
|
||||
return response.JSON(w, customTemplate)
|
||||
}
|
||||
|
||||
@@ -5,22 +5,17 @@ import (
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
const (
|
||||
handlerActivityContext = "Portainer"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle endpoint group operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DataStore portainer.DataStore
|
||||
FileService portainer.FileService
|
||||
GitService portainer.GitService
|
||||
UserActivityStore portainer.UserActivityStore
|
||||
DataStore portainer.DataStore
|
||||
FileService portainer.FileService
|
||||
GitService portainer.GitService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage endpoint group operations.
|
||||
|
||||
@@ -9,8 +9,6 @@ import (
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/useractivity"
|
||||
consts "github.com/portainer/portainer/api/useractivity"
|
||||
)
|
||||
|
||||
type dockerhubUpdatePayload struct {
|
||||
@@ -66,8 +64,5 @@ func (handler *Handler) dockerhubUpdate(w http.ResponseWriter, r *http.Request)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the Dockerhub changes inside the database", err}
|
||||
}
|
||||
|
||||
payload.Password = consts.RedactedValue
|
||||
useractivity.LogHttpActivity(handler.UserActivityStore, "Portainer", r, payload)
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
@@ -16,8 +16,7 @@ func hideFields(dockerHub *portainer.DockerHub) {
|
||||
// Handler is the HTTP handler used to handle DockerHub operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DataStore portainer.DataStore
|
||||
UserActivityStore portainer.UserActivityStore
|
||||
DataStore portainer.DataStore
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage Dockerhub operations.
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/useractivity"
|
||||
)
|
||||
|
||||
type edgeGroupCreatePayload struct {
|
||||
@@ -93,7 +92,5 @@ func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the Edge group inside the database", err}
|
||||
}
|
||||
|
||||
useractivity.LogHttpActivity(handler.UserActivityStore, "Portainer", r, payload)
|
||||
|
||||
return response.JSON(w, edgeGroup)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/http/useractivity"
|
||||
)
|
||||
|
||||
// @id EdgeGroupDelete
|
||||
@@ -55,8 +54,6 @@ func (handler *Handler) edgeGroupDelete(w http.ResponseWriter, r *http.Request)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the Edge group from the database", err}
|
||||
}
|
||||
|
||||
useractivity.LogHttpActivity(handler.UserActivityStore, "Portainer", r, nil)
|
||||
|
||||
return response.Empty(w)
|
||||
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/http/useractivity"
|
||||
"github.com/portainer/portainer/api/internal/edge"
|
||||
)
|
||||
|
||||
@@ -129,8 +128,6 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
useractivity.LogHttpActivity(handler.UserActivityStore, "Portainer", r, payload)
|
||||
|
||||
return response.JSON(w, edgeGroup)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,15 +5,14 @@ import (
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle endpoint group operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DataStore portainer.DataStore
|
||||
UserActivityStore portainer.UserActivityStore
|
||||
DataStore portainer.DataStore
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage endpoint group operations.
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/useractivity"
|
||||
)
|
||||
|
||||
// @id EdgeJobCreate
|
||||
@@ -91,8 +90,6 @@ func (handler *Handler) createEdgeJobFromFileContent(w http.ResponseWriter, r *h
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to schedule Edge job", err}
|
||||
}
|
||||
|
||||
useractivity.LogHttpActivity(handler.UserActivityStore, "Portainer", r, payload)
|
||||
|
||||
return response.JSON(w, edgeJob)
|
||||
}
|
||||
|
||||
@@ -151,8 +148,6 @@ func (handler *Handler) createEdgeJobFromFile(w http.ResponseWriter, r *http.Req
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to schedule Edge job", err}
|
||||
}
|
||||
|
||||
useractivity.LogHttpActivity(handler.UserActivityStore, "Portainer", r, payload)
|
||||
|
||||
return response.JSON(w, edgeJob)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/http/useractivity"
|
||||
)
|
||||
|
||||
// @id EdgeJobDelete
|
||||
@@ -51,7 +50,5 @@ func (handler *Handler) edgeJobDelete(w http.ResponseWriter, r *http.Request) *h
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the Edge job from the database", err}
|
||||
}
|
||||
|
||||
useractivity.LogHttpActivity(handler.UserActivityStore, "Portainer", r, nil)
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/http/useractivity"
|
||||
)
|
||||
|
||||
// @id EdgeJobTasksClear
|
||||
@@ -63,7 +62,5 @@ func (handler *Handler) edgeJobTasksClear(w http.ResponseWriter, r *http.Request
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Edge job changes in the database", err}
|
||||
}
|
||||
|
||||
useractivity.LogHttpActivity(handler.UserActivityStore, "Portainer", r, nil)
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/http/useractivity"
|
||||
)
|
||||
|
||||
// @id EdgeJobTasksCollect
|
||||
@@ -57,7 +56,5 @@ func (handler *Handler) edgeJobTasksCollect(w http.ResponseWriter, r *http.Reque
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Edge job changes in the database", err}
|
||||
}
|
||||
|
||||
useractivity.LogHttpActivity(handler.UserActivityStore, "Portainer", r, nil)
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/http/useractivity"
|
||||
)
|
||||
|
||||
type edgeJobUpdatePayload struct {
|
||||
@@ -72,8 +71,6 @@ func (handler *Handler) edgeJobUpdate(w http.ResponseWriter, r *http.Request) *h
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Edge job changes inside the database", err}
|
||||
}
|
||||
|
||||
useractivity.LogHttpActivity(handler.UserActivityStore, "Portainer", r, payload)
|
||||
|
||||
return response.JSON(w, edgeJob)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
@@ -15,7 +15,6 @@ type Handler struct {
|
||||
DataStore portainer.DataStore
|
||||
FileService portainer.FileService
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
UserActivityStore portainer.UserActivityStore
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage Edge job operations.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user