Merge branch 'release/2.0.0'

pull/4280/head
Anthony Lapenna 2020-08-27 17:48:15 +12:00
commit 1bc0c1baa9
1017 changed files with 42582 additions and 23179 deletions

View File

@ -1,62 +1,44 @@
version: "2"
checks:
argument-count:
enabled: true
config:
threshold: 4
enabled: false
complex-logic:
enabled: true
config:
threshold: 4
enabled: false
file-lines:
enabled: true
config:
threshold: 300
enabled: false
method-complexity:
enabled: false
method-count:
enabled: true
config:
threshold: 20
enabled: false
method-lines:
enabled: true
config:
threshold: 50
enabled: false
nested-control-flow:
enabled: true
config:
threshold: 4
enabled: false
return-statements:
enabled: false
similar-code:
enabled: true
config:
threshold: #language-specific defaults. overrides affect all languages.
enabled: false
identical-code:
enabled: true
config:
threshold: #language-specific defaults. overrides affect all languages.
enabled: false
plugins:
gofmt:
enabled: true
golint:
enabled: true
govet:
enabled: true
csslint:
enabled: true
duplication:
enabled: true
config:
languages:
javascript:
mass_threshold: 80
eslint:
enabled: true
channel: "eslint-5"
config:
config: .eslintrc.yml
fixme:
enabled: true
exclude_patterns:
- assets/
- build/
- dist/
- distribution/
- node_modules
- test/
- webpack/
- gruntfile.js
- webpack.config.js
- api/
- "!app/kubernetes/**"
- .github/
- .tmp/

View File

@ -6,7 +6,6 @@ env:
globals:
angular: true
__CONFIG_GA_ID: true
extends:
- 'eslint:recommended'

View File

@ -1,47 +1,48 @@
---
name: Bug report
about: Create a bug report
---
<!--
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/.
Before opening a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this
repository. If there is a duplicate, please close your issue and add a comment
to the existing issue instead.
Also, be sure to check our FAQ and documentation first: https://portainer.readthedocs.io
-->
**Bug description**
A clear and concise description of what the bug is.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Portainer Logs**
Provide the logs of your Portainer container or Service.
You can see how [here](https://portainer.readthedocs.io/en/stable/faq.html#how-do-i-get-the-logs-from-portainer)
**Steps to reproduce the issue:**
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Technical details:**
* Portainer version:
* Docker version (managed by Portainer):
* Platform (windows/linux):
* Command used to start Portainer (`docker run -p 9000:9000 portainer/portainer`):
* Browser:
**Additional context**
Add any other context about the problem here.
---
name: Bug report
about: Create a bug report
---
<!--
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/.
Before opening a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this
repository. If there is a duplicate, please close your issue and add a comment
to the existing issue instead.
Also, be sure to check our FAQ and documentation first: https://portainer.readthedocs.io
-->
**Bug description**
A clear and concise description of what the bug is.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Portainer Logs**
Provide the logs of your Portainer container or Service.
You can see how [here](https://portainer.readthedocs.io/en/stable/faq.html#how-do-i-get-the-logs-from-portainer)
**Steps to reproduce the issue:**
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Technical details:**
- Portainer version:
- Docker version (managed by Portainer):
- Platform (windows/linux):
- Command used to start Portainer (`docker run -p 9000:9000 portainer/portainer`):
- Browser:
**Additional context**
Add any other context about the problem here.

10
.github/stale.yml vendored
View File

@ -12,14 +12,13 @@ issues:
# Issues with these labels will never be considered stale
exemptLabels:
- kind/enhancement
- kind/feature
- kind/question
- kind/style
- kind/workaround
- bug/need-confirmation
- bug/confirmed
- status/discuss
# Only issues with all of these labels are checked if stale. Defaults to `[]` (disabled)
onlyLabels: []
@ -35,9 +34,9 @@ issues:
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been marked as stale as it has not had recent activity,
This issue has been marked as stale as it has not had recent activity,
it will be closed if no further activity occurs in the next 7 days.
If you believe that it has been incorrectly labelled as stale,
If you believe that it has been incorrectly labelled as stale,
leave a comment and the label will be removed.
# Comment to post when removing the stale label.
@ -49,7 +48,6 @@ issues:
Since no further activity has appeared on this issue it will be closed.
If you believe that it has been incorrectly closed, leave a comment
and mention @itsconquest. One of our staff will then review the issue.
Note - If it is an old bug report, make sure that it is reproduceable in the
latest version of Portainer as it may have already been fixed.

6
.gitignore vendored
View File

@ -4,5 +4,7 @@ dist
portainer-checksum.txt
api/cmd/portainer/portainer*
.tmp
.vscode
.eslintcache
**/.vscode/settings.json
**/.vscode/tasks.json
.eslintcache

162
.vscode/portainer.code-snippets vendored Normal file
View File

@ -0,0 +1,162 @@
{
// Place your portainer workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
// Placeholders with the same ids are connected.
// Example:
// "Print to console": {
// "scope": "javascript,typescript",
// "prefix": "log",
// "body": [
// "console.log('$1');",
// "$2"
// ],
// "description": "Log output to console"
// }
"Component": {
"scope": "javascript",
"prefix": "mycomponent",
"description": "Dummy Angularjs Component",
"body": [
"import angular from 'angular';",
"import ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}Controller from './${TM_FILENAME_BASE}Controller'",
"",
"angular.module('portainer.${TM_DIRECTORY/.*\\/app\\/([^\\/]*)(\\/.*)?$/$1/}').component('$TM_FILENAME_BASE', {",
" templateUrl: './$TM_FILENAME_BASE.html',",
" controller: ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}Controller,",
"});",
""
]
},
"Controller": {
"scope": "javascript",
"prefix": "mycontroller",
"body": [
"class ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/} {",
"\t/* @ngInject */",
"\tconstructor($0) {",
"\t}",
"}",
"",
"export default ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/};"
],
"description": "Dummy ES6+ controller"
},
"Model": {
"scope": "javascript",
"prefix": "mymodel",
"description": "Dummy ES6+ model",
"body": [
"/**",
" * $1 Model",
" */",
"const _$1 = Object.freeze({",
" $0",
"});",
"",
"export class $1 {",
" constructor() {",
" Object.assign(this, JSON.parse(JSON.stringify(_$1)));",
" }",
"}"
]
},
"Service": {
"scope": "javascript",
"prefix": "myservice",
"description": "Dummy ES6+ service",
"body": [
"import angular from 'angular';",
"import PortainerError from 'Portainer/error';",
"",
"class $1 {",
" /* @ngInject */",
" constructor(\\$async, $0) {",
" this.\\$async = \\$async;",
"",
" this.getAsync = this.getAsync.bind(this);",
" this.getAllAsync = this.getAllAsync.bind(this);",
" this.createAsync = this.createAsync.bind(this);",
" this.updateAsync = this.updateAsync.bind(this);",
" this.deleteAsync = this.deleteAsync.bind(this);",
" }",
"",
" /**",
" * GET",
" */",
" async getAsync() {",
" try {",
"",
" } catch (err) {",
" throw new PortainerError('', err);",
" }",
" }",
"",
" async getAllAsync() {",
" try {",
"",
" } catch (err) {",
" throw new PortainerError('', err);",
" }",
" }",
"",
" get() {",
" if () {",
" return this.\\$async(this.getAsync);",
" }",
" return this.\\$async(this.getAllAsync);",
" }",
"",
" /**",
" * CREATE",
" */",
" async createAsync() {",
" try {",
"",
" } catch (err) {",
" throw new PortainerError('', err);",
" }",
" }",
"",
" create() {",
" return this.\\$async(this.createAsync);",
" }",
"",
" /**",
" * UPDATE",
" */",
" async updateAsync() {",
" try {",
"",
" } catch (err) {",
" throw new PortainerError('', err);",
" }",
" }",
"",
" update() {",
" return this.\\$async(this.updateAsync);",
" }",
"",
" /**",
" * DELETE",
" */",
" async deleteAsync() {",
" try {",
"",
" } catch (err) {",
" throw new PortainerError('', err);",
" }",
" }",
"",
" delete() {",
" return this.\\$async(this.deleteAsync);",
" }",
"}",
"",
"export default $1;",
"angular.module('portainer.${TM_DIRECTORY/.*\\/app\\/([^\\/]*)(\\/.*)?$/$1/}').service('$1', $1);"
]
}
}

View File

@ -6,10 +6,10 @@ Some basic conventions for contributing to this project.
Please make sure that there aren't existing pull requests attempting to address the issue mentioned. Likewise, please check for issues related to update, as someone else may be working on the issue in a branch or fork.
* Please open a discussion in a new issue / existing issue to talk about the changes you'd like to bring
* Develop in a topic branch, not master/develop
- Please open a discussion in a new issue / existing issue to talk about the changes you'd like to bring
- Develop in a topic branch, not master/develop
When creating a new branch, prefix it with the *type* of the change (see section **Commit Message Format** below), the associated opened issue number, a dash and some text describing the issue (using dash as a separator).
When creating a new branch, prefix it with the _type_ of the change (see section **Commit Message Format** below), the associated opened issue number, a dash and some text describing the issue (using dash as a separator).
For example, if you work on a bugfix for the issue #361, you could name the branch `fix361-template-selection`.
@ -37,14 +37,14 @@ Lines should not exceed 100 characters. This allows the message to be easier to
Must be one of the following:
* **feat**: A new feature
* **fix**: A bug fix
* **docs**: Documentation only changes
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
- **feat**: A new feature
- **fix**: A bug fix
- **docs**: Documentation only changes
- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
semi-colons, etc)
* **refactor**: A code change that neither fixes a bug or adds a feature
* **test**: Adding missing tests
* **chore**: Changes to the build process or auxiliary tools and libraries such as documentation
- **refactor**: A code change that neither fixes a bug or adds a feature
- **test**: Adding missing tests
- **chore**: Changes to the build process or auxiliary tools and libraries such as documentation
generation
### Scope
@ -57,9 +57,9 @@ You can use the **area** label tag associated on the issue here (for `area/conta
The subject contains succinct description of the change:
* use the imperative, present tense: "change" not "changed" nor "changes"
* don't capitalize first letter
* no dot (.) at the end
- use the imperative, present tense: "change" not "changed" nor "changes"
- don't capitalize first letter
- no dot (.) at the end
## Contribution process

View File

@ -1,5 +1,5 @@
<p align="center">
<img title="portainer" src='https://github.com/portainer/portainer/blob/develop/assets/images/logo_alt.png?raw=true' />
<img title="portainer" src='https://github.com/portainer/portainer/blob/develop/app/assets/images/logo_alt.png?raw=true' />
</p>
[![Docker Pulls](https://img.shields.io/docker/pulls/portainer/portainer.svg)](https://hub.docker.com/r/portainer/portainer/)

View File

@ -1,795 +0,0 @@
package portainer
// AuthorizationService represents a service used to
// update authorizations associated to a user or team.
type AuthorizationService struct {
endpointService EndpointService
endpointGroupService EndpointGroupService
registryService RegistryService
roleService RoleService
teamMembershipService TeamMembershipService
userService UserService
}
// AuthorizationServiceParameters are the required parameters
// used to create a new AuthorizationService.
type AuthorizationServiceParameters struct {
EndpointService EndpointService
EndpointGroupService EndpointGroupService
RegistryService RegistryService
RoleService RoleService
TeamMembershipService TeamMembershipService
UserService UserService
}
// NewAuthorizationService returns a point to a new AuthorizationService instance.
func NewAuthorizationService(parameters *AuthorizationServiceParameters) *AuthorizationService {
return &AuthorizationService{
endpointService: parameters.EndpointService,
endpointGroupService: parameters.EndpointGroupService,
registryService: parameters.RegistryService,
roleService: parameters.RoleService,
teamMembershipService: parameters.TeamMembershipService,
userService: parameters.UserService,
}
}
// DefaultEndpointAuthorizationsForEndpointAdministratorRole returns the default endpoint authorizations
// associated to the endpoint administrator role.
func DefaultEndpointAuthorizationsForEndpointAdministratorRole() Authorizations {
return map[Authorization]bool{
OperationDockerContainerArchiveInfo: true,
OperationDockerContainerList: true,
OperationDockerContainerExport: true,
OperationDockerContainerChanges: true,
OperationDockerContainerInspect: true,
OperationDockerContainerTop: true,
OperationDockerContainerLogs: true,
OperationDockerContainerStats: true,
OperationDockerContainerAttachWebsocket: true,
OperationDockerContainerArchive: true,
OperationDockerContainerCreate: true,
OperationDockerContainerPrune: true,
OperationDockerContainerKill: true,
OperationDockerContainerPause: true,
OperationDockerContainerUnpause: true,
OperationDockerContainerRestart: true,
OperationDockerContainerStart: true,
OperationDockerContainerStop: true,
OperationDockerContainerWait: true,
OperationDockerContainerResize: true,
OperationDockerContainerAttach: true,
OperationDockerContainerExec: true,
OperationDockerContainerRename: true,
OperationDockerContainerUpdate: true,
OperationDockerContainerPutContainerArchive: true,
OperationDockerContainerDelete: true,
OperationDockerImageList: true,
OperationDockerImageSearch: true,
OperationDockerImageGetAll: true,
OperationDockerImageGet: true,
OperationDockerImageHistory: true,
OperationDockerImageInspect: true,
OperationDockerImageLoad: true,
OperationDockerImageCreate: true,
OperationDockerImagePrune: true,
OperationDockerImagePush: true,
OperationDockerImageTag: true,
OperationDockerImageDelete: true,
OperationDockerImageCommit: true,
OperationDockerImageBuild: true,
OperationDockerNetworkList: true,
OperationDockerNetworkInspect: true,
OperationDockerNetworkCreate: true,
OperationDockerNetworkConnect: true,
OperationDockerNetworkDisconnect: true,
OperationDockerNetworkPrune: true,
OperationDockerNetworkDelete: true,
OperationDockerVolumeList: true,
OperationDockerVolumeInspect: true,
OperationDockerVolumeCreate: true,
OperationDockerVolumePrune: true,
OperationDockerVolumeDelete: true,
OperationDockerExecInspect: true,
OperationDockerExecStart: true,
OperationDockerExecResize: true,
OperationDockerSwarmInspect: true,
OperationDockerSwarmUnlockKey: true,
OperationDockerSwarmInit: true,
OperationDockerSwarmJoin: true,
OperationDockerSwarmLeave: true,
OperationDockerSwarmUpdate: true,
OperationDockerSwarmUnlock: true,
OperationDockerNodeList: true,
OperationDockerNodeInspect: true,
OperationDockerNodeUpdate: true,
OperationDockerNodeDelete: true,
OperationDockerServiceList: true,
OperationDockerServiceInspect: true,
OperationDockerServiceLogs: true,
OperationDockerServiceCreate: true,
OperationDockerServiceUpdate: true,
OperationDockerServiceDelete: true,
OperationDockerSecretList: true,
OperationDockerSecretInspect: true,
OperationDockerSecretCreate: true,
OperationDockerSecretUpdate: true,
OperationDockerSecretDelete: true,
OperationDockerConfigList: true,
OperationDockerConfigInspect: true,
OperationDockerConfigCreate: true,
OperationDockerConfigUpdate: true,
OperationDockerConfigDelete: true,
OperationDockerTaskList: true,
OperationDockerTaskInspect: true,
OperationDockerTaskLogs: true,
OperationDockerPluginList: true,
OperationDockerPluginPrivileges: true,
OperationDockerPluginInspect: true,
OperationDockerPluginPull: true,
OperationDockerPluginCreate: true,
OperationDockerPluginEnable: true,
OperationDockerPluginDisable: true,
OperationDockerPluginPush: true,
OperationDockerPluginUpgrade: true,
OperationDockerPluginSet: true,
OperationDockerPluginDelete: true,
OperationDockerSessionStart: true,
OperationDockerDistributionInspect: true,
OperationDockerBuildPrune: true,
OperationDockerBuildCancel: true,
OperationDockerPing: true,
OperationDockerInfo: true,
OperationDockerVersion: true,
OperationDockerEvents: true,
OperationDockerSystem: true,
OperationDockerUndefined: true,
OperationDockerAgentPing: true,
OperationDockerAgentList: true,
OperationDockerAgentHostInfo: true,
OperationDockerAgentBrowseDelete: true,
OperationDockerAgentBrowseGet: true,
OperationDockerAgentBrowseList: true,
OperationDockerAgentBrowsePut: true,
OperationDockerAgentBrowseRename: true,
OperationDockerAgentUndefined: true,
OperationPortainerResourceControlCreate: true,
OperationPortainerResourceControlUpdate: true,
OperationPortainerStackList: true,
OperationPortainerStackInspect: true,
OperationPortainerStackFile: true,
OperationPortainerStackCreate: true,
OperationPortainerStackMigrate: true,
OperationPortainerStackUpdate: true,
OperationPortainerStackDelete: true,
OperationPortainerWebsocketExec: true,
OperationPortainerWebhookList: true,
OperationPortainerWebhookCreate: true,
OperationPortainerWebhookDelete: true,
OperationIntegrationStoridgeAdmin: true,
EndpointResourcesAccess: true,
}
}
// DefaultEndpointAuthorizationsForHelpDeskRole returns the default endpoint authorizations
// associated to the helpdesk role.
func DefaultEndpointAuthorizationsForHelpDeskRole(volumeBrowsingAuthorizations bool) Authorizations {
authorizations := map[Authorization]bool{
OperationDockerContainerArchiveInfo: true,
OperationDockerContainerList: true,
OperationDockerContainerChanges: true,
OperationDockerContainerInspect: true,
OperationDockerContainerTop: true,
OperationDockerContainerLogs: true,
OperationDockerContainerStats: true,
OperationDockerImageList: true,
OperationDockerImageSearch: true,
OperationDockerImageGetAll: true,
OperationDockerImageGet: true,
OperationDockerImageHistory: true,
OperationDockerImageInspect: true,
OperationDockerNetworkList: true,
OperationDockerNetworkInspect: true,
OperationDockerVolumeList: true,
OperationDockerVolumeInspect: true,
OperationDockerSwarmInspect: true,
OperationDockerNodeList: true,
OperationDockerNodeInspect: true,
OperationDockerServiceList: true,
OperationDockerServiceInspect: true,
OperationDockerServiceLogs: true,
OperationDockerSecretList: true,
OperationDockerSecretInspect: true,
OperationDockerConfigList: true,
OperationDockerConfigInspect: true,
OperationDockerTaskList: true,
OperationDockerTaskInspect: true,
OperationDockerTaskLogs: true,
OperationDockerPluginList: true,
OperationDockerDistributionInspect: true,
OperationDockerPing: true,
OperationDockerInfo: true,
OperationDockerVersion: true,
OperationDockerEvents: true,
OperationDockerSystem: true,
OperationDockerAgentPing: true,
OperationDockerAgentList: true,
OperationDockerAgentHostInfo: true,
OperationPortainerStackList: true,
OperationPortainerStackInspect: true,
OperationPortainerStackFile: true,
OperationPortainerWebhookList: true,
EndpointResourcesAccess: true,
}
if volumeBrowsingAuthorizations {
authorizations[OperationDockerAgentBrowseGet] = true
authorizations[OperationDockerAgentBrowseList] = true
}
return authorizations
}
// DefaultEndpointAuthorizationsForStandardUserRole returns the default endpoint authorizations
// associated to the standard user role.
func DefaultEndpointAuthorizationsForStandardUserRole(volumeBrowsingAuthorizations bool) Authorizations {
authorizations := map[Authorization]bool{
OperationDockerContainerArchiveInfo: true,
OperationDockerContainerList: true,
OperationDockerContainerExport: true,
OperationDockerContainerChanges: true,
OperationDockerContainerInspect: true,
OperationDockerContainerTop: true,
OperationDockerContainerLogs: true,
OperationDockerContainerStats: true,
OperationDockerContainerAttachWebsocket: true,
OperationDockerContainerArchive: true,
OperationDockerContainerCreate: true,
OperationDockerContainerKill: true,
OperationDockerContainerPause: true,
OperationDockerContainerUnpause: true,
OperationDockerContainerRestart: true,
OperationDockerContainerStart: true,
OperationDockerContainerStop: true,
OperationDockerContainerWait: true,
OperationDockerContainerResize: true,
OperationDockerContainerAttach: true,
OperationDockerContainerExec: true,
OperationDockerContainerRename: true,
OperationDockerContainerUpdate: true,
OperationDockerContainerPutContainerArchive: true,
OperationDockerContainerDelete: true,
OperationDockerImageList: true,
OperationDockerImageSearch: true,
OperationDockerImageGetAll: true,
OperationDockerImageGet: true,
OperationDockerImageHistory: true,
OperationDockerImageInspect: true,
OperationDockerImageLoad: true,
OperationDockerImageCreate: true,
OperationDockerImagePush: true,
OperationDockerImageTag: true,
OperationDockerImageDelete: true,
OperationDockerImageCommit: true,
OperationDockerImageBuild: true,
OperationDockerNetworkList: true,
OperationDockerNetworkInspect: true,
OperationDockerNetworkCreate: true,
OperationDockerNetworkConnect: true,
OperationDockerNetworkDisconnect: true,
OperationDockerNetworkDelete: true,
OperationDockerVolumeList: true,
OperationDockerVolumeInspect: true,
OperationDockerVolumeCreate: true,
OperationDockerVolumeDelete: true,
OperationDockerExecInspect: true,
OperationDockerExecStart: true,
OperationDockerExecResize: true,
OperationDockerSwarmInspect: true,
OperationDockerSwarmUnlockKey: true,
OperationDockerSwarmInit: true,
OperationDockerSwarmJoin: true,
OperationDockerSwarmLeave: true,
OperationDockerSwarmUpdate: true,
OperationDockerSwarmUnlock: true,
OperationDockerNodeList: true,
OperationDockerNodeInspect: true,
OperationDockerNodeUpdate: true,
OperationDockerNodeDelete: true,
OperationDockerServiceList: true,
OperationDockerServiceInspect: true,
OperationDockerServiceLogs: true,
OperationDockerServiceCreate: true,
OperationDockerServiceUpdate: true,
OperationDockerServiceDelete: true,
OperationDockerSecretList: true,
OperationDockerSecretInspect: true,
OperationDockerSecretCreate: true,
OperationDockerSecretUpdate: true,
OperationDockerSecretDelete: true,
OperationDockerConfigList: true,
OperationDockerConfigInspect: true,
OperationDockerConfigCreate: true,
OperationDockerConfigUpdate: true,
OperationDockerConfigDelete: true,
OperationDockerTaskList: true,
OperationDockerTaskInspect: true,
OperationDockerTaskLogs: true,
OperationDockerPluginList: true,
OperationDockerPluginPrivileges: true,
OperationDockerPluginInspect: true,
OperationDockerPluginPull: true,
OperationDockerPluginCreate: true,
OperationDockerPluginEnable: true,
OperationDockerPluginDisable: true,
OperationDockerPluginPush: true,
OperationDockerPluginUpgrade: true,
OperationDockerPluginSet: true,
OperationDockerPluginDelete: true,
OperationDockerSessionStart: true,
OperationDockerDistributionInspect: true,
OperationDockerBuildPrune: true,
OperationDockerBuildCancel: true,
OperationDockerPing: true,
OperationDockerInfo: true,
OperationDockerVersion: true,
OperationDockerEvents: true,
OperationDockerSystem: true,
OperationDockerUndefined: true,
OperationDockerAgentPing: true,
OperationDockerAgentList: true,
OperationDockerAgentHostInfo: true,
OperationDockerAgentUndefined: true,
OperationPortainerResourceControlUpdate: true,
OperationPortainerStackList: true,
OperationPortainerStackInspect: true,
OperationPortainerStackFile: true,
OperationPortainerStackCreate: true,
OperationPortainerStackMigrate: true,
OperationPortainerStackUpdate: true,
OperationPortainerStackDelete: true,
OperationPortainerWebsocketExec: true,
OperationPortainerWebhookList: true,
OperationPortainerWebhookCreate: true,
}
if volumeBrowsingAuthorizations {
authorizations[OperationDockerAgentBrowseGet] = true
authorizations[OperationDockerAgentBrowseList] = true
authorizations[OperationDockerAgentBrowseDelete] = true
authorizations[OperationDockerAgentBrowsePut] = true
authorizations[OperationDockerAgentBrowseRename] = true
}
return authorizations
}
// DefaultEndpointAuthorizationsForReadOnlyUserRole returns the default endpoint authorizations
// associated to the readonly user role.
func DefaultEndpointAuthorizationsForReadOnlyUserRole(volumeBrowsingAuthorizations bool) Authorizations {
authorizations := map[Authorization]bool{
OperationDockerContainerArchiveInfo: true,
OperationDockerContainerList: true,
OperationDockerContainerChanges: true,
OperationDockerContainerInspect: true,
OperationDockerContainerTop: true,
OperationDockerContainerLogs: true,
OperationDockerContainerStats: true,
OperationDockerImageList: true,
OperationDockerImageSearch: true,
OperationDockerImageGetAll: true,
OperationDockerImageGet: true,
OperationDockerImageHistory: true,
OperationDockerImageInspect: true,
OperationDockerNetworkList: true,
OperationDockerNetworkInspect: true,
OperationDockerVolumeList: true,
OperationDockerVolumeInspect: true,
OperationDockerSwarmInspect: true,
OperationDockerNodeList: true,
OperationDockerNodeInspect: true,
OperationDockerServiceList: true,
OperationDockerServiceInspect: true,
OperationDockerServiceLogs: true,
OperationDockerSecretList: true,
OperationDockerSecretInspect: true,
OperationDockerConfigList: true,
OperationDockerConfigInspect: true,
OperationDockerTaskList: true,
OperationDockerTaskInspect: true,
OperationDockerTaskLogs: true,
OperationDockerPluginList: true,
OperationDockerDistributionInspect: true,
OperationDockerPing: true,
OperationDockerInfo: true,
OperationDockerVersion: true,
OperationDockerEvents: true,
OperationDockerSystem: true,
OperationDockerAgentPing: true,
OperationDockerAgentList: true,
OperationDockerAgentHostInfo: true,
OperationPortainerStackList: true,
OperationPortainerStackInspect: true,
OperationPortainerStackFile: true,
OperationPortainerWebhookList: true,
}
if volumeBrowsingAuthorizations {
authorizations[OperationDockerAgentBrowseGet] = true
authorizations[OperationDockerAgentBrowseList] = true
}
return authorizations
}
// DefaultPortainerAuthorizations returns the default Portainer authorizations used by non-admin users.
func DefaultPortainerAuthorizations() Authorizations {
return map[Authorization]bool{
OperationPortainerDockerHubInspect: true,
OperationPortainerEndpointGroupList: true,
OperationPortainerEndpointList: true,
OperationPortainerEndpointInspect: true,
OperationPortainerEndpointExtensionAdd: true,
OperationPortainerEndpointExtensionRemove: true,
OperationPortainerExtensionList: true,
OperationPortainerMOTD: true,
OperationPortainerRegistryList: true,
OperationPortainerRegistryInspect: true,
OperationPortainerTeamList: true,
OperationPortainerTemplateList: true,
OperationPortainerTemplateInspect: true,
OperationPortainerUserList: true,
OperationPortainerUserInspect: true,
OperationPortainerUserMemberships: true,
}
}
// UpdateVolumeBrowsingAuthorizations will update all the volume browsing authorizations for each role (except endpoint administrator)
// based on the specified removeAuthorizations parameter. If removeAuthorizations is set to true, all
// the authorizations will be dropped for the each role. If removeAuthorizations is set to false, the authorizations
// will be reset based for each role.
func (service AuthorizationService) UpdateVolumeBrowsingAuthorizations(remove bool) error {
roles, err := service.roleService.Roles()
if err != nil {
return err
}
for _, role := range roles {
// all roles except endpoint administrator
if role.ID != RoleID(1) {
updateRoleVolumeBrowsingAuthorizations(&role, remove)
err := service.roleService.UpdateRole(role.ID, &role)
if err != nil {
return err
}
}
}
return nil
}
func updateRoleVolumeBrowsingAuthorizations(role *Role, removeAuthorizations bool) {
if !removeAuthorizations {
delete(role.Authorizations, OperationDockerAgentBrowseDelete)
delete(role.Authorizations, OperationDockerAgentBrowseGet)
delete(role.Authorizations, OperationDockerAgentBrowseList)
delete(role.Authorizations, OperationDockerAgentBrowsePut)
delete(role.Authorizations, OperationDockerAgentBrowseRename)
return
}
role.Authorizations[OperationDockerAgentBrowseGet] = true
role.Authorizations[OperationDockerAgentBrowseList] = true
// Standard-user
if role.ID == RoleID(3) {
role.Authorizations[OperationDockerAgentBrowseDelete] = true
role.Authorizations[OperationDockerAgentBrowsePut] = true
role.Authorizations[OperationDockerAgentBrowseRename] = true
}
}
// RemoveTeamAccessPolicies will remove all existing access policies associated to the specified team
func (service *AuthorizationService) RemoveTeamAccessPolicies(teamID TeamID) error {
endpoints, err := service.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range endpoints {
for policyTeamID := range endpoint.TeamAccessPolicies {
if policyTeamID == teamID {
delete(endpoint.TeamAccessPolicies, policyTeamID)
err := service.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
break
}
}
}
endpointGroups, err := service.endpointGroupService.EndpointGroups()
if err != nil {
return err
}
for _, endpointGroup := range endpointGroups {
for policyTeamID := range endpointGroup.TeamAccessPolicies {
if policyTeamID == teamID {
delete(endpointGroup.TeamAccessPolicies, policyTeamID)
err := service.endpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
if err != nil {
return err
}
break
}
}
}
registries, err := service.registryService.Registries()
if err != nil {
return err
}
for _, registry := range registries {
for policyTeamID := range registry.TeamAccessPolicies {
if policyTeamID == teamID {
delete(registry.TeamAccessPolicies, policyTeamID)
err := service.registryService.UpdateRegistry(registry.ID, &registry)
if err != nil {
return err
}
break
}
}
}
return service.UpdateUsersAuthorizations()
}
// RemoveUserAccessPolicies will remove all existing access policies associated to the specified user
func (service *AuthorizationService) RemoveUserAccessPolicies(userID UserID) error {
endpoints, err := service.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range endpoints {
for policyUserID := range endpoint.UserAccessPolicies {
if policyUserID == userID {
delete(endpoint.UserAccessPolicies, policyUserID)
err := service.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
break
}
}
}
endpointGroups, err := service.endpointGroupService.EndpointGroups()
if err != nil {
return err
}
for _, endpointGroup := range endpointGroups {
for policyUserID := range endpointGroup.UserAccessPolicies {
if policyUserID == userID {
delete(endpointGroup.UserAccessPolicies, policyUserID)
err := service.endpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
if err != nil {
return err
}
break
}
}
}
registries, err := service.registryService.Registries()
if err != nil {
return err
}
for _, registry := range registries {
for policyUserID := range registry.UserAccessPolicies {
if policyUserID == userID {
delete(registry.UserAccessPolicies, policyUserID)
err := service.registryService.UpdateRegistry(registry.ID, &registry)
if err != nil {
return err
}
break
}
}
}
return nil
}
// UpdateUsersAuthorizations will trigger an update of the authorizations for all the users.
func (service *AuthorizationService) UpdateUsersAuthorizations() error {
users, err := service.userService.Users()
if err != nil {
return err
}
for _, user := range users {
err := service.updateUserAuthorizations(user.ID)
if err != nil {
return err
}
}
return nil
}
func (service *AuthorizationService) updateUserAuthorizations(userID UserID) error {
user, err := service.userService.User(userID)
if err != nil {
return err
}
endpointAuthorizations, err := service.getAuthorizations(user)
if err != nil {
return err
}
user.EndpointAuthorizations = endpointAuthorizations
return service.userService.UpdateUser(userID, user)
}
func (service *AuthorizationService) getAuthorizations(user *User) (EndpointAuthorizations, error) {
endpointAuthorizations := EndpointAuthorizations{}
if user.Role == AdministratorRole {
return endpointAuthorizations, nil
}
userMemberships, err := service.teamMembershipService.TeamMembershipsByUserID(user.ID)
if err != nil {
return endpointAuthorizations, err
}
endpoints, err := service.endpointService.Endpoints()
if err != nil {
return endpointAuthorizations, err
}
endpointGroups, err := service.endpointGroupService.EndpointGroups()
if err != nil {
return endpointAuthorizations, err
}
roles, err := service.roleService.Roles()
if err != nil {
return endpointAuthorizations, err
}
endpointAuthorizations = getUserEndpointAuthorizations(user, endpoints, endpointGroups, roles, userMemberships)
return endpointAuthorizations, nil
}
func getUserEndpointAuthorizations(user *User, endpoints []Endpoint, endpointGroups []EndpointGroup, roles []Role, userMemberships []TeamMembership) EndpointAuthorizations {
endpointAuthorizations := make(EndpointAuthorizations)
groupUserAccessPolicies := map[EndpointGroupID]UserAccessPolicies{}
groupTeamAccessPolicies := map[EndpointGroupID]TeamAccessPolicies{}
for _, endpointGroup := range endpointGroups {
groupUserAccessPolicies[endpointGroup.ID] = endpointGroup.UserAccessPolicies
groupTeamAccessPolicies[endpointGroup.ID] = endpointGroup.TeamAccessPolicies
}
for _, endpoint := range endpoints {
authorizations := getAuthorizationsFromUserEndpointPolicy(user, &endpoint, roles)
if len(authorizations) > 0 {
endpointAuthorizations[endpoint.ID] = authorizations
continue
}
authorizations = getAuthorizationsFromUserEndpointGroupPolicy(user, &endpoint, roles, groupUserAccessPolicies)
if len(authorizations) > 0 {
endpointAuthorizations[endpoint.ID] = authorizations
continue
}
authorizations = getAuthorizationsFromTeamEndpointPolicies(userMemberships, &endpoint, roles)
if len(authorizations) > 0 {
endpointAuthorizations[endpoint.ID] = authorizations
continue
}
authorizations = getAuthorizationsFromTeamEndpointGroupPolicies(userMemberships, &endpoint, roles, groupTeamAccessPolicies)
if len(authorizations) > 0 {
endpointAuthorizations[endpoint.ID] = authorizations
}
}
return endpointAuthorizations
}
func getAuthorizationsFromUserEndpointPolicy(user *User, endpoint *Endpoint, roles []Role) Authorizations {
policyRoles := make([]RoleID, 0)
policy, ok := endpoint.UserAccessPolicies[user.ID]
if ok {
policyRoles = append(policyRoles, policy.RoleID)
}
return getAuthorizationsFromRoles(policyRoles, roles)
}
func getAuthorizationsFromUserEndpointGroupPolicy(user *User, endpoint *Endpoint, roles []Role, groupAccessPolicies map[EndpointGroupID]UserAccessPolicies) Authorizations {
policyRoles := make([]RoleID, 0)
policy, ok := groupAccessPolicies[endpoint.GroupID][user.ID]
if ok {
policyRoles = append(policyRoles, policy.RoleID)
}
return getAuthorizationsFromRoles(policyRoles, roles)
}
func getAuthorizationsFromTeamEndpointPolicies(memberships []TeamMembership, endpoint *Endpoint, roles []Role) Authorizations {
policyRoles := make([]RoleID, 0)
for _, membership := range memberships {
policy, ok := endpoint.TeamAccessPolicies[membership.TeamID]
if ok {
policyRoles = append(policyRoles, policy.RoleID)
}
}
return getAuthorizationsFromRoles(policyRoles, roles)
}
func getAuthorizationsFromTeamEndpointGroupPolicies(memberships []TeamMembership, endpoint *Endpoint, roles []Role, groupAccessPolicies map[EndpointGroupID]TeamAccessPolicies) Authorizations {
policyRoles := make([]RoleID, 0)
for _, membership := range memberships {
policy, ok := groupAccessPolicies[endpoint.GroupID][membership.TeamID]
if ok {
policyRoles = append(policyRoles, policy.RoleID)
}
}
return getAuthorizationsFromRoles(policyRoles, roles)
}
func getAuthorizationsFromRoles(roleIdentifiers []RoleID, roles []Role) Authorizations {
var associatedRoles []Role
for _, id := range roleIdentifiers {
for _, role := range roles {
if role.ID == id {
associatedRoles = append(associatedRoles, role)
break
}
}
}
var authorizations Authorizations
highestPriority := 0
for _, role := range associatedRoles {
if role.Priority > highestPriority {
highestPriority = role.Priority
authorizations = role.Authorizations
}
}
return authorizations
}

View File

@ -0,0 +1,96 @@
package customtemplate
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "customtemplates"
)
// Service represents a service for managing custom template data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// CustomTemplates return an array containing all the custom templates.
func (service *Service) CustomTemplates() ([]portainer.CustomTemplate, error) {
var customTemplates = make([]portainer.CustomTemplate, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var customTemplate portainer.CustomTemplate
err := internal.UnmarshalObjectWithJsoniter(v, &customTemplate)
if err != nil {
return err
}
customTemplates = append(customTemplates, customTemplate)
}
return nil
})
return customTemplates, err
}
// CustomTemplate returns an custom template by ID.
func (service *Service) CustomTemplate(ID portainer.CustomTemplateID) (*portainer.CustomTemplate, error) {
var customTemplate portainer.CustomTemplate
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &customTemplate)
if err != nil {
return nil, err
}
return &customTemplate, nil
}
// UpdateCustomTemplate updates an custom template.
func (service *Service) UpdateCustomTemplate(ID portainer.CustomTemplateID, customTemplate *portainer.CustomTemplate) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, customTemplate)
}
// DeleteCustomTemplate deletes an custom template.
func (service *Service) DeleteCustomTemplate(ID portainer.CustomTemplateID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
// CreateCustomTemplate assign an ID to a new custom template and saves it.
func (service *Service) CreateCustomTemplate(customTemplate *portainer.CustomTemplate) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data, err := internal.MarshalObject(customTemplate)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(customTemplate.ID)), data)
})
}
// GetNextIdentifier returns the next identifier for a custom template.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
}

View File

@ -5,16 +5,17 @@ import (
"path"
"time"
"github.com/portainer/portainer/api/bolt/edgegroup"
"github.com/portainer/portainer/api/bolt/edgestack"
"github.com/portainer/portainer/api/bolt/endpointrelation"
"github.com/portainer/portainer/api/bolt/tunnelserver"
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/customtemplate"
"github.com/portainer/portainer/api/bolt/dockerhub"
"github.com/portainer/portainer/api/bolt/edgegroup"
"github.com/portainer/portainer/api/bolt/edgejob"
"github.com/portainer/portainer/api/bolt/edgestack"
"github.com/portainer/portainer/api/bolt/endpoint"
"github.com/portainer/portainer/api/bolt/endpointgroup"
"github.com/portainer/portainer/api/bolt/endpointrelation"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/extension"
"github.com/portainer/portainer/api/bolt/migrator"
"github.com/portainer/portainer/api/bolt/registry"
@ -26,10 +27,11 @@ import (
"github.com/portainer/portainer/api/bolt/tag"
"github.com/portainer/portainer/api/bolt/team"
"github.com/portainer/portainer/api/bolt/teammembership"
"github.com/portainer/portainer/api/bolt/template"
"github.com/portainer/portainer/api/bolt/tunnelserver"
"github.com/portainer/portainer/api/bolt/user"
"github.com/portainer/portainer/api/bolt/version"
"github.com/portainer/portainer/api/bolt/webhook"
"github.com/portainer/portainer/api/internal/authorization"
)
const (
@ -41,11 +43,12 @@ const (
type Store struct {
path string
db *bolt.DB
checkForDataMigration bool
isNew bool
fileService portainer.FileService
RoleService *role.Service
CustomTemplateService *customtemplate.Service
DockerHubService *dockerhub.Service
EdgeGroupService *edgegroup.Service
EdgeJobService *edgejob.Service
EdgeStackService *edgestack.Service
EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service
@ -53,17 +56,17 @@ type Store struct {
ExtensionService *extension.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
RoleService *role.Service
ScheduleService *schedule.Service
SettingsService *settings.Service
StackService *stack.Service
TagService *tag.Service
TeamMembershipService *teammembership.Service
TeamService *team.Service
TemplateService *template.Service
TunnelServerService *tunnelserver.Service
UserService *user.Service
VersionService *version.Service
WebhookService *webhook.Service
ScheduleService *schedule.Service
}
// NewStore initializes a new Store and the associated services
@ -71,6 +74,7 @@ func NewStore(storePath string, fileService portainer.FileService) (*Store, erro
store := &Store{
path: storePath,
fileService: fileService,
isNew: true,
}
databasePath := path.Join(storePath, databaseFileName)
@ -79,10 +83,8 @@ func NewStore(storePath string, fileService portainer.FileService) (*Store, erro
return nil, err
}
if !databaseFileExists {
store.checkForDataMigration = false
} else {
store.checkForDataMigration = true
if databaseFileExists {
store.isNew = false
}
return store, nil
@ -108,14 +110,21 @@ func (store *Store) Close() error {
return nil
}
// IsNew returns true if the database was just created and false if it is re-using
// existing data.
func (store *Store) IsNew() bool {
return store.isNew
}
// MigrateData automatically migrate the data based on the DBVersion.
// This process is only triggered on an existing database, not if the database was just created.
func (store *Store) MigrateData() error {
if !store.checkForDataMigration {
if store.isNew {
return store.VersionService.StoreDBVersion(portainer.DBVersion)
}
version, err := store.VersionService.DBVersion()
if err == portainer.ErrObjectNotFound {
if err == errors.ErrObjectNotFound {
version = 0
} else if err != nil {
return err
@ -137,10 +146,10 @@ func (store *Store) MigrateData() error {
StackService: store.StackService,
TagService: store.TagService,
TeamMembershipService: store.TeamMembershipService,
TemplateService: store.TemplateService,
UserService: store.UserService,
VersionService: store.VersionService,
FileService: store.fileService,
AuthorizationService: authorization.NewService(store),
}
migrator := migrator.NewMigrator(migratorParams)
@ -162,6 +171,12 @@ func (store *Store) initServices() error {
}
store.RoleService = authorizationsetService
customTemplateService, err := customtemplate.NewService(store.db)
if err != nil {
return err
}
store.CustomTemplateService = customTemplateService
dockerhubService, err := dockerhub.NewService(store.db)
if err != nil {
return err
@ -180,6 +195,12 @@ func (store *Store) initServices() error {
}
store.EdgeGroupService = edgeGroupService
edgeJobService, err := edgejob.NewService(store.db)
if err != nil {
return err
}
store.EdgeJobService = edgeJobService
endpointgroupService, err := endpointgroup.NewService(store.db)
if err != nil {
return err
@ -246,12 +267,6 @@ func (store *Store) initServices() error {
}
store.TeamService = teamService
templateService, err := template.NewService(store.db)
if err != nil {
return err
}
store.TemplateService = templateService
tunnelServerService, err := tunnelserver.NewService(store.db)
if err != nil {
return err
@ -284,3 +299,103 @@ func (store *Store) initServices() error {
return nil
}
// CustomTemplate gives access to the CustomTemplate data management layer
func (store *Store) CustomTemplate() portainer.CustomTemplateService {
return store.CustomTemplateService
}
// DockerHub gives access to the DockerHub data management layer
func (store *Store) DockerHub() portainer.DockerHubService {
return store.DockerHubService
}
// EdgeGroup gives access to the EdgeGroup data management layer
func (store *Store) EdgeGroup() portainer.EdgeGroupService {
return store.EdgeGroupService
}
// EdgeJob gives access to the EdgeJob data management layer
func (store *Store) EdgeJob() portainer.EdgeJobService {
return store.EdgeJobService
}
// EdgeStack gives access to the EdgeStack data management layer
func (store *Store) EdgeStack() portainer.EdgeStackService {
return store.EdgeStackService
}
// Endpoint gives access to the Endpoint data management layer
func (store *Store) Endpoint() portainer.EndpointService {
return store.EndpointService
}
// EndpointGroup gives access to the EndpointGroup data management layer
func (store *Store) EndpointGroup() portainer.EndpointGroupService {
return store.EndpointGroupService
}
// EndpointRelation gives access to the EndpointRelation data management layer
func (store *Store) EndpointRelation() portainer.EndpointRelationService {
return store.EndpointRelationService
}
// Registry gives access to the Registry data management layer
func (store *Store) Registry() portainer.RegistryService {
return store.RegistryService
}
// ResourceControl gives access to the ResourceControl data management layer
func (store *Store) ResourceControl() portainer.ResourceControlService {
return store.ResourceControlService
}
// Role gives access to the Role data management layer
func (store *Store) Role() portainer.RoleService {
return store.RoleService
}
// Settings gives access to the Settings data management layer
func (store *Store) Settings() portainer.SettingsService {
return store.SettingsService
}
// Stack gives access to the Stack data management layer
func (store *Store) Stack() portainer.StackService {
return store.StackService
}
// Tag gives access to the Tag data management layer
func (store *Store) Tag() portainer.TagService {
return store.TagService
}
// TeamMembership gives access to the TeamMembership data management layer
func (store *Store) TeamMembership() portainer.TeamMembershipService {
return store.TeamMembershipService
}
// Team gives access to the Team data management layer
func (store *Store) Team() portainer.TeamService {
return store.TeamService
}
// TunnelServer gives access to the TunnelServer data management layer
func (store *Store) TunnelServer() portainer.TunnelServerService {
return store.TunnelServerService
}
// User gives access to the User data management layer
func (store *Store) User() portainer.UserService {
return store.UserService
}
// Version gives access to the Version data management layer
func (store *Store) Version() portainer.VersionService {
return store.VersionService
}
// Webhook gives access to the Webhook data management layer
func (store *Store) Webhook() portainer.WebhookService {
return store.WebhookService
}

101
api/bolt/edgejob/edgejob.go Normal file
View File

@ -0,0 +1,101 @@
package edgejob
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "edgejobs"
)
// Service represents a service for managing edge jobs data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// EdgeJobs returns a list of Edge jobs
func (service *Service) EdgeJobs() ([]portainer.EdgeJob, error) {
var edgeJobs = make([]portainer.EdgeJob, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var edgeJob portainer.EdgeJob
err := internal.UnmarshalObject(v, &edgeJob)
if err != nil {
return err
}
edgeJobs = append(edgeJobs, edgeJob)
}
return nil
})
return edgeJobs, err
}
// EdgeJob returns an Edge job by ID
func (service *Service) EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, error) {
var edgeJob portainer.EdgeJob
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &edgeJob)
if err != nil {
return nil, err
}
return &edgeJob, nil
}
// CreateEdgeJob creates a new Edge job
func (service *Service) CreateEdgeJob(edgeJob *portainer.EdgeJob) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
if edgeJob.ID == 0 {
id, _ := bucket.NextSequence()
edgeJob.ID = portainer.EdgeJobID(id)
}
data, err := internal.MarshalObject(edgeJob)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(edgeJob.ID)), data)
})
}
// UpdateEdgeJob updates an Edge job by ID
func (service *Service) UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, edgeJob)
}
// DeleteEdgeJob deletes an Edge job
func (service *Service) DeleteEdgeJob(ID portainer.EdgeJobID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
// GetNextIdentifier returns the next identifier for an endpoint.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
}

View File

@ -0,0 +1,7 @@
package errors
import "errors"
var (
ErrObjectNotFound = errors.New("Object not found inside the database")
)

View File

@ -1,9 +1,83 @@
package bolt
import portainer "github.com/portainer/portainer/api"
import (
"github.com/gofrs/uuid"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
)
// Init creates the default data set.
func (store *Store) Init() error {
instanceID, err := store.VersionService.InstanceID()
if err == errors.ErrObjectNotFound {
uid, err := uuid.NewV4()
if err != nil {
return err
}
instanceID = uid.String()
err = store.VersionService.StoreInstanceID(instanceID)
if err != nil {
return err
}
} else if err != nil {
return err
}
_, err = store.SettingsService.Settings()
if err == errors.ErrObjectNotFound {
defaultSettings := &portainer.Settings{
AuthenticationMethod: portainer.AuthenticationInternal,
BlackListedLabels: make([]portainer.Pair, 0),
LDAPSettings: portainer.LDAPSettings{
AnonymousMode: true,
AutoCreateUsers: true,
TLSConfig: portainer.TLSConfiguration{},
SearchSettings: []portainer.LDAPSearchSettings{
portainer.LDAPSearchSettings{},
},
GroupSearchSettings: []portainer.LDAPGroupSearchSettings{
portainer.LDAPGroupSearchSettings{},
},
},
OAuthSettings: portainer.OAuthSettings{},
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowVolumeBrowserForRegularUsers: false,
AllowHostNamespaceForRegularUsers: true,
AllowDeviceMappingForRegularUsers: true,
AllowStackManagementForRegularUsers: true,
AllowContainerCapabilitiesForRegularUsers: true,
EnableHostManagementFeatures: false,
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
TemplatesURL: portainer.DefaultTemplatesURL,
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
}
err = store.SettingsService.UpdateSettings(defaultSettings)
if err != nil {
return err
}
} else if err != nil {
return err
}
_, err = store.DockerHubService.DockerHub()
if err == errors.ErrObjectNotFound {
defaultDockerHub := &portainer.DockerHub{
Authentication: false,
Username: "",
Password: "",
}
err := store.DockerHubService.UpdateDockerHub(defaultDockerHub)
if err != nil {
return err
}
} else if err != nil {
return err
}
groups, err := store.EndpointGroupService.EndpointGroups()
if err != nil {
return err
@ -25,60 +99,5 @@ func (store *Store) Init() error {
}
}
roles, err := store.RoleService.Roles()
if err != nil {
return err
}
if len(roles) == 0 {
environmentAdministratorRole := &portainer.Role{
Name: "Endpoint administrator",
Description: "Full control of all resources in an endpoint",
Priority: 1,
Authorizations: portainer.DefaultEndpointAuthorizationsForEndpointAdministratorRole(),
}
err = store.RoleService.CreateRole(environmentAdministratorRole)
if err != nil {
return err
}
environmentReadOnlyUserRole := &portainer.Role{
Name: "Helpdesk",
Description: "Read-only access of all resources in an endpoint",
Priority: 2,
Authorizations: portainer.DefaultEndpointAuthorizationsForHelpDeskRole(false),
}
err = store.RoleService.CreateRole(environmentReadOnlyUserRole)
if err != nil {
return err
}
standardUserRole := &portainer.Role{
Name: "Standard user",
Description: "Full control of assigned resources in an endpoint",
Priority: 3,
Authorizations: portainer.DefaultEndpointAuthorizationsForStandardUserRole(false),
}
err = store.RoleService.CreateRole(standardUserRole)
if err != nil {
return err
}
readOnlyUserRole := &portainer.Role{
Name: "Read-only user",
Description: "Read-only access of assigned resources in an endpoint",
Priority: 4,
Authorizations: portainer.DefaultEndpointAuthorizationsForReadOnlyUserRole(false),
}
err = store.RoleService.CreateRole(readOnlyUserRole)
if err != nil {
return err
}
}
return nil
}

View File

@ -4,7 +4,7 @@ import (
"encoding/binary"
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
)
// Itob returns an 8-byte big endian representation of v.
@ -36,7 +36,7 @@ func GetObject(db *bolt.DB, bucketName string, key []byte, object interface{}) e
value := bucket.Get(key)
if value == nil {
return portainer.ErrObjectNotFound
return errors.ErrObjectNotFound
}
data = make([]byte, len(value))

View File

@ -3,6 +3,7 @@ package migrator
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/user"
)
@ -22,7 +23,7 @@ func (m *Migrator) updateAdminUserToDBVersion1() error {
if err != nil {
return err
}
} else if err != nil && err != portainer.ErrObjectNotFound {
} else if err != nil && err != errors.ErrObjectNotFound {
return err
}
return nil

View File

@ -1,11 +1,5 @@
package migrator
import (
"strings"
"github.com/portainer/portainer/api"
)
func (m *Migrator) updateSettingsToDBVersion15() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
@ -17,19 +11,6 @@ func (m *Migrator) updateSettingsToDBVersion15() error {
}
func (m *Migrator) updateTemplatesToVersion15() error {
legacyTemplates, err := m.templateService.Templates()
if err != nil {
return err
}
for _, template := range legacyTemplates {
template.Logo = strings.Replace(template.Logo, "https://portainer.io/images", portainer.AssetsServerURL, -1)
err = m.templateService.UpdateTemplate(template.ID, &template)
if err != nil {
return err
}
}
// Removed with the entire template management layer, part of https://github.com/portainer/portainer/issues/3707
return nil
}

View File

@ -2,22 +2,12 @@ package migrator
import (
"strings"
portainer "github.com/portainer/portainer/api"
)
func (m *Migrator) updateUsersToDBVersion20() error {
authorizationServiceParameters := &portainer.AuthorizationServiceParameters{
EndpointService: m.endpointService,
EndpointGroupService: m.endpointGroupService,
RegistryService: m.registryService,
RoleService: m.roleService,
TeamMembershipService: m.teamMembershipService,
UserService: m.userService,
}
const scheduleScriptExecutionJobType = 1
authorizationService := portainer.NewAuthorizationService(authorizationServiceParameters)
return authorizationService.UpdateUsersAuthorizations()
func (m *Migrator) updateUsersToDBVersion20() error {
return m.authorizationService.UpdateUsersAuthorizations()
}
func (m *Migrator) updateSettingsToDBVersion20() error {
@ -38,7 +28,7 @@ func (m *Migrator) updateSchedulesToDBVersion20() error {
}
for _, schedule := range legacySchedules {
if schedule.JobType == portainer.ScriptExecutionJobType {
if schedule.JobType == scheduleScriptExecutionJobType {
if schedule.CronExpression == "0 0 * * *" {
schedule.CronExpression = "0 * * * *"
} else if schedule.CronExpression == "0 0 0/2 * *" {

View File

@ -1,6 +1,9 @@
package migrator
import portainer "github.com/portainer/portainer/api"
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/authorization"
)
func (m *Migrator) updateResourceControlsToDBVersion22() error {
legacyResourceControls, err := m.resourceControlService.ResourceControls()
@ -32,7 +35,7 @@ func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
}
for _, user := range legacyUsers {
user.PortainerAuthorizations = portainer.DefaultPortainerAuthorizations()
user.PortainerAuthorizations = authorization.DefaultPortainerAuthorizations()
err = m.userService.UpdateUser(user.ID, &user)
if err != nil {
return err
@ -44,7 +47,7 @@ func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
return err
}
endpointAdministratorRole.Priority = 1
endpointAdministratorRole.Authorizations = portainer.DefaultEndpointAuthorizationsForEndpointAdministratorRole()
endpointAdministratorRole.Authorizations = authorization.DefaultEndpointAuthorizationsForEndpointAdministratorRole()
err = m.roleService.UpdateRole(endpointAdministratorRole.ID, endpointAdministratorRole)
@ -53,7 +56,7 @@ func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
return err
}
helpDeskRole.Priority = 2
helpDeskRole.Authorizations = portainer.DefaultEndpointAuthorizationsForHelpDeskRole(settings.AllowVolumeBrowserForRegularUsers)
helpDeskRole.Authorizations = authorization.DefaultEndpointAuthorizationsForHelpDeskRole(settings.AllowVolumeBrowserForRegularUsers)
err = m.roleService.UpdateRole(helpDeskRole.ID, helpDeskRole)
@ -62,7 +65,7 @@ func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
return err
}
standardUserRole.Priority = 3
standardUserRole.Authorizations = portainer.DefaultEndpointAuthorizationsForStandardUserRole(settings.AllowVolumeBrowserForRegularUsers)
standardUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForStandardUserRole(settings.AllowVolumeBrowserForRegularUsers)
err = m.roleService.UpdateRole(standardUserRole.ID, standardUserRole)
@ -71,19 +74,12 @@ func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
return err
}
readOnlyUserRole.Priority = 4
readOnlyUserRole.Authorizations = portainer.DefaultEndpointAuthorizationsForReadOnlyUserRole(settings.AllowVolumeBrowserForRegularUsers)
readOnlyUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForReadOnlyUserRole(settings.AllowVolumeBrowserForRegularUsers)
err = m.roleService.UpdateRole(readOnlyUserRole.ID, readOnlyUserRole)
authorizationServiceParameters := &portainer.AuthorizationServiceParameters{
EndpointService: m.endpointService,
EndpointGroupService: m.endpointGroupService,
RegistryService: m.registryService,
RoleService: m.roleService,
TeamMembershipService: m.teamMembershipService,
UserService: m.userService,
if err != nil {
return err
}
authorizationService := portainer.NewAuthorizationService(authorizationServiceParameters)
return authorizationService.UpdateUsersAuthorizations()
return m.authorizationService.UpdateUsersAuthorizations()
}

View File

@ -0,0 +1,34 @@
package migrator
import portainer "github.com/portainer/portainer/api"
func (m *Migrator) updateSettingsToDB24() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.AllowHostNamespaceForRegularUsers = true
legacySettings.AllowDeviceMappingForRegularUsers = true
legacySettings.AllowStackManagementForRegularUsers = true
return m.settingsService.UpdateSettings(legacySettings)
}
func (m *Migrator) updateStacksToDB24() error {
stacks, err := m.stackService.Stacks()
if err != nil {
return err
}
for idx := range stacks {
stack := &stacks[idx]
stack.Status = portainer.StackStatusActive
err := m.stackService.UpdateStack(stack.ID, stack)
if err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,23 @@
package migrator
import (
"github.com/portainer/portainer/api"
)
func (m *Migrator) updateSettingsToDB25() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
if legacySettings.TemplatesURL == "" {
legacySettings.TemplatesURL = portainer.DefaultTemplatesURL
}
legacySettings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
legacySettings.EnableTelemetry = true
legacySettings.AllowContainerCapabilitiesForRegularUsers = true
return m.settingsService.UpdateSettings(legacySettings)
}

View File

@ -15,9 +15,9 @@ import (
"github.com/portainer/portainer/api/bolt/stack"
"github.com/portainer/portainer/api/bolt/tag"
"github.com/portainer/portainer/api/bolt/teammembership"
"github.com/portainer/portainer/api/bolt/template"
"github.com/portainer/portainer/api/bolt/user"
"github.com/portainer/portainer/api/bolt/version"
"github.com/portainer/portainer/api/internal/authorization"
)
type (
@ -37,10 +37,10 @@ type (
stackService *stack.Service
tagService *tag.Service
teamMembershipService *teammembership.Service
templateService *template.Service
userService *user.Service
versionService *version.Service
fileService portainer.FileService
authorizationService *authorization.Service
}
// Parameters represents the required parameters to create a new Migrator instance.
@ -59,10 +59,10 @@ type (
StackService *stack.Service
TagService *tag.Service
TeamMembershipService *teammembership.Service
TemplateService *template.Service
UserService *user.Service
VersionService *version.Service
FileService portainer.FileService
AuthorizationService *authorization.Service
}
)
@ -82,17 +82,16 @@ func NewMigrator(parameters *Parameters) *Migrator {
settingsService: parameters.SettingsService,
tagService: parameters.TagService,
teamMembershipService: parameters.TeamMembershipService,
templateService: parameters.TemplateService,
stackService: parameters.StackService,
userService: parameters.UserService,
versionService: parameters.VersionService,
fileService: parameters.FileService,
authorizationService: parameters.AuthorizationService,
}
}
// 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()
@ -322,5 +321,26 @@ func (m *Migrator) Migrate() error {
}
}
// 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
}
}
return m.versionService.StoreDBVersion(portainer.DBVersion)
}

View File

@ -2,6 +2,7 @@ package stack
import (
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
@ -64,7 +65,7 @@ func (service *Service) StackByName(name string) (*portainer.Stack, error) {
}
if stack == nil {
return portainer.ErrObjectNotFound
return errors.ErrObjectNotFound
}
return nil

View File

@ -2,6 +2,7 @@ package team
import (
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
@ -64,7 +65,7 @@ func (service *Service) TeamByName(name string) (*portainer.Team, error) {
}
if team == nil {
return portainer.ErrObjectNotFound
return errors.ErrObjectNotFound
}
return nil

View File

@ -1,95 +0,0 @@
package template
import (
"github.com/portainer/portainer/api"
"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 = "templates"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// Templates return an array containing all the templates.
func (service *Service) Templates() ([]portainer.Template, error) {
var templates = make([]portainer.Template, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var template portainer.Template
err := internal.UnmarshalObject(v, &template)
if err != nil {
return err
}
templates = append(templates, template)
}
return nil
})
return templates, err
}
// Template returns a template by ID.
func (service *Service) Template(ID portainer.TemplateID) (*portainer.Template, error) {
var template portainer.Template
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &template)
if err != nil {
return nil, err
}
return &template, nil
}
// CreateTemplate creates a new template.
func (service *Service) CreateTemplate(template *portainer.Template) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
template.ID = portainer.TemplateID(id)
data, err := internal.MarshalObject(template)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(template.ID)), data)
})
}
// UpdateTemplate saves a template.
func (service *Service) UpdateTemplate(ID portainer.TemplateID, template *portainer.Template) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, template)
}
// DeleteTemplate deletes a template.
func (service *Service) DeleteTemplate(ID portainer.TemplateID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}

View File

@ -2,6 +2,7 @@ package user
import (
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
@ -64,7 +65,7 @@ func (service *Service) UserByUsername(username string) (*portainer.User, error)
}
if user == nil {
return portainer.ErrObjectNotFound
return errors.ErrObjectNotFound
}
return nil
})

View File

@ -4,14 +4,15 @@ import (
"strconv"
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "version"
versionKey = "DB_VERSION"
BucketName = "version"
versionKey = "DB_VERSION"
instanceKey = "INSTANCE_ID"
)
// Service represents a service to manage stored versions.
@ -40,7 +41,7 @@ func (service *Service) DBVersion() (int, error) {
value := bucket.Get([]byte(versionKey))
if value == nil {
return portainer.ErrObjectNotFound
return errors.ErrObjectNotFound
}
data = make([]byte, len(value))
@ -64,3 +65,37 @@ func (service *Service) StoreDBVersion(version int) error {
return bucket.Put([]byte(versionKey), data)
})
}
// InstanceID retrieves the stored instance ID.
func (service *Service) InstanceID() (string, error) {
var data []byte
err := service.db.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(data), nil
}
// StoreInstanceID store the instance ID.
func (service *Service) StoreInstanceID(ID string) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data := []byte(ID)
return bucket.Put([]byte(instanceKey), data)
})
}

View File

@ -2,6 +2,7 @@ package webhook
import (
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
@ -87,7 +88,7 @@ func (service *Service) WebhookByResourceID(ID string) (*portainer.Webhook, erro
}
if webhook == nil {
return portainer.ErrObjectNotFound
return errors.ErrObjectNotFound
}
return nil
@ -118,7 +119,7 @@ func (service *Service) WebhookByToken(token string) (*portainer.Webhook, error)
}
if webhook == nil {
return portainer.ErrObjectNotFound
return errors.ErrObjectNotFound
}
return nil

View File

@ -6,42 +6,42 @@ import (
portainer "github.com/portainer/portainer/api"
)
// AddSchedule register a schedule inside the tunnel details associated to an endpoint.
func (service *Service) AddSchedule(endpointID portainer.EndpointID, schedule *portainer.EdgeSchedule) {
// AddEdgeJob register an EdgeJob inside the tunnel details associated to an endpoint.
func (service *Service) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *portainer.EdgeJob) {
tunnel := service.GetTunnelDetails(endpointID)
existingScheduleIndex := -1
for idx, existingSchedule := range tunnel.Schedules {
if existingSchedule.ID == schedule.ID {
existingScheduleIndex = idx
existingJobIndex := -1
for idx, existingJob := range tunnel.Jobs {
if existingJob.ID == edgeJob.ID {
existingJobIndex = idx
break
}
}
if existingScheduleIndex == -1 {
tunnel.Schedules = append(tunnel.Schedules, *schedule)
if existingJobIndex == -1 {
tunnel.Jobs = append(tunnel.Jobs, *edgeJob)
} else {
tunnel.Schedules[existingScheduleIndex] = *schedule
tunnel.Jobs[existingJobIndex] = *edgeJob
}
key := strconv.Itoa(int(endpointID))
service.tunnelDetailsMap.Set(key, tunnel)
}
// RemoveSchedule will remove the specified schedule from each tunnel it was registered with.
func (service *Service) RemoveSchedule(scheduleID portainer.ScheduleID) {
// RemoveEdgeJob will remove the specified Edge job from each tunnel it was registered with.
func (service *Service) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {
for item := range service.tunnelDetailsMap.IterBuffered() {
tunnelDetails := item.Val.(*portainer.TunnelDetails)
updatedSchedules := make([]portainer.EdgeSchedule, 0)
for _, schedule := range tunnelDetails.Schedules {
if schedule.ID == scheduleID {
updatedJobs := make([]portainer.EdgeJob, 0)
for _, edgeJob := range tunnelDetails.Jobs {
if edgeJob.ID == edgeJobID {
continue
}
updatedSchedules = append(updatedSchedules, schedule)
updatedJobs = append(updatedJobs, edgeJob)
}
tunnelDetails.Schedules = updatedSchedules
tunnelDetails.Jobs = updatedJobs
service.tunnelDetailsMap.Set(item.Key, tunnelDetails)
}
}

View File

@ -7,11 +7,10 @@ import (
"time"
"github.com/dchest/uniuri"
cmap "github.com/orcaman/concurrent-map"
chserver "github.com/jpillora/chisel/server"
portainer "github.com/portainer/portainer/api"
cmap "github.com/orcaman/concurrent-map"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
)
const (
@ -24,21 +23,19 @@ const (
// It is used to start a reverse tunnel server and to manage the connection status of each tunnel
// connected to the tunnel server.
type Service struct {
serverFingerprint string
serverPort string
tunnelDetailsMap cmap.ConcurrentMap
endpointService portainer.EndpointService
tunnelServerService portainer.TunnelServerService
snapshotter portainer.Snapshotter
chiselServer *chserver.Server
serverFingerprint string
serverPort string
tunnelDetailsMap cmap.ConcurrentMap
dataStore portainer.DataStore
snapshotService portainer.SnapshotService
chiselServer *chserver.Server
}
// NewService returns a pointer to a new instance of Service
func NewService(endpointService portainer.EndpointService, tunnelServerService portainer.TunnelServerService) *Service {
func NewService(dataStore portainer.DataStore) *Service {
return &Service{
tunnelDetailsMap: cmap.New(),
endpointService: endpointService,
tunnelServerService: tunnelServerService,
tunnelDetailsMap: cmap.New(),
dataStore: dataStore,
}
}
@ -47,7 +44,7 @@ func NewService(endpointService portainer.EndpointService, tunnelServerService p
// be found inside the database, it will generate a new one randomly and persist it.
// It starts the tunnel status verification process in the background.
// The snapshotter is used in the tunnel status verification process.
func (service *Service) StartTunnelServer(addr, port string, snapshotter portainer.Snapshotter) error {
func (service *Service) StartTunnelServer(addr, port string, snapshotService portainer.SnapshotService) error {
keySeed, err := service.retrievePrivateKeySeed()
if err != nil {
return err
@ -80,7 +77,7 @@ func (service *Service) StartTunnelServer(addr, port string, snapshotter portain
return err
}
service.snapshotter = snapshotter
service.snapshotService = snapshotService
go service.startTunnelVerificationLoop()
return nil
@ -89,15 +86,15 @@ func (service *Service) StartTunnelServer(addr, port string, snapshotter portain
func (service *Service) retrievePrivateKeySeed() (string, error) {
var serverInfo *portainer.TunnelServerInfo
serverInfo, err := service.tunnelServerService.Info()
if err == portainer.ErrObjectNotFound {
serverInfo, err := service.dataStore.TunnelServer().Info()
if err == errors.ErrObjectNotFound {
keySeed := uniuri.NewLen(16)
serverInfo = &portainer.TunnelServerInfo{
PrivateKeySeed: keySeed,
}
err := service.tunnelServerService.UpdateInfo(serverInfo)
err := service.dataStore.TunnelServer().UpdateInfo(serverInfo)
if err != nil {
return "", err
}
@ -157,7 +154,7 @@ func (service *Service) checkTunnels() {
}
}
if len(tunnel.Schedules) > 0 {
if len(tunnel.Jobs) > 0 {
endpointID, err := strconv.Atoi(item.Key)
if err != nil {
log.Printf("[ERROR] [chisel,conversion] Invalid endpoint identifier (id: %s): %s", item.Key, err)
@ -173,19 +170,19 @@ func (service *Service) checkTunnels() {
}
func (service *Service) snapshotEnvironment(endpointID portainer.EndpointID, tunnelPort int) error {
endpoint, err := service.endpointService.Endpoint(portainer.EndpointID(endpointID))
endpoint, err := service.dataStore.Endpoint().Endpoint(endpointID)
if err != nil {
return err
}
endpointURL := endpoint.URL
endpoint.URL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnelPort)
snapshot, err := service.snapshotter.CreateSnapshot(endpoint)
err = service.snapshotService.SnapshotEndpoint(endpoint)
if err != nil {
return err
}
endpoint.Snapshots = []portainer.Snapshot{*snapshot}
endpoint.URL = endpointURL
return service.endpointService.UpdateEndpoint(endpoint.ID, endpoint)
return service.dataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
}

View File

@ -47,11 +47,11 @@ func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) *porta
return tunnelDetails
}
schedules := make([]portainer.EdgeSchedule, 0)
jobs := make([]portainer.EdgeJob, 0)
return &portainer.TunnelDetails{
Status: portainer.EdgeAgentIdle,
Port: 0,
Schedules: schedules,
Jobs: jobs,
Credentials: "",
}
}
@ -97,7 +97,7 @@ func (service *Service) SetTunnelStatusToRequired(endpointID portainer.EndpointI
tunnel := service.GetTunnelDetails(endpointID)
if tunnel.Port == 0 {
endpoint, err := service.endpointService.Endpoint(endpointID)
endpoint, err := service.dataStore.Endpoint().Endpoint(endpointID)
if err != nil {
return err
}

View File

@ -1,6 +1,7 @@
package cli
import (
"errors"
"log"
"time"
@ -16,16 +17,11 @@ import (
// Service implements the CLIService interface
type Service struct{}
const (
errInvalidEndpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix://, npipe:// or tcp://")
errSocketOrNamedPipeNotFound = portainer.Error("Unable to locate Unix socket or named pipe")
errEndpointsFileNotFound = portainer.Error("Unable to locate external endpoints file")
errTemplateFileNotFound = portainer.Error("Unable to locate template file on disk")
errInvalidSyncInterval = portainer.Error("Invalid synchronization interval")
errInvalidSnapshotInterval = portainer.Error("Invalid snapshot interval")
errEndpointExcludeExternal = portainer.Error("Cannot use the -H flag mutually with --external-endpoints")
errNoAuthExcludeAdminPassword = portainer.Error("Cannot use --no-auth with --admin-password or --admin-password-file")
errAdminPassExcludeAdminPassFile = portainer.Error("Cannot use --admin-password with --admin-password-file")
var (
errInvalidEndpointProtocol = errors.New("Invalid endpoint protocol: Portainer only supports unix://, npipe:// or tcp://")
errSocketOrNamedPipeNotFound = errors.New("Unable to locate Unix socket or named pipe")
errInvalidSnapshotInterval = errors.New("Invalid snapshot interval")
errAdminPassExcludeAdminPassFile = errors.New("Cannot use --admin-password with --admin-password-file")
)
// ParseFlags parse the CLI flags and return a portainer.Flags struct
@ -33,32 +29,28 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
kingpin.Version(version)
flags := &portainer.CLIFlags{
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
TunnelAddr: kingpin.Flag("tunnel-addr", "Address to serve the tunnel server").Default(defaultTunnelServerAddress).String(),
TunnelPort: kingpin.Flag("tunnel-port", "Port to serve the tunnel server").Default(defaultTunnelServerPort).String(),
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
EndpointURL: kingpin.Flag("host", "Endpoint URL").Short('H').String(),
ExternalEndpoints: kingpin.Flag("external-endpoints", "Path to a file defining available endpoints (deprecated)").String(),
NoAuth: kingpin.Flag("no-auth", "Disable authentication (deprecated)").Default(defaultNoAuth).Bool(),
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAnalytics).Bool(),
TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(),
TLSSkipVerify: kingpin.Flag("tlsskipverify", "Disable TLS server verification").Default(defaultTLSSkipVerify).Bool(),
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(),
TLSKey: kingpin.Flag("tlskey", "Path to the TLS key").Default(defaultTLSKeyPath).String(),
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL").Default(defaultSSL).Bool(),
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").Default(defaultSSLCertPath).String(),
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").Default(defaultSSLKeyPath).String(),
SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source (deprecated)").Default(defaultSyncInterval).String(),
Snapshot: kingpin.Flag("snapshot", "Start a background job to create endpoint snapshots (deprecated)").Default(defaultSnapshot).Bool(),
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each endpoint snapshot job").Default(defaultSnapshotInterval).String(),
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
Templates: kingpin.Flag("templates", "URL to the templates definitions.").Short('t').String(),
TemplateFile: kingpin.Flag("template-file", "Path to the App templates definitions on the filesystem (deprecated)").Default(defaultTemplateFile).String(),
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
TunnelAddr: kingpin.Flag("tunnel-addr", "Address to serve the tunnel server").Default(defaultTunnelServerAddress).String(),
TunnelPort: kingpin.Flag("tunnel-port", "Port to serve the tunnel server").Default(defaultTunnelServerPort).String(),
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
EndpointURL: kingpin.Flag("host", "Endpoint URL").Short('H').String(),
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app (deprecated)").Bool(),
TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(),
TLSSkipVerify: kingpin.Flag("tlsskipverify", "Disable TLS server verification").Default(defaultTLSSkipVerify).Bool(),
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(),
TLSKey: kingpin.Flag("tlskey", "Path to the TLS key").Default(defaultTLSKeyPath).String(),
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL").Default(defaultSSL).Bool(),
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").Default(defaultSSLCertPath).String(),
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").Default(defaultSSLKeyPath).String(),
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each endpoint snapshot job").Default(defaultSnapshotInterval).String(),
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
Templates: kingpin.Flag("templates", "URL to the templates definitions.").Short('t').String(),
}
kingpin.Parse()
@ -79,26 +71,7 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
displayDeprecationWarnings(flags)
if *flags.EndpointURL != "" && *flags.ExternalEndpoints != "" {
return errEndpointExcludeExternal
}
err := validateTemplateFile(*flags.TemplateFile)
if err != nil {
return err
}
err = validateEndpointURL(*flags.EndpointURL)
if err != nil {
return err
}
err = validateExternalEndpoints(*flags.ExternalEndpoints)
if err != nil {
return err
}
err = validateSyncInterval(*flags.SyncInterval)
err := validateEndpointURL(*flags.EndpointURL)
if err != nil {
return err
}
@ -108,10 +81,6 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
return err
}
if *flags.NoAuth && (*flags.AdminPassword != "" || *flags.AdminPasswordFile != "") {
return errNoAuthExcludeAdminPassword
}
if *flags.AdminPassword != "" && *flags.AdminPasswordFile != "" {
return errAdminPassExcludeAdminPassFile
}
@ -120,24 +89,8 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
}
func displayDeprecationWarnings(flags *portainer.CLIFlags) {
if *flags.ExternalEndpoints != "" {
log.Println("Warning: the --external-endpoint flag is deprecated and will likely be removed in a future version of Portainer.")
}
if *flags.SyncInterval != defaultSyncInterval {
log.Println("Warning: the --sync-interval flag is deprecated and will likely be removed in a future version of Portainer.")
}
if *flags.NoAuth {
log.Println("Warning: the --no-auth flag is deprecated and will likely be removed in a future version of Portainer.")
}
if !*flags.Snapshot {
log.Println("Warning: the --no-snapshot flag is deprecated and will likely be removed in a future version of Portainer.")
}
if *flags.TemplateFile != "" {
log.Println("Warning: the --template-file flag is deprecated and will likely be removed in a future version of Portainer.")
if *flags.NoAnalytics {
log.Println("Warning: The --no-analytics flag has been kept to allow migration of instances running a previous version of Portainer with this flag enabled, to version 2.0 where enabling this flag will have no effect.")
}
}
@ -161,38 +114,6 @@ func validateEndpointURL(endpointURL string) error {
return nil
}
func validateExternalEndpoints(externalEndpoints string) error {
if externalEndpoints != "" {
if _, err := os.Stat(externalEndpoints); err != nil {
if os.IsNotExist(err) {
return errEndpointsFileNotFound
}
return err
}
}
return nil
}
func validateTemplateFile(templateFile string) error {
if _, err := os.Stat(templateFile); err != nil {
if os.IsNotExist(err) {
return errTemplateFileNotFound
}
return err
}
return nil
}
func validateSyncInterval(syncInterval string) error {
if syncInterval != defaultSyncInterval {
_, err := time.ParseDuration(syncInterval)
if err != nil {
return errInvalidSyncInterval
}
}
return nil
}
func validateSnapshotInterval(snapshotInterval string) error {
if snapshotInterval != defaultSnapshotInterval {
_, err := time.ParseDuration(snapshotInterval)

View File

@ -8,8 +8,6 @@ const (
defaultTunnelServerPort = "8000"
defaultDataDirectory = "/data"
defaultAssetsDirectory = "./"
defaultNoAuth = "false"
defaultNoAnalytics = "false"
defaultTLS = "false"
defaultTLSSkipVerify = "false"
defaultTLSCACertPath = "/certs/ca.pem"
@ -18,8 +16,5 @@ const (
defaultSSL = "false"
defaultSSLCertPath = "/certs/portainer.crt"
defaultSSLKeyPath = "/certs/portainer.key"
defaultSyncInterval = "60s"
defaultSnapshot = "true"
defaultSnapshotInterval = "5m"
defaultTemplateFile = "/templates.json"
)

View File

@ -6,8 +6,6 @@ const (
defaultTunnelServerPort = "8000"
defaultDataDirectory = "C:\\data"
defaultAssetsDirectory = "./"
defaultNoAuth = "false"
defaultNoAnalytics = "false"
defaultTLS = "false"
defaultTLSSkipVerify = "false"
defaultTLSCACertPath = "C:\\certs\\ca.pem"
@ -16,8 +14,5 @@ const (
defaultSSL = "false"
defaultSSLCertPath = "C:\\certs\\portainer.crt"
defaultSSLKeyPath = "C:\\certs\\portainer.key"
defaultSyncInterval = "60s"
defaultSnapshot = "true"
defaultSnapshotInterval = "5m"
defaultTemplateFile = "/templates.json"
)

View File

@ -1,18 +1,15 @@
package main
import (
"encoding/json"
"log"
"os"
"strings"
"time"
"github.com/portainer/portainer/api/chisel"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt"
"github.com/portainer/portainer/api/chisel"
"github.com/portainer/portainer/api/cli"
"github.com/portainer/portainer/api/cron"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/exec"
@ -20,19 +17,23 @@ import (
"github.com/portainer/portainer/api/git"
"github.com/portainer/portainer/api/http"
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/portainer/api/internal/snapshot"
"github.com/portainer/portainer/api/jwt"
"github.com/portainer/portainer/api/kubernetes"
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/oauth"
)
func initCLI() *portainer.CLIFlags {
var cli portainer.CLIService = &cli.Service{}
flags, err := cli.ParseFlags(portainer.APIVersion)
var cliService portainer.CLIService = &cli.Service{}
flags, err := cliService.ParseFlags(portainer.APIVersion)
if err != nil {
log.Fatal(err)
}
err = cli.ValidateFlags(flags)
err = cliService.ValidateFlags(flags)
if err != nil {
log.Fatal(err)
}
@ -47,7 +48,7 @@ func initFileService(dataStorePath string) portainer.FileService {
return fileService
}
func initStore(dataStorePath string, fileService portainer.FileService) *bolt.Store {
func initDataStore(dataStorePath string, fileService portainer.FileService) portainer.DataStore {
store, err := bolt.NewStore(dataStorePath, fileService)
if err != nil {
log.Fatal(err)
@ -78,15 +79,21 @@ func initSwarmStackManager(assetsPath string, dataStorePath string, signatureSer
return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService, reverseTunnelService)
}
func initJWTService(authenticationEnabled bool) portainer.JWTService {
if authenticationEnabled {
jwtService, err := jwt.NewService()
if err != nil {
log.Fatal(err)
}
return jwtService
func initKubernetesDeployer(assetsPath string) portainer.KubernetesDeployer {
return exec.NewKubernetesDeployer(assetsPath)
}
func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error) {
settings, err := dataStore.Settings().Settings()
if err != nil {
return nil, err
}
return nil
jwtService, err := jwt.NewService(settings.UserSessionTimeout)
if err != nil {
return nil, err
}
return jwtService, nil
}
func initDigitalSignatureService() portainer.DigitalSignatureService {
@ -101,246 +108,75 @@ func initLDAPService() portainer.LDAPService {
return &ldap.Service{}
}
func initOAuthService() portainer.OAuthService {
return oauth.NewService()
}
func initGitService() portainer.GitService {
return git.NewService()
}
func initClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *docker.ClientFactory {
func initDockerClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *docker.ClientFactory {
return docker.NewClientFactory(signatureService, reverseTunnelService)
}
func initSnapshotter(clientFactory *docker.ClientFactory) portainer.Snapshotter {
return docker.NewSnapshotter(clientFactory)
func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, instanceID string) *kubecli.ClientFactory {
return kubecli.NewClientFactory(signatureService, reverseTunnelService, instanceID)
}
func initJobScheduler() portainer.JobScheduler {
return cron.NewJobScheduler()
func initSnapshotService(snapshotInterval string, dataStore portainer.DataStore, dockerClientFactory *docker.ClientFactory, kubernetesClientFactory *kubecli.ClientFactory) (portainer.SnapshotService, error) {
dockerSnapshotter := docker.NewSnapshotter(dockerClientFactory)
kubernetesSnapshotter := kubernetes.NewSnapshotter(kubernetesClientFactory)
snapshotService, err := snapshot.NewService(snapshotInterval, dataStore, dockerSnapshotter, kubernetesSnapshotter)
if err != nil {
return nil, err
}
return snapshotService, nil
}
func loadSnapshotSystemSchedule(jobScheduler portainer.JobScheduler, snapshotter portainer.Snapshotter, scheduleService portainer.ScheduleService, endpointService portainer.EndpointService, settingsService portainer.SettingsService) error {
settings, err := settingsService.Settings()
func loadEdgeJobsFromDatabase(dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService) error {
edgeJobs, err := dataStore.EdgeJob().EdgeJobs()
if err != nil {
return err
}
schedules, err := scheduleService.SchedulesByJobType(portainer.SnapshotJobType)
if err != nil {
return err
}
var snapshotSchedule *portainer.Schedule
if len(schedules) == 0 {
snapshotJob := &portainer.SnapshotJob{}
snapshotSchedule = &portainer.Schedule{
ID: portainer.ScheduleID(scheduleService.GetNextIdentifier()),
Name: "system_snapshot",
CronExpression: "@every " + settings.SnapshotInterval,
Recurring: true,
JobType: portainer.SnapshotJobType,
SnapshotJob: snapshotJob,
Created: time.Now().Unix(),
for _, edgeJob := range edgeJobs {
for endpointID := range edgeJob.Endpoints {
reverseTunnelService.AddEdgeJob(endpointID, &edgeJob)
}
} else {
snapshotSchedule = &schedules[0]
}
snapshotJobContext := cron.NewSnapshotJobContext(endpointService, snapshotter)
snapshotJobRunner := cron.NewSnapshotJobRunner(snapshotSchedule, snapshotJobContext)
err = jobScheduler.ScheduleJob(snapshotJobRunner)
if err != nil {
return err
}
if len(schedules) == 0 {
return scheduleService.CreateSchedule(snapshotSchedule)
}
return nil
}
func loadEndpointSyncSystemSchedule(jobScheduler portainer.JobScheduler, scheduleService portainer.ScheduleService, endpointService portainer.EndpointService, flags *portainer.CLIFlags) error {
if *flags.ExternalEndpoints == "" {
return nil
}
log.Println("Using external endpoint definition. Endpoint management via the API will be disabled.")
schedules, err := scheduleService.SchedulesByJobType(portainer.EndpointSyncJobType)
if err != nil {
return err
}
if len(schedules) != 0 {
return nil
}
endpointSyncJob := &portainer.EndpointSyncJob{}
endpointSyncSchedule := &portainer.Schedule{
ID: portainer.ScheduleID(scheduleService.GetNextIdentifier()),
Name: "system_endpointsync",
CronExpression: "@every " + *flags.SyncInterval,
Recurring: true,
JobType: portainer.EndpointSyncJobType,
EndpointSyncJob: endpointSyncJob,
Created: time.Now().Unix(),
}
endpointSyncJobContext := cron.NewEndpointSyncJobContext(endpointService, *flags.ExternalEndpoints)
endpointSyncJobRunner := cron.NewEndpointSyncJobRunner(endpointSyncSchedule, endpointSyncJobContext)
err = jobScheduler.ScheduleJob(endpointSyncJobRunner)
if err != nil {
return err
}
return scheduleService.CreateSchedule(endpointSyncSchedule)
}
func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService portainer.JobService, scheduleService portainer.ScheduleService, endpointService portainer.EndpointService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) error {
schedules, err := scheduleService.Schedules()
if err != nil {
return err
}
for _, schedule := range schedules {
if schedule.JobType == portainer.ScriptExecutionJobType {
jobContext := cron.NewScriptExecutionJobContext(jobService, endpointService, fileService)
jobRunner := cron.NewScriptExecutionJobRunner(&schedule, jobContext)
err = jobScheduler.ScheduleJob(jobRunner)
if err != nil {
return err
}
}
if schedule.EdgeSchedule != nil {
for _, endpointID := range schedule.EdgeSchedule.Endpoints {
reverseTunnelService.AddSchedule(endpointID, schedule.EdgeSchedule)
}
}
}
return nil
}
func initStatus(endpointManagement, snapshot bool, flags *portainer.CLIFlags) *portainer.Status {
func initStatus(flags *portainer.CLIFlags) *portainer.Status {
return &portainer.Status{
Analytics: !*flags.NoAnalytics,
Authentication: !*flags.NoAuth,
EndpointManagement: endpointManagement,
Snapshot: snapshot,
Version: portainer.APIVersion,
Version: portainer.APIVersion,
}
}
func initDockerHub(dockerHubService portainer.DockerHubService) error {
_, err := dockerHubService.DockerHub()
if err == portainer.ErrObjectNotFound {
dockerhub := &portainer.DockerHub{
Authentication: false,
Username: "",
Password: "",
}
return dockerHubService.UpdateDockerHub(dockerhub)
} else if err != nil {
return err
}
return nil
}
func initSettings(settingsService portainer.SettingsService, flags *portainer.CLIFlags) error {
_, err := settingsService.Settings()
if err == portainer.ErrObjectNotFound {
settings := &portainer.Settings{
LogoURL: *flags.Logo,
AuthenticationMethod: portainer.AuthenticationInternal,
LDAPSettings: portainer.LDAPSettings{
AnonymousMode: true,
AutoCreateUsers: true,
TLSConfig: portainer.TLSConfiguration{},
SearchSettings: []portainer.LDAPSearchSettings{
portainer.LDAPSearchSettings{},
},
GroupSearchSettings: []portainer.LDAPGroupSearchSettings{
portainer.LDAPGroupSearchSettings{},
},
},
OAuthSettings: portainer.OAuthSettings{},
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowVolumeBrowserForRegularUsers: false,
EnableHostManagementFeatures: false,
SnapshotInterval: *flags.SnapshotInterval,
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
}
if *flags.Templates != "" {
settings.TemplatesURL = *flags.Templates
}
if *flags.Labels != nil {
settings.BlackListedLabels = *flags.Labels
} else {
settings.BlackListedLabels = make([]portainer.Pair, 0)
}
return settingsService.UpdateSettings(settings)
} else if err != nil {
return err
}
return nil
}
func initTemplates(templateService portainer.TemplateService, fileService portainer.FileService, templateURL, templateFile string) error {
if templateURL != "" {
log.Printf("Portainer started with the --templates flag. Using external templates, template management will be disabled.")
return nil
}
existingTemplates, err := templateService.Templates()
func updateSettingsFromFlags(dataStore portainer.DataStore, flags *portainer.CLIFlags) error {
settings, err := dataStore.Settings().Settings()
if err != nil {
return err
}
if len(existingTemplates) != 0 {
log.Printf("Templates already registered inside the database. Skipping template import.")
return nil
settings.LogoURL = *flags.Logo
settings.SnapshotInterval = *flags.SnapshotInterval
settings.EnableEdgeComputeFeatures = *flags.EnableEdgeComputeFeatures
settings.EnableTelemetry = true
if *flags.Templates != "" {
settings.TemplatesURL = *flags.Templates
}
templatesJSON, err := fileService.GetFileContent(templateFile)
if err != nil {
log.Println("Unable to retrieve template definitions via filesystem")
return err
if *flags.Labels != nil {
settings.BlackListedLabels = *flags.Labels
}
var templates []portainer.Template
err = json.Unmarshal(templatesJSON, &templates)
if err != nil {
log.Println("Unable to parse templates file. Please review your template definition file.")
return err
}
for _, template := range templates {
err := templateService.CreateTemplate(&template)
if err != nil {
return err
}
}
return nil
}
func retrieveFirstEndpointFromDatabase(endpointService portainer.EndpointService) *portainer.Endpoint {
endpoints, err := endpointService.Endpoints()
if err != nil {
log.Fatal(err)
}
return &endpoints[0]
return dataStore.Settings().UpdateSettings(settings)
}
func loadAndParseKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
@ -372,7 +208,7 @@ func initKeyPair(fileService portainer.FileService, signatureService portainer.D
return generateAndStoreKeyPair(fileService, signatureService)
}
func createTLSSecuredEndpoint(flags *portainer.CLIFlags, endpointService portainer.EndpointService, snapshotter portainer.Snapshotter) error {
func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore portainer.DataStore, snapshotService portainer.SnapshotService) error {
tlsConfiguration := portainer.TLSConfiguration{
TLS: *flags.TLS,
TLSSkipVerify: *flags.TLSSkipVerify,
@ -386,7 +222,7 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, endpointService portain
tlsConfiguration.TLS = true
}
endpointID := endpointService.GetNextIdentifier()
endpointID := dataStore.Endpoint().GetNextIdentifier()
endpoint := &portainer.Endpoint{
ID: portainer.EndpointID(endpointID),
Name: "primary",
@ -399,7 +235,8 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, endpointService portain
Extensions: []portainer.EndpointExtension{},
TagIDs: []portainer.TagID{},
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
Snapshots: []portainer.DockerSnapshot{},
Kubernetes: portainer.KubernetesDefault(),
}
if strings.HasPrefix(endpoint.URL, "tcp://") {
@ -418,10 +255,15 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, endpointService portain
}
}
return snapshotAndPersistEndpoint(endpoint, endpointService, snapshotter)
err := snapshotService.SnapshotEndpoint(endpoint)
if err != nil {
log.Printf("http error: endpoint snapshot error (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
}
return dataStore.Endpoint().CreateEndpoint(endpoint)
}
func createUnsecuredEndpoint(endpointURL string, endpointService portainer.EndpointService, snapshotter portainer.Snapshotter) error {
func createUnsecuredEndpoint(endpointURL string, dataStore portainer.DataStore, snapshotService portainer.SnapshotService) error {
if strings.HasPrefix(endpointURL, "tcp://") {
_, err := client.ExecutePingOperation(endpointURL, nil)
if err != nil {
@ -429,7 +271,7 @@ func createUnsecuredEndpoint(endpointURL string, endpointService portainer.Endpo
}
}
endpointID := endpointService.GetNextIdentifier()
endpointID := dataStore.Endpoint().GetNextIdentifier()
endpoint := &portainer.Endpoint{
ID: portainer.EndpointID(endpointID),
Name: "primary",
@ -442,32 +284,24 @@ func createUnsecuredEndpoint(endpointURL string, endpointService portainer.Endpo
Extensions: []portainer.EndpointExtension{},
TagIDs: []portainer.TagID{},
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
Snapshots: []portainer.DockerSnapshot{},
Kubernetes: portainer.KubernetesDefault(),
}
return snapshotAndPersistEndpoint(endpoint, endpointService, snapshotter)
}
func snapshotAndPersistEndpoint(endpoint *portainer.Endpoint, endpointService portainer.EndpointService, snapshotter portainer.Snapshotter) error {
snapshot, err := snapshotter.CreateSnapshot(endpoint)
endpoint.Status = portainer.EndpointStatusUp
err := snapshotService.SnapshotEndpoint(endpoint)
if err != nil {
log.Printf("http error: endpoint snapshot error (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
}
if snapshot != nil {
endpoint.Snapshots = []portainer.Snapshot{*snapshot}
}
return endpointService.CreateEndpoint(endpoint)
return dataStore.Endpoint().CreateEndpoint(endpoint)
}
func initEndpoint(flags *portainer.CLIFlags, endpointService portainer.EndpointService, snapshotter portainer.Snapshotter) error {
func initEndpoint(flags *portainer.CLIFlags, dataStore portainer.DataStore, snapshotService portainer.SnapshotService) error {
if *flags.EndpointURL == "" {
return nil
}
endpoints, err := endpointService.Endpoints()
endpoints, err := dataStore.Endpoint().Endpoints()
if err != nil {
return err
}
@ -478,31 +312,16 @@ func initEndpoint(flags *portainer.CLIFlags, endpointService portainer.EndpointS
}
if *flags.TLS || *flags.TLSSkipVerify {
return createTLSSecuredEndpoint(flags, endpointService, snapshotter)
return createTLSSecuredEndpoint(flags, dataStore, snapshotService)
}
return createUnsecuredEndpoint(*flags.EndpointURL, endpointService, snapshotter)
return createUnsecuredEndpoint(*flags.EndpointURL, dataStore, snapshotService)
}
func initJobService(dockerClientFactory *docker.ClientFactory) portainer.JobService {
return docker.NewJobService(dockerClientFactory)
}
func initExtensionManager(fileService portainer.FileService, extensionService portainer.ExtensionService) (portainer.ExtensionManager, error) {
extensionManager := exec.NewExtensionManager(fileService, extensionService)
err := extensionManager.StartExtensions()
if err != nil {
return nil, err
}
return extensionManager, nil
}
func terminateIfNoAdminCreated(userService portainer.UserService) {
func terminateIfNoAdminCreated(dataStore portainer.DataStore) {
timer1 := time.NewTimer(5 * time.Minute)
<-timer1.C
users, err := userService.UsersByRole(portainer.AdministratorRole)
users, err := dataStore.User().UsersByRole(portainer.AdministratorRole)
if err != nil {
log.Fatal(err)
}
@ -518,41 +337,44 @@ func main() {
fileService := initFileService(*flags.Data)
store := initStore(*flags.Data, fileService)
defer store.Close()
dataStore := initDataStore(*flags.Data, fileService)
defer dataStore.Close()
jwtService := initJWTService(!*flags.NoAuth)
jwtService, err := initJWTService(dataStore)
if err != nil {
log.Fatal(err)
}
ldapService := initLDAPService()
oauthService := initOAuthService()
gitService := initGitService()
cryptoService := initCryptoService()
digitalSignatureService := initDigitalSignatureService()
err := initKeyPair(fileService, digitalSignatureService)
err = initKeyPair(fileService, digitalSignatureService)
if err != nil {
log.Fatal(err)
}
extensionManager, err := initExtensionManager(fileService, store.ExtensionService)
reverseTunnelService := chisel.NewService(dataStore)
instanceID, err := dataStore.Version().InstanceID()
if err != nil {
log.Fatal(err)
}
reverseTunnelService := chisel.NewService(store.EndpointService, store.TunnelServerService)
dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
kubernetesClientFactory := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, instanceID)
clientFactory := initClientFactory(digitalSignatureService, reverseTunnelService)
jobService := initJobService(clientFactory)
snapshotter := initSnapshotter(clientFactory)
endpointManagement := true
if *flags.ExternalEndpoints != "" {
endpointManagement = false
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory)
if err != nil {
log.Fatal(err)
}
snapshotService.Start()
swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService, reverseTunnelService)
if err != nil {
@ -561,45 +383,23 @@ func main() {
composeStackManager := initComposeStackManager(*flags.Data, reverseTunnelService)
err = initTemplates(store.TemplateService, fileService, *flags.Templates, *flags.TemplateFile)
if err != nil {
log.Fatal(err)
}
kubernetesDeployer := initKubernetesDeployer(*flags.Assets)
err = initSettings(store.SettingsService, flags)
if err != nil {
log.Fatal(err)
}
jobScheduler := initJobScheduler()
err = loadSchedulesFromDatabase(jobScheduler, jobService, store.ScheduleService, store.EndpointService, fileService, reverseTunnelService)
if err != nil {
log.Fatal(err)
}
err = loadEndpointSyncSystemSchedule(jobScheduler, store.ScheduleService, store.EndpointService, flags)
if err != nil {
log.Fatal(err)
}
if *flags.Snapshot {
err = loadSnapshotSystemSchedule(jobScheduler, snapshotter, store.ScheduleService, store.EndpointService, store.SettingsService)
if dataStore.IsNew() {
err = updateSettingsFromFlags(dataStore, flags)
if err != nil {
log.Fatal(err)
}
}
jobScheduler.Start()
err = initDockerHub(store.DockerHubService)
err = loadEdgeJobsFromDatabase(dataStore, reverseTunnelService)
if err != nil {
log.Fatal(err)
}
applicationStatus := initStatus(endpointManagement, *flags.Snapshot, flags)
applicationStatus := initStatus(flags)
err = initEndpoint(flags, store.EndpointService, snapshotter)
err = initEndpoint(flags, dataStore, snapshotService)
if err != nil {
log.Fatal(err)
}
@ -619,7 +419,7 @@ func main() {
}
if adminPasswordHash != "" {
users, err := store.UserService.UsersByRole(portainer.AdministratorRole)
users, err := dataStore.User().UsersByRole(portainer.AdministratorRole)
if err != nil {
log.Fatal(err)
}
@ -627,12 +427,11 @@ func main() {
if len(users) == 0 {
log.Println("Created admin user with the given password.")
user := &portainer.User{
Username: "admin",
Role: portainer.AdministratorRole,
Password: adminPasswordHash,
PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(),
Username: "admin",
Role: portainer.AdministratorRole,
Password: adminPasswordHash,
}
err := store.UserService.CreateUser(user)
err := dataStore.User().CreateUser(user)
if err != nil {
log.Fatal(err)
}
@ -641,11 +440,9 @@ func main() {
}
}
if !*flags.NoAuth {
go terminateIfNoAdminCreated(store.UserService)
}
go terminateIfNoAdminCreated(dataStore)
err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotter)
err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService)
if err != nil {
log.Fatal(err)
}
@ -655,43 +452,23 @@ func main() {
Status: applicationStatus,
BindAddress: *flags.Addr,
AssetsPath: *flags.Assets,
AuthDisabled: *flags.NoAuth,
EndpointManagement: endpointManagement,
RoleService: store.RoleService,
UserService: store.UserService,
TeamService: store.TeamService,
TeamMembershipService: store.TeamMembershipService,
EdgeGroupService: store.EdgeGroupService,
EdgeStackService: store.EdgeStackService,
EndpointService: store.EndpointService,
EndpointGroupService: store.EndpointGroupService,
EndpointRelationService: store.EndpointRelationService,
ExtensionService: store.ExtensionService,
ResourceControlService: store.ResourceControlService,
SettingsService: store.SettingsService,
RegistryService: store.RegistryService,
DockerHubService: store.DockerHubService,
StackService: store.StackService,
ScheduleService: store.ScheduleService,
TagService: store.TagService,
TemplateService: store.TemplateService,
WebhookService: store.WebhookService,
DataStore: dataStore,
SwarmStackManager: swarmStackManager,
ComposeStackManager: composeStackManager,
ExtensionManager: extensionManager,
KubernetesDeployer: kubernetesDeployer,
CryptoService: cryptoService,
JWTService: jwtService,
FileService: fileService,
LDAPService: ldapService,
OAuthService: oauthService,
GitService: gitService,
SignatureService: digitalSignatureService,
JobScheduler: jobScheduler,
Snapshotter: snapshotter,
SnapshotService: snapshotService,
SSL: *flags.SSL,
SSLCert: *flags.SSLCert,
SSLKey: *flags.SSLKey,
DockerClientFactory: clientFactory,
JobService: jobService,
DockerClientFactory: dockerClientFactory,
KubernetesClientFactory: kubernetesClientFactory,
}
log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr)

View File

@ -1,214 +0,0 @@
package cron
import (
"encoding/json"
"io/ioutil"
"log"
"strings"
"github.com/portainer/portainer/api"
)
// EndpointSyncJobRunner is used to run a EndpointSyncJob
type EndpointSyncJobRunner struct {
schedule *portainer.Schedule
context *EndpointSyncJobContext
}
// EndpointSyncJobContext represents the context of execution of a EndpointSyncJob
type EndpointSyncJobContext struct {
endpointService portainer.EndpointService
endpointFilePath string
}
// NewEndpointSyncJobContext returns a new context that can be used to execute a EndpointSyncJob
func NewEndpointSyncJobContext(endpointService portainer.EndpointService, endpointFilePath string) *EndpointSyncJobContext {
return &EndpointSyncJobContext{
endpointService: endpointService,
endpointFilePath: endpointFilePath,
}
}
// NewEndpointSyncJobRunner returns a new runner that can be scheduled
func NewEndpointSyncJobRunner(schedule *portainer.Schedule, context *EndpointSyncJobContext) *EndpointSyncJobRunner {
return &EndpointSyncJobRunner{
schedule: schedule,
context: context,
}
}
type synchronization struct {
endpointsToCreate []*portainer.Endpoint
endpointsToUpdate []*portainer.Endpoint
endpointsToDelete []*portainer.Endpoint
}
type fileEndpoint struct {
Name string `json:"Name"`
URL string `json:"URL"`
TLS bool `json:"TLS,omitempty"`
TLSSkipVerify bool `json:"TLSSkipVerify,omitempty"`
TLSCACert string `json:"TLSCACert,omitempty"`
TLSCert string `json:"TLSCert,omitempty"`
TLSKey string `json:"TLSKey,omitempty"`
}
// GetSchedule returns the schedule associated to the runner
func (runner *EndpointSyncJobRunner) GetSchedule() *portainer.Schedule {
return runner.schedule
}
// Run triggers the execution of the endpoint synchronization process.
func (runner *EndpointSyncJobRunner) Run() {
data, err := ioutil.ReadFile(runner.context.endpointFilePath)
if endpointSyncError(err) {
return
}
var fileEndpoints []fileEndpoint
err = json.Unmarshal(data, &fileEndpoints)
if endpointSyncError(err) {
return
}
if len(fileEndpoints) == 0 {
log.Println("background job error (endpoint synchronization). External endpoint source is empty")
return
}
storedEndpoints, err := runner.context.endpointService.Endpoints()
if endpointSyncError(err) {
return
}
convertedFileEndpoints := convertFileEndpoints(fileEndpoints)
sync := prepareSyncData(storedEndpoints, convertedFileEndpoints)
if sync.requireSync() {
err = runner.context.endpointService.Synchronize(sync.endpointsToCreate, sync.endpointsToUpdate, sync.endpointsToDelete)
if endpointSyncError(err) {
return
}
log.Printf("Endpoint synchronization ended. [created: %v] [updated: %v] [deleted: %v]", len(sync.endpointsToCreate), len(sync.endpointsToUpdate), len(sync.endpointsToDelete))
}
}
func endpointSyncError(err error) bool {
if err != nil {
log.Printf("background job error (endpoint synchronization). Unable to synchronize endpoints (err=%s)\n", err)
return true
}
return false
}
func isValidEndpoint(endpoint *portainer.Endpoint) bool {
if endpoint.Name != "" && endpoint.URL != "" {
if !strings.HasPrefix(endpoint.URL, "unix://") && !strings.HasPrefix(endpoint.URL, "tcp://") {
return false
}
return true
}
return false
}
func convertFileEndpoints(fileEndpoints []fileEndpoint) []portainer.Endpoint {
convertedEndpoints := make([]portainer.Endpoint, 0)
for _, e := range fileEndpoints {
endpoint := portainer.Endpoint{
Name: e.Name,
URL: e.URL,
TLSConfig: portainer.TLSConfiguration{},
}
if e.TLS {
endpoint.TLSConfig.TLS = true
endpoint.TLSConfig.TLSSkipVerify = e.TLSSkipVerify
endpoint.TLSConfig.TLSCACertPath = e.TLSCACert
endpoint.TLSConfig.TLSCertPath = e.TLSCert
endpoint.TLSConfig.TLSKeyPath = e.TLSKey
}
convertedEndpoints = append(convertedEndpoints, endpoint)
}
return convertedEndpoints
}
func endpointExists(endpoint *portainer.Endpoint, endpoints []portainer.Endpoint) int {
for idx, v := range endpoints {
if endpoint.Name == v.Name && isValidEndpoint(&v) {
return idx
}
}
return -1
}
func mergeEndpointIfRequired(original, updated *portainer.Endpoint) *portainer.Endpoint {
var endpoint *portainer.Endpoint
if original.URL != updated.URL || original.TLSConfig.TLS != updated.TLSConfig.TLS ||
(updated.TLSConfig.TLS && original.TLSConfig.TLSSkipVerify != updated.TLSConfig.TLSSkipVerify) ||
(updated.TLSConfig.TLS && original.TLSConfig.TLSCACertPath != updated.TLSConfig.TLSCACertPath) ||
(updated.TLSConfig.TLS && original.TLSConfig.TLSCertPath != updated.TLSConfig.TLSCertPath) ||
(updated.TLSConfig.TLS && original.TLSConfig.TLSKeyPath != updated.TLSConfig.TLSKeyPath) {
endpoint = original
endpoint.URL = updated.URL
if updated.TLSConfig.TLS {
endpoint.TLSConfig.TLS = true
endpoint.TLSConfig.TLSSkipVerify = updated.TLSConfig.TLSSkipVerify
endpoint.TLSConfig.TLSCACertPath = updated.TLSConfig.TLSCACertPath
endpoint.TLSConfig.TLSCertPath = updated.TLSConfig.TLSCertPath
endpoint.TLSConfig.TLSKeyPath = updated.TLSConfig.TLSKeyPath
} else {
endpoint.TLSConfig.TLS = false
endpoint.TLSConfig.TLSSkipVerify = false
endpoint.TLSConfig.TLSCACertPath = ""
endpoint.TLSConfig.TLSCertPath = ""
endpoint.TLSConfig.TLSKeyPath = ""
}
}
return endpoint
}
func (sync synchronization) requireSync() bool {
if len(sync.endpointsToCreate) != 0 || len(sync.endpointsToUpdate) != 0 || len(sync.endpointsToDelete) != 0 {
return true
}
return false
}
func prepareSyncData(storedEndpoints, fileEndpoints []portainer.Endpoint) *synchronization {
endpointsToCreate := make([]*portainer.Endpoint, 0)
endpointsToUpdate := make([]*portainer.Endpoint, 0)
endpointsToDelete := make([]*portainer.Endpoint, 0)
for idx := range storedEndpoints {
fidx := endpointExists(&storedEndpoints[idx], fileEndpoints)
if fidx != -1 {
endpoint := mergeEndpointIfRequired(&storedEndpoints[idx], &fileEndpoints[fidx])
if endpoint != nil {
log.Printf("New definition for a stored endpoint found in file, updating database. [name: %v] [url: %v]\n", endpoint.Name, endpoint.URL)
endpointsToUpdate = append(endpointsToUpdate, endpoint)
}
} else {
log.Printf("Stored endpoint not found in file (definition might be invalid), removing from database. [name: %v] [url: %v]", storedEndpoints[idx].Name, storedEndpoints[idx].URL)
endpointsToDelete = append(endpointsToDelete, &storedEndpoints[idx])
}
}
for idx, endpoint := range fileEndpoints {
if !isValidEndpoint(&endpoint) {
log.Printf("Invalid file endpoint definition, skipping. [name: %v] [url: %v]", endpoint.Name, endpoint.URL)
continue
}
sidx := endpointExists(&fileEndpoints[idx], storedEndpoints)
if sidx == -1 {
log.Printf("File endpoint not found in database, adding to database. [name: %v] [url: %v]", fileEndpoints[idx].Name, fileEndpoints[idx].URL)
endpointsToCreate = append(endpointsToCreate, &fileEndpoints[idx])
}
}
return &synchronization{
endpointsToCreate: endpointsToCreate,
endpointsToUpdate: endpointsToUpdate,
endpointsToDelete: endpointsToDelete,
}
}

View File

@ -1,96 +0,0 @@
package cron
import (
"log"
"time"
"github.com/portainer/portainer/api"
)
// ScriptExecutionJobRunner is used to run a ScriptExecutionJob
type ScriptExecutionJobRunner struct {
schedule *portainer.Schedule
context *ScriptExecutionJobContext
executedOnce bool
}
// ScriptExecutionJobContext represents the context of execution of a ScriptExecutionJob
type ScriptExecutionJobContext struct {
jobService portainer.JobService
endpointService portainer.EndpointService
fileService portainer.FileService
}
// NewScriptExecutionJobContext returns a new context that can be used to execute a ScriptExecutionJob
func NewScriptExecutionJobContext(jobService portainer.JobService, endpointService portainer.EndpointService, fileService portainer.FileService) *ScriptExecutionJobContext {
return &ScriptExecutionJobContext{
jobService: jobService,
endpointService: endpointService,
fileService: fileService,
}
}
// NewScriptExecutionJobRunner returns a new runner that can be scheduled
func NewScriptExecutionJobRunner(schedule *portainer.Schedule, context *ScriptExecutionJobContext) *ScriptExecutionJobRunner {
return &ScriptExecutionJobRunner{
schedule: schedule,
context: context,
executedOnce: false,
}
}
// Run triggers the execution of the job.
// It will iterate through all the endpoints specified in the context to
// execute the script associated to the job.
func (runner *ScriptExecutionJobRunner) Run() {
if !runner.schedule.Recurring && runner.executedOnce {
return
}
runner.executedOnce = true
scriptFile, err := runner.context.fileService.GetFileContent(runner.schedule.ScriptExecutionJob.ScriptPath)
if err != nil {
log.Printf("scheduled job error (script execution). Unable to retrieve script file (err=%s)\n", err)
return
}
targets := make([]*portainer.Endpoint, 0)
for _, endpointID := range runner.schedule.ScriptExecutionJob.Endpoints {
endpoint, err := runner.context.endpointService.Endpoint(endpointID)
if err != nil {
log.Printf("scheduled job error (script execution). Unable to retrieve information about endpoint (id=%d) (err=%s)\n", endpointID, err)
return
}
targets = append(targets, endpoint)
}
runner.executeAndRetry(targets, scriptFile, 0)
}
func (runner *ScriptExecutionJobRunner) executeAndRetry(endpoints []*portainer.Endpoint, script []byte, retryCount int) {
retryTargets := make([]*portainer.Endpoint, 0)
for _, endpoint := range endpoints {
err := runner.context.jobService.ExecuteScript(endpoint, "", runner.schedule.ScriptExecutionJob.Image, script, runner.schedule)
if err == portainer.ErrUnableToPingEndpoint {
retryTargets = append(retryTargets, endpoint)
} else if err != nil {
log.Printf("scheduled job error (script execution). Unable to execute script (endpoint=%s) (err=%s)\n", endpoint.Name, err)
}
}
retryCount++
if retryCount >= runner.schedule.ScriptExecutionJob.RetryCount {
return
}
time.Sleep(time.Duration(runner.schedule.ScriptExecutionJob.RetryInterval) * time.Second)
runner.executeAndRetry(retryTargets, script, retryCount)
}
// GetSchedule returns the schedule associated to the runner
func (runner *ScriptExecutionJobRunner) GetSchedule() *portainer.Schedule {
return runner.schedule
}

View File

@ -1,85 +0,0 @@
package cron
import (
"log"
"github.com/portainer/portainer/api"
)
// SnapshotJobRunner is used to run a SnapshotJob
type SnapshotJobRunner struct {
schedule *portainer.Schedule
context *SnapshotJobContext
}
// SnapshotJobContext represents the context of execution of a SnapshotJob
type SnapshotJobContext struct {
endpointService portainer.EndpointService
snapshotter portainer.Snapshotter
}
// NewSnapshotJobContext returns a new context that can be used to execute a SnapshotJob
func NewSnapshotJobContext(endpointService portainer.EndpointService, snapshotter portainer.Snapshotter) *SnapshotJobContext {
return &SnapshotJobContext{
endpointService: endpointService,
snapshotter: snapshotter,
}
}
// NewSnapshotJobRunner returns a new runner that can be scheduled
func NewSnapshotJobRunner(schedule *portainer.Schedule, context *SnapshotJobContext) *SnapshotJobRunner {
return &SnapshotJobRunner{
schedule: schedule,
context: context,
}
}
// GetSchedule returns the schedule associated to the runner
func (runner *SnapshotJobRunner) GetSchedule() *portainer.Schedule {
return runner.schedule
}
// Run triggers the execution of the schedule.
// It will iterate through all the endpoints available in the database to
// create a snapshot of each one of them.
// As a snapshot can be a long process, to avoid any concurrency issue we
// retrieve the latest version of the endpoint right after a snapshot.
func (runner *SnapshotJobRunner) Run() {
go func() {
endpoints, err := runner.context.endpointService.Endpoints()
if err != nil {
log.Printf("background schedule error (endpoint snapshot). Unable to retrieve endpoint list (err=%s)\n", err)
return
}
for _, endpoint := range endpoints {
if endpoint.Type == portainer.AzureEnvironment || endpoint.Type == portainer.EdgeAgentEnvironment {
continue
}
snapshot, snapshotError := runner.context.snapshotter.CreateSnapshot(&endpoint)
latestEndpointReference, err := runner.context.endpointService.Endpoint(endpoint.ID)
if latestEndpointReference == nil {
log.Printf("background schedule error (endpoint snapshot). Endpoint not found inside the database anymore (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
continue
}
latestEndpointReference.Status = portainer.EndpointStatusUp
if snapshotError != nil {
log.Printf("background schedule error (endpoint snapshot). Unable to create snapshot (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, snapshotError)
latestEndpointReference.Status = portainer.EndpointStatusDown
}
if snapshot != nil {
latestEndpointReference.Snapshots = []portainer.Snapshot{*snapshot}
}
err = runner.context.endpointService.UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference)
if err != nil {
log.Printf("background schedule error (endpoint snapshot). Unable to update endpoint (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
return
}
}
}()
}

View File

@ -1,116 +0,0 @@
package cron
import (
"github.com/portainer/portainer/api"
"github.com/robfig/cron/v3"
)
// JobScheduler represents a service for managing crons
type JobScheduler struct {
cron *cron.Cron
}
// NewJobScheduler initializes a new service
func NewJobScheduler() *JobScheduler {
return &JobScheduler{
cron: cron.New(),
}
}
// ScheduleJob schedules the execution of a job via a runner
func (scheduler *JobScheduler) ScheduleJob(runner portainer.JobRunner) error {
_, err := scheduler.cron.AddJob(runner.GetSchedule().CronExpression, runner)
return err
}
// UpdateSystemJobSchedule updates the first occurence of the specified
// scheduled job based on the specified job type.
// It does so by re-creating a new cron
// and adding all the existing jobs. It will then re-schedule the new job
// with the update cron expression passed in parameter.
// NOTE: the cron library do not support updating schedules directly
// hence the work-around
func (scheduler *JobScheduler) UpdateSystemJobSchedule(jobType portainer.JobType, newCronExpression string) error {
cronEntries := scheduler.cron.Entries()
newCron := cron.New()
for _, entry := range cronEntries {
if entry.Job.(portainer.JobRunner).GetSchedule().JobType == jobType {
_, err := newCron.AddJob(newCronExpression, entry.Job)
if err != nil {
return err
}
continue
}
newCron.Schedule(entry.Schedule, entry.Job)
}
scheduler.cron.Stop()
scheduler.cron = newCron
scheduler.cron.Start()
return nil
}
// UpdateJobSchedule updates a specific scheduled job by re-creating a new cron
// and adding all the existing jobs. It will then re-schedule the new job
// via the specified JobRunner parameter.
// NOTE: the cron library do not support updating schedules directly
// hence the work-around
func (scheduler *JobScheduler) UpdateJobSchedule(runner portainer.JobRunner) error {
cronEntries := scheduler.cron.Entries()
newCron := cron.New()
for _, entry := range cronEntries {
if entry.Job.(portainer.JobRunner).GetSchedule().ID == runner.GetSchedule().ID {
var jobRunner cron.Job = runner
if entry.Job.(portainer.JobRunner).GetSchedule().JobType == portainer.SnapshotJobType {
jobRunner = entry.Job
}
_, err := newCron.AddJob(runner.GetSchedule().CronExpression, jobRunner)
if err != nil {
return err
}
continue
}
newCron.Schedule(entry.Schedule, entry.Job)
}
scheduler.cron.Stop()
scheduler.cron = newCron
scheduler.cron.Start()
return nil
}
// UnscheduleJob remove a scheduled job by re-creating a new cron
// and adding all the existing jobs except for the one specified via scheduleID.
// NOTE: the cron library do not support removing schedules directly
// hence the work-around
func (scheduler *JobScheduler) UnscheduleJob(scheduleID portainer.ScheduleID) {
cronEntries := scheduler.cron.Entries()
newCron := cron.New()
for _, entry := range cronEntries {
if entry.Job.(portainer.JobRunner).GetSchedule().ID == scheduleID {
continue
}
newCron.Schedule(entry.Schedule, entry.Job)
}
scheduler.cron.Stop()
scheduler.cron = newCron
scheduler.cron.Start()
}
// Start starts the scheduled jobs
func (scheduler *JobScheduler) Start() {
if len(scheduler.cron.Entries()) > 0 {
scheduler.cron.Start()
}
}

View File

@ -6,6 +6,24 @@ import (
"io/ioutil"
)
// CreateServerTLSConfiguration creates a basic tls.Config to be used by servers with recommended TLS settings
func CreateServerTLSConfiguration() *tls.Config {
return &tls.Config{
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
tls.TLS_AES_128_GCM_SHA256,
tls.TLS_AES_256_GCM_SHA384,
tls.TLS_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
},
}
}
// CreateTLSConfigurationFromBytes initializes a tls.Config using a CA certificate, a certificate and a key
// loaded from memory.
func CreateTLSConfigurationFromBytes(caCert, cert, key []byte, skipClientVerification, skipServerVerification bool) (*tls.Config, error) {

View File

@ -1,6 +1,7 @@
package docker
import (
"errors"
"fmt"
"net/http"
"strings"
@ -11,8 +12,9 @@ import (
"github.com/portainer/portainer/api/crypto"
)
var errUnsupportedEnvironmentType = errors.New("Environment not supported")
const (
unsupportedEnvironmentType = portainer.Error("Environment not supported")
defaultDockerRequestTimeout = 60
dockerClientVersion = "1.37"
)
@ -31,15 +33,15 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService, revers
}
}
// CreateClient is a generic function to create a Docker client based on
// createClient is a generic function to create a Docker client based on
// a specific endpoint configuration. The nodeName parameter can be used
// with an agent enabled endpoint to target a specific node in an agent cluster.
func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeName string) (*client.Client, error) {
if endpoint.Type == portainer.AzureEnvironment {
return nil, unsupportedEnvironmentType
return nil, errUnsupportedEnvironmentType
} else if endpoint.Type == portainer.AgentOnDockerEnvironment {
return createAgentClient(endpoint, factory.signatureService, nodeName)
} else if endpoint.Type == portainer.EdgeAgentEnvironment {
} else if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
return createEdgeClient(endpoint, factory.reverseTunnelService, nodeName)
}

8
api/docker/errors.go Normal file
View File

@ -0,0 +1,8 @@
package docker
import "errors"
// Docker errors
var (
ErrUnableToPingEndpoint = errors.New("Unable to communicate with the endpoint")
)

View File

@ -1,115 +0,0 @@
package docker
import (
"bytes"
"context"
"io"
"io/ioutil"
"strconv"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/strslice"
"github.com/docker/docker/client"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/archive"
)
// JobService represents a service that handles the execution of jobs
type JobService struct {
dockerClientFactory *ClientFactory
}
// NewJobService returns a pointer to a new job service
func NewJobService(dockerClientFactory *ClientFactory) *JobService {
return &JobService{
dockerClientFactory: dockerClientFactory,
}
}
// ExecuteScript will leverage a privileged container to execute a script against the specified endpoint/nodename.
// It will copy the script content specified as a parameter inside a container based on the specified image and execute it.
func (service *JobService) ExecuteScript(endpoint *portainer.Endpoint, nodeName, image string, script []byte, schedule *portainer.Schedule) error {
buffer, err := archive.TarFileInBuffer(script, "script.sh", 0700)
if err != nil {
return err
}
cli, err := service.dockerClientFactory.CreateClient(endpoint, nodeName)
if err != nil {
return err
}
defer cli.Close()
_, err = cli.Ping(context.Background())
if err != nil {
return portainer.ErrUnableToPingEndpoint
}
err = pullImage(cli, image)
if err != nil {
return err
}
containerConfig := &container.Config{
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
Tty: true,
WorkingDir: "/tmp",
Image: image,
Labels: map[string]string{
"io.portainer.job.endpoint": strconv.Itoa(int(endpoint.ID)),
},
Cmd: strslice.StrSlice([]string{"sh", "/tmp/script.sh"}),
}
if schedule != nil {
containerConfig.Labels["io.portainer.schedule.id"] = strconv.Itoa(int(schedule.ID))
}
hostConfig := &container.HostConfig{
Binds: []string{"/:/host", "/etc:/etc:ro", "/usr:/usr:ro", "/run:/run:ro", "/sbin:/sbin:ro", "/var:/var:ro"},
NetworkMode: "host",
Privileged: true,
}
networkConfig := &network.NetworkingConfig{}
body, err := cli.ContainerCreate(context.Background(), containerConfig, hostConfig, networkConfig, "")
if err != nil {
return err
}
if schedule != nil {
err = cli.ContainerRename(context.Background(), body.ID, schedule.Name+"_"+body.ID)
if err != nil {
return err
}
}
copyOptions := types.CopyToContainerOptions{}
err = cli.CopyToContainer(context.Background(), body.ID, "/tmp", bytes.NewReader(buffer), copyOptions)
if err != nil {
return err
}
startOptions := types.ContainerStartOptions{}
return cli.ContainerStart(context.Background(), body.ID, startOptions)
}
func pullImage(cli *client.Client, image string) error {
imageReadCloser, err := cli.ImagePull(context.Background(), image, types.ImagePullOptions{})
if err != nil {
return err
}
defer imageReadCloser.Close()
_, err = io.Copy(ioutil.Discard, imageReadCloser)
if err != nil {
return err
}
return nil
}

View File

@ -12,13 +12,36 @@ import (
"github.com/portainer/portainer/api"
)
func snapshot(cli *client.Client, endpoint *portainer.Endpoint) (*portainer.Snapshot, error) {
// Snapshotter represents a service used to create endpoint snapshots
type Snapshotter struct {
clientFactory *ClientFactory
}
// NewSnapshotter returns a new Snapshotter instance
func NewSnapshotter(clientFactory *ClientFactory) *Snapshotter {
return &Snapshotter{
clientFactory: clientFactory,
}
}
// CreateSnapshot creates a snapshot of a specific Docker endpoint
func (snapshotter *Snapshotter) CreateSnapshot(endpoint *portainer.Endpoint) (*portainer.DockerSnapshot, error) {
cli, err := snapshotter.clientFactory.CreateClient(endpoint, "")
if err != nil {
return nil, err
}
defer cli.Close()
return snapshot(cli, endpoint)
}
func snapshot(cli *client.Client, endpoint *portainer.Endpoint) (*portainer.DockerSnapshot, error) {
_, err := cli.Ping(context.Background())
if err != nil {
return nil, err
}
snapshot := &portainer.Snapshot{
snapshot := &portainer.DockerSnapshot{
StackCount: 0,
}
@ -68,7 +91,7 @@ func snapshot(cli *client.Client, endpoint *portainer.Endpoint) (*portainer.Snap
return snapshot, nil
}
func snapshotInfo(snapshot *portainer.Snapshot, cli *client.Client) error {
func snapshotInfo(snapshot *portainer.DockerSnapshot, cli *client.Client) error {
info, err := cli.Info(context.Background())
if err != nil {
return err
@ -82,7 +105,7 @@ func snapshotInfo(snapshot *portainer.Snapshot, cli *client.Client) error {
return nil
}
func snapshotNodes(snapshot *portainer.Snapshot, cli *client.Client) error {
func snapshotNodes(snapshot *portainer.DockerSnapshot, cli *client.Client) error {
nodes, err := cli.NodeList(context.Background(), types.NodeListOptions{})
if err != nil {
return err
@ -98,7 +121,7 @@ func snapshotNodes(snapshot *portainer.Snapshot, cli *client.Client) error {
return nil
}
func snapshotSwarmServices(snapshot *portainer.Snapshot, cli *client.Client) error {
func snapshotSwarmServices(snapshot *portainer.DockerSnapshot, cli *client.Client) error {
stacks := make(map[string]struct{})
services, err := cli.ServiceList(context.Background(), types.ServiceListOptions{})
@ -119,7 +142,7 @@ func snapshotSwarmServices(snapshot *portainer.Snapshot, cli *client.Client) err
return nil
}
func snapshotContainers(snapshot *portainer.Snapshot, cli *client.Client) error {
func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client) error {
containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{All: true})
if err != nil {
return err
@ -159,7 +182,7 @@ func snapshotContainers(snapshot *portainer.Snapshot, cli *client.Client) error
return nil
}
func snapshotImages(snapshot *portainer.Snapshot, cli *client.Client) error {
func snapshotImages(snapshot *portainer.DockerSnapshot, cli *client.Client) error {
images, err := cli.ImageList(context.Background(), types.ImageListOptions{})
if err != nil {
return err
@ -170,7 +193,7 @@ func snapshotImages(snapshot *portainer.Snapshot, cli *client.Client) error {
return nil
}
func snapshotVolumes(snapshot *portainer.Snapshot, cli *client.Client) error {
func snapshotVolumes(snapshot *portainer.DockerSnapshot, cli *client.Client) error {
volumes, err := cli.VolumeList(context.Background(), filters.Args{})
if err != nil {
return err
@ -181,7 +204,7 @@ func snapshotVolumes(snapshot *portainer.Snapshot, cli *client.Client) error {
return nil
}
func snapshotNetworks(snapshot *portainer.Snapshot, cli *client.Client) error {
func snapshotNetworks(snapshot *portainer.DockerSnapshot, cli *client.Client) error {
networks, err := cli.NetworkList(context.Background(), types.NetworkListOptions{})
if err != nil {
return err
@ -190,7 +213,7 @@ func snapshotNetworks(snapshot *portainer.Snapshot, cli *client.Client) error {
return nil
}
func snapshotVersion(snapshot *portainer.Snapshot, cli *client.Client) error {
func snapshotVersion(snapshot *portainer.DockerSnapshot, cli *client.Client) error {
version, err := cli.ServerVersion(context.Background())
if err != nil {
return err

View File

@ -1,28 +0,0 @@
package docker
import (
"github.com/portainer/portainer/api"
)
// Snapshotter represents a service used to create endpoint snapshots
type Snapshotter struct {
clientFactory *ClientFactory
}
// NewSnapshotter returns a new Snapshotter instance
func NewSnapshotter(clientFactory *ClientFactory) *Snapshotter {
return &Snapshotter{
clientFactory: clientFactory,
}
}
// CreateSnapshot creates a snapshot of a specific endpoint
func (snapshotter *Snapshotter) CreateSnapshot(endpoint *portainer.Endpoint) (*portainer.Snapshot, error) {
cli, err := snapshotter.clientFactory.CreateClient(endpoint, "")
if err != nil {
return nil, err
}
defer cli.Close()
return snapshot(cli, endpoint)
}

View File

@ -1,54 +0,0 @@
package portainer
// EdgeGroupRelatedEndpoints returns a list of endpoints related to this Edge group
func EdgeGroupRelatedEndpoints(edgeGroup *EdgeGroup, endpoints []Endpoint, endpointGroups []EndpointGroup) []EndpointID {
if !edgeGroup.Dynamic {
return edgeGroup.Endpoints
}
endpointIDs := []EndpointID{}
for _, endpoint := range endpoints {
if endpoint.Type != EdgeAgentEnvironment {
continue
}
var endpointGroup EndpointGroup
for _, group := range endpointGroups {
if endpoint.GroupID == group.ID {
endpointGroup = group
break
}
}
if edgeGroupRelatedToEndpoint(edgeGroup, &endpoint, &endpointGroup) {
endpointIDs = append(endpointIDs, endpoint.ID)
}
}
return endpointIDs
}
// edgeGroupRelatedToEndpoint returns true is edgeGroup is associated with endpoint
func edgeGroupRelatedToEndpoint(edgeGroup *EdgeGroup, endpoint *Endpoint, endpointGroup *EndpointGroup) bool {
if !edgeGroup.Dynamic {
for _, endpointID := range edgeGroup.Endpoints {
if endpoint.ID == endpointID {
return true
}
}
return false
}
endpointTags := TagSet(endpoint.TagIDs)
if endpointGroup.TagIDs != nil {
endpointTags = TagUnion(endpointTags, TagSet(endpointGroup.TagIDs))
}
edgeGroupTags := TagSet(edgeGroup.TagIDs)
if edgeGroup.PartialMatch {
intersection := TagIntersection(endpointTags, edgeGroupTags)
return len(intersection) != 0
}
return TagContains(edgeGroupTags, endpointTags)
}

View File

@ -1,117 +0,0 @@
package portainer
// General errors.
const (
ErrUnauthorized = Error("Unauthorized")
ErrResourceAccessDenied = Error("Access denied to resource")
ErrAuthorizationRequired = Error("Authorization required for this operation")
ErrObjectNotFound = Error("Object not found inside the database")
ErrMissingSecurityContext = Error("Unable to find security details in request context")
)
// User errors.
const (
ErrUserAlreadyExists = Error("User already exists")
ErrInvalidUsername = Error("Invalid username. White spaces are not allowed")
ErrAdminAlreadyInitialized = Error("An administrator user already exists")
ErrAdminCannotRemoveSelf = Error("Cannot remove your own user account. Contact another administrator")
ErrCannotRemoveLastLocalAdmin = Error("Cannot remove the last local administrator account")
)
// Team errors.
const (
ErrTeamAlreadyExists = Error("Team already exists")
)
// TeamMembership errors.
const (
ErrTeamMembershipAlreadyExists = Error("Team membership already exists for this user and team")
)
// ResourceControl errors.
const (
ErrResourceControlAlreadyExists = Error("A resource control is already applied on this resource")
ErrInvalidResourceControlType = Error("Unsupported resource control type")
)
// Endpoint errors.
const (
ErrEndpointAccessDenied = Error("Access denied to endpoint")
)
// Azure environment errors
const (
ErrAzureInvalidCredentials = Error("Invalid Azure credentials")
)
// Endpoint group errors.
const (
ErrCannotRemoveDefaultGroup = Error("Cannot remove the default endpoint group")
)
// Registry errors.
const (
ErrRegistryAlreadyExists = Error("A registry is already defined for this URL")
)
// Stack errors
const (
ErrStackAlreadyExists = Error("A stack already exists with this name")
ErrComposeFileNotFoundInRepository = Error("Unable to find a Compose file in the repository")
ErrStackNotExternal = Error("Not an external stack")
)
// Tag errors
const (
ErrTagAlreadyExists = Error("A tag already exists with this name")
)
// Endpoint extensions error
const (
ErrEndpointExtensionNotSupported = Error("This extension is not supported")
ErrEndpointExtensionAlreadyAssociated = Error("This extension is already associated to the endpoint")
)
// Crypto errors.
const (
ErrCryptoHashFailure = Error("Unable to hash data")
)
// JWT errors.
const (
ErrSecretGeneration = Error("Unable to generate secret key")
ErrInvalidJWTToken = Error("Invalid JWT token")
ErrMissingContextData = Error("Unable to find JWT data in request context")
)
// File errors.
const (
ErrUndefinedTLSFileType = Error("Undefined TLS file type")
)
// Extension errors.
const (
ErrExtensionAlreadyEnabled = Error("This extension is already enabled")
)
// Docker errors.
const (
ErrUnableToPingEndpoint = Error("Unable to communicate with the endpoint")
)
// Schedule errors.
const (
ErrHostManagementFeaturesDisabled = Error("Host management features are disabled")
)
// Error represents an application error.
type Error string
// Error returns the error message.
func (e Error) Error() string { return string(e) }
// Webhook errors
const (
ErrWebhookAlreadyExists = Error("A webhook for this resource already exists")
ErrUnsupportedWebhookType = Error("Webhooks for this resource are not currently supported")
)

View File

@ -1,313 +0,0 @@
package exec
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"log"
"os"
"os/exec"
"path"
"regexp"
"runtime"
"strconv"
"strings"
"time"
"github.com/coreos/go-semver/semver"
"github.com/orcaman/concurrent-map"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/client"
)
var extensionDownloadBaseURL = portainer.AssetsServerURL + "/extensions/"
var extensionVersionRegexp = regexp.MustCompile(`\d+(\.\d+)+`)
var extensionBinaryMap = map[portainer.ExtensionID]string{
portainer.RegistryManagementExtension: "extension-registry-management",
portainer.OAuthAuthenticationExtension: "extension-oauth-authentication",
portainer.RBACExtension: "extension-rbac",
}
// ExtensionManager represents a service used to
// manage extension processes.
type ExtensionManager struct {
processes cmap.ConcurrentMap
fileService portainer.FileService
extensionService portainer.ExtensionService
}
// NewExtensionManager returns a pointer to an ExtensionManager
func NewExtensionManager(fileService portainer.FileService, extensionService portainer.ExtensionService) *ExtensionManager {
return &ExtensionManager{
processes: cmap.New(),
fileService: fileService,
extensionService: extensionService,
}
}
func processKey(ID portainer.ExtensionID) string {
return strconv.Itoa(int(ID))
}
func buildExtensionURL(extension *portainer.Extension) string {
return fmt.Sprintf("%s%s-%s-%s-%s.zip", extensionDownloadBaseURL, extensionBinaryMap[extension.ID], runtime.GOOS, runtime.GOARCH, extension.Version)
}
func buildExtensionPath(binaryPath string, extension *portainer.Extension) string {
extensionFilename := fmt.Sprintf("%s-%s-%s-%s", extensionBinaryMap[extension.ID], runtime.GOOS, runtime.GOARCH, extension.Version)
if runtime.GOOS == "windows" {
extensionFilename += ".exe"
}
extensionPath := path.Join(
binaryPath,
extensionFilename)
return extensionPath
}
// FetchExtensionDefinitions will fetch the list of available
// extension definitions from the official Portainer assets server.
// If it cannot retrieve the data from the Internet it will fallback to the locally cached
// manifest file.
func (manager *ExtensionManager) FetchExtensionDefinitions() ([]portainer.Extension, error) {
var extensionData []byte
extensionData, err := client.Get(portainer.ExtensionDefinitionsURL, 5)
if err != nil {
log.Printf("[WARN] [exec,extensions] [message: unable to retrieve extensions manifest via Internet. Extensions will be retrieved from local cache and might not be up to date] [err: %s]", err)
extensionData, err = manager.fileService.GetFileContent(portainer.LocalExtensionManifestFile)
if err != nil {
return nil, err
}
}
var extensions []portainer.Extension
err = json.Unmarshal(extensionData, &extensions)
if err != nil {
return nil, err
}
return extensions, nil
}
// InstallExtension will install the extension from an archive. It will extract the extension version number from
// the archive file name first and return an error if the file name is not valid (cannot find extension version).
// It will then extract the archive and execute the EnableExtension function to enable the extension.
// Since we're missing information about this extension (stored on Portainer.io server) we need to assume
// default information based on the extension ID.
func (manager *ExtensionManager) InstallExtension(extension *portainer.Extension, licenseKey string, archiveFileName string, extensionArchive []byte) error {
extensionVersion := extensionVersionRegexp.FindString(archiveFileName)
if extensionVersion == "" {
return errors.New("invalid extension archive filename: unable to retrieve extension version")
}
err := manager.fileService.ExtractExtensionArchive(extensionArchive)
if err != nil {
return err
}
switch extension.ID {
case portainer.RegistryManagementExtension:
extension.Name = "Registry Manager"
case portainer.OAuthAuthenticationExtension:
extension.Name = "External Authentication"
case portainer.RBACExtension:
extension.Name = "Role-Based Access Control"
}
extension.ShortDescription = "Extension enabled offline"
extension.Version = extensionVersion
extension.Available = true
return manager.EnableExtension(extension, licenseKey)
}
// EnableExtension will check for the existence of the extension binary on the filesystem
// first. If it does not exist, it will download it from the official Portainer assets server.
// After installing the binary on the filesystem, it will execute the binary in license check
// mode to validate the extension license. If the license is valid, it will then start
// the extension process and register it in the processes map.
func (manager *ExtensionManager) EnableExtension(extension *portainer.Extension, licenseKey string) error {
extensionBinaryPath := buildExtensionPath(manager.fileService.GetBinaryFolder(), extension)
extensionBinaryExists, err := manager.fileService.FileExists(extensionBinaryPath)
if err != nil {
return err
}
if !extensionBinaryExists {
err := manager.downloadExtension(extension)
if err != nil {
return err
}
}
licenseDetails, err := validateLicense(extensionBinaryPath, licenseKey)
if err != nil {
return err
}
extension.License = portainer.LicenseInformation{
LicenseKey: licenseKey,
Company: licenseDetails[0],
Expiration: licenseDetails[1],
Valid: true,
}
extension.Version = licenseDetails[2]
return manager.startExtensionProcess(extension, extensionBinaryPath)
}
// DisableExtension will retrieve the process associated to the extension
// from the processes map and kill the process. It will then remove the process
// from the processes map and remove the binary associated to the extension
// from the filesystem
func (manager *ExtensionManager) DisableExtension(extension *portainer.Extension) error {
process, ok := manager.processes.Get(processKey(extension.ID))
if !ok {
return nil
}
err := process.(*exec.Cmd).Process.Kill()
if err != nil {
return err
}
manager.processes.Remove(processKey(extension.ID))
extensionBinaryPath := buildExtensionPath(manager.fileService.GetBinaryFolder(), extension)
return manager.fileService.RemoveDirectory(extensionBinaryPath)
}
// StartExtensions will retrieve the extensions definitions from the Internet and check if a new version of each
// extension is available. If so, it will automatically install the new version of the extension. If no update is
// available it will simply start the extension.
// The purpose of this function is to be ran at startup, as such most of the error handling won't block the program execution
// and will log warning messages instead.
func (manager *ExtensionManager) StartExtensions() error {
extensions, err := manager.extensionService.Extensions()
if err != nil {
return err
}
definitions, err := manager.FetchExtensionDefinitions()
if err != nil {
log.Printf("[WARN] [exec,extensions] [message: unable to retrieve extension information from Internet. Skipping extensions update check.] [err: %s]", err)
return nil
}
return manager.updateAndStartExtensions(extensions, definitions)
}
func (manager *ExtensionManager) updateAndStartExtensions(extensions []portainer.Extension, definitions []portainer.Extension) error {
for _, definition := range definitions {
for _, extension := range extensions {
if extension.ID == definition.ID {
definitionVersion := semver.New(definition.Version)
extensionVersion := semver.New(extension.Version)
if extensionVersion.LessThan(*definitionVersion) {
log.Printf("[INFO] [exec,extensions] [message: new version detected, updating extension] [extension: %s] [current_version: %s] [available_version: %s]", extension.Name, extension.Version, definition.Version)
err := manager.UpdateExtension(&extension, definition.Version)
if err != nil {
log.Printf("[WARN] [exec,extensions] [message: unable to update extension automatically] [extension: %s] [current_version: %s] [available_version: %s] [err: %s]", extension.Name, extension.Version, definition.Version, err)
}
} else {
err := manager.EnableExtension(&extension, extension.License.LicenseKey)
if err != nil {
log.Printf("[WARN] [exec,extensions] [message: unable to start extension] [extension: %s] [err: %s]", extension.Name, err)
extension.Enabled = false
extension.License.Valid = false
}
}
err := manager.extensionService.Persist(&extension)
if err != nil {
return err
}
break
}
}
}
return nil
}
// UpdateExtension will download the new extension binary from the official Portainer assets
// server, disable the previous extension via DisableExtension, trigger a license check
// and then start the extension process and add it to the processes map
func (manager *ExtensionManager) UpdateExtension(extension *portainer.Extension, version string) error {
oldVersion := extension.Version
extension.Version = version
err := manager.downloadExtension(extension)
if err != nil {
return err
}
extension.Version = oldVersion
err = manager.DisableExtension(extension)
if err != nil {
return err
}
extension.Version = version
extensionBinaryPath := buildExtensionPath(manager.fileService.GetBinaryFolder(), extension)
licenseDetails, err := validateLicense(extensionBinaryPath, extension.License.LicenseKey)
if err != nil {
return err
}
extension.Version = licenseDetails[2]
return manager.startExtensionProcess(extension, extensionBinaryPath)
}
func (manager *ExtensionManager) downloadExtension(extension *portainer.Extension) error {
extensionURL := buildExtensionURL(extension)
data, err := client.Get(extensionURL, 30)
if err != nil {
return err
}
return manager.fileService.ExtractExtensionArchive(data)
}
func validateLicense(binaryPath, licenseKey string) ([]string, error) {
licenseCheckProcess := exec.Command(binaryPath, "-license", licenseKey, "-check")
cmdOutput := &bytes.Buffer{}
licenseCheckProcess.Stdout = cmdOutput
err := licenseCheckProcess.Run()
if err != nil {
log.Printf("[DEBUG] [exec,extension] [message: unable to run extension process] [err: %s]", err)
return nil, errors.New("invalid extension license key")
}
output := string(cmdOutput.Bytes())
return strings.Split(output, "|"), nil
}
func (manager *ExtensionManager) startExtensionProcess(extension *portainer.Extension, binaryPath string) error {
extensionProcess := exec.Command(binaryPath, "-license", extension.License.LicenseKey)
extensionProcess.Stdout = os.Stdout
extensionProcess.Stderr = os.Stderr
err := extensionProcess.Start()
if err != nil {
log.Printf("[DEBUG] [exec,extension] [message: unable to start extension process] [err: %s]", err)
return err
}
time.Sleep(3 * time.Second)
manager.processes.Set(processKey(extension.ID), extensionProcess)
return nil
}

View File

@ -0,0 +1,89 @@
package exec
import (
"bytes"
"errors"
"io/ioutil"
"os/exec"
"path"
"runtime"
"strings"
portainer "github.com/portainer/portainer/api"
)
// KubernetesDeployer represents a service to deploy resources inside a Kubernetes environment.
type KubernetesDeployer struct {
binaryPath string
}
// NewKubernetesDeployer initializes a new KubernetesDeployer service.
func NewKubernetesDeployer(binaryPath string) *KubernetesDeployer {
return &KubernetesDeployer{
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, data string, composeFormat bool, namespace string) ([]byte, error) {
if composeFormat {
convertedData, err := deployer.convertComposeData(data)
if err != nil {
return nil, err
}
data = string(convertedData)
}
token, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
if err != nil {
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(data)
output, err := cmd.Output()
if err != nil {
return nil, errors.New(stderr.String())
}
return output, nil
}
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")
}
args := make([]string, 0)
args = append(args, "convert", "-f", "-", "--stdout")
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 nil, errors.New(stderr.String())
}
return output, nil
}

View File

@ -3,6 +3,7 @@ package exec
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
@ -103,7 +104,7 @@ func runCommandAndCaptureStdErr(command string, args []string, env []string, wor
err := cmd.Run()
if err != nil {
return portainer.Error(stderr.String())
return errors.New(stderr.String())
}
return nil
@ -121,7 +122,7 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, dataPa
args = append(args, "--config", dataPath)
endpointURL := endpoint.URL
if endpoint.Type == portainer.EdgeAgentEnvironment {
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID)
endpointURL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnel.Port)
}

View File

@ -4,10 +4,12 @@ import (
"bytes"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"github.com/gofrs/uuid"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/archive"
"io"
"os"
@ -37,13 +39,20 @@ const (
PublicKeyFile = "portainer.pub"
// BinaryStorePath represents the subfolder where binaries are stored in the file store folder.
BinaryStorePath = "bin"
// ScheduleStorePath represents the subfolder where schedule files are stored.
ScheduleStorePath = "schedules"
// EdgeJobStorePath represents the subfolder where schedule files are stored.
EdgeJobStorePath = "edge_jobs"
// ExtensionRegistryManagementStorePath represents the subfolder where files related to the
// registry management extension are stored.
ExtensionRegistryManagementStorePath = "extensions"
// CustomTemplateStorePath represents the subfolder where custom template files are stored in the file store folder.
CustomTemplateStorePath = "custom_templates"
// TempPath represent the subfolder where temporary files are saved
TempPath = "tmp"
)
// ErrUndefinedTLSFileType represents an error returned on undefined TLS file type
var ErrUndefinedTLSFileType = errors.New("Undefined TLS file type")
// Service represents a service for managing files and directories.
type Service struct {
dataStorePath string
@ -86,12 +95,6 @@ func (service *Service) GetBinaryFolder() string {
return path.Join(service.fileStorePath, BinaryStorePath)
}
// ExtractExtensionArchive extracts the content of an extension archive
// specified as raw data into the binary store on the filesystem
func (service *Service) ExtractExtensionArchive(data []byte) error {
return archive.UnzipArchive(data, path.Join(service.fileStorePath, BinaryStorePath))
}
// RemoveDirectory removes a directory on the filesystem.
func (service *Service) RemoveDirectory(directoryPath string) error {
return os.RemoveAll(directoryPath)
@ -188,7 +191,7 @@ func (service *Service) StoreTLSFileFromBytes(folder string, fileType portainer.
case portainer.TLSFileKey:
fileName = TLSKeyFile
default:
return "", portainer.ErrUndefinedTLSFileType
return "", ErrUndefinedTLSFileType
}
tlsFilePath := path.Join(storePath, fileName)
@ -211,7 +214,7 @@ func (service *Service) GetPathForTLSFile(folder string, fileType portainer.TLSF
case portainer.TLSFileKey:
fileName = TLSKeyFile
default:
return "", portainer.ErrUndefinedTLSFileType
return "", ErrUndefinedTLSFileType
}
return path.Join(service.fileStorePath, TLSStorePath, folder, fileName), nil
}
@ -237,7 +240,7 @@ func (service *Service) DeleteTLSFile(folder string, fileType portainer.TLSFileT
case portainer.TLSFileKey:
fileName = TLSKeyFile
default:
return portainer.ErrUndefinedTLSFileType
return ErrUndefinedTLSFileType
}
filePath := path.Join(service.fileStorePath, TLSStorePath, folder, fileName)
@ -392,22 +395,48 @@ func (service *Service) getContentFromPEMFile(filePath string) ([]byte, error) {
return block.Bytes, nil
}
// GetScheduleFolder returns the absolute path on the filesystem for a schedule based
// GetCustomTemplateProjectPath returns the absolute path on the FS for a custom template based
// on its identifier.
func (service *Service) GetScheduleFolder(identifier string) string {
return path.Join(service.fileStorePath, ScheduleStorePath, identifier)
func (service *Service) GetCustomTemplateProjectPath(identifier string) string {
return path.Join(service.fileStorePath, CustomTemplateStorePath, identifier)
}
// StoreScheduledJobFileFromBytes creates a subfolder in the ScheduleStorePath and stores a new file from bytes.
// StoreCustomTemplateFileFromBytes creates a subfolder in the CustomTemplateStorePath and stores a new file from bytes.
// It returns the path to the folder where the file is stored.
func (service *Service) StoreScheduledJobFileFromBytes(identifier string, data []byte) (string, error) {
scheduleStorePath := path.Join(ScheduleStorePath, identifier)
err := service.createDirectoryInStore(scheduleStorePath)
func (service *Service) StoreCustomTemplateFileFromBytes(identifier, fileName string, data []byte) (string, error) {
customTemplateStorePath := path.Join(CustomTemplateStorePath, identifier)
err := service.createDirectoryInStore(customTemplateStorePath)
if err != nil {
return "", err
}
filePath := path.Join(scheduleStorePath, createScheduledJobFileName(identifier))
templateFilePath := path.Join(customTemplateStorePath, fileName)
r := bytes.NewReader(data)
err = service.createFileInStore(templateFilePath, r)
if err != nil {
return "", err
}
return path.Join(service.fileStorePath, customTemplateStorePath), nil
}
// GetEdgeJobFolder returns the absolute path on the filesystem for an Edge job based
// on its identifier.
func (service *Service) GetEdgeJobFolder(identifier string) string {
return path.Join(service.fileStorePath, EdgeJobStorePath, identifier)
}
// StoreEdgeJobFileFromBytes creates a subfolder in the EdgeJobStorePath and stores a new file from bytes.
// It returns the path to the folder where the file is stored.
func (service *Service) StoreEdgeJobFileFromBytes(identifier string, data []byte) (string, error) {
edgeJobStorePath := path.Join(EdgeJobStorePath, identifier)
err := service.createDirectoryInStore(edgeJobStorePath)
if err != nil {
return "", err
}
filePath := path.Join(edgeJobStorePath, createEdgeJobFileName(identifier))
r := bytes.NewReader(data)
err = service.createFileInStore(filePath, r)
if err != nil {
@ -417,6 +446,62 @@ func (service *Service) StoreScheduledJobFileFromBytes(identifier string, data [
return path.Join(service.fileStorePath, filePath), nil
}
func createScheduledJobFileName(identifier string) string {
func createEdgeJobFileName(identifier string) string {
return "job_" + identifier + ".sh"
}
// ClearEdgeJobTaskLogs clears the Edge job task logs
func (service *Service) ClearEdgeJobTaskLogs(edgeJobID string, taskID string) error {
path := service.getEdgeJobTaskLogPath(edgeJobID, taskID)
err := os.Remove(path)
if err != nil {
return err
}
return nil
}
// GetEdgeJobTaskLogFileContent fetches the Edge job task logs
func (service *Service) GetEdgeJobTaskLogFileContent(edgeJobID string, taskID string) (string, error) {
path := service.getEdgeJobTaskLogPath(edgeJobID, taskID)
fileContent, err := ioutil.ReadFile(path)
if err != nil {
return "", err
}
return string(fileContent), nil
}
// StoreEdgeJobTaskLogFileFromBytes stores the log file
func (service *Service) StoreEdgeJobTaskLogFileFromBytes(edgeJobID, taskID string, data []byte) error {
edgeJobStorePath := path.Join(EdgeJobStorePath, edgeJobID)
err := service.createDirectoryInStore(edgeJobStorePath)
if err != nil {
return err
}
filePath := path.Join(edgeJobStorePath, fmt.Sprintf("logs_%s", taskID))
r := bytes.NewReader(data)
err = service.createFileInStore(filePath, r)
if err != nil {
return err
}
return nil
}
func (service *Service) getEdgeJobTaskLogPath(edgeJobID string, taskID string) string {
return fmt.Sprintf("%s/logs_%s", service.GetEdgeJobFolder(edgeJobID), taskID)
}
// GetTemporaryPath returns a temp folder
func (service *Service) GetTemporaryPath() (string, error) {
uid, err := uuid.NewV4()
if err != nil {
return "", err
}
return path.Join(service.fileStorePath, TempPath, uid.String()), nil
}

View File

@ -13,6 +13,7 @@ require (
github.com/docker/cli v0.0.0-20191126203649-54d085b857e9
github.com/docker/docker v0.0.0-00010101000000-000000000000
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814
github.com/go-ldap/ldap/v3 v3.1.8
github.com/gofrs/uuid v3.2.0+incompatible
github.com/gorilla/mux v1.7.3
github.com/gorilla/securecookie v1.1.1
@ -27,14 +28,14 @@ require (
github.com/portainer/libcompose v0.5.3
github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33
github.com/robfig/cron/v3 v3.0.0
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 // indirect
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/asn1-ber.v1 v1.0.0-00010101000000-000000000000 // indirect
gopkg.in/ldap.v2 v2.5.1
gopkg.in/src-d/go-git.v4 v4.13.1
k8s.io/api v0.17.2
k8s.io/apimachinery v0.17.2
k8s.io/client-go v0.17.2
)
replace github.com/docker/docker => github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203
replace gopkg.in/asn1-ber.v1 => github.com/go-asn1-ber/asn1-ber v1.3.1

View File

@ -1,6 +1,15 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.3.8 h1:dvxbxtpTIjdAbx2OtL26p4eq0iEvys/U5yrsTJb3NZI=
github.com/Microsoft/go-winio v0.3.8/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
@ -8,6 +17,9 @@ github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+q
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/Microsoft/hcsshim v0.8.6 h1:ZfF0+zZeYdzMIVMZHKtDKJvLHj76XCuVae/jNkjj0IA=
github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
github.com/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/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=
@ -36,6 +48,7 @@ github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -50,12 +63,8 @@ github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BU
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ=
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
github.com/docker/engine v1.4.2-0.20191127222017-3152f9436292 h1:qQ7mw+CVWpRj5DWBL4CVHtBbGQdlPCj4j1evDh0ethw=
github.com/docker/engine v1.4.2-0.20191127222017-3152f9436292/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY=
github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203 h1:QeBh8wW8pIZKlXxlMOQ8hSCMdJA+2Z/bD/iDyCAS8XU=
github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY=
github.com/docker/engine v1.13.1 h1:Cks33UT9YBW5Xyc3MtGDq2IPgqfJtJ+qkFaxc2b0Euc=
github.com/docker/engine v1.13.1/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY=
github.com/docker/go-connections v0.3.0 h1:3lOnM9cSzgGwx8VfK/NGOW5fLQ0GjIlCkaktF+n1M6o=
github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-metrics v0.0.0-20181218153428-b84716841b82 h1:X0fj836zx99zFu83v/M79DuBn84IL/Syx1SY6Y5ZEMA=
@ -64,36 +73,65 @@ github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
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/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=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814 h1:gWvniJ4GbFfkf700kykAImbLiEMU0Q3QN9hQ26Js1pU=
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814/go.mod h1:secRm32Ro77eD23BmPVbgLbWN+JWDw7pJszenjxI4bI=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-asn1-ber/asn1-ber v1.3.1 h1:gvPdv/Hr++TRFCl0UbPFHC54P9N9jgsRPnmnr419Uck=
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-ldap/ldap/v3 v3.1.8 h1:5vU/2jOh9HqprwXp8aF915s9p6Z8wmbSEVF7/gdTFhM=
github.com/go-ldap/ldap/v3 v3.1.8/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I=
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/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=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k=
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v0.0.0-20160317213430-0eeaf8392f5b/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
@ -103,6 +141,12 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
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/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
@ -117,13 +161,17 @@ github.com/jpillora/requestlog v0.0.0-20181015073026-df8817be5f82 h1:7ufdyC3aMxF
github.com/jpillora/requestlog v0.0.0-20181015073026-df8817be5f82/go.mod h1:w8buj+yNfmLEP0ENlbG/FRnK6bVmuhqXnukYCs9sDvY=
github.com/jpillora/sizestr v0.0.0-20160130011556-e2ea2fa42fb9 h1:0c9jcgBtHRtDU//jTrcCgWG6UHjMZytiq/3WhraNgUM=
github.com/jpillora/sizestr v0.0.0-20160130011556-e2ea2fa42fb9/go.mod h1:1ffp+CRe0eAwwRb0/BownUAjMBsmTLwgAvRbfj9dRwE=
github.com/json-iterator/go 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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c h1:N7A4JCA2G+j5fuFxCsJqjFU/sZe0mj8H0sSoSwbaikw=
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c/go.mod h1:Nn5wlyECw3iJrzi0AhIWg+AJUb4PlRQVW4/3XHH1LZA=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
@ -136,12 +184,11 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-shellwords v1.0.6 h1:9Jok5pILi5S1MnDirGVTufYGtksUs/V2BWUP3ZkeUUI=
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microsoft/go-winio v0.4.8 h1:N4SmTFXUK7/jnn/UG/gm2mrHiYu9LVGvtsvULyody/c=
github.com/microsoft/go-winio v0.4.8/go.mod h1:kcIxxtKZE55DEncT/EOvFiygPobhUWpSDqDb47poQOU=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
@ -149,12 +196,22 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE=
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/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.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
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.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
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=
github.com/opencontainers/image-spec v0.0.0-20170515205857-f03dbe35d449 h1:Aq8iG72akPb/kszE7ksZ5ldV+JYPYii/KZOxlpJF07s=
@ -164,9 +221,11 @@ github.com/opencontainers/runc v0.0.0-20161109192122-51371867a01c/go.mod h1:qT5X
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 h1:lNCW6THrCKBiJBpz8kbVGjC7MgdCGKwuvBgc7LoD6sw=
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/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/libcompose v0.5.3 h1:tE4WcPuGvo+NKeDkDWpwNavNLZ5GHIJ4RvuZXsI9uI8=
@ -189,22 +248,26 @@ 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.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E=
github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/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=
@ -216,30 +279,58 @@ 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.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=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 h1:anGSYQpPhQwXlwsu5wmfq0nWkCNaMEMUwAv13Y92hd8=
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181017193950-04a2e542c03f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
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-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc h1:gkKoSkUmnU6bpS/VhkuO27bzQeSA51uaEfbOW5dNb68=
golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 h1:e6HwijUxhDe+hPNjZQQn9bA5PW3vNmnN64U2ZW759Lk=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -247,20 +338,36 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c h1:jceGD5YNJGgGMkJz79agzOln1K9TaZUjv5ird16qniQ=
golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7 h1:ZUjXAXmrAyrmmCPHgCA/vChHcpsX27MZ3yBonD/z1KE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.22.1 h1:/7cs52RnTJmD43s3uxzlq2U7nqVTd/37viQwMrMNlOM=
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
@ -268,19 +375,53 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ldap.v2 v2.5.1 h1:wiu0okdNfjlBzg6UWvd1Hn8Y+Ux17/u/4nlk4CQr6tU=
gopkg.in/ldap.v2 v2.5.1/go.mod h1:oI0cpe/D7HRtBQl8aTg+ZmzFUAvu4lsv3eLXMLGFxWk=
gopkg.in/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/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.0.0-20191114100352-16d7abae0d2a h1:86XISgFlG7lPOWj6wYLxd+xqhhVt/WQjS4Tf39rP09s=
k8s.io/api v0.0.0-20191114100352-16d7abae0d2a/go.mod h1:qetVJgs5i8jwdFIdoOZ70ks0ecgU+dYwqZ2uD1srwOU=
k8s.io/api v0.17.2 h1:NF1UFXcKN7/OOv1uxdRz3qfra8AHsPav5M93hlV9+Dc=
k8s.io/api v0.17.2/go.mod h1:BS9fjjLc4CMuqfSO8vgbHPKMt5+SF0ET6u/RVDihTo4=
k8s.io/apimachinery v0.0.0-20191028221656-72ed19daf4bb h1:ZUNsbuPdXWrj0rZziRfCWcFg9ZP31OKkziqCbiphznI=
k8s.io/apimachinery v0.0.0-20191028221656-72ed19daf4bb/go.mod h1:llRdnznGEAqC3DcNm6yEj472xaFVfLM7hnYofMb12tQ=
k8s.io/apimachinery v0.17.2 h1:hwDQQFbdRlpnnsR64Asdi55GyCaIP/3WQpMmbNBeWr4=
k8s.io/apimachinery v0.17.2/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg=
k8s.io/client-go v0.0.0-20191114101535-6c5935290e33 h1:07mhG/2oEoo3N+sHVOo0L9PJ/qvbk3N5n2dj8IWefnQ=
k8s.io/client-go v0.0.0-20191114101535-6c5935290e33/go.mod h1:4L/zQOBkEf4pArQJ+CMk1/5xjA30B5oyWv+Bzb44DOw=
k8s.io/client-go v0.17.2 h1:ndIfkfXEGrNhLIgkr0+qhRguSD3u6DCmonepn1O6NYc=
k8s.io/client-go v0.17.2/go.mod h1:QAzRgsa0C2xl4/eVpeVAZMvikCn8Nm81yqVx3Kk9XYI=
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/klog v0.4.0 h1:lCJCxf/LIowc2IGS9TPjWDyXY4nOmdGdfcwwDQCOURQ=
k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
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-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
k8s.io/utils v0.0.0-20190801114015-581e00157fb1 h1:+ySTxfHnfzZb9ys375PXNlLhkJPLKgHajBU0N62BDvE=
k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo=
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=

View File

@ -3,6 +3,7 @@ package client
import (
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
@ -14,9 +15,10 @@ import (
"github.com/portainer/portainer/api"
)
var errInvalidResponseStatus = errors.New("Invalid response status (expecting 200)")
const (
errInvalidResponseStatus = portainer.Error("Invalid response status (expecting 200)")
defaultHTTPTimeout = 5
defaultHTTPTimeout = 5
)
// HTTPClient represents a client to send HTTP requests.
@ -56,7 +58,7 @@ func (client *HTTPClient) ExecuteAzureAuthenticationRequest(credentials *portain
}
if response.StatusCode != http.StatusOK {
return nil, portainer.ErrAzureInvalidCredentials
return nil, errors.New("Invalid Azure credentials")
}
var token AzureAuthenticationResponse

12
api/http/errors/errors.go Normal file
View File

@ -0,0 +1,12 @@
package errors
import "errors"
var (
// ErrEndpointAccessDenied Access denied to endpoint error
ErrEndpointAccessDenied = errors.New("Access denied to endpoint")
// ErrUnauthorized Unauthorized error
ErrUnauthorized = errors.New("Unauthorized")
// ErrResourceAccessDenied Access denied to resource error
ErrResourceAccessDenied = errors.New("Access denied to resource")
)

View File

@ -1,6 +1,7 @@
package auth
import (
"errors"
"log"
"net/http"
"strings"
@ -10,6 +11,8 @@ import (
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
httperrors "github.com/portainer/portainer/api/http/errors"
)
type authenticatePayload struct {
@ -23,44 +26,40 @@ type authenticateResponse struct {
func (payload *authenticatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Username) {
return portainer.Error("Invalid username")
return errors.New("Invalid username")
}
if govalidator.IsNull(payload.Password) {
return portainer.Error("Invalid password")
return errors.New("Invalid password")
}
return nil
}
func (handler *Handler) authenticate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
if handler.authDisabled {
return &httperror.HandlerError{http.StatusServiceUnavailable, "Cannot authenticate user. Portainer was started with the --no-auth flag", ErrAuthDisabled}
}
var payload authenticatePayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
settings, err := handler.SettingsService.Settings()
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
}
u, err := handler.UserService.UserByUsername(payload.Username)
if err != nil && err != portainer.ErrObjectNotFound {
u, err := handler.DataStore.User().UserByUsername(payload.Username)
if err != nil && err != bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err}
}
if err == portainer.ErrObjectNotFound && (settings.AuthenticationMethod == portainer.AuthenticationInternal || settings.AuthenticationMethod == portainer.AuthenticationOAuth) {
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", portainer.ErrUnauthorized}
if err == bolterrors.ErrObjectNotFound && (settings.AuthenticationMethod == portainer.AuthenticationInternal || settings.AuthenticationMethod == portainer.AuthenticationOAuth) {
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized}
}
if settings.AuthenticationMethod == portainer.AuthenticationLDAP {
if u == nil && settings.LDAPSettings.AutoCreateUsers {
return handler.authenticateLDAPAndCreateUser(w, payload.Username, payload.Password, &settings.LDAPSettings)
} else if u == nil && !settings.LDAPSettings.AutoCreateUsers {
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", portainer.ErrUnauthorized}
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized}
}
return handler.authenticateLDAP(w, u, payload.Password, &settings.LDAPSettings)
}
@ -79,18 +78,13 @@ func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
}
err = handler.AuthorizationService.UpdateUsersAuthorizations()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
}
return handler.writeToken(w, user)
}
func (handler *Handler) authenticateInternal(w http.ResponseWriter, user *portainer.User, password string) *httperror.HandlerError {
err := handler.CryptoService.CompareHashAndData(user.Password, password)
if err != nil {
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", portainer.ErrUnauthorized}
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized}
}
return handler.writeToken(w, user)
@ -103,12 +97,11 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use
}
user := &portainer.User{
Username: username,
Role: portainer.StandardUserRole,
PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(),
Username: username,
Role: portainer.StandardUserRole,
}
err = handler.UserService.CreateUser(user)
err = handler.DataStore.User().CreateUser(user)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err}
}
@ -118,11 +111,6 @@ 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()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
}
return handler.writeToken(w, user)
}
@ -146,7 +134,7 @@ func (handler *Handler) persistAndWriteToken(w http.ResponseWriter, tokenData *p
}
func (handler *Handler) addUserIntoTeams(user *portainer.User, settings *portainer.LDAPSettings) error {
teams, err := handler.TeamService.Teams()
teams, err := handler.DataStore.Team().Teams()
if err != nil {
return err
}
@ -156,7 +144,7 @@ func (handler *Handler) addUserIntoTeams(user *portainer.User, settings *portain
return err
}
userMemberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(user.ID)
userMemberships, err := handler.DataStore.TeamMembership().TeamMembershipsByUserID(user.ID)
if err != nil {
return err
}
@ -174,7 +162,7 @@ func (handler *Handler) addUserIntoTeams(user *portainer.User, settings *portain
Role: portainer.TeamMember,
}
err := handler.TeamMembershipService.CreateTeamMembership(membership)
err := handler.DataStore.TeamMembership().CreateTeamMembership(membership)
if err != nil {
return err
}

View File

@ -1,8 +1,7 @@
package auth
import (
"encoding/json"
"io/ioutil"
"errors"
"log"
"net/http"
@ -10,6 +9,8 @@ import (
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
httperrors "github.com/portainer/portainer/api/http/errors"
)
type oauthPayload struct {
@ -18,57 +19,26 @@ type oauthPayload struct {
func (payload *oauthPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Code) {
return portainer.Error("Invalid OAuth authorization code")
return errors.New("Invalid OAuth authorization code")
}
return nil
}
func (handler *Handler) authenticateThroughExtension(code, licenseKey string, settings *portainer.OAuthSettings) (string, error) {
extensionURL := handler.ProxyManager.GetExtensionURL(portainer.OAuthAuthenticationExtension)
encodedConfiguration, err := json.Marshal(settings)
if err != nil {
return "", nil
func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuthSettings) (string, error) {
if code == "" {
return "", errors.New("Invalid OAuth authorization code")
}
req, err := http.NewRequest("GET", extensionURL+"/validate", nil)
if settings == nil {
return "", errors.New("Invalid OAuth configuration")
}
username, err := handler.OAuthService.Authenticate(code, settings)
if err != nil {
return "", err
}
client := &http.Client{}
req.Header.Set("X-OAuth-Config", string(encodedConfiguration))
req.Header.Set("X-OAuth-Code", code)
req.Header.Set("X-PortainerExtension-License", licenseKey)
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
type extensionResponse struct {
Username string `json:"Username,omitempty"`
Err string `json:"err,omitempty"`
Details string `json:"details,omitempty"`
}
var extResp extensionResponse
err = json.Unmarshal(body, &extResp)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", portainer.Error(extResp.Err + ":" + extResp.Details)
}
return extResp.Username, nil
return username, nil
}
func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
@ -78,45 +48,37 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
settings, err := handler.SettingsService.Settings()
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
}
if settings.AuthenticationMethod != 3 {
return &httperror.HandlerError{http.StatusForbidden, "OAuth authentication is not enabled", portainer.Error("OAuth authentication is not enabled")}
return &httperror.HandlerError{http.StatusForbidden, "OAuth authentication is not enabled", errors.New("OAuth authentication is not enabled")}
}
extension, err := handler.ExtensionService.Extension(portainer.OAuthAuthenticationExtension)
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Oauth authentication extension is not enabled", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
}
username, err := handler.authenticateThroughExtension(payload.Code, extension.License.LicenseKey, &settings.OAuthSettings)
username, err := handler.authenticateOAuth(payload.Code, &settings.OAuthSettings)
if err != nil {
log.Printf("[DEBUG] - OAuth authentication error: %s", err)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate through OAuth", portainer.ErrUnauthorized}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate through OAuth", httperrors.ErrUnauthorized}
}
user, err := handler.UserService.UserByUsername(username)
if err != nil && err != portainer.ErrObjectNotFound {
user, err := handler.DataStore.User().UserByUsername(username)
if err != nil && err != bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err}
}
if user == nil && !settings.OAuthSettings.OAuthAutoCreateUsers {
return &httperror.HandlerError{http.StatusForbidden, "Account not created beforehand in Portainer and automatic user provisioning not enabled", portainer.ErrUnauthorized}
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: username,
Role: portainer.StandardUserRole,
PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(),
Username: username,
Role: portainer.StandardUserRole,
}
err = handler.UserService.CreateUser(user)
err = handler.DataStore.User().CreateUser(user)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err}
}
@ -128,16 +90,12 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
Role: portainer.TeamMember,
}
err = handler.TeamMembershipService.CreateTeamMembership(membership)
err = handler.DataStore.TeamMembership().CreateTeamMembership(membership)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist team membership inside the database", err}
}
}
err = handler.AuthorizationService.UpdateUsersAuthorizations()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
}
}
return handler.writeToken(w, user)

View File

@ -7,47 +7,34 @@ import (
httperror "github.com/portainer/libhttp/error"
"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"
)
const (
// ErrInvalidCredentials is an error raised when credentials for a user are invalid
ErrInvalidCredentials = portainer.Error("Invalid credentials")
// ErrAuthDisabled is an error raised when trying to access the authentication endpoints
// when the server has been started with the --no-auth flag
ErrAuthDisabled = portainer.Error("Authentication is disabled")
)
// Handler is the HTTP handler used to handle authentication operations.
type Handler struct {
*mux.Router
authDisabled bool
UserService portainer.UserService
CryptoService portainer.CryptoService
JWTService portainer.JWTService
LDAPService portainer.LDAPService
SettingsService portainer.SettingsService
TeamService portainer.TeamService
TeamMembershipService portainer.TeamMembershipService
ExtensionService portainer.ExtensionService
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
RoleService portainer.RoleService
ProxyManager *proxy.Manager
AuthorizationService *portainer.AuthorizationService
DataStore portainer.DataStore
CryptoService portainer.CryptoService
JWTService portainer.JWTService
LDAPService portainer.LDAPService
OAuthService portainer.OAuthService
ProxyManager *proxy.Manager
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
}
// NewHandler creates a handler to manage authentication operations.
func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimiter, authDisabled bool) *Handler {
func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimiter) *Handler {
h := &Handler{
Router: mux.NewRouter(),
authDisabled: authDisabled,
Router: mux.NewRouter(),
}
h.Handle("/auth/oauth/validate",
rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.validateOAuth)))).Methods(http.MethodPost)
h.Handle("/auth",
rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.authenticate)))).Methods(http.MethodPost)
h.Handle("/auth/logout",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.logout))).Methods(http.MethodPost)
return h
}

View File

@ -0,0 +1,21 @@
package auth
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api/http/security"
)
// POST request on /logout
func (handler *Handler) logout(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user details from authentication token", err}
}
handler.KubernetesTokenCacheManager.RemoveUserFromCache(int(tokenData.ID))
return response.Empty(w)
}

View File

@ -0,0 +1,290 @@
package customtemplates
import (
"errors"
"net/http"
"strconv"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
)
func (handler *Handler) customTemplateCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
method, err := request.RetrieveQueryParameter(r, "method", false)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: method", err}
}
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user details from authentication token", err}
}
customTemplate, err := handler.createCustomTemplate(method, r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create custom template", err}
}
customTemplate.CreatedByUserID = tokenData.ID
customTemplates, err := handler.DataStore.CustomTemplate().CustomTemplates()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve custom templates from the database", err}
}
for _, existingTemplate := range customTemplates {
if existingTemplate.Title == customTemplate.Title {
return &httperror.HandlerError{http.StatusInternalServerError, "Template name must be unique", errors.New("Template name must be unique")}
}
}
err = handler.DataStore.CustomTemplate().CreateCustomTemplate(customTemplate)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create custom template", err}
}
resourceControl := authorization.NewPrivateResourceControl(strconv.Itoa(int(customTemplate.ID)), portainer.CustomTemplateResourceControl, tokenData.ID)
err = handler.DataStore.ResourceControl().CreateResourceControl(resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist resource control inside the database", err}
}
customTemplate.ResourceControl = resourceControl
return response.JSON(w, customTemplate)
}
func (handler *Handler) createCustomTemplate(method string, r *http.Request) (*portainer.CustomTemplate, error) {
switch method {
case "string":
return handler.createCustomTemplateFromFileContent(r)
case "repository":
return handler.createCustomTemplateFromGitRepository(r)
case "file":
return handler.createCustomTemplateFromFileUpload(r)
}
return nil, errors.New("Invalid value for query parameter: method. Value must be one of: string, repository or file")
}
type customTemplateFromFileContentPayload struct {
Logo string
Title string
FileContent string
Description string
Note string
Platform portainer.CustomTemplatePlatform
Type portainer.StackType
}
func (payload *customTemplateFromFileContentPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Title) {
return errors.New("Invalid custom template title")
}
if govalidator.IsNull(payload.Description) {
return errors.New("Invalid custom template description")
}
if govalidator.IsNull(payload.FileContent) {
return errors.New("Invalid file content")
}
if payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
return errors.New("Invalid custom template platform")
}
if payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack {
return errors.New("Invalid custom template type")
}
return nil
}
func (handler *Handler) createCustomTemplateFromFileContent(r *http.Request) (*portainer.CustomTemplate, error) {
var payload customTemplateFromFileContentPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return nil, err
}
customTemplateID := handler.DataStore.CustomTemplate().GetNextIdentifier()
customTemplate := &portainer.CustomTemplate{
ID: portainer.CustomTemplateID(customTemplateID),
Title: payload.Title,
EntryPoint: filesystem.ComposeFileDefaultName,
Description: payload.Description,
Note: payload.Note,
Platform: (payload.Platform),
Type: (payload.Type),
Logo: payload.Logo,
}
templateFolder := strconv.Itoa(customTemplateID)
projectPath, err := handler.FileService.StoreCustomTemplateFileFromBytes(templateFolder, customTemplate.EntryPoint, []byte(payload.FileContent))
if err != nil {
return nil, err
}
customTemplate.ProjectPath = projectPath
return customTemplate, nil
}
type customTemplateFromGitRepositoryPayload struct {
Logo string
Title string
Description string
Note string
Platform portainer.CustomTemplatePlatform
Type portainer.StackType
RepositoryURL string
RepositoryReferenceName string
RepositoryAuthentication bool
RepositoryUsername string
RepositoryPassword string
ComposeFilePathInRepository string
}
func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Title) {
return errors.New("Invalid custom template title")
}
if govalidator.IsNull(payload.Description) {
return errors.New("Invalid custom template description")
}
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
}
if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) {
return errors.New("Invalid repository credentials. Username and password must be specified when authentication is enabled")
}
if govalidator.IsNull(payload.ComposeFilePathInRepository) {
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
}
if payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
return errors.New("Invalid custom template platform")
}
if payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack {
return errors.New("Invalid custom template type")
}
return nil
}
func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (*portainer.CustomTemplate, error) {
var payload customTemplateFromGitRepositoryPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return nil, err
}
customTemplateID := handler.DataStore.CustomTemplate().GetNextIdentifier()
customTemplate := &portainer.CustomTemplate{
ID: portainer.CustomTemplateID(customTemplateID),
Title: payload.Title,
EntryPoint: payload.ComposeFilePathInRepository,
Description: payload.Description,
Note: payload.Note,
Platform: payload.Platform,
Type: payload.Type,
Logo: payload.Logo,
}
projectPath := handler.FileService.GetCustomTemplateProjectPath(strconv.Itoa(customTemplateID))
customTemplate.ProjectPath = projectPath
gitCloneParams := &cloneRepositoryParameters{
url: payload.RepositoryURL,
referenceName: payload.RepositoryReferenceName,
path: projectPath,
authentication: payload.RepositoryAuthentication,
username: payload.RepositoryUsername,
password: payload.RepositoryPassword,
}
err = handler.cloneGitRepository(gitCloneParams)
if err != nil {
return nil, err
}
return customTemplate, nil
}
type customTemplateFromFileUploadPayload struct {
Logo string
Title string
Description string
Note string
Platform portainer.CustomTemplatePlatform
Type portainer.StackType
FileContent []byte
}
func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) error {
title, err := request.RetrieveMultiPartFormValue(r, "Title", false)
if err != nil {
return errors.New("Invalid custom template title")
}
payload.Title = title
description, err := request.RetrieveMultiPartFormValue(r, "Description", false)
if err != nil {
return errors.New("Invalid custom template description")
}
payload.Description = description
note, _ := request.RetrieveMultiPartFormValue(r, "Note", true)
payload.Note = note
platform, _ := request.RetrieveNumericMultiPartFormValue(r, "Platform", true)
templatePlatform := portainer.CustomTemplatePlatform(platform)
if templatePlatform != portainer.CustomTemplatePlatformLinux && templatePlatform != portainer.CustomTemplatePlatformWindows {
return errors.New("Invalid custom template platform")
}
payload.Platform = templatePlatform
typeNumeral, _ := request.RetrieveNumericMultiPartFormValue(r, "Type", true)
templateType := portainer.StackType(typeNumeral)
if templateType != portainer.DockerComposeStack && templateType != portainer.DockerSwarmStack {
return errors.New("Invalid custom template type")
}
payload.Type = templateType
composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "file")
if err != nil {
return errors.New("Invalid Compose file. Ensure that the Compose file is uploaded correctly")
}
payload.FileContent = composeFileContent
return nil
}
func (handler *Handler) createCustomTemplateFromFileUpload(r *http.Request) (*portainer.CustomTemplate, error) {
payload := &customTemplateFromFileUploadPayload{}
err := payload.Validate(r)
if err != nil {
return nil, err
}
customTemplateID := handler.DataStore.CustomTemplate().GetNextIdentifier()
customTemplate := &portainer.CustomTemplate{
ID: portainer.CustomTemplateID(customTemplateID),
Title: payload.Title,
Description: payload.Description,
Note: payload.Note,
Platform: payload.Platform,
Type: payload.Type,
Logo: payload.Logo,
EntryPoint: filesystem.ComposeFileDefaultName,
}
templateFolder := strconv.Itoa(customTemplateID)
projectPath, err := handler.FileService.StoreCustomTemplateFileFromBytes(templateFolder, customTemplate.EntryPoint, []byte(payload.FileContent))
if err != nil {
return nil, err
}
customTemplate.ProjectPath = projectPath
return customTemplate, nil
}

View File

@ -0,0 +1,63 @@
package customtemplates
import (
"net/http"
"strconv"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
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/http/security"
)
func (handler *Handler) customTemplateDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
customTemplateID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Custom template identifier route variable", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
customTemplate, err := handler.DataStore.CustomTemplate().CustomTemplate(portainer.CustomTemplateID(customTemplateID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a custom template with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a custom template with the specified identifier inside the database", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(strconv.Itoa(customTemplateID), portainer.CustomTemplateResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the custom template", err}
}
access := userCanEditTemplate(customTemplate, securityContext)
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
err = handler.DataStore.CustomTemplate().DeleteCustomTemplate(portainer.CustomTemplateID(customTemplateID))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the custom template from the database", err}
}
err = handler.FileService.RemoveDirectory(customTemplate.ProjectPath)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove custom template files from disk", err}
}
if resourceControl != nil {
err = handler.DataStore.ResourceControl().DeleteResourceControl(resourceControl.ID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the associated resource control from the database", err}
}
}
return response.Empty(w)
}

View File

@ -0,0 +1,38 @@
package customtemplates
import (
"net/http"
"path"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
)
type fileResponse struct {
FileContent string
}
// GET request on /api/custom_templates/:id/file
func (handler *Handler) customTemplateFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
customTemplateID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid custom template identifier route variable", err}
}
customTemplate, err := handler.DataStore.CustomTemplate().CustomTemplate(portainer.CustomTemplateID(customTemplateID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a custom template with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a custom template with the specified identifier inside the database", err}
}
fileContent, err := handler.FileService.GetFileContent(path.Join(customTemplate.ProjectPath, customTemplate.EntryPoint))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve custom template file from disk", err}
}
return response.JSON(w, &fileResponse{FileContent: string(fileContent)})
}

View File

@ -0,0 +1,49 @@
package customtemplates
import (
"net/http"
"strconv"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"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/http/security"
)
func (handler *Handler) customTemplateInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
customTemplateID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Custom template identifier route variable", err}
}
customTemplate, err := handler.DataStore.CustomTemplate().CustomTemplate(portainer.CustomTemplateID(customTemplateID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a custom template with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a custom template with the specified identifier inside the database", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(strconv.Itoa(customTemplateID), portainer.CustomTemplateResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the custom template", err}
}
access := userCanEditTemplate(customTemplate, securityContext)
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
if resourceControl != nil {
customTemplate.ResourceControl = resourceControl
}
return response.JSON(w, customTemplate)
}

View File

@ -0,0 +1,67 @@
package customtemplates
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
)
func (handler *Handler) customTemplateList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
customTemplates, err := handler.DataStore.CustomTemplate().CustomTemplates()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve custom templates from the database", err}
}
stackType, _ := request.RetrieveNumericQueryParameter(r, "type", true)
resourceControls, err := handler.DataStore.ResourceControl().ResourceControls()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve resource controls from the database", err}
}
customTemplates = authorization.DecorateCustomTemplates(customTemplates, resourceControls)
customTemplates = filterTemplatesByEngineType(customTemplates, portainer.StackType(stackType))
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
if !securityContext.IsAdmin {
user, err := handler.DataStore.User().User(securityContext.UserID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user information from the database", err}
}
userTeamIDs := make([]portainer.TeamID, 0)
for _, membership := range securityContext.UserMemberships {
userTeamIDs = append(userTeamIDs, membership.TeamID)
}
customTemplates = authorization.FilterAuthorizedCustomTemplates(customTemplates, user, userTeamIDs)
}
return response.JSON(w, customTemplates)
}
func filterTemplatesByEngineType(templates []portainer.CustomTemplate, stackType portainer.StackType) []portainer.CustomTemplate {
if stackType == 0 {
return templates
}
filteredTemplates := []portainer.CustomTemplate{}
for _, template := range templates {
if template.Type == stackType {
filteredTemplates = append(filteredTemplates, template)
}
}
return filteredTemplates
}

View File

@ -0,0 +1,107 @@
package customtemplates
import (
"errors"
"net/http"
"strconv"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
)
type customTemplateUpdatePayload struct {
Logo string
Title string
Description string
Note string
Platform portainer.CustomTemplatePlatform
Type portainer.StackType
FileContent string
}
func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Title) {
return errors.New("Invalid custom template title")
}
if govalidator.IsNull(payload.FileContent) {
return errors.New("Invalid file content")
}
if payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
return errors.New("Invalid custom template platform")
}
if payload.Type != portainer.DockerComposeStack && payload.Type != portainer.DockerSwarmStack {
return errors.New("Invalid custom template type")
}
if govalidator.IsNull(payload.Description) {
return errors.New("Invalid custom template description")
}
return nil
}
func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
customTemplateID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Custom template identifier route variable", err}
}
var payload customTemplateUpdatePayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
customTemplates, err := handler.DataStore.CustomTemplate().CustomTemplates()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve custom templates from the database", err}
}
for _, existingTemplate := range customTemplates {
if existingTemplate.ID != portainer.CustomTemplateID(customTemplateID) && existingTemplate.Title == payload.Title {
return &httperror.HandlerError{http.StatusInternalServerError, "Template name must be unique", errors.New("Template name must be unique")}
}
}
customTemplate, err := handler.DataStore.CustomTemplate().CustomTemplate(portainer.CustomTemplateID(customTemplateID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a custom template with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a custom template with the specified identifier inside the database", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
access := userCanEditTemplate(customTemplate, securityContext)
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
templateFolder := strconv.Itoa(customTemplateID)
_, err = handler.FileService.StoreCustomTemplateFileFromBytes(templateFolder, customTemplate.EntryPoint, []byte(payload.FileContent))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist updated custom template file on disk", err}
}
customTemplate.Title = payload.Title
customTemplate.Logo = payload.Logo
customTemplate.Description = payload.Description
customTemplate.Note = payload.Note
customTemplate.Platform = payload.Platform
customTemplate.Type = payload.Type
err = handler.DataStore.CustomTemplate().UpdateCustomTemplate(customTemplate.ID, customTemplate)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist custom template changes inside the database", err}
}
return response.JSON(w, customTemplate)
}

View File

@ -0,0 +1,17 @@
package customtemplates
type cloneRepositoryParameters struct {
url string
referenceName string
path string
authentication bool
username string
password string
}
func (handler *Handler) cloneGitRepository(parameters *cloneRepositoryParameters) error {
if parameters.authentication {
return handler.GitService.ClonePrivateRepositoryWithBasicAuth(parameters.url, parameters.referenceName, parameters.path, parameters.username, parameters.password)
}
return handler.GitService.ClonePublicRepository(parameters.url, parameters.referenceName, parameters.path)
}

View File

@ -0,0 +1,60 @@
package customtemplates
import (
"net/http"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
)
// 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
}
// NewHandler creates a handler to manage endpoint group operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
}
h.Handle("/custom_templates",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateCreate))).Methods(http.MethodPost)
h.Handle("/custom_templates",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateList))).Methods(http.MethodGet)
h.Handle("/custom_templates/{id}",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateInspect))).Methods(http.MethodGet)
h.Handle("/custom_templates/{id}/file",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateFile))).Methods(http.MethodGet)
h.Handle("/custom_templates/{id}",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateUpdate))).Methods(http.MethodPut)
h.Handle("/custom_templates/{id}",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateDelete))).Methods(http.MethodDelete)
return h
}
func userCanEditTemplate(customTemplate *portainer.CustomTemplate, securityContext *security.RestrictedRequestContext) bool {
return securityContext.IsAdmin || customTemplate.CreatedByUserID == securityContext.UserID
}
func userCanAccessTemplate(customTemplate portainer.CustomTemplate, securityContext *security.RestrictedRequestContext, resourceControl *portainer.ResourceControl) bool {
if securityContext.IsAdmin || customTemplate.CreatedByUserID == securityContext.UserID {
return true
}
userTeamIDs := make([]portainer.TeamID, 0)
for _, membership := range securityContext.UserMemberships {
userTeamIDs = append(userTeamIDs, membership.TeamID)
}
if resourceControl != nil && authorization.UserCanAccessResource(securityContext.UserID, userTeamIDs, resourceControl) {
return true
}
return false
}

View File

@ -9,7 +9,7 @@ import (
// GET request on /api/dockerhub
func (handler *Handler) dockerhubInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
dockerhub, err := handler.DockerHubService.DockerHub()
dockerhub, err := handler.DataStore.DockerHub().DockerHub()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve DockerHub details from the database", err}
}

View File

@ -1,6 +1,7 @@
package dockerhub
import (
"errors"
"net/http"
"github.com/asaskevich/govalidator"
@ -18,7 +19,7 @@ type dockerhubUpdatePayload struct {
func (payload *dockerhubUpdatePayload) Validate(r *http.Request) error {
if payload.Authentication && (govalidator.IsNull(payload.Username) || govalidator.IsNull(payload.Password)) {
return portainer.Error("Invalid credentials. Username and password must be specified when authentication is enabled")
return errors.New("Invalid credentials. Username and password must be specified when authentication is enabled")
}
return nil
}
@ -43,7 +44,7 @@ func (handler *Handler) dockerhubUpdate(w http.ResponseWriter, r *http.Request)
dockerhub.Password = payload.Password
}
err = handler.DockerHubService.UpdateDockerHub(dockerhub)
err = handler.DataStore.DockerHub().UpdateDockerHub(dockerhub)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the Dockerhub changes inside the database", err}
}

View File

@ -16,7 +16,7 @@ func hideFields(dockerHub *portainer.DockerHub) {
// Handler is the HTTP handler used to handle DockerHub operations.
type Handler struct {
*mux.Router
DockerHubService portainer.DockerHubService
DataStore portainer.DataStore
}
// NewHandler creates a handler to manage Dockerhub operations.

View File

@ -11,7 +11,7 @@ func (handler *Handler) getEndpointsByTags(tagIDs []portainer.TagID, partialMatc
return []portainer.EndpointID{}, nil
}
endpoints, err := handler.EndpointService.Endpoints()
endpoints, err := handler.DataStore.Endpoint().Endpoints()
if err != nil {
return nil, err
}
@ -20,7 +20,7 @@ func (handler *Handler) getEndpointsByTags(tagIDs []portainer.TagID, partialMatc
tags := []portainer.Tag{}
for _, tagID := range tagIDs {
tag, err := handler.TagService.Tag(tagID)
tag, err := handler.DataStore.Tag().Tag(tagID)
if err != nil {
return nil, err
}
@ -38,7 +38,7 @@ func (handler *Handler) getEndpointsByTags(tagIDs []portainer.TagID, partialMatc
results := []portainer.EndpointID{}
for _, endpoint := range endpoints {
if _, ok := endpointSet[endpoint.ID]; ok && endpoint.Type == portainer.EdgeAgentEnvironment {
if _, ok := endpointSet[endpoint.ID]; ok && (endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment) {
results = append(results, endpoint.ID)
}
}

View File

@ -1,6 +1,7 @@
package edgegroups
import (
"errors"
"net/http"
"github.com/asaskevich/govalidator"
@ -20,13 +21,13 @@ type edgeGroupCreatePayload struct {
func (payload *edgeGroupCreatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
return portainer.Error("Invalid Edge group name")
return errors.New("Invalid Edge group name")
}
if payload.Dynamic && (payload.TagIDs == nil || len(payload.TagIDs) == 0) {
return portainer.Error("TagIDs is mandatory for a dynamic Edge group")
return errors.New("TagIDs is mandatory for a dynamic Edge group")
}
if !payload.Dynamic && (payload.Endpoints == nil || len(payload.Endpoints) == 0) {
return portainer.Error("Endpoints is mandatory for a static Edge group")
return errors.New("Endpoints is mandatory for a static Edge group")
}
return nil
}
@ -38,14 +39,14 @@ func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request)
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge groups from the database", err}
}
for _, edgeGroup := range edgeGroups {
if edgeGroup.Name == payload.Name {
return &httperror.HandlerError{http.StatusBadRequest, "Edge group name must be unique", portainer.Error("Edge group name must be unique")}
return &httperror.HandlerError{http.StatusBadRequest, "Edge group name must be unique", errors.New("Edge group name must be unique")}
}
}
@ -62,19 +63,19 @@ func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request)
} else {
endpointIDs := []portainer.EndpointID{}
for _, endpointID := range payload.Endpoints {
endpoint, err := handler.EndpointService.Endpoint(endpointID)
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint from the database", err}
}
if endpoint.Type == portainer.EdgeAgentEnvironment {
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
endpointIDs = append(endpointIDs, endpoint.ID)
}
}
edgeGroup.Endpoints = endpointIDs
}
err = handler.EdgeGroupService.CreateEdgeGroup(edgeGroup)
err = handler.DataStore.EdgeGroup().CreateEdgeGroup(edgeGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the Edge group inside the database", err}
}

View File

@ -1,12 +1,14 @@
package edgegroups
import (
"errors"
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
)
func (handler *Handler) edgeGroupDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
@ -15,14 +17,14 @@ func (handler *Handler) edgeGroupDelete(w http.ResponseWriter, r *http.Request)
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge group identifier route variable", err}
}
_, err = handler.EdgeGroupService.EdgeGroup(portainer.EdgeGroupID(edgeGroupID))
if err == portainer.ErrObjectNotFound {
_, err = handler.DataStore.EdgeGroup().EdgeGroup(portainer.EdgeGroupID(edgeGroupID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge group with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge group with the specified identifier inside the database", err}
}
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge stacks from the database", err}
}
@ -30,12 +32,12 @@ func (handler *Handler) edgeGroupDelete(w http.ResponseWriter, r *http.Request)
for _, edgeStack := range edgeStacks {
for _, groupID := range edgeStack.EdgeGroups {
if groupID == portainer.EdgeGroupID(edgeGroupID) {
return &httperror.HandlerError{http.StatusForbidden, "Edge group is used by an Edge stack", portainer.Error("Edge group is used by an Edge stack")}
return &httperror.HandlerError{http.StatusForbidden, "Edge group is used by an Edge stack", errors.New("Edge group is used by an Edge stack")}
}
}
}
err = handler.EdgeGroupService.DeleteEdgeGroup(portainer.EdgeGroupID(edgeGroupID))
err = handler.DataStore.EdgeGroup().DeleteEdgeGroup(portainer.EdgeGroupID(edgeGroupID))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the Edge group from the database", err}
}

View File

@ -7,6 +7,7 @@ import (
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
)
func (handler *Handler) edgeGroupInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
@ -15,8 +16,8 @@ func (handler *Handler) edgeGroupInspect(w http.ResponseWriter, r *http.Request)
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge group identifier route variable", err}
}
edgeGroup, err := handler.EdgeGroupService.EdgeGroup(portainer.EdgeGroupID(edgeGroupID))
if err == portainer.ErrObjectNotFound {
edgeGroup, err := handler.DataStore.EdgeGroup().EdgeGroup(portainer.EdgeGroupID(edgeGroupID))
if err == errors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge group with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge group with the specified identifier inside the database", err}

View File

@ -14,12 +14,12 @@ type decoratedEdgeGroup struct {
}
func (handler *Handler) edgeGroupList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge groups from the database", err}
}
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge stacks from the database", err}
}

View File

@ -1,13 +1,16 @@
package edgegroups
import (
"errors"
"net/http"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/internal/edge"
)
type edgeGroupUpdatePayload struct {
@ -20,13 +23,13 @@ type edgeGroupUpdatePayload struct {
func (payload *edgeGroupUpdatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
return portainer.Error("Invalid Edge group name")
return errors.New("Invalid Edge group name")
}
if payload.Dynamic && (payload.TagIDs == nil || len(payload.TagIDs) == 0) {
return portainer.Error("TagIDs is mandatory for a dynamic Edge group")
return errors.New("TagIDs is mandatory for a dynamic Edge group")
}
if !payload.Dynamic && (payload.Endpoints == nil || len(payload.Endpoints) == 0) {
return portainer.Error("Endpoints is mandatory for a static Edge group")
return errors.New("Endpoints is mandatory for a static Edge group")
}
return nil
}
@ -43,37 +46,37 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request)
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
edgeGroup, err := handler.EdgeGroupService.EdgeGroup(portainer.EdgeGroupID(edgeGroupID))
if err == portainer.ErrObjectNotFound {
edgeGroup, err := handler.DataStore.EdgeGroup().EdgeGroup(portainer.EdgeGroupID(edgeGroupID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge group with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge group with the specified identifier inside the database", err}
}
if payload.Name != "" {
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge groups from the database", err}
}
for _, edgeGroup := range edgeGroups {
if edgeGroup.Name == payload.Name && edgeGroup.ID != portainer.EdgeGroupID(edgeGroupID) {
return &httperror.HandlerError{http.StatusBadRequest, "Edge group name must be unique", portainer.Error("Edge group name must be unique")}
return &httperror.HandlerError{http.StatusBadRequest, "Edge group name must be unique", errors.New("Edge group name must be unique")}
}
}
edgeGroup.Name = payload.Name
}
endpoints, err := handler.EndpointService.Endpoints()
endpoints, err := handler.DataStore.Endpoint().Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err}
}
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err}
}
oldRelatedEndpoints := portainer.EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups)
oldRelatedEndpoints := edge.EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups)
edgeGroup.Dynamic = payload.Dynamic
if edgeGroup.Dynamic {
@ -81,12 +84,12 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request)
} else {
endpointIDs := []portainer.EndpointID{}
for _, endpointID := range payload.Endpoints {
endpoint, err := handler.EndpointService.Endpoint(endpointID)
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint from the database", err}
}
if endpoint.Type == portainer.EdgeAgentEnvironment {
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
endpointIDs = append(endpointIDs, endpoint.ID)
}
}
@ -97,12 +100,12 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request)
edgeGroup.PartialMatch = *payload.PartialMatch
}
err = handler.EdgeGroupService.UpdateEdgeGroup(edgeGroup.ID, edgeGroup)
err = handler.DataStore.EdgeGroup().UpdateEdgeGroup(edgeGroup.ID, edgeGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Edge group changes inside the database", err}
}
newRelatedEndpoints := portainer.EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups)
newRelatedEndpoints := edge.EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups)
endpointsToUpdate := append(newRelatedEndpoints, oldRelatedEndpoints...)
for _, endpointID := range endpointsToUpdate {
@ -116,39 +119,39 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request)
}
func (handler *Handler) updateEndpoint(endpointID portainer.EndpointID) error {
relation, err := handler.EndpointRelationService.EndpointRelation(endpointID)
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID)
if err != nil {
return err
}
endpoint, err := handler.EndpointService.Endpoint(endpointID)
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
if err != nil {
return err
}
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID)
endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(endpoint.GroupID)
if err != nil {
return err
}
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups()
if err != nil {
return err
}
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks()
if err != nil {
return err
}
edgeStackSet := map[portainer.EdgeStackID]bool{}
endpointEdgeStacks := portainer.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks)
endpointEdgeStacks := edge.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks)
for _, edgeStackID := range endpointEdgeStacks {
edgeStackSet[edgeStackID] = true
}
relation.EdgeStacks = edgeStackSet
return handler.EndpointRelationService.UpdateEndpointRelation(endpoint.ID, relation)
return handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, relation)
}

View File

@ -12,12 +12,7 @@ import (
// Handler is the HTTP handler used to handle endpoint group operations.
type Handler struct {
*mux.Router
EdgeGroupService portainer.EdgeGroupService
EdgeStackService portainer.EdgeStackService
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
EndpointRelationService portainer.EndpointRelationService
TagService portainer.TagService
DataStore portainer.DataStore
}
// NewHandler creates a handler to manage endpoint group operations.
@ -26,14 +21,14 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
Router: mux.NewRouter(),
}
h.Handle("/edge_groups",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeGroupCreate))).Methods(http.MethodPost)
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeGroupCreate)))).Methods(http.MethodPost)
h.Handle("/edge_groups",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeGroupList))).Methods(http.MethodGet)
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeGroupList)))).Methods(http.MethodGet)
h.Handle("/edge_groups/{id}",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeGroupInspect))).Methods(http.MethodGet)
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeGroupInspect)))).Methods(http.MethodGet)
h.Handle("/edge_groups/{id}",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeGroupUpdate))).Methods(http.MethodPut)
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeGroupUpdate)))).Methods(http.MethodPut)
h.Handle("/edge_groups/{id}",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeGroupDelete))).Methods(http.MethodDelete)
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeGroupDelete)))).Methods(http.MethodDelete)
return h
}

View File

@ -0,0 +1,220 @@
package edgejobs
import (
"errors"
"net/http"
"strconv"
"strings"
"time"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
)
// POST /api/edge_jobs?method=file|string
func (handler *Handler) edgeJobCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
method, err := request.RetrieveQueryParameter(r, "method", false)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: method. Valid values are: file or string", err}
}
switch method {
case "string":
return handler.createEdgeJobFromFileContent(w, r)
case "file":
return handler.createEdgeJobFromFile(w, r)
default:
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: method. Valid values are: file or string", errors.New(request.ErrInvalidQueryParameter)}
}
}
type edgeJobCreateFromFileContentPayload struct {
Name string
CronExpression string
Recurring bool
Endpoints []portainer.EndpointID
FileContent string
}
func (payload *edgeJobCreateFromFileContentPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
return errors.New("Invalid Edge job name")
}
if !govalidator.Matches(payload.Name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]*$`) {
return errors.New("Invalid Edge job name format. Allowed characters are: [a-zA-Z0-9_.-]")
}
if govalidator.IsNull(payload.CronExpression) {
return errors.New("Invalid cron expression")
}
if payload.Endpoints == nil || len(payload.Endpoints) == 0 {
return errors.New("Invalid endpoints payload")
}
if govalidator.IsNull(payload.FileContent) {
return errors.New("Invalid script file content")
}
return nil
}
func (handler *Handler) createEdgeJobFromFileContent(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload edgeJobCreateFromFileContentPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
edgeJob := handler.createEdgeJobObjectFromFileContentPayload(&payload)
err = handler.addAndPersistEdgeJob(edgeJob, []byte(payload.FileContent))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to schedule Edge job", err}
}
return response.JSON(w, edgeJob)
}
type edgeJobCreateFromFilePayload struct {
Name string
CronExpression string
Recurring bool
Endpoints []portainer.EndpointID
File []byte
}
func (payload *edgeJobCreateFromFilePayload) Validate(r *http.Request) error {
name, err := request.RetrieveMultiPartFormValue(r, "Name", false)
if err != nil {
return errors.New("Invalid Edge job name")
}
if !govalidator.Matches(name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]+$`) {
return errors.New("Invalid Edge job name format. Allowed characters are: [a-zA-Z0-9_.-]")
}
payload.Name = name
cronExpression, err := request.RetrieveMultiPartFormValue(r, "CronExpression", false)
if err != nil {
return errors.New("Invalid cron expression")
}
payload.CronExpression = cronExpression
var endpoints []portainer.EndpointID
err = request.RetrieveMultiPartFormJSONValue(r, "Endpoints", &endpoints, false)
if err != nil {
return errors.New("Invalid endpoints")
}
payload.Endpoints = endpoints
file, _, err := request.RetrieveMultiPartFormFile(r, "file")
if err != nil {
return errors.New("Invalid script file. Ensure that the file is uploaded correctly")
}
payload.File = file
return nil
}
func (handler *Handler) createEdgeJobFromFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
payload := &edgeJobCreateFromFilePayload{}
err := payload.Validate(r)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
edgeJob := handler.createEdgeJobObjectFromFilePayload(payload)
err = handler.addAndPersistEdgeJob(edgeJob, payload.File)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to schedule Edge job", err}
}
return response.JSON(w, edgeJob)
}
func (handler *Handler) createEdgeJobObjectFromFilePayload(payload *edgeJobCreateFromFilePayload) *portainer.EdgeJob {
edgeJobIdentifier := portainer.EdgeJobID(handler.DataStore.EdgeJob().GetNextIdentifier())
endpoints := convertEndpointsToMetaObject(payload.Endpoints)
edgeJob := &portainer.EdgeJob{
ID: edgeJobIdentifier,
Name: payload.Name,
CronExpression: payload.CronExpression,
Recurring: payload.Recurring,
Created: time.Now().Unix(),
Endpoints: endpoints,
Version: 1,
}
return edgeJob
}
func (handler *Handler) createEdgeJobObjectFromFileContentPayload(payload *edgeJobCreateFromFileContentPayload) *portainer.EdgeJob {
edgeJobIdentifier := portainer.EdgeJobID(handler.DataStore.EdgeJob().GetNextIdentifier())
endpoints := convertEndpointsToMetaObject(payload.Endpoints)
edgeJob := &portainer.EdgeJob{
ID: edgeJobIdentifier,
Name: payload.Name,
CronExpression: payload.CronExpression,
Recurring: payload.Recurring,
Created: time.Now().Unix(),
Endpoints: endpoints,
Version: 1,
}
return edgeJob
}
func (handler *Handler) addAndPersistEdgeJob(edgeJob *portainer.EdgeJob, file []byte) error {
edgeCronExpression := strings.Split(edgeJob.CronExpression, " ")
if len(edgeCronExpression) == 6 {
edgeCronExpression = edgeCronExpression[1:]
}
edgeJob.CronExpression = strings.Join(edgeCronExpression, " ")
for ID := range edgeJob.Endpoints {
endpoint, err := handler.DataStore.Endpoint().Endpoint(ID)
if err != nil {
return err
}
if endpoint.Type != portainer.EdgeAgentOnDockerEnvironment && endpoint.Type != portainer.EdgeAgentOnKubernetesEnvironment {
delete(edgeJob.Endpoints, ID)
}
}
if len(edgeJob.Endpoints) == 0 {
return errors.New("Endpoints are mandatory for an Edge job")
}
scriptPath, err := handler.FileService.StoreEdgeJobFileFromBytes(strconv.Itoa(int(edgeJob.ID)), file)
if err != nil {
return err
}
edgeJob.ScriptPath = scriptPath
for endpointID := range edgeJob.Endpoints {
handler.ReverseTunnelService.AddEdgeJob(endpointID, edgeJob)
}
return handler.DataStore.EdgeJob().CreateEdgeJob(edgeJob)
}
func convertEndpointsToMetaObject(endpoints []portainer.EndpointID) map[portainer.EndpointID]portainer.EdgeJobEndpointMeta {
endpointsMap := map[portainer.EndpointID]portainer.EdgeJobEndpointMeta{}
for _, endpointID := range endpoints {
endpointsMap[endpointID] = portainer.EdgeJobEndpointMeta{}
}
return endpointsMap
}

View File

@ -0,0 +1,41 @@
package edgejobs
import (
"net/http"
"strconv"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
)
func (handler *Handler) edgeJobDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge job identifier route variable", err}
}
edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge job with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err}
}
edgeJobFolder := handler.FileService.GetEdgeJobFolder(strconv.Itoa(edgeJobID))
err = handler.FileService.RemoveDirectory(edgeJobFolder)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the files associated to the Edge job on the filesystem", err}
}
handler.ReverseTunnelService.RemoveEdgeJob(edgeJob.ID)
err = handler.DataStore.EdgeJob().DeleteEdgeJob(edgeJob.ID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the Edge job from the database", err}
}
return response.Empty(w)
}

View File

@ -0,0 +1,37 @@
package edgejobs
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
)
type edgeJobFileResponse struct {
FileContent string `json:"FileContent"`
}
// GET request on /api/edge_jobs/:id/file
func (handler *Handler) edgeJobFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge job identifier route variable", err}
}
edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge job with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err}
}
edgeJobFileContent, err := handler.FileService.GetFileContent(edgeJob.ScriptPath)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge job script file from disk", err}
}
return response.JSON(w, &edgeJobFileResponse{FileContent: string(edgeJobFileContent)})
}

View File

@ -0,0 +1,43 @@
package edgejobs
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
)
type edgeJobInspectResponse struct {
*portainer.EdgeJob
Endpoints []portainer.EndpointID
}
func (handler *Handler) edgeJobInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge job identifier route variable", err}
}
edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge job with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err}
}
endpointIDs := []portainer.EndpointID{}
for endpointID := range edgeJob.Endpoints {
endpointIDs = append(endpointIDs, endpointID)
}
responseObj := edgeJobInspectResponse{
EdgeJob: edgeJob,
Endpoints: endpointIDs,
}
return response.JSON(w, responseObj)
}

View File

@ -0,0 +1,18 @@
package edgejobs
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
)
// GET request on /api/edge_jobs
func (handler *Handler) edgeJobList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeJobs, err := handler.DataStore.EdgeJob().EdgeJobs()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge jobs from the database", err}
}
return response.JSON(w, edgeJobs)
}

View File

@ -0,0 +1,53 @@
package edgejobs
import (
"net/http"
"strconv"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
)
// DELETE request on /api/edge_jobs/:id/tasks/:taskID/logs
func (handler *Handler) edgeJobTasksClear(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge job identifier route variable", err}
}
taskID, err := request.RetrieveNumericRouteVariableValue(r, "taskID")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Task identifier route variable", err}
}
edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge job with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err}
}
endpointID := portainer.EndpointID(taskID)
meta := edgeJob.Endpoints[endpointID]
meta.CollectLogs = false
meta.LogsStatus = portainer.EdgeJobLogsStatusIdle
edgeJob.Endpoints[endpointID] = meta
err = handler.FileService.ClearEdgeJobTaskLogs(strconv.Itoa(edgeJobID), strconv.Itoa(taskID))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to clear log file from disk", err}
}
handler.ReverseTunnelService.AddEdgeJob(endpointID, edgeJob)
err = handler.DataStore.EdgeJob().UpdateEdgeJob(edgeJob.ID, edgeJob)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Edge job changes in the database", err}
}
return response.Empty(w)
}

View File

@ -0,0 +1,47 @@
package edgejobs
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
)
// POST request on /api/edge_jobs/:id/tasks/:taskID/logs
func (handler *Handler) edgeJobTasksCollect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge job identifier route variable", err}
}
taskID, err := request.RetrieveNumericRouteVariableValue(r, "taskID")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Task identifier route variable", err}
}
edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge job with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err}
}
endpointID := portainer.EndpointID(taskID)
meta := edgeJob.Endpoints[endpointID]
meta.CollectLogs = true
meta.LogsStatus = portainer.EdgeJobLogsStatusPending
edgeJob.Endpoints[endpointID] = meta
handler.ReverseTunnelService.AddEdgeJob(endpointID, edgeJob)
err = handler.DataStore.EdgeJob().UpdateEdgeJob(edgeJob.ID, edgeJob)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Edge job changes in the database", err}
}
return response.Empty(w)
}

View File

@ -0,0 +1,36 @@
package edgejobs
import (
"net/http"
"strconv"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
)
type fileResponse struct {
FileContent string `json:"FileContent"`
}
// GET request on /api/edge_jobs/:id/tasks/:taskID/logs
func (handler *Handler) edgeJobTaskLogsInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge job identifier route variable", err}
}
taskID, err := request.RetrieveNumericRouteVariableValue(r, "taskID")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Task identifier route variable", err}
}
logFileContent, err := handler.FileService.GetEdgeJobTaskLogFileContent(strconv.Itoa(edgeJobID), strconv.Itoa(taskID))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve log file from disk", err}
}
return response.JSON(w, &fileResponse{FileContent: string(logFileContent)})
}
// fmt.Sprintf("/tmp/edge_jobs/%s/logs_%s", edgeJobID, taskID)

View File

@ -0,0 +1,48 @@
package edgejobs
import (
"fmt"
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
)
type taskContainer struct {
ID string `json:"Id"`
EndpointID portainer.EndpointID `json:"EndpointId"`
LogsStatus portainer.EdgeJobLogsStatus `json:"LogsStatus"`
}
// GET request on /api/edge_jobs/:id/tasks
func (handler *Handler) edgeJobTasksList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge job identifier route variable", err}
}
edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge job with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err}
}
tasks := make([]taskContainer, 0)
for endpointID, meta := range edgeJob.Endpoints {
cronTask := taskContainer{
ID: fmt.Sprintf("edgejob_task_%d_%d", edgeJob.ID, endpointID),
EndpointID: endpointID,
LogsStatus: meta.LogsStatus,
}
tasks = append(tasks, cronTask)
}
return response.JSON(w, tasks)
}

View File

@ -0,0 +1,120 @@
package edgejobs
import (
"errors"
"net/http"
"strconv"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
)
type edgeJobUpdatePayload struct {
Name *string
CronExpression *string
Recurring *bool
Endpoints []portainer.EndpointID
FileContent *string
}
func (payload *edgeJobUpdatePayload) Validate(r *http.Request) error {
if payload.Name != nil && !govalidator.Matches(*payload.Name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]+$`) {
return errors.New("Invalid Edge job name format. Allowed characters are: [a-zA-Z0-9_.-]")
}
return nil
}
func (handler *Handler) edgeJobUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge job identifier route variable", err}
}
var payload edgeJobUpdatePayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge job with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err}
}
err = handler.updateEdgeSchedule(edgeJob, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update Edge job", err}
}
err = handler.DataStore.EdgeJob().UpdateEdgeJob(edgeJob.ID, edgeJob)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Edge job changes inside the database", err}
}
return response.JSON(w, edgeJob)
}
func (handler *Handler) updateEdgeSchedule(edgeJob *portainer.EdgeJob, payload *edgeJobUpdatePayload) error {
if payload.Name != nil {
edgeJob.Name = *payload.Name
}
if payload.Endpoints != nil {
endpointsMap := map[portainer.EndpointID]portainer.EdgeJobEndpointMeta{}
for _, endpointID := range payload.Endpoints {
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
if err != nil {
return err
}
if endpoint.Type != portainer.EdgeAgentOnDockerEnvironment && endpoint.Type != portainer.EdgeAgentOnKubernetesEnvironment {
continue
}
if meta, ok := edgeJob.Endpoints[endpointID]; ok {
endpointsMap[endpointID] = meta
} else {
endpointsMap[endpointID] = portainer.EdgeJobEndpointMeta{}
}
}
edgeJob.Endpoints = endpointsMap
}
updateVersion := false
if payload.CronExpression != nil {
edgeJob.CronExpression = *payload.CronExpression
updateVersion = true
}
if payload.FileContent != nil {
_, err := handler.FileService.StoreEdgeJobFileFromBytes(strconv.Itoa(int(edgeJob.ID)), []byte(*payload.FileContent))
if err != nil {
return err
}
updateVersion = true
}
if payload.Recurring != nil {
edgeJob.Recurring = *payload.Recurring
updateVersion = true
}
if updateVersion {
edgeJob.Version++
}
for endpointID := range edgeJob.Endpoints {
handler.ReverseTunnelService.AddEdgeJob(endpointID, edgeJob)
}
return nil
}

View File

@ -0,0 +1,47 @@
package edgejobs
import (
"net/http"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
)
// Handler is the HTTP handler used to handle Edge job operations.
type Handler struct {
*mux.Router
DataStore portainer.DataStore
FileService portainer.FileService
ReverseTunnelService portainer.ReverseTunnelService
}
// NewHandler creates a handler to manage Edge job operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
}
h.Handle("/edge_jobs",
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobList)))).Methods(http.MethodGet)
h.Handle("/edge_jobs",
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobCreate)))).Methods(http.MethodPost)
h.Handle("/edge_jobs/{id}",
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobInspect)))).Methods(http.MethodGet)
h.Handle("/edge_jobs/{id}",
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobUpdate)))).Methods(http.MethodPut)
h.Handle("/edge_jobs/{id}",
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobDelete)))).Methods(http.MethodDelete)
h.Handle("/edge_jobs/{id}/file",
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobFile)))).Methods(http.MethodGet)
h.Handle("/edge_jobs/{id}/tasks",
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobTasksList)))).Methods(http.MethodGet)
h.Handle("/edge_jobs/{id}/tasks/{taskID}/logs",
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobTaskLogsInspect)))).Methods(http.MethodGet)
h.Handle("/edge_jobs/{id}/tasks/{taskID}/logs",
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobTasksCollect)))).Methods(http.MethodPost)
h.Handle("/edge_jobs/{id}/tasks/{taskID}/logs",
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobTasksClear)))).Methods(http.MethodDelete)
return h
}

View File

@ -13,6 +13,7 @@ import (
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/internal/edge"
)
// POST request on /api/endpoint_groups
@ -27,32 +28,32 @@ func (handler *Handler) edgeStackCreate(w http.ResponseWriter, r *http.Request)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Edge stack", err}
}
endpoints, err := handler.EndpointService.Endpoints()
endpoints, err := handler.DataStore.Endpoint().Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err}
}
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err}
}
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from database", err}
}
relatedEndpoints, err := portainer.EdgeStackRelatedEndpoints(edgeStack.EdgeGroups, endpoints, endpointGroups, edgeGroups)
relatedEndpoints, err := edge.EdgeStackRelatedEndpoints(edgeStack.EdgeGroups, endpoints, endpointGroups, edgeGroups)
for _, endpointID := range relatedEndpoints {
relation, err := handler.EndpointRelationService.EndpointRelation(endpointID)
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err}
}
relation.EdgeStacks[edgeStack.ID] = true
err = handler.EndpointRelationService.UpdateEndpointRelation(endpointID, relation)
err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpointID, relation)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err}
}
@ -81,13 +82,13 @@ type swarmStackFromFileContentPayload struct {
func (payload *swarmStackFromFileContentPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
return portainer.Error("Invalid stack name")
return errors.New("Invalid stack name")
}
if govalidator.IsNull(payload.StackFileContent) {
return portainer.Error("Invalid stack file content")
return errors.New("Invalid stack file content")
}
if payload.EdgeGroups == nil || len(payload.EdgeGroups) == 0 {
return portainer.Error("Edge Groups are mandatory for an Edge stack")
return errors.New("Edge Groups are mandatory for an Edge stack")
}
return nil
}
@ -104,7 +105,7 @@ func (handler *Handler) createSwarmStackFromFileContent(r *http.Request) (*porta
return nil, err
}
stackID := handler.EdgeStackService.GetNextIdentifier()
stackID := handler.DataStore.EdgeStack().GetNextIdentifier()
stack := &portainer.EdgeStack{
ID: portainer.EdgeStackID(stackID),
Name: payload.Name,
@ -122,7 +123,7 @@ func (handler *Handler) createSwarmStackFromFileContent(r *http.Request) (*porta
}
stack.ProjectPath = projectPath
err = handler.EdgeStackService.CreateEdgeStack(stack)
err = handler.DataStore.EdgeStack().CreateEdgeStack(stack)
if err != nil {
return nil, err
}
@ -143,19 +144,19 @@ type swarmStackFromGitRepositoryPayload struct {
func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
return portainer.Error("Invalid stack name")
return errors.New("Invalid stack name")
}
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
return portainer.Error("Invalid repository URL. Must correspond to a valid URL format")
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
}
if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) {
return portainer.Error("Invalid repository credentials. Username and password must be specified when authentication is enabled")
return errors.New("Invalid repository credentials. Username and password must be specified when authentication is enabled")
}
if govalidator.IsNull(payload.ComposeFilePathInRepository) {
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
}
if payload.EdgeGroups == nil || len(payload.EdgeGroups) == 0 {
return portainer.Error("Edge Groups are mandatory for an Edge stack")
return errors.New("Edge Groups are mandatory for an Edge stack")
}
return nil
}
@ -172,7 +173,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(r *http.Request) (*por
return nil, err
}
stackID := handler.EdgeStackService.GetNextIdentifier()
stackID := handler.DataStore.EdgeStack().GetNextIdentifier()
stack := &portainer.EdgeStack{
ID: portainer.EdgeStackID(stackID),
Name: payload.Name,
@ -200,7 +201,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(r *http.Request) (*por
return nil, err
}
err = handler.EdgeStackService.CreateEdgeStack(stack)
err = handler.DataStore.EdgeStack().CreateEdgeStack(stack)
if err != nil {
return nil, err
}
@ -217,20 +218,20 @@ type swarmStackFromFileUploadPayload struct {
func (payload *swarmStackFromFileUploadPayload) Validate(r *http.Request) error {
name, err := request.RetrieveMultiPartFormValue(r, "Name", false)
if err != nil {
return portainer.Error("Invalid stack name")
return errors.New("Invalid stack name")
}
payload.Name = name
composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "file")
if err != nil {
return portainer.Error("Invalid Compose file. Ensure that the Compose file is uploaded correctly")
return errors.New("Invalid Compose file. Ensure that the Compose file is uploaded correctly")
}
payload.StackFileContent = composeFileContent
var edgeGroups []portainer.EdgeGroupID
err = request.RetrieveMultiPartFormJSONValue(r, "EdgeGroups", &edgeGroups, false)
if err != nil || len(edgeGroups) == 0 {
return portainer.Error("Edge Groups are mandatory for an Edge stack")
return errors.New("Edge Groups are mandatory for an Edge stack")
}
payload.EdgeGroups = edgeGroups
return nil
@ -248,7 +249,7 @@ func (handler *Handler) createSwarmStackFromFileUpload(r *http.Request) (*portai
return nil, err
}
stackID := handler.EdgeStackService.GetNextIdentifier()
stackID := handler.DataStore.EdgeStack().GetNextIdentifier()
stack := &portainer.EdgeStack{
ID: portainer.EdgeStackID(stackID),
Name: payload.Name,
@ -266,7 +267,7 @@ func (handler *Handler) createSwarmStackFromFileUpload(r *http.Request) (*portai
}
stack.ProjectPath = projectPath
err = handler.EdgeStackService.CreateEdgeStack(stack)
err = handler.DataStore.EdgeStack().CreateEdgeStack(stack)
if err != nil {
return nil, err
}
@ -275,14 +276,14 @@ func (handler *Handler) createSwarmStackFromFileUpload(r *http.Request) (*portai
}
func (handler *Handler) validateUniqueName(name string) error {
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks()
if err != nil {
return err
}
for _, stack := range edgeStacks {
if strings.EqualFold(stack.Name, name) {
return portainer.Error("Edge stack name must be unique")
return errors.New("Edge stack name must be unique")
}
}
return nil

View File

@ -7,6 +7,8 @@ import (
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/internal/edge"
)
func (handler *Handler) edgeStackDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
@ -15,44 +17,44 @@ func (handler *Handler) edgeStackDelete(w http.ResponseWriter, r *http.Request)
return &httperror.HandlerError{http.StatusBadRequest, "Invalid edge stack identifier route variable", err}
}
edgeStack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(edgeStackID))
if err == portainer.ErrObjectNotFound {
edgeStack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(edgeStackID))
if err == errors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge stack with the specified identifier inside the database", err}
}
err = handler.EdgeStackService.DeleteEdgeStack(portainer.EdgeStackID(edgeStackID))
err = handler.DataStore.EdgeStack().DeleteEdgeStack(portainer.EdgeStackID(edgeStackID))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the edge stack from the database", err}
}
endpoints, err := handler.EndpointService.Endpoints()
endpoints, err := handler.DataStore.Endpoint().Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err}
}
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err}
}
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from database", err}
}
relatedEndpoints, err := portainer.EdgeStackRelatedEndpoints(edgeStack.EdgeGroups, endpoints, endpointGroups, edgeGroups)
relatedEndpoints, err := edge.EdgeStackRelatedEndpoints(edgeStack.EdgeGroups, endpoints, endpointGroups, edgeGroups)
for _, endpointID := range relatedEndpoints {
relation, err := handler.EndpointRelationService.EndpointRelation(endpointID)
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err}
}
delete(relation.EdgeStacks, edgeStack.ID)
err = handler.EndpointRelationService.UpdateEndpointRelation(endpointID, relation)
err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpointID, relation)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err}
}

View File

@ -8,6 +8,7 @@ import (
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
)
type stackFileResponse struct {
@ -21,8 +22,8 @@ func (handler *Handler) edgeStackFile(w http.ResponseWriter, r *http.Request) *h
return &httperror.HandlerError{http.StatusBadRequest, "Invalid edge stack identifier route variable", err}
}
stack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(stackID))
if err == portainer.ErrObjectNotFound {
stack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID))
if err == errors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge stack with the specified identifier inside the database", err}

View File

@ -7,6 +7,7 @@ import (
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
)
func (handler *Handler) edgeStackInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
@ -15,8 +16,8 @@ func (handler *Handler) edgeStackInspect(w http.ResponseWriter, r *http.Request)
return &httperror.HandlerError{http.StatusBadRequest, "Invalid edge stack identifier route variable", err}
}
edgeStack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(edgeStackID))
if err == portainer.ErrObjectNotFound {
edgeStack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(edgeStackID))
if err == errors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge stack with the specified identifier inside the database", err}

View File

@ -8,7 +8,7 @@ import (
)
func (handler *Handler) edgeStackList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stacks from the database", err}
}

View File

@ -1,6 +1,7 @@
package edgestacks
import (
"errors"
"net/http"
"github.com/asaskevich/govalidator"
@ -8,6 +9,7 @@ import (
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
)
type updateStatusPayload struct {
@ -18,13 +20,13 @@ type updateStatusPayload struct {
func (payload *updateStatusPayload) Validate(r *http.Request) error {
if payload.Status == nil {
return portainer.Error("Invalid status")
return errors.New("Invalid status")
}
if payload.EndpointID == nil {
return portainer.Error("Invalid EndpointID")
return errors.New("Invalid EndpointID")
}
if *payload.Status == portainer.StatusError && govalidator.IsNull(payload.Error) {
return portainer.Error("Error message is mandatory when status is error")
return errors.New("Error message is mandatory when status is error")
}
return nil
}
@ -35,8 +37,8 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
}
stack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(stackID))
if err == portainer.ErrObjectNotFound {
stack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
@ -48,8 +50,8 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(*payload.EndpointID))
if err == portainer.ErrObjectNotFound {
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(*payload.EndpointID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
@ -66,7 +68,7 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req
EndpointID: *payload.EndpointID,
}
err = handler.EdgeStackService.UpdateEdgeStack(stack.ID, stack)
err = handler.DataStore.EdgeStack().UpdateEdgeStack(stack.ID, stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err}
}

View File

@ -1,6 +1,7 @@
package edgestacks
import (
"errors"
"net/http"
"strconv"
@ -9,6 +10,8 @@ import (
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/internal/edge"
)
type updateEdgeStackPayload struct {
@ -20,10 +23,10 @@ type updateEdgeStackPayload struct {
func (payload *updateEdgeStackPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.StackFileContent) {
return portainer.Error("Invalid stack file content")
return errors.New("Invalid stack file content")
}
if payload.EdgeGroups != nil && len(payload.EdgeGroups) == 0 {
return portainer.Error("Edge Groups are mandatory for an Edge stack")
return errors.New("Edge Groups are mandatory for an Edge stack")
}
return nil
}
@ -34,8 +37,8 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
}
stack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(stackID))
if err == portainer.ErrObjectNotFound {
stack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
@ -48,27 +51,27 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
}
if payload.EdgeGroups != nil {
endpoints, err := handler.EndpointService.Endpoints()
endpoints, err := handler.DataStore.Endpoint().Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err}
}
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err}
}
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from database", err}
}
oldRelated, err := portainer.EdgeStackRelatedEndpoints(stack.EdgeGroups, endpoints, endpointGroups, edgeGroups)
oldRelated, err := edge.EdgeStackRelatedEndpoints(stack.EdgeGroups, endpoints, endpointGroups, edgeGroups)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack related endpoints from database", err}
}
newRelated, err := portainer.EdgeStackRelatedEndpoints(payload.EdgeGroups, endpoints, endpointGroups, edgeGroups)
newRelated, err := edge.EdgeStackRelatedEndpoints(payload.EdgeGroups, endpoints, endpointGroups, edgeGroups)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack related endpoints from database", err}
}
@ -84,14 +87,14 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
}
for endpointID := range endpointsToRemove {
relation, err := handler.EndpointRelationService.EndpointRelation(endpointID)
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err}
}
delete(relation.EdgeStacks, stack.ID)
err = handler.EndpointRelationService.UpdateEndpointRelation(endpointID, relation)
err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpointID, relation)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err}
}
@ -105,14 +108,14 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
}
for endpointID := range endpointsToAdd {
relation, err := handler.EndpointRelationService.EndpointRelation(endpointID)
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err}
}
relation.EdgeStacks[stack.ID] = true
err = handler.EndpointRelationService.UpdateEndpointRelation(endpointID, relation)
err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpointID, relation)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err}
}
@ -137,7 +140,7 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
stack.Status = map[portainer.EndpointID]portainer.EdgeStackStatus{}
}
err = handler.EdgeStackService.UpdateEdgeStack(stack.ID, stack)
err = handler.DataStore.EdgeStack().UpdateEdgeStack(stack.ID, stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err}
}

View File

@ -12,14 +12,10 @@ import (
// Handler is the HTTP handler used to handle endpoint group operations.
type Handler struct {
*mux.Router
requestBouncer *security.RequestBouncer
EdgeGroupService portainer.EdgeGroupService
EdgeStackService portainer.EdgeStackService
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
EndpointRelationService portainer.EndpointRelationService
FileService portainer.FileService
GitService portainer.GitService
requestBouncer *security.RequestBouncer
DataStore portainer.DataStore
FileService portainer.FileService
GitService portainer.GitService
}
// NewHandler creates a handler to manage endpoint group operations.
@ -29,17 +25,17 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
requestBouncer: bouncer,
}
h.Handle("/edge_stacks",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackCreate))).Methods(http.MethodPost)
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackCreate)))).Methods(http.MethodPost)
h.Handle("/edge_stacks",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackList))).Methods(http.MethodGet)
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackList)))).Methods(http.MethodGet)
h.Handle("/edge_stacks/{id}",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackInspect))).Methods(http.MethodGet)
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackInspect)))).Methods(http.MethodGet)
h.Handle("/edge_stacks/{id}",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackUpdate))).Methods(http.MethodPut)
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackUpdate)))).Methods(http.MethodPut)
h.Handle("/edge_stacks/{id}",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackDelete))).Methods(http.MethodDelete)
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackDelete)))).Methods(http.MethodDelete)
h.Handle("/edge_stacks/{id}/file",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackFile))).Methods(http.MethodGet)
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackFile)))).Methods(http.MethodGet)
h.Handle("/edge_stacks/{id}/status",
bouncer.PublicAccess(httperror.LoggerHandler(h.edgeStackStatusUpdate))).Methods(http.MethodPut)
return h

View File

@ -2,7 +2,6 @@ package edgetemplates
import (
"encoding/json"
"log"
"net/http"
httperror "github.com/portainer/libhttp/error"
@ -11,35 +10,39 @@ import (
"github.com/portainer/portainer/api/http/client"
)
type templateFileFormat struct {
Version string `json:"version"`
Templates []portainer.Template `json:"templates"`
}
// GET request on /api/edgetemplates
func (handler *Handler) edgeTemplateList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
settings, err := handler.SettingsService.Settings()
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
}
url := portainer.EdgeTemplatesURL
url := portainer.DefaultTemplatesURL
if settings.TemplatesURL != "" {
url = settings.TemplatesURL
}
var templateData []byte
templateData, err = client.Get(url, 0)
templateData, err = client.Get(url, 10)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve external templates", err}
}
var templates []portainer.Template
var templateFile templateFileFormat
err = json.Unmarshal(templateData, &templates)
err = json.Unmarshal(templateData, &templateFile)
if err != nil {
log.Printf("[DEBUG] [http,edge,templates] [failed parsing edge templates] [body: %s]", templateData)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to parse external templates", err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to parse template file", err}
}
filteredTemplates := []portainer.Template{}
filteredTemplates := make([]portainer.Template, 0)
for _, template := range templates {
for _, template := range templateFile.Templates {
if template.Type == portainer.EdgeStackTemplate {
filteredTemplates = append(filteredTemplates, template)
}

View File

@ -13,8 +13,8 @@ import (
// Handler is the HTTP handler used to handle edge endpoint operations.
type Handler struct {
*mux.Router
requestBouncer *security.RequestBouncer
SettingsService portainer.SettingsService
requestBouncer *security.RequestBouncer
DataStore portainer.DataStore
}
// NewHandler creates a handler to manage endpoint operations.

View File

@ -0,0 +1,78 @@
package endpointedge
import (
"net/http"
"strconv"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
)
type logsPayload struct {
FileContent string
}
func (payload *logsPayload) Validate(r *http.Request) error {
return nil
}
// POST request on api/endpoints/:id/edge/jobs/:jobID/logs
func (handler *Handler) endpointEdgeJobsLogs(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "jobID")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid edge job identifier route variable", err}
}
var payload logsPayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge job with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge job with the specified identifier inside the database", err}
}
err = handler.FileService.StoreEdgeJobTaskLogFileFromBytes(strconv.Itoa(edgeJobID), strconv.Itoa(endpointID), []byte(payload.FileContent))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to save task log to the filesystem", err}
}
meta := edgeJob.Endpoints[endpoint.ID]
meta.CollectLogs = false
meta.LogsStatus = portainer.EdgeJobLogsStatusCollected
edgeJob.Endpoints[endpoint.ID] = meta
err = handler.DataStore.EdgeJob().UpdateEdgeJob(edgeJob.ID, edgeJob)
handler.ReverseTunnelService.AddEdgeJob(endpoint.ID, edgeJob)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist edge job changes to the database", err}
}
return response.JSON(w, nil)
}

View File

@ -8,6 +8,7 @@ import (
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
)
type configResponse struct {
@ -23,8 +24,8 @@ func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http.
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrObjectNotFound {
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if err == errors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
@ -40,8 +41,8 @@ func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http.
return &httperror.HandlerError{http.StatusBadRequest, "Invalid edge stack identifier route variable", err}
}
edgeStack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(edgeStackID))
if err == portainer.ErrObjectNotFound {
edgeStack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(edgeStackID))
if err == errors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge stack with the specified identifier inside the database", err}

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