diff --git a/.babelrc b/.babelrc index 6db8c8c05..e85c6b12a 100644 --- a/.babelrc +++ b/.babelrc @@ -5,7 +5,8 @@ "@babel/preset-env", { "modules": false, - "useBuiltIns": "entry" + "useBuiltIns": "entry", + "corejs": "2" } ] ] diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md index 00b13e087..52a031d48 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -1,6 +1,10 @@ --- name: Bug report about: Create a bug report +title: '' +labels: bug/need-confirmation, kind/bug +assignees: '' + --- **Bug description** @@ -27,7 +31,7 @@ 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) +You can see how [here](https://documentation.portainer.io/archive/1.23.2/faq/#how-do-i-get-the-logs-from-portainer) **Steps to reproduce the issue:** @@ -40,9 +44,12 @@ You can see how [here](https://portainer.readthedocs.io/en/stable/faq.html#how-d - Portainer version: - Docker version (managed by Portainer): +- Kubernetes version (managed by Portainer): - Platform (windows/linux): - Command used to start Portainer (`docker run -p 9000:9000 portainer/portainer`): - Browser: +- Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commerical setup. +- Have you reviewed our technical documentation and knowledge base? Yes/No **Additional context** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/Custom.md b/.github/ISSUE_TEMPLATE/Custom.md index b4d55f559..a25a333e2 100644 --- a/.github/ISSUE_TEMPLATE/Custom.md +++ b/.github/ISSUE_TEMPLATE/Custom.md @@ -1,17 +1,25 @@ ---- -name: Question -about: Ask us a question about Portainer usage or deployment - ---- - - - -**Question**: -How can I deploy Portainer on... ? +--- +name: Question +about: Ask us a question about Portainer usage or deployment +title: '' +labels: '' +assignees: '' + +--- +Before you start, we need a little bit more information from you: + +Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commerical setup. + +Have you reviewed our technical documentation and knowledge base? Yes/No + + + +**Question**: +How can I deploy Portainer on... ? diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md index 6da5f4265..8c2bee587 100644 --- a/.github/ISSUE_TEMPLATE/Feature_request.md +++ b/.github/ISSUE_TEMPLATE/Feature_request.md @@ -1,31 +1,34 @@ ---- -name: Feature request -about: Suggest a feature/enhancement that should be added in Portainer - ---- - - - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. +--- +name: Feature request +about: Suggest a feature/enhancement that should be added in Portainer +title: '' +labels: '' +assignees: '' + +--- + + + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/stale.yml b/.github/stale.yml index a5d534d01..fce5d3700 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -15,6 +15,7 @@ issues: - kind/question - kind/style - kind/workaround + - kind/refactor - bug/need-confirmation - bug/confirmed - status/discuss diff --git a/.github/workflows/rebase.yml b/.github/workflows/rebase.yml new file mode 100644 index 000000000..db5203798 --- /dev/null +++ b/.github/workflows/rebase.yml @@ -0,0 +1,19 @@ +name: Automatic Rebase +on: + issue_comment: + types: [created] +jobs: + rebase: + name: Rebase + if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase') + runs-on: ubuntu-latest + steps: + - name: Checkout the latest code + uses: actions/checkout@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 # otherwise, you will fail to push refs to dest repo + - name: Automatic Rebase + uses: cirrus-actions/rebase@1.4 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index d0ac052cc..6f3dc2d33 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ api/cmd/portainer/portainer* **/.vscode/tasks.json .eslintcache +__debug_bin + +api/docs diff --git a/.vscode.example/launch.json b/.vscode.example/launch.json new file mode 100644 index 000000000..1d0e1fd51 --- /dev/null +++ b/.vscode.example/launch.json @@ -0,0 +1,19 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${workspaceRoot}/api/cmd/portainer/main.go", + "cwd": "${workspaceRoot}", + "env": {}, + "showLog": true, + "args": ["--data", "${env:HOME}/portainer-data", "--assets", "${workspaceRoot}/dist"] + } + ] +} diff --git a/.vscode/portainer.code-snippets b/.vscode.example/portainer.code-snippets similarity index 83% rename from .vscode/portainer.code-snippets rename to .vscode.example/portainer.code-snippets index 6f622dc6b..fefb732ce 100644 --- a/.vscode/portainer.code-snippets +++ b/.vscode.example/portainer.code-snippets @@ -21,11 +21,11 @@ "description": "Dummy Angularjs Component", "body": [ "import angular from 'angular';", - "import ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}Controller from './${TM_FILENAME_BASE}Controller'", + "import 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,", "});", "" ] @@ -44,25 +44,6 @@ ], "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", @@ -158,5 +139,29 @@ "export default $1;", "angular.module('portainer.${TM_DIRECTORY/.*\\/app\\/([^\\/]*)(\\/.*)?$/$1/}').service('$1', $1);" ] + }, + "swagger-api-doc": { + "prefix": "swapi", + "scope": "go", + "description": "Snippet for a api doc", + "body": [ + "// @id ", + "// @summary ", + "// @description ", + "// @description **Access policy**: ", + "// @tags ", + "// @security jwt", + "// @accept json", + "// @produce json", + "// @param id path int true \"identifier\"", + "// @param body body Object true \"details\"", + "// @success 200 {object} portainer. \"Success\"", + "// @success 204 \"Success\"", + "// @failure 400 \"Invalid request\"", + "// @failure 403 \"Permission denied\"", + "// @failure 404 \" not found\"", + "// @failure 500 \"Server error\"", + "// @router /{id} [get]" + ] } } diff --git a/ATTRIBUTIONS.md b/ATTRIBUTIONS.md new file mode 100644 index 000000000..8758a541a --- /dev/null +++ b/ATTRIBUTIONS.md @@ -0,0 +1,32 @@ +# Open Source License Attribution + +This application uses Open Source components. You can find the source +code of their open source projects along with license information below. +We acknowledge and are grateful to these developers for their contributions +to open source. + +### [angular-json-tree](https://github.com/awendland/angular-json-tree) + +by [Alex Wendland](https://github.com/awendland) is licensed under [CC BY 4.0 License](https://creativecommons.org/licenses/by/4.0/) + +### [caniuse-db](https://github.com/Fyrd/caniuse) + +by [caniuse.com](caniuse.com) is licensed under [CC BY 4.0 License](https://creativecommons.org/licenses/by/4.0/) + +### [caniuse-lite](https://github.com/ben-eb/caniuse-lite) + +by [caniuse.com](caniuse.com) is licensed under [CC BY 4.0 License](https://creativecommons.org/licenses/by/4.0/) + +### [spdx-exceptions](https://github.com/jslicense/spdx-exceptions.json) + +by Kyle Mitchell using [SPDX](https://spdx.dev/) from Linux Foundation licensed under [CC BY 3.0 License](https://creativecommons.org/licenses/by/3.0/) + +### [fontawesome-free](https://github.com/FortAwesome/Font-Awesome) Icons + +by [Fort Awesome](https://fortawesome.com/) is licensed under [CC BY 4.0 License](https://creativecommons.org/licenses/by/4.0/) + +Portainer also contains the following code, which is licensed under the [MIT license](https://opensource.org/licenses/MIT): + +UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (portainer.io) + +rdash-angular: Copyright (c) [2014][elliot hesp] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 537ae511f..0a9e336cf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,3 +74,62 @@ Our contribution process is described below. Some of the steps can be visualized The feature request process is similar to the bug report process but has an extra functional validation before the technical validation as well as a documentation validation before the testing phase. ![portainer_featurerequest_workflow](https://user-images.githubusercontent.com/5485061/45727229-5ad39f00-bbf5-11e8-9550-16ba66c50615.png) + +## Build Portainer locally + +Ensure you have Docker, Node.js, yarn, and Golang installed in the correct versions. + +Install dependencies with yarn: + +```sh +$ yarn +``` + +Then build and run the project: + +```sh +$ yarn start +``` + +Portainer can now be accessed at . + +Find more detailed steps at . + +## Adding api docs + +When adding a new resource (or a route handler), we should add a new tag to api/http/handler/handler.go#L136 like this: + +``` +// @tag.name +// @tag.description a short description +``` + +When adding a new route to an existing handler use the following as a template (you can use `swapi` snippet if you're using vscode): + +``` +// @id +// @summary +// @description +// @description **Access policy**: +// @tags +// @security jwt +// @accept json +// @produce json +// @param id path int true "identifier" +// @param body body Object true "details" +// @success 200 {object} portainer. "Success" +// @success 204 "Success" +// @failure 400 "Invalid request" +// @failure 403 "Permission denied" +// @failure 404 " not found" +// @failure 500 "Server error" +// @router /{id} [get] +``` + +explanation about each line can be found (here)[https://github.com/swaggo/swag#api-operation] + +## Licensing + +See the [LICENSE](https://github.com/portainer/portainer/blob/develop/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. + +We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/README.md b/README.md index 010c8ffa1..deb337483 100644 --- a/README.md +++ b/README.md @@ -28,14 +28,15 @@ Unlike the public demo, the playground sessions are deleted after 4 hours. Apart ## Getting started -- [Deploy Portainer](https://www.portainer.io/installation/) +- [Deploy Portainer](https://documentation.portainer.io/quickstart/) - [Documentation](https://documentation.portainer.io) +- [Building Portainer](https://documentation.portainer.io/contributing/instructions/) ## Getting help -For FORMAL Support, please purchase a support subscription from here: https://www.portainer.io/products-services/portainer-business-support/ +For FORMAL Support, please purchase a support subscription from here: https://www.portainer.io/products/portainer-business -For community support: You can find more information about Portainer's community support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/ +For community support: You can find more information about Portainer's community support framework policy here: https://www.portainer.io/products/community-edition/customer-success - Issues: https://github.com/portainer/portainer/issues - FAQ: https://documentation.portainer.io @@ -44,7 +45,7 @@ For community support: You can find more information about Portainer's community ## Reporting bugs and contributing - Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new). -- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://www.portainer.io/documentation/how-to-contribute/) to build it locally and make a pull request. We need all the help we can get! +- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://documentation.portainer.io/contributing/instructions/) to build it locally and make a pull request. We need all the help we can get! ## Security @@ -52,7 +53,7 @@ For community support: You can find more information about Portainer's community ## Privacy -**To make sure we focus our development effort in the right places we need to know which features get used most often. To give us this information we use [Matamo Analytics](https://matomo.org/), which is hosted in Germany and is fully GDPR compliant.** +**To make sure we focus our development effort in the right places we need to know which features get used most often. To give us this information we use [Matomo Analytics](https://matomo.org/), which is hosted in Germany and is fully GDPR compliant.** When Portainer first starts, you are given the option to DISABLE analytics. If you **don't** choose to disable it, we collect anonymous usage as per [our privacy policy](https://www.portainer.io/documentation/in-app-analytics-and-privacy-policy/). **Please note**, there is no personally identifiable information sent or stored at any time and we only use the data to help us improve Portainer. @@ -64,8 +65,4 @@ Portainer supports "Current - 2 docker versions only. Prior versions may operate Portainer is licensed under the zlib license. See [LICENSE](./LICENSE) for reference. -Portainer also contains the following code, which is licensed under the [MIT license](https://opensource.org/licenses/MIT): - -UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (portainer.io) - -rdash-angular: Copyright (c) [2014][elliot hesp] +Portainer also contains code from open source projects. See [ATTRIBUTIONS.md](./ATTRIBUTIONS.md) for a list. diff --git a/api/api-description.md b/api/api-description.md new file mode 100644 index 000000000..030687259 --- /dev/null +++ b/api/api-description.md @@ -0,0 +1,53 @@ +Portainer API is an HTTP API served by Portainer. It is used by the Portainer UI and everything you can do with the UI can be done using the HTTP API. +Examples are available at https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8 +You can find out more about Portainer at [http://portainer.io](http://portainer.io) and get some support on [Slack](http://portainer.io/slack/). + +# Authentication + +Most of the API endpoints require to be authenticated as well as some level of authorization to be used. +Portainer API uses JSON Web Token to manage authentication and thus requires you to provide a token in the **Authorization** header of each request +with the **Bearer** authentication mechanism. + +Example: + +``` +Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsImV4cCI6MTQ5OTM3NjE1NH0.NJ6vE8FY1WG6jsRQzfMqeatJ4vh2TWAeeYfDhP71YEE +``` + +# Security + +Each API endpoint has an associated access policy, it is documented in the description of each endpoint. + +Different access policies are available: + +- Public access +- Authenticated access +- Restricted access +- Administrator access + +### Public access + +No authentication is required to access the endpoints with this access policy. + +### Authenticated access + +Authentication is required to access the endpoints with this access policy. + +### Restricted access + +Authentication is required to access the endpoints with this access policy. +Extra-checks might be added to ensure access to the resource is granted. Returned data might also be filtered. + +### Administrator access + +Authentication as well as an administrator role are required to access the endpoints with this access policy. + +# Execute Docker requests + +Portainer **DO NOT** expose specific endpoints to manage your Docker resources (create a container, remove a volume, etc...). + +Instead, it acts as a reverse-proxy to the Docker HTTP API. This means that you can execute Docker requests **via** the Portainer HTTP API. + +To do so, you can use the `/endpoints/{id}/docker` Portainer API endpoint (which is not documented below due to Swagger limitations). This endpoint has a restricted access policy so you still need to be authenticated to be able to query this endpoint. Any query on this endpoint will be proxied to the Docker API of the associated endpoint (requests and responses objects are the same as documented in the Docker API). + +**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8). diff --git a/api/bolt/datastore.go b/api/bolt/datastore.go index 9df787d71..77e6adac1 100644 --- a/api/bolt/datastore.go +++ b/api/bolt/datastore.go @@ -6,7 +6,7 @@ import ( "time" "github.com/boltdb/bolt" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/customtemplate" "github.com/portainer/portainer/api/bolt/dockerhub" "github.com/portainer/portainer/api/bolt/edgegroup" @@ -69,6 +69,14 @@ type Store struct { WebhookService *webhook.Service } +func (store *Store) edition() portainer.SoftwareEdition { + edition, err := store.VersionService.Edition() + if err == errors.ErrObjectNotFound { + edition = portainer.PortainerCE + } + return edition +} + // NewStore initializes a new Store and the associated services func NewStore(storePath string, fileService portainer.FileService) (*Store, error) { store := &Store{ @@ -116,6 +124,14 @@ func (store *Store) IsNew() bool { return store.isNew } +// CheckCurrentEdition checks if current edition is community edition +func (store *Store) CheckCurrentEdition() error { + if store.edition() != portainer.PortainerCE { + return errors.ErrWrongDBEdition + } + return nil +} + // MigrateData automatically migrate the data based on the DBVersion. // This process is only triggered on an existing database, not if the database was just created. func (store *Store) MigrateData() error { diff --git a/api/bolt/errors/errors.go b/api/bolt/errors/errors.go index c9a142189..8c171686d 100644 --- a/api/bolt/errors/errors.go +++ b/api/bolt/errors/errors.go @@ -4,4 +4,5 @@ import "errors" var ( ErrObjectNotFound = errors.New("Object not found inside the database") + ErrWrongDBEdition = errors.New("The Portainer database is set for Portainer Business Edition, please follow the instructions in our documention to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/") ) diff --git a/api/bolt/init.go b/api/bolt/init.go index b67c39df0..7ce23f138 100644 --- a/api/bolt/init.go +++ b/api/bolt/init.go @@ -40,18 +40,11 @@ func (store *Store) Init() error { 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, + OAuthSettings: portainer.OAuthSettings{}, + + EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds, + TemplatesURL: portainer.DefaultTemplatesURL, + UserSessionTimeout: portainer.DefaultUserSessionTimeout, } err = store.SettingsService.UpdateSettings(defaultSettings) diff --git a/api/bolt/migrator/migrate_dbversion25.go b/api/bolt/migrator/migrate_dbversion25.go new file mode 100644 index 000000000..98fbb083a --- /dev/null +++ b/api/bolt/migrator/migrate_dbversion25.go @@ -0,0 +1,51 @@ +package migrator + +import ( + portainer "github.com/portainer/portainer/api" +) + +func (m *Migrator) updateEndpointSettingsToDB25() error { + settings, err := m.settingsService.Settings() + if err != nil { + return err + } + + endpoints, err := m.endpointService.Endpoints() + if err != nil { + return err + } + + for i := range endpoints { + endpoint := endpoints[i] + + securitySettings := portainer.EndpointSecuritySettings{} + + if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || + endpoint.Type == portainer.AgentOnDockerEnvironment || + endpoint.Type == portainer.DockerEnvironment { + + securitySettings = portainer.EndpointSecuritySettings{ + AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers, + AllowContainerCapabilitiesForRegularUsers: settings.AllowContainerCapabilitiesForRegularUsers, + AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers, + AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers, + AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers, + AllowStackManagementForRegularUsers: settings.AllowStackManagementForRegularUsers, + } + + if endpoint.Type == portainer.AgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnDockerEnvironment { + securitySettings.AllowVolumeBrowserForRegularUsers = settings.AllowVolumeBrowserForRegularUsers + securitySettings.EnableHostManagementFeatures = settings.EnableHostManagementFeatures + } + } + + endpoint.SecuritySettings = securitySettings + + err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint) + if err != nil { + return err + } + } + + return nil +} diff --git a/api/bolt/migrator/migrate_dbversion26.go b/api/bolt/migrator/migrate_dbversion26.go new file mode 100644 index 000000000..c073532f2 --- /dev/null +++ b/api/bolt/migrator/migrate_dbversion26.go @@ -0,0 +1,40 @@ +package migrator + +import ( + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" + "github.com/portainer/portainer/api/internal/stackutils" +) + +func (m *Migrator) updateStackResourceControlToDB27() error { + resourceControls, err := m.resourceControlService.ResourceControls() + if err != nil { + return err + } + + for _, resource := range resourceControls { + if resource.Type != portainer.StackResourceControl { + continue + } + + stackName := resource.ResourceID + + stack, err := m.stackService.StackByName(stackName) + if err != nil { + if err == errors.ErrObjectNotFound { + continue + } + + return err + } + + resource.ResourceID = stackutils.ResourceControlID(stack.EndpointID, stack.Name) + + err = m.resourceControlService.UpdateResourceControl(resource.ID, &resource) + if err != nil { + return err + } + } + + return nil +} diff --git a/api/bolt/migrator/migrator.go b/api/bolt/migrator/migrator.go index 8217dc302..e366bd3df 100644 --- a/api/bolt/migrator/migrator.go +++ b/api/bolt/migrator/migrator.go @@ -2,7 +2,7 @@ package migrator import ( "github.com/boltdb/bolt" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/endpoint" "github.com/portainer/portainer/api/bolt/endpointgroup" "github.com/portainer/portainer/api/bolt/endpointrelation" @@ -342,5 +342,21 @@ func (m *Migrator) Migrate() error { } } + // Portainer 2.1.0 + if m.currentDBVersion < 26 { + err := m.updateEndpointSettingsToDB25() + if err != nil { + return err + } + } + + // Portainer 2.2.0 + if m.currentDBVersion < 27 { + err := m.updateStackResourceControlToDB27() + if err != nil { + return err + } + } + return m.versionService.StoreDBVersion(portainer.DBVersion) } diff --git a/api/bolt/team/team.go b/api/bolt/team/team.go index e88e6a3f8..a503e8285 100644 --- a/api/bolt/team/team.go +++ b/api/bolt/team/team.go @@ -1,7 +1,9 @@ package team import ( - "github.com/portainer/portainer/api" + "strings" + + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/bolt/internal" @@ -58,7 +60,7 @@ func (service *Service) TeamByName(name string) (*portainer.Team, error) { return err } - if t.Name == name { + if strings.EqualFold(t.Name, name) { team = &t break } diff --git a/api/bolt/user/user.go b/api/bolt/user/user.go index fa0c5c151..de628e8d4 100644 --- a/api/bolt/user/user.go +++ b/api/bolt/user/user.go @@ -1,7 +1,9 @@ package user import ( - "github.com/portainer/portainer/api" + "strings" + + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/bolt/internal" @@ -47,6 +49,8 @@ func (service *Service) User(ID portainer.UserID) (*portainer.User, error) { func (service *Service) UserByUsername(username string) (*portainer.User, error) { var user *portainer.User + username = strings.ToLower(username) + err := service.db.View(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(BucketName)) cursor := bucket.Cursor() @@ -58,7 +62,7 @@ func (service *Service) UserByUsername(username string) (*portainer.User, error) return err } - if u.Username == username { + if strings.EqualFold(u.Username, username) { user = &u break } @@ -123,6 +127,7 @@ func (service *Service) UsersByRole(role portainer.UserRole) ([]portainer.User, // UpdateUser saves a user. func (service *Service) UpdateUser(ID portainer.UserID, user *portainer.User) error { identifier := internal.Itob(int(ID)) + user.Username = strings.ToLower(user.Username) return internal.UpdateObject(service.db, BucketName, identifier, user) } @@ -133,6 +138,7 @@ func (service *Service) CreateUser(user *portainer.User) error { id, _ := bucket.NextSequence() user.ID = portainer.UserID(id) + user.Username = strings.ToLower(user.Username) data, err := internal.MarshalObject(user) if err != nil { diff --git a/api/bolt/version/version.go b/api/bolt/version/version.go index eca755a57..f879d2759 100644 --- a/api/bolt/version/version.go +++ b/api/bolt/version/version.go @@ -4,6 +4,7 @@ import ( "strconv" "github.com/boltdb/bolt" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/bolt/internal" ) @@ -13,6 +14,7 @@ const ( BucketName = "version" versionKey = "DB_VERSION" instanceKey = "INSTANCE_ID" + editionKey = "EDITION" ) // Service represents a service to manage stored versions. @@ -56,6 +58,21 @@ func (service *Service) DBVersion() (int, error) { return strconv.Atoi(string(data)) } +// Edition retrieves the stored portainer edition. +func (service *Service) Edition() (portainer.SoftwareEdition, error) { + editionData, err := service.getKey(editionKey) + if err != nil { + return 0, err + } + + edition, err := strconv.Atoi(string(editionData)) + if err != nil { + return 0, err + } + + return portainer.SoftwareEdition(edition), nil +} + // StoreDBVersion store the database version. func (service *Service) StoreDBVersion(version int) error { return service.db.Update(func(tx *bolt.Tx) error { @@ -99,3 +116,36 @@ func (service *Service) StoreInstanceID(ID string) error { return bucket.Put([]byte(instanceKey), data) }) } + +func (service *Service) getKey(key string) ([]byte, error) { + var data []byte + + err := service.db.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(BucketName)) + + value := bucket.Get([]byte(key)) + if value == nil { + return errors.ErrObjectNotFound + } + + data = make([]byte, len(value)) + copy(data, value) + + return nil + }) + + if err != nil { + return nil, err + } + + return data, nil +} + +func (service *Service) setKey(key string, value string) error { + return service.db.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(BucketName)) + + data := []byte(value) + return bucket.Put([]byte(key), data) + }) +} diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 5d83916be..f10100c25 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -6,17 +6,20 @@ import ( "strings" "time" - "github.com/portainer/portainer/api" + portainer "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/crypto" "github.com/portainer/portainer/api/docker" + "github.com/portainer/portainer/api/exec" "github.com/portainer/portainer/api/filesystem" "github.com/portainer/portainer/api/git" "github.com/portainer/portainer/api/http" "github.com/portainer/portainer/api/http/client" + "github.com/portainer/portainer/api/http/proxy" + kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes" "github.com/portainer/portainer/api/internal/snapshot" "github.com/portainer/portainer/api/jwt" "github.com/portainer/portainer/api/kubernetes" @@ -30,12 +33,12 @@ func initCLI() *portainer.CLIFlags { var cliService portainer.CLIService = &cli.Service{} flags, err := cliService.ParseFlags(portainer.APIVersion) if err != nil { - log.Fatal(err) + log.Fatalf("failed parsing flags: %v", err) } err = cliService.ValidateFlags(flags) if err != nil { - log.Fatal(err) + log.Fatalf("failed validating flags:%v", err) } return flags } @@ -43,7 +46,7 @@ func initCLI() *portainer.CLIFlags { func initFileService(dataStorePath string) portainer.FileService { fileService, err := filesystem.NewService(dataStorePath, "") if err != nil { - log.Fatal(err) + log.Fatalf("failed creating file service: %v", err) } return fileService } @@ -51,27 +54,32 @@ func initFileService(dataStorePath string) portainer.FileService { func initDataStore(dataStorePath string, fileService portainer.FileService) portainer.DataStore { store, err := bolt.NewStore(dataStorePath, fileService) if err != nil { - log.Fatal(err) + log.Fatalf("failed creating data store: %v", err) } err = store.Open() if err != nil { - log.Fatal(err) + log.Fatalf("failed opening store: %v", err) } err = store.Init() if err != nil { - log.Fatal(err) + log.Fatalf("failed initializing data store: %v", err) } err = store.MigrateData() if err != nil { - log.Fatal(err) + log.Fatalf("failed migration: %v", err) } return store } -func initComposeStackManager(dataStorePath string, reverseTunnelService portainer.ReverseTunnelService) portainer.ComposeStackManager { +func initComposeStackManager(assetsPath string, dataStorePath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager { + composeWrapper := exec.NewComposeWrapper(assetsPath, proxyManager) + if composeWrapper != nil { + return composeWrapper + } + return libcompose.NewComposeStackManager(dataStorePath, reverseTunnelService) } @@ -89,6 +97,10 @@ func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error) return nil, err } + if settings.UserSessionTimeout == "" { + settings.UserSessionTimeout = portainer.DefaultUserSessionTimeout + dataStore.Settings().UpdateSettings(settings) + } jwtService, err := jwt.NewService(settings.UserSessionTimeout) if err != nil { return nil, err @@ -199,7 +211,7 @@ func generateAndStoreKeyPair(fileService portainer.FileService, signatureService func initKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error { existingKeyPair, err := fileService.KeyPairFilesExist() if err != nil { - log.Fatal(err) + log.Fatalf("failed checking for existing key pair: %v", err) } if existingKeyPair { @@ -237,6 +249,18 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore portainer.Dat Status: portainer.EndpointStatusUp, Snapshots: []portainer.DockerSnapshot{}, Kubernetes: portainer.KubernetesDefault(), + + SecuritySettings: portainer.EndpointSecuritySettings{ + AllowVolumeBrowserForRegularUsers: false, + EnableHostManagementFeatures: false, + + AllowBindMountsForRegularUsers: true, + AllowPrivilegedModeForRegularUsers: true, + AllowHostNamespaceForRegularUsers: true, + AllowContainerCapabilitiesForRegularUsers: true, + AllowDeviceMappingForRegularUsers: true, + AllowStackManagementForRegularUsers: true, + }, } if strings.HasPrefix(endpoint.URL, "tcp://") { @@ -286,6 +310,18 @@ func createUnsecuredEndpoint(endpointURL string, dataStore portainer.DataStore, Status: portainer.EndpointStatusUp, Snapshots: []portainer.DockerSnapshot{}, Kubernetes: portainer.KubernetesDefault(), + + SecuritySettings: portainer.EndpointSecuritySettings{ + AllowVolumeBrowserForRegularUsers: false, + EnableHostManagementFeatures: false, + + AllowBindMountsForRegularUsers: true, + AllowPrivilegedModeForRegularUsers: true, + AllowHostNamespaceForRegularUsers: true, + AllowContainerCapabilitiesForRegularUsers: true, + AllowDeviceMappingForRegularUsers: true, + AllowStackManagementForRegularUsers: true, + }, } err := snapshotService.SnapshotEndpoint(endpoint) @@ -323,7 +359,7 @@ func terminateIfNoAdminCreated(dataStore portainer.DataStore) { users, err := dataStore.User().UsersByRole(portainer.AdministratorRole) if err != nil { - log.Fatal(err) + log.Fatalf("failed getting admin user: %v", err) } if len(users) == 0 { @@ -340,9 +376,13 @@ func main() { dataStore := initDataStore(*flags.Data, fileService) defer dataStore.Close() + if err := dataStore.CheckCurrentEdition(); err != nil { + log.Fatal(err) + } + jwtService, err := initJWTService(dataStore) if err != nil { - log.Fatal(err) + log.Fatalf("failed initializing JWT service: %v", err) } ldapService := initLDAPService() @@ -357,14 +397,14 @@ func main() { err = initKeyPair(fileService, digitalSignatureService) if err != nil { - log.Fatal(err) + log.Fatalf("failed initializing key pai: %v", err) } reverseTunnelService := chisel.NewService(dataStore) instanceID, err := dataStore.Version().InstanceID() if err != nil { - log.Fatal(err) + log.Fatalf("failed getting instance id: %v", err) } dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService) @@ -372,47 +412,49 @@ func main() { snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory) if err != nil { - log.Fatal(err) + log.Fatalf("failed initializing snapshot service: %v", err) } snapshotService.Start() swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService, reverseTunnelService) if err != nil { - log.Fatal(err) + log.Fatalf("failed initializing swarm stack manager: %v", err) } + kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager() + proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager) - composeStackManager := initComposeStackManager(*flags.Data, reverseTunnelService) + composeStackManager := initComposeStackManager(*flags.Assets, *flags.Data, reverseTunnelService, proxyManager) kubernetesDeployer := initKubernetesDeployer(*flags.Assets) if dataStore.IsNew() { err = updateSettingsFromFlags(dataStore, flags) if err != nil { - log.Fatal(err) + log.Fatalf("failed updating settings from flags: %v", err) } } err = loadEdgeJobsFromDatabase(dataStore, reverseTunnelService) if err != nil { - log.Fatal(err) + log.Fatalf("failed loading edge jobs from database: %v", err) } applicationStatus := initStatus(flags) err = initEndpoint(flags, dataStore, snapshotService) if err != nil { - log.Fatal(err) + log.Fatalf("failed initializing endpoint: %v", err) } adminPasswordHash := "" if *flags.AdminPasswordFile != "" { content, err := fileService.GetFileContent(*flags.AdminPasswordFile) if err != nil { - log.Fatal(err) + log.Fatalf("failed getting admin password file: %v", err) } adminPasswordHash, err = cryptoService.Hash(strings.TrimSuffix(string(content), "\n")) if err != nil { - log.Fatal(err) + log.Fatalf("failed hashing admin password: %v", err) } } else if *flags.AdminPassword != "" { adminPasswordHash = *flags.AdminPassword @@ -421,7 +463,7 @@ func main() { if adminPasswordHash != "" { users, err := dataStore.User().UsersByRole(portainer.AdministratorRole) if err != nil { - log.Fatal(err) + log.Fatalf("failed getting admin user: %v", err) } if len(users) == 0 { @@ -433,7 +475,7 @@ func main() { } err := dataStore.User().CreateUser(user) if err != nil { - log.Fatal(err) + log.Fatalf("failed creating admin user: %v", err) } } else { log.Println("Instance already has an administrator user defined. Skipping admin password related flags.") @@ -444,36 +486,38 @@ func main() { err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService) if err != nil { - log.Fatal(err) + log.Fatalf("failed starting tunnel server: %v", err) } var server portainer.Server = &http.Server{ - ReverseTunnelService: reverseTunnelService, - Status: applicationStatus, - BindAddress: *flags.Addr, - AssetsPath: *flags.Assets, - DataStore: dataStore, - SwarmStackManager: swarmStackManager, - ComposeStackManager: composeStackManager, - KubernetesDeployer: kubernetesDeployer, - CryptoService: cryptoService, - JWTService: jwtService, - FileService: fileService, - LDAPService: ldapService, - OAuthService: oauthService, - GitService: gitService, - SignatureService: digitalSignatureService, - SnapshotService: snapshotService, - SSL: *flags.SSL, - SSLCert: *flags.SSLCert, - SSLKey: *flags.SSLKey, - DockerClientFactory: dockerClientFactory, - KubernetesClientFactory: kubernetesClientFactory, + ReverseTunnelService: reverseTunnelService, + Status: applicationStatus, + BindAddress: *flags.Addr, + AssetsPath: *flags.Assets, + DataStore: dataStore, + SwarmStackManager: swarmStackManager, + ComposeStackManager: composeStackManager, + KubernetesDeployer: kubernetesDeployer, + CryptoService: cryptoService, + JWTService: jwtService, + FileService: fileService, + LDAPService: ldapService, + OAuthService: oauthService, + GitService: gitService, + ProxyManager: proxyManager, + KubernetesTokenCacheManager: kubernetesTokenCacheManager, + SignatureService: digitalSignatureService, + SnapshotService: snapshotService, + SSL: *flags.SSL, + SSLCert: *flags.SSLCert, + SSLKey: *flags.SSLKey, + DockerClientFactory: dockerClientFactory, + KubernetesClientFactory: kubernetesClientFactory, } log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr) err = server.Start() if err != nil { - log.Fatal(err) + log.Fatalf("failed starting server: %v", err) } } diff --git a/api/docker/snapshot.go b/api/docker/snapshot.go index f9ead6d3e..f3e96ff7e 100644 --- a/api/docker/snapshot.go +++ b/api/docker/snapshot.go @@ -118,6 +118,7 @@ func snapshotNodes(snapshot *portainer.DockerSnapshot, cli *client.Client) error } snapshot.TotalCPU = int(nanoCpus / 1e9) snapshot.TotalMemory = totalMem + snapshot.NodeCount = len(nodes) return nil } diff --git a/api/exec/compose_wrapper.go b/api/exec/compose_wrapper.go new file mode 100644 index 000000000..918411685 --- /dev/null +++ b/api/exec/compose_wrapper.go @@ -0,0 +1,137 @@ +package exec + +import ( + "bytes" + "errors" + "fmt" + "os" + "os/exec" + "path" + "strings" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy" +) + +// ComposeWrapper is a wrapper for docker-compose binary +type ComposeWrapper struct { + binaryPath string + proxyManager *proxy.Manager +} + +// NewComposeWrapper returns a docker-compose wrapper if corresponding binary present, otherwise nil +func NewComposeWrapper(binaryPath string, proxyManager *proxy.Manager) *ComposeWrapper { + if !IsBinaryPresent(programPath(binaryPath, "docker-compose")) { + return nil + } + + return &ComposeWrapper{ + binaryPath: binaryPath, + proxyManager: proxyManager, + } +} + +// ComposeSyntaxMaxVersion returns the maximum supported version of the docker compose syntax +func (w *ComposeWrapper) ComposeSyntaxMaxVersion() string { + return portainer.ComposeSyntaxMaxVersion +} + +// NormalizeStackName returns a new stack name with unsupported characters replaced +func (w *ComposeWrapper) NormalizeStackName(name string) string { + return name +} + +// Up builds, (re)creates and starts containers in the background. Wraps `docker-compose up -d` command +func (w *ComposeWrapper) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error { + _, err := w.command([]string{"up", "-d"}, stack, endpoint) + return err +} + +// Down stops and removes containers, networks, images, and volumes. Wraps `docker-compose down --remove-orphans` command +func (w *ComposeWrapper) Down(stack *portainer.Stack, endpoint *portainer.Endpoint) error { + _, err := w.command([]string{"down", "--remove-orphans"}, stack, endpoint) + return err +} + +func (w *ComposeWrapper) command(command []string, stack *portainer.Stack, endpoint *portainer.Endpoint) ([]byte, error) { + if endpoint == nil { + return nil, errors.New("cannot call a compose command on an empty endpoint") + } + + program := programPath(w.binaryPath, "docker-compose") + + options := setComposeFile(stack) + + options = addProjectNameOption(options, stack) + options, err := addEnvFileOption(options, stack) + if err != nil { + return nil, err + } + + if !(endpoint.URL == "" || strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://")) { + + proxy, err := w.proxyManager.CreateComposeProxyServer(endpoint) + if err != nil { + return nil, err + } + + defer proxy.Close() + + options = append(options, "-H", fmt.Sprintf("http://127.0.0.1:%d", proxy.Port)) + } + + args := append(options, command...) + + var stderr bytes.Buffer + cmd := exec.Command(program, args...) + cmd.Stderr = &stderr + + out, err := cmd.Output() + if err != nil { + return out, errors.New(stderr.String()) + } + + return out, nil +} + +func setComposeFile(stack *portainer.Stack) []string { + options := make([]string, 0) + + if stack == nil || stack.EntryPoint == "" { + return options + } + + composeFilePath := path.Join(stack.ProjectPath, stack.EntryPoint) + options = append(options, "-f", composeFilePath) + return options +} + +func addProjectNameOption(options []string, stack *portainer.Stack) []string { + if stack == nil || stack.Name == "" { + return options + } + + options = append(options, "-p", stack.Name) + return options +} + +func addEnvFileOption(options []string, stack *portainer.Stack) ([]string, error) { + if stack == nil || stack.Env == nil || len(stack.Env) == 0 { + return options, nil + } + + envFilePath := path.Join(stack.ProjectPath, "stack.env") + + envfile, err := os.OpenFile(envFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return options, err + } + + for _, v := range stack.Env { + envfile.WriteString(fmt.Sprintf("%s=%s\n", v.Name, v.Value)) + } + envfile.Close() + + options = append(options, "--env-file", envFilePath) + return options, nil +} diff --git a/api/exec/compose_wrapper_integration_test.go b/api/exec/compose_wrapper_integration_test.go new file mode 100644 index 000000000..766622614 --- /dev/null +++ b/api/exec/compose_wrapper_integration_test.go @@ -0,0 +1,75 @@ +// +build integration + +package exec + +import ( + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + portainer "github.com/portainer/portainer/api" +) + +const composeFile = `version: "3.9" +services: + busybox: + image: "alpine:latest" + container_name: "compose_wrapper_test"` +const composedContainerName = "compose_wrapper_test" + +func setup(t *testing.T) (*portainer.Stack, *portainer.Endpoint) { + dir := t.TempDir() + composeFileName := "compose_wrapper_test.yml" + f, _ := os.Create(filepath.Join(dir, composeFileName)) + f.WriteString(composeFile) + + stack := &portainer.Stack{ + ProjectPath: dir, + EntryPoint: composeFileName, + Name: "project-name", + } + + endpoint := &portainer.Endpoint{} + + return stack, endpoint +} + +func Test_UpAndDown(t *testing.T) { + + stack, endpoint := setup(t) + + w := NewComposeWrapper("", nil) + + err := w.Up(stack, endpoint) + if err != nil { + t.Fatalf("Error calling docker-compose up: %s", err) + } + + if containerExists(composedContainerName) == false { + t.Fatal("container should exist") + } + + err = w.Down(stack, endpoint) + if err != nil { + t.Fatalf("Error calling docker-compose down: %s", err) + } + + if containerExists(composedContainerName) { + t.Fatal("container should be removed") + } +} + +func containerExists(contaierName string) bool { + cmd := exec.Command(osProgram("docker"), "ps", "-a", "-f", fmt.Sprintf("name=%s", contaierName)) + + out, err := cmd.Output() + if err != nil { + log.Fatalf("failed to list containers: %s", err) + } + + return strings.Contains(string(out), contaierName) +} diff --git a/api/exec/compose_wrapper_test.go b/api/exec/compose_wrapper_test.go new file mode 100644 index 000000000..caee859ef --- /dev/null +++ b/api/exec/compose_wrapper_test.go @@ -0,0 +1,143 @@ +package exec + +import ( + "io/ioutil" + "os" + "path" + "testing" + + portainer "github.com/portainer/portainer/api" + "github.com/stretchr/testify/assert" +) + +func Test_setComposeFile(t *testing.T) { + tests := []struct { + name string + stack *portainer.Stack + expected []string + }{ + { + name: "should return empty result if stack is missing", + stack: nil, + expected: []string{}, + }, + { + name: "should return empty result if stack don't have entrypoint", + stack: &portainer.Stack{}, + expected: []string{}, + }, + { + name: "should allow file name and dir", + stack: &portainer.Stack{ + ProjectPath: "dir", + EntryPoint: "file", + }, + expected: []string{"-f", path.Join("dir", "file")}, + }, + { + name: "should allow file name only", + stack: &portainer.Stack{ + EntryPoint: "file", + }, + expected: []string{"-f", "file"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := setComposeFile(tt.stack) + assert.ElementsMatch(t, tt.expected, result) + }) + } +} + +func Test_addProjectNameOption(t *testing.T) { + tests := []struct { + name string + stack *portainer.Stack + expected []string + }{ + { + name: "should not add project option if stack is missing", + stack: nil, + expected: []string{}, + }, + { + name: "should not add project option if stack doesn't have name", + stack: &portainer.Stack{}, + expected: []string{}, + }, + { + name: "should add project name option if stack has a name", + stack: &portainer.Stack{ + Name: "project-name", + }, + expected: []string{"-p", "project-name"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + options := []string{"-a", "b"} + result := addProjectNameOption(options, tt.stack) + assert.ElementsMatch(t, append(options, tt.expected...), result) + }) + } +} + +func Test_addEnvFileOption(t *testing.T) { + dir := t.TempDir() + + tests := []struct { + name string + stack *portainer.Stack + expected []string + expectedContent string + }{ + { + name: "should not add env file option if stack is missing", + stack: nil, + expected: []string{}, + }, + { + name: "should not add env file option if stack doesn't have env variables", + stack: &portainer.Stack{}, + expected: []string{}, + }, + { + name: "should not add env file option if stack's env variables are empty", + stack: &portainer.Stack{ + ProjectPath: dir, + Env: []portainer.Pair{}, + }, + expected: []string{}, + }, + { + name: "should add env file option if stack has env variables", + stack: &portainer.Stack{ + ProjectPath: dir, + Env: []portainer.Pair{ + {Name: "var1", Value: "value1"}, + {Name: "var2", Value: "value2"}, + }, + }, + expected: []string{"--env-file", path.Join(dir, "stack.env")}, + expectedContent: "var1=value1\nvar2=value2\n", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + options := []string{"-a", "b"} + result, _ := addEnvFileOption(options, tt.stack) + assert.ElementsMatch(t, append(options, tt.expected...), result) + + if tt.expectedContent != "" { + f, _ := os.Open(path.Join(dir, "stack.env")) + content, _ := ioutil.ReadAll(f) + + assert.Equal(t, tt.expectedContent, string(content)) + } + }) + } +} diff --git a/api/exec/swarm_stack.go b/api/exec/swarm_stack.go index 31fb48836..cf59f7607 100644 --- a/api/exec/swarm_stack.go +++ b/api/exec/swarm_stack.go @@ -134,6 +134,8 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, dataPa if !endpoint.TLSConfig.TLSSkipVerify { args = append(args, "--tlsverify", "--tlscacert", endpoint.TLSConfig.TLSCACertPath) + } else { + args = append(args, "--tlscacert", "''") } if endpoint.TLSConfig.TLSCertPath != "" && endpoint.TLSConfig.TLSKeyPath != "" { diff --git a/api/exec/utils.go b/api/exec/utils.go new file mode 100644 index 000000000..75a896f65 --- /dev/null +++ b/api/exec/utils.go @@ -0,0 +1,24 @@ +package exec + +import ( + "os/exec" + "path/filepath" + "runtime" +) + +func osProgram(program string) string { + if runtime.GOOS == "windows" { + program += ".exe" + } + return program +} + +func programPath(rootPath, program string) string { + return filepath.Join(rootPath, osProgram(program)) +} + +// IsBinaryPresent returns true if corresponding program exists on PATH +func IsBinaryPresent(program string) bool { + _, err := exec.LookPath(program) + return err == nil +} diff --git a/api/exec/utils_test.go b/api/exec/utils_test.go new file mode 100644 index 000000000..38695488a --- /dev/null +++ b/api/exec/utils_test.go @@ -0,0 +1,16 @@ +package exec + +import ( + "testing" +) + +func Test_isBinaryPresent(t *testing.T) { + + if !IsBinaryPresent("docker") { + t.Error("expect docker binary to exist on the path") + } + + if IsBinaryPresent("executable-with-this-name-should-not-exist") { + t.Error("expect binary with a random name to be missing on the path") + } +} diff --git a/api/go.mod b/api/go.mod index fcee3b6a8..f8cca5616 100644 --- a/api/go.mod +++ b/api/go.mod @@ -28,6 +28,7 @@ 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/stretchr/testify v1.6.1 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 diff --git a/api/go.sum b/api/go.sum index d7b7db557..237ef4cd2 100644 --- a/api/go.sum +++ b/api/go.sum @@ -75,6 +75,7 @@ github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU 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 h1:p1yVGRW3nmb85p1Sh1ZJSDm4A4iKLS5QNbvUHMgGu/M= 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= @@ -141,7 +142,6 @@ 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= @@ -207,10 +207,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW 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= @@ -262,12 +260,15 @@ github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/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= @@ -285,7 +286,6 @@ golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-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= @@ -309,8 +309,6 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL 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= @@ -392,34 +390,25 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= 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= diff --git a/api/http/handler/auth/authenticate.go b/api/http/handler/auth/authenticate.go index ad39e93df..d7a398b79 100644 --- a/api/http/handler/auth/authenticate.go +++ b/api/http/handler/auth/authenticate.go @@ -10,18 +10,21 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" httperrors "github.com/portainer/portainer/api/http/errors" ) type authenticatePayload struct { - Username string - Password string + // Username + Username string `example:"admin" validate:"required"` + // Password + Password string `example:"mypassword" validate:"required"` } type authenticateResponse struct { - JWT string `json:"jwt"` + // JWT token used to authenticate against the API + JWT string `json:"jwt" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsImV4cCI6MTQ5OTM3NjE1NH0.NJ6vE8FY1WG6jsRQzfMqeatJ4vh2TWAeeYfDhP71YEE"` } func (payload *authenticatePayload) Validate(r *http.Request) error { @@ -34,6 +37,18 @@ func (payload *authenticatePayload) Validate(r *http.Request) error { return nil } +// @id AuthenticateUser +// @summary Authenticate +// @description Use this endpoint to authenticate against Portainer using a username and password. +// @tags auth +// @accept json +// @produce json +// @param body body authenticatePayload true "Credentials used for authentication" +// @success 200 {object} authenticateResponse "Success" +// @failure 400 "Invalid request" +// @failure 422 "Invalid Credentials" +// @failure 500 "Server error" +// @router /auth [post] func (handler *Handler) authenticate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { var payload authenticatePayload err := request.DecodeAndValidateJSONPayload(r, &payload) diff --git a/api/http/handler/auth/authenticate_oauth.go b/api/http/handler/auth/authenticate_oauth.go index b090cb98f..627c24878 100644 --- a/api/http/handler/auth/authenticate_oauth.go +++ b/api/http/handler/auth/authenticate_oauth.go @@ -8,12 +8,13 @@ import ( "github.com/asaskevich/govalidator" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" httperrors "github.com/portainer/portainer/api/http/errors" ) type oauthPayload struct { + // OAuth code returned from OAuth Provided Code string } @@ -24,6 +25,17 @@ func (payload *oauthPayload) Validate(r *http.Request) error { return nil } +// @id AuthenticateOauth +// @summary Authenticate with OAuth +// @tags auth +// @accept json +// @produce json +// @param body body oauthPayload true "OAuth Credentials used for authentication" +// @success 200 {object} authenticateResponse "Success" +// @failure 400 "Invalid request" +// @failure 422 "Invalid Credentials" +// @failure 500 "Server error" +// @router /auth/oauth/validate [post] func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuthSettings) (string, error) { if code == "" { return "", errors.New("Invalid OAuth authorization code") diff --git a/api/http/handler/auth/logout.go b/api/http/handler/auth/logout.go index 90519d5b9..03f63566c 100644 --- a/api/http/handler/auth/logout.go +++ b/api/http/handler/auth/logout.go @@ -8,7 +8,13 @@ import ( "github.com/portainer/portainer/api/http/security" ) -// POST request on /logout +// @id Logout +// @summary Logout +// @security jwt +// @tags auth +// @success 204 "Success" +// @failure 500 "Server error" +// @router /auth/logout [post] func (handler *Handler) logout(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { tokenData, err := security.RetrieveTokenData(r) if err != nil { diff --git a/api/http/handler/customtemplates/customtemplate_create.go b/api/http/handler/customtemplates/customtemplate_create.go index af1a81e48..b8768b74c 100644 --- a/api/http/handler/customtemplates/customtemplate_create.go +++ b/api/http/handler/customtemplates/customtemplate_create.go @@ -15,6 +15,27 @@ import ( "github.com/portainer/portainer/api/internal/authorization" ) +// @id CustomTemplateCreate +// @summary Create a custom template +// @description Create a custom template. +// @description **Access policy**: authenticated +// @tags custom_templates +// @security jwt +// @accept json, multipart/form-data +// @produce json +// @param method query string true "method for creating template" Enums(string, file, repository) +// @param body_string body customTemplateFromFileContentPayload false "Required when using method=string" +// @param body_repository body customTemplateFromGitRepositoryPayload false "Required when using method=repository" +// @param Title formData string false "Title of the template. required when method is file" +// @param Description formData string false "Description of the template. required when method is file" +// @param Note formData string false "A note that will be displayed in the UI. Supports HTML content" +// @param Platform formData int false "Platform associated to the template (1 - 'linux', 2 - 'windows'). required when method is file" Enums(1,2) +// @param Type formData int false "Type of created stack (1 - swarm, 2 - compose), required when method is file" Enums(1,2) +// @param file formData file false "required when method is file" +// @success 200 {object} portainer.CustomTemplate +// @failure 400 "Invalid request" +// @failure 500 "Server error" +// @router /custom_templates [post] func (handler *Handler) customTemplateCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { method, err := request.RetrieveQueryParameter(r, "method", false) if err != nil { @@ -74,13 +95,21 @@ func (handler *Handler) createCustomTemplate(method string, r *http.Request) (*p } type customTemplateFromFileContentPayload struct { - Logo string - Title string - FileContent string - Description string - Note string - Platform portainer.CustomTemplatePlatform - Type portainer.StackType + // URL of the template's logo + Logo string `example:"https://cloudinovasi.id/assets/img/logos/nginx.png"` + // Title of the template + Title string `example:"Nginx" validate:"required"` + // Description of the template + Description string `example:"High performance web server" validate:"required"` + // A note that will be displayed in the UI. Supports HTML content + Note string `example:"This is my custom template"` + // Platform associated to the template. + // Valid values are: 1 - 'linux', 2 - 'windows' + Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2" validate:"required"` + // Type of created stack (1 - swarm, 2 - compose) + Type portainer.StackType `example:"1" enums:"1,2" validate:"required"` + // Content of stack file + FileContent string `validate:"required"` } func (payload *customTemplateFromFileContentPayload) Validate(r *http.Request) error { @@ -132,18 +161,32 @@ func (handler *Handler) createCustomTemplateFromFileContent(r *http.Request) (*p } 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 + // URL of the template's logo + Logo string `example:"https://cloudinovasi.id/assets/img/logos/nginx.png"` + // Title of the template + Title string `example:"Nginx" validate:"required"` + // Description of the template + Description string `example:"High performance web server" validate:"required"` + // A note that will be displayed in the UI. Supports HTML content + Note string `example:"This is my custom template"` + // Platform associated to the template. + // Valid values are: 1 - 'linux', 2 - 'windows' + Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2" validate:"required"` + // Type of created stack (1 - swarm, 2 - compose) + Type portainer.StackType `example:"1" enums:"1,2" validate:"required"` + + // URL of a Git repository hosting the Stack file + RepositoryURL string `example:"https://github.com/openfaas/faas" validate:"required"` + // Reference name of a Git repository hosting the Stack file + RepositoryReferenceName string `example:"refs/heads/master"` + // Use basic authentication to clone the Git repository + RepositoryAuthentication bool `example:"true"` + // Username used in basic authentication. Required when RepositoryAuthentication is true. + RepositoryUsername string `example:"myGitUsername"` + // Password used in basic authentication. Required when RepositoryAuthentication is true. + RepositoryPassword string `example:"myGitPassword"` + // Path to the Stack file inside the Git repository + ComposeFilePathInRepository string `example:"docker-compose.yml" default:"docker-compose.yml"` } func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request) error { @@ -251,7 +294,7 @@ func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) er } payload.Type = templateType - composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "file") + composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "File") if err != nil { return errors.New("Invalid Compose file. Ensure that the Compose file is uploaded correctly") } diff --git a/api/http/handler/customtemplates/customtemplate_delete.go b/api/http/handler/customtemplates/customtemplate_delete.go index 24500894a..edb9cb8fe 100644 --- a/api/http/handler/customtemplates/customtemplate_delete.go +++ b/api/http/handler/customtemplates/customtemplate_delete.go @@ -13,6 +13,19 @@ import ( "github.com/portainer/portainer/api/http/security" ) +// @id CustomTemplateDelete +// @summary Remove a template +// @description Remove a template. +// @description **Access policy**: authorized +// @tags custom_templates +// @security jwt +// @param id path int true "Template identifier" +// @success 204 "Success" +// @failure 400 "Invalid request" +// @failure 403 "Access denied to resource" +// @failure 404 "Template not found" +// @failure 500 "Server error" +// @router /custom_templates/{id} [delete] func (handler *Handler) customTemplateDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { customTemplateID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/customtemplates/customtemplate_file.go b/api/http/handler/customtemplates/customtemplate_file.go index 5fa74d813..c8de77de3 100644 --- a/api/http/handler/customtemplates/customtemplate_file.go +++ b/api/http/handler/customtemplates/customtemplate_file.go @@ -7,7 +7,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" ) @@ -15,7 +15,19 @@ type fileResponse struct { FileContent string } -// GET request on /api/custom_templates/:id/file +// @id CustomTemplateFile +// @summary Get Template stack file content. +// @description Retrieve the content of the Stack file for the specified custom template +// @description **Access policy**: authorized +// @tags custom_templates +// @security jwt +// @produce json +// @param id path int true "Template identifier" +// @success 200 {object} fileResponse "Success" +// @failure 400 "Invalid request" +// @failure 404 "Custom template not found" +// @failure 500 "Server error" +// @router /custom_templates/{id}/file [get] func (handler *Handler) customTemplateFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { customTemplateID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/customtemplates/customtemplate_inspect.go b/api/http/handler/customtemplates/customtemplate_inspect.go index 6259e3eb7..500716817 100644 --- a/api/http/handler/customtemplates/customtemplate_inspect.go +++ b/api/http/handler/customtemplates/customtemplate_inspect.go @@ -7,12 +7,26 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" httperrors "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) +// @id CustomTemplateInspect +// @summary Inspect a custom template +// @description Retrieve details about a template. +// @description **Access policy**: authenticated +// @tags custom_templates +// @security jwt +// @accept json +// @produce json +// @param id path int true "Template identifier" +// @success 200 {object} portainer.CustomTemplate "Success" +// @failure 400 "Invalid request" +// @failure 404 "Template not found" +// @failure 500 "Server error" +// @router /custom_templates/{id} [get] func (handler *Handler) customTemplateInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { customTemplateID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/customtemplates/customtemplate_list.go b/api/http/handler/customtemplates/customtemplate_list.go index b33cb8297..13dd10363 100644 --- a/api/http/handler/customtemplates/customtemplate_list.go +++ b/api/http/handler/customtemplates/customtemplate_list.go @@ -4,21 +4,28 @@ import ( "net/http" httperror "github.com/portainer/libhttp/error" - "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" ) +// @id CustomTemplateList +// @summary List available custom templates +// @description List available custom templates. +// @description **Access policy**: authenticated +// @tags custom_templates +// @security jwt +// @produce json +// @success 200 {array} portainer.CustomTemplate "Success" +// @failure 500 "Server error" +// @router /custom_templates [get] 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} @@ -26,8 +33,6 @@ func (handler *Handler) customTemplateList(w http.ResponseWriter, r *http.Reques 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} @@ -49,19 +54,3 @@ func (handler *Handler) customTemplateList(w http.ResponseWriter, r *http.Reques 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 -} diff --git a/api/http/handler/customtemplates/customtemplate_update.go b/api/http/handler/customtemplates/customtemplate_update.go index fd10d6e23..ecbd9c48a 100644 --- a/api/http/handler/customtemplates/customtemplate_update.go +++ b/api/http/handler/customtemplates/customtemplate_update.go @@ -17,13 +17,21 @@ import ( ) type customTemplateUpdatePayload struct { - Logo string - Title string - Description string - Note string - Platform portainer.CustomTemplatePlatform - Type portainer.StackType - FileContent string + // URL of the template's logo + Logo string `example:"https://cloudinovasi.id/assets/img/logos/nginx.png"` + // Title of the template + Title string `example:"Nginx" validate:"required"` + // Description of the template + Description string `example:"High performance web server" validate:"required"` + // A note that will be displayed in the UI. Supports HTML content + Note string `example:"This is my custom template"` + // Platform associated to the template. + // Valid values are: 1 - 'linux', 2 - 'windows' + Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2" validate:"required"` + // Type of created stack (1 - swarm, 2 - compose) + Type portainer.StackType `example:"1" enums:"1,2" validate:"required"` + // Content of stack file + FileContent string `validate:"required"` } func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error { @@ -45,6 +53,22 @@ func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error { return nil } +// @id CustomTemplateUpdate +// @summary Update a template +// @description Update a template. +// @description **Access policy**: authenticated +// @tags custom_templates +// @security jwt +// @accept json +// @produce json +// @param id path int true "Template identifier" +// @param body body customTemplateUpdatePayload true "Template details" +// @success 200 {object} portainer.CustomTemplate "Success" +// @failure 400 "Invalid request" +// @failure 403 "Permission denied to access template" +// @failure 404 "Template not found" +// @failure 500 "Server error" +// @router /custom_templates/{id} [put] func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { customTemplateID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/dockerhub/dockerhub_inspect.go b/api/http/handler/dockerhub/dockerhub_inspect.go index fd4713849..e7dc713f8 100644 --- a/api/http/handler/dockerhub/dockerhub_inspect.go +++ b/api/http/handler/dockerhub/dockerhub_inspect.go @@ -7,7 +7,16 @@ import ( "github.com/portainer/libhttp/response" ) -// GET request on /api/dockerhub +// @id DockerHubInspect +// @summary Retrieve DockerHub information +// @description Use this endpoint to retrieve the information used to connect to the DockerHub +// @description **Access policy**: authenticated +// @tags dockerhub +// @security jwt +// @produce json +// @success 200 {object} portainer.DockerHub +// @failure 500 "Server error" +// @router /dockerhub [get] func (handler *Handler) dockerhubInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { dockerhub, err := handler.DataStore.DockerHub().DockerHub() if err != nil { diff --git a/api/http/handler/dockerhub/dockerhub_update.go b/api/http/handler/dockerhub/dockerhub_update.go index e12d3ad24..536b84420 100644 --- a/api/http/handler/dockerhub/dockerhub_update.go +++ b/api/http/handler/dockerhub/dockerhub_update.go @@ -8,13 +8,16 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" ) type dockerhubUpdatePayload struct { - Authentication bool - Username string - Password string + // Enable authentication against DockerHub + Authentication bool `validate:"required" example:"false"` + // Username used to authenticate against the DockerHub + Username string `validate:"required" example:"hub_user"` + // Password used to authenticate against the DockerHub + Password string `validate:"required" example:"hub_password"` } func (payload *dockerhubUpdatePayload) Validate(r *http.Request) error { @@ -24,7 +27,19 @@ func (payload *dockerhubUpdatePayload) Validate(r *http.Request) error { return nil } -// PUT request on /api/dockerhub +// @id DockerHubUpdate +// @summary Update DockerHub information +// @description Use this endpoint to update the information used to connect to the DockerHub +// @description **Access policy**: administrator +// @tags dockerhub +// @security jwt +// @accept json +// @produce json +// @param body body dockerhubUpdatePayload true "DockerHub information" +// @success 204 "Success" +// @failure 400 "Invalid request" +// @failure 500 "Server error" +// @router /dockerhub [put] func (handler *Handler) dockerhubUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { var payload dockerhubUpdatePayload err := request.DecodeAndValidateJSONPayload(r, &payload) diff --git a/api/http/handler/edgegroups/edgegroup_create.go b/api/http/handler/edgegroups/edgegroup_create.go index 3e767891e..81f832bb2 100644 --- a/api/http/handler/edgegroups/edgegroup_create.go +++ b/api/http/handler/edgegroups/edgegroup_create.go @@ -8,7 +8,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" ) type edgeGroupCreatePayload struct { @@ -32,6 +32,18 @@ func (payload *edgeGroupCreatePayload) Validate(r *http.Request) error { return nil } +// @id EdgeGroupCreate +// @summary Create an EdgeGroup +// @description +// @tags edge_groups +// @security jwt +// @accept json +// @produce json +// @param body body edgeGroupCreatePayload true "EdgeGroup data" +// @success 200 {object} portainer.EdgeGroup +// @failure 503 Edge compute features are disabled +// @failure 500 +// @router /edge_groups [post] func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { var payload edgeGroupCreatePayload err := request.DecodeAndValidateJSONPayload(r, &payload) diff --git a/api/http/handler/edgegroups/edgegroup_delete.go b/api/http/handler/edgegroups/edgegroup_delete.go index a45486bfc..e9ed2446d 100644 --- a/api/http/handler/edgegroups/edgegroup_delete.go +++ b/api/http/handler/edgegroups/edgegroup_delete.go @@ -11,6 +11,18 @@ import ( bolterrors "github.com/portainer/portainer/api/bolt/errors" ) +// @id EdgeGroupDelete +// @summary Deletes an EdgeGroup +// @description +// @tags edge_groups +// @security jwt +// @accept json +// @produce json +// @param id path int true "EdgeGroup Id" +// @success 204 +// @failure 503 Edge compute features are disabled +// @failure 500 +// @router /edge_groups/{id} [delete] func (handler *Handler) edgeGroupDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { edgeGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/edgegroups/edgegroup_inspect.go b/api/http/handler/edgegroups/edgegroup_inspect.go index 9f7f31173..938624c29 100644 --- a/api/http/handler/edgegroups/edgegroup_inspect.go +++ b/api/http/handler/edgegroups/edgegroup_inspect.go @@ -6,10 +6,22 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/errors" ) +// @id EdgeGroupInspect +// @summary Inspects an EdgeGroup +// @description +// @tags edge_groups +// @security jwt +// @accept json +// @produce json +// @param id path int true "EdgeGroup Id" +// @success 200 {object} portainer.EdgeGroup +// @failure 503 Edge compute features are disabled +// @failure 500 +// @router /edge_groups/{id} [get] func (handler *Handler) edgeGroupInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { edgeGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/edgegroups/edgegroup_list.go b/api/http/handler/edgegroups/edgegroup_list.go index 7ba9fbdcf..5e56a35b7 100644 --- a/api/http/handler/edgegroups/edgegroup_list.go +++ b/api/http/handler/edgegroups/edgegroup_list.go @@ -5,7 +5,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" ) type decoratedEdgeGroup struct { @@ -13,6 +13,17 @@ type decoratedEdgeGroup struct { HasEdgeStack bool `json:"HasEdgeStack"` } +// @id EdgeGroupList +// @summary list EdgeGroups +// @description +// @tags edge_groups +// @security jwt +// @accept json +// @produce json +// @success 200 {array} portainer.EdgeGroup{HasEdgeStack=bool} "EdgeGroups" +// @failure 500 +// @failure 503 Edge compute features are disabled +// @router /edge_groups [get] func (handler *Handler) edgeGroupList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups() if err != nil { diff --git a/api/http/handler/edgegroups/edgegroup_update.go b/api/http/handler/edgegroups/edgegroup_update.go index e71f48457..2a5105d70 100644 --- a/api/http/handler/edgegroups/edgegroup_update.go +++ b/api/http/handler/edgegroups/edgegroup_update.go @@ -8,7 +8,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/internal/edge" ) @@ -34,6 +34,19 @@ func (payload *edgeGroupUpdatePayload) Validate(r *http.Request) error { return nil } +// @id EgeGroupUpdate +// @summary Updates an EdgeGroup +// @description +// @tags edge_groups +// @security jwt +// @accept json +// @produce json +// @param id path int true "EdgeGroup Id" +// @param body body edgeGroupUpdatePayload true "EdgeGroup data" +// @success 200 {object} portainer.EdgeGroup +// @failure 503 Edge compute features are disabled +// @failure 500 +// @router /edge_groups/{id} [put] func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { edgeGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/edgejobs/edgejob_create.go b/api/http/handler/edgejobs/edgejob_create.go index e329316a7..3e78914d5 100644 --- a/api/http/handler/edgejobs/edgejob_create.go +++ b/api/http/handler/edgejobs/edgejob_create.go @@ -11,10 +11,23 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" ) -// POST /api/edge_jobs?method=file|string +// @id EdgeJobCreate +// @summary Create an EdgeJob +// @description +// @tags edge_jobs +// @security jwt +// @accept json +// @produce json +// @param method query string true "Creation Method" Enums(file, string) +// @param body body edgeJobCreateFromFileContentPayload true "EdgeGroup data when method is string" +// @param body body edgeJobCreateFromFilePayload true "EdgeGroup data when method is file" +// @success 200 {object} portainer.EdgeGroup +// @failure 503 Edge compute features are disabled +// @failure 500 +// @router /edge_jobs [post] func (handler *Handler) edgeJobCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { method, err := request.RetrieveQueryParameter(r, "method", false) if err != nil { diff --git a/api/http/handler/edgejobs/edgejob_delete.go b/api/http/handler/edgejobs/edgejob_delete.go index 66efdd55e..e90d3e1fe 100644 --- a/api/http/handler/edgejobs/edgejob_delete.go +++ b/api/http/handler/edgejobs/edgejob_delete.go @@ -7,10 +7,23 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" ) +// @id EdgeJobDelete +// @summary Delete an EdgeJob +// @description +// @tags edge_jobs +// @security jwt +// @accept json +// @produce json +// @param id path string true "EdgeJob Id" +// @success 204 +// @failure 500 +// @failure 400 +// @failure 503 Edge compute features are disabled +// @router /edge_jobs/{id} [delete] func (handler *Handler) edgeJobDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/edgejobs/edgejob_file.go b/api/http/handler/edgejobs/edgejob_file.go index 25e71366a..85fe20ce2 100644 --- a/api/http/handler/edgejobs/edgejob_file.go +++ b/api/http/handler/edgejobs/edgejob_file.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" ) @@ -14,7 +14,19 @@ type edgeJobFileResponse struct { FileContent string `json:"FileContent"` } -// GET request on /api/edge_jobs/:id/file +// @id EdgeJobFile +// @summary Fetch a file of an EdgeJob +// @description +// @tags edge_jobs +// @security jwt +// @accept json +// @produce json +// @param id path string true "EdgeJob Id" +// @success 200 {object} edgeJobFileResponse +// @failure 500 +// @failure 400 +// @failure 503 Edge compute features are disabled +// @router /edge_jobs/{id}/file [get] func (handler *Handler) edgeJobFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/edgejobs/edgejob_inspect.go b/api/http/handler/edgejobs/edgejob_inspect.go index 0625987cf..c86b8b80b 100644 --- a/api/http/handler/edgejobs/edgejob_inspect.go +++ b/api/http/handler/edgejobs/edgejob_inspect.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" ) @@ -15,6 +15,19 @@ type edgeJobInspectResponse struct { Endpoints []portainer.EndpointID } +// @id EdgeJobInspect +// @summary Inspect an EdgeJob +// @description +// @tags edge_jobs +// @security jwt +// @accept json +// @produce json +// @param id path string true "EdgeJob Id" +// @success 200 {object} portainer.EdgeJob +// @failure 500 +// @failure 400 +// @failure 503 Edge compute features are disabled +// @router /edge_jobs/{id} [get] func (handler *Handler) edgeJobInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/edgejobs/edgejob_list.go b/api/http/handler/edgejobs/edgejob_list.go index c95d0946b..d3776df18 100644 --- a/api/http/handler/edgejobs/edgejob_list.go +++ b/api/http/handler/edgejobs/edgejob_list.go @@ -7,6 +7,18 @@ import ( "github.com/portainer/libhttp/response" ) +// @id EdgeJobList +// @summary Fetch EdgeJobs list +// @description +// @tags edge_jobs +// @security jwt +// @accept json +// @produce json +// @success 200 {array} portainer.EdgeJob +// @failure 500 +// @failure 400 +// @failure 503 Edge compute features are disabled +// @router /edge_jobs [get] // GET request on /api/edge_jobs func (handler *Handler) edgeJobList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { edgeJobs, err := handler.DataStore.EdgeJob().EdgeJobs() diff --git a/api/http/handler/edgejobs/edgejob_tasklogs_clear.go b/api/http/handler/edgejobs/edgejob_tasklogs_clear.go index e49f3c978..7a4afd8ab 100644 --- a/api/http/handler/edgejobs/edgejob_tasklogs_clear.go +++ b/api/http/handler/edgejobs/edgejob_tasklogs_clear.go @@ -7,11 +7,24 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" ) -// DELETE request on /api/edge_jobs/:id/tasks/:taskID/logs +// @id EdgeJobTasksClear +// @summary Clear the log for a specifc task on an EdgeJob +// @description +// @tags edge_jobs +// @security jwt +// @accept json +// @produce json +// @param id path string true "EdgeJob Id" +// @param taskID path string true "Task Id" +// @success 204 +// @failure 500 +// @failure 400 +// @failure 503 Edge compute features are disabled +// @router /edge_jobs/{id}/tasks/{taskID}/logs [delete] func (handler *Handler) edgeJobTasksClear(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/edgejobs/edgejob_tasklogs_collect.go b/api/http/handler/edgejobs/edgejob_tasklogs_collect.go index cb09aa2db..e08fe0c8b 100644 --- a/api/http/handler/edgejobs/edgejob_tasklogs_collect.go +++ b/api/http/handler/edgejobs/edgejob_tasklogs_collect.go @@ -6,11 +6,24 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" ) -// POST request on /api/edge_jobs/:id/tasks/:taskID/logs +// @id EdgeJobTasksCollect +// @summary Collect the log for a specifc task on an EdgeJob +// @description +// @tags edge_jobs +// @security jwt +// @accept json +// @produce json +// @param id path string true "EdgeJob Id" +// @param taskID path string true "Task Id" +// @success 204 +// @failure 500 +// @failure 400 +// @failure 503 Edge compute features are disabled +// @router /edge_jobs/{id}/tasks/{taskID}/logs [post] func (handler *Handler) edgeJobTasksCollect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/edgejobs/edgejob_tasklogs_inspect.go b/api/http/handler/edgejobs/edgejob_tasklogs_inspect.go index 5e63efa1b..d5d25b91b 100644 --- a/api/http/handler/edgejobs/edgejob_tasklogs_inspect.go +++ b/api/http/handler/edgejobs/edgejob_tasklogs_inspect.go @@ -13,7 +13,20 @@ type fileResponse struct { FileContent string `json:"FileContent"` } -// GET request on /api/edge_jobs/:id/tasks/:taskID/logs +// @id EdgeJobTaskLogsInspect +// @summary Fetch the log for a specifc task on an EdgeJob +// @description +// @tags edge_jobs +// @security jwt +// @accept json +// @produce json +// @param id path string true "EdgeJob Id" +// @param taskID path string true "Task Id" +// @success 200 {object} fileResponse +// @failure 500 +// @failure 400 +// @failure 503 Edge compute features are disabled +// @router /edge_jobs/{id}/tasks/{taskID}/logs [get] func (handler *Handler) edgeJobTaskLogsInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/edgejobs/edgejob_tasks_list.go b/api/http/handler/edgejobs/edgejob_tasks_list.go index 6b021255c..769dfc205 100644 --- a/api/http/handler/edgejobs/edgejob_tasks_list.go +++ b/api/http/handler/edgejobs/edgejob_tasks_list.go @@ -7,7 +7,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" ) @@ -17,7 +17,19 @@ type taskContainer struct { LogsStatus portainer.EdgeJobLogsStatus `json:"LogsStatus"` } -// GET request on /api/edge_jobs/:id/tasks +// @id EdgeJobTasksList +// @summary Fetch the list of tasks on an EdgeJob +// @description +// @tags edge_jobs +// @security jwt +// @accept json +// @produce json +// @param id path string true "EdgeJob Id" +// @success 200 {array} taskContainer +// @failure 500 +// @failure 400 +// @failure 503 Edge compute features are disabled +// @router /edge_jobs/{id}/tasks [get] func (handler *Handler) edgeJobTasksList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/edgejobs/edgejob_update.go b/api/http/handler/edgejobs/edgejob_update.go index 26d756320..a0f473b52 100644 --- a/api/http/handler/edgejobs/edgejob_update.go +++ b/api/http/handler/edgejobs/edgejob_update.go @@ -9,7 +9,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" ) @@ -28,6 +28,20 @@ func (payload *edgeJobUpdatePayload) Validate(r *http.Request) error { return nil } +// @id EdgeJobUpdate +// @summary Update an EdgeJob +// @description +// @tags edge_jobs +// @security jwt +// @accept json +// @produce json +// @param id path string true "EdgeJob Id" +// @param body body edgeJobUpdatePayload true "EdgeGroup data" +// @success 200 {object} portainer.EdgeJob +// @failure 500 +// @failure 400 +// @failure 503 Edge compute features are disabled +// @router /edge_jobs/{id} [post] func (handler *Handler) edgeJobUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/edgestacks/edgestack_create.go b/api/http/handler/edgestacks/edgestack_create.go index 0f3224e95..0e5a74d7c 100644 --- a/api/http/handler/edgestacks/edgestack_create.go +++ b/api/http/handler/edgestacks/edgestack_create.go @@ -11,12 +11,26 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/filesystem" "github.com/portainer/portainer/api/internal/edge" ) -// POST request on /api/endpoint_groups +// @id EdgeStackCreate +// @summary Create an EdgeStack +// @description +// @tags edge_stacks +// @security jwt +// @accept json +// @produce json +// @param method query string true "Creation Method" Enums(file,string,repository) +// @param body_string body swarmStackFromFileContentPayload true "Required when using method=string" +// @param body_file body swarmStackFromFileUploadPayload true "Required when using method=file" +// @param body_repository body swarmStackFromGitRepositoryPayload true "Required when using method=repository" +// @success 200 {object} portainer.EdgeStack +// @failure 500 +// @failure 503 Edge compute features are disabled +// @router /edge_stacks [post] func (handler *Handler) edgeStackCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { method, err := request.RetrieveQueryParameter(r, "method", false) if err != nil { @@ -75,9 +89,12 @@ func (handler *Handler) createSwarmStack(method string, r *http.Request) (*porta } type swarmStackFromFileContentPayload struct { - Name string - StackFileContent string - EdgeGroups []portainer.EdgeGroupID + // Name of the stack + Name string `example:"myStack" validate:"required"` + // Content of the Stack file + StackFileContent string `example:"version: 3\n services:\n web:\n image:nginx" validate:"required"` + // List of identifiers of EdgeGroups + EdgeGroups []portainer.EdgeGroupID `example:"1"` } func (payload *swarmStackFromFileContentPayload) Validate(r *http.Request) error { @@ -132,14 +149,22 @@ func (handler *Handler) createSwarmStackFromFileContent(r *http.Request) (*porta } type swarmStackFromGitRepositoryPayload struct { - Name string - RepositoryURL string - RepositoryReferenceName string - RepositoryAuthentication bool - RepositoryUsername string - RepositoryPassword string - ComposeFilePathInRepository string - EdgeGroups []portainer.EdgeGroupID + // Name of the stack + Name string `example:"myStack" validate:"required"` + // URL of a Git repository hosting the Stack file + RepositoryURL string `example:"https://github.com/openfaas/faas" validate:"required"` + // Reference name of a Git repository hosting the Stack file + RepositoryReferenceName string `example:"refs/heads/master"` + // Use basic authentication to clone the Git repository + RepositoryAuthentication bool `example:"true"` + // Username used in basic authentication. Required when RepositoryAuthentication is true. + RepositoryUsername string `example:"myGitUsername"` + // Password used in basic authentication. Required when RepositoryAuthentication is true. + RepositoryPassword string `example:"myGitPassword"` + // Path to the Stack file inside the Git repository + ComposeFilePathInRepository string `example:"docker-compose.yml" default:"docker-compose.yml"` + // List of identifiers of EdgeGroups + EdgeGroups []portainer.EdgeGroupID `example:"1"` } func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error { diff --git a/api/http/handler/edgestacks/edgestack_delete.go b/api/http/handler/edgestacks/edgestack_delete.go index ee01443f5..a1937e03f 100644 --- a/api/http/handler/edgestacks/edgestack_delete.go +++ b/api/http/handler/edgestacks/edgestack_delete.go @@ -6,11 +6,24 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/internal/edge" ) +// @id EdgeStackDelete +// @summary Delete an EdgeStack +// @description +// @tags edge_stacks +// @security jwt +// @accept json +// @produce json +// @param id path string true "EdgeStack Id" +// @success 204 +// @failure 500 +// @failure 400 +// @failure 503 Edge compute features are disabled +// @router /edge_stacks/{id} [delete] func (handler *Handler) edgeStackDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { edgeStackID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/edgestacks/edgestack_file.go b/api/http/handler/edgestacks/edgestack_file.go index d45dc433c..02bfbbe84 100644 --- a/api/http/handler/edgestacks/edgestack_file.go +++ b/api/http/handler/edgestacks/edgestack_file.go @@ -7,7 +7,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/errors" ) @@ -15,7 +15,19 @@ type stackFileResponse struct { StackFileContent string `json:"StackFileContent"` } -// GET request on /api/edge_stacks/:id/file +// @id EdgeStackFile +// @summary Fetches the stack file for an EdgeStack +// @description +// @tags edge_stacks +// @security jwt +// @accept json +// @produce json +// @param id path string true "EdgeStack Id" +// @success 200 {object} stackFileResponse +// @failure 500 +// @failure 400 +// @failure 503 Edge compute features are disabled +// @router /edge_stacks/{id}/file [get] func (handler *Handler) edgeStackFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { stackID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/edgestacks/edgestack_inspect.go b/api/http/handler/edgestacks/edgestack_inspect.go index 50a960817..c6450f321 100644 --- a/api/http/handler/edgestacks/edgestack_inspect.go +++ b/api/http/handler/edgestacks/edgestack_inspect.go @@ -6,10 +6,23 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/errors" ) +// @id EdgeStackInspect +// @summary Inspect an EdgeStack +// @description +// @tags edge_stacks +// @security jwt +// @accept json +// @produce json +// @param id path string true "EdgeStack Id" +// @success 200 {object} portainer.EdgeStack +// @failure 500 +// @failure 400 +// @failure 503 Edge compute features are disabled +// @router /edge_stacks/{id} [get] func (handler *Handler) edgeStackInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { edgeStackID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/edgestacks/edgestack_list.go b/api/http/handler/edgestacks/edgestack_list.go index 1db0159c6..2d0cb423f 100644 --- a/api/http/handler/edgestacks/edgestack_list.go +++ b/api/http/handler/edgestacks/edgestack_list.go @@ -7,6 +7,18 @@ import ( "github.com/portainer/libhttp/response" ) +// @id EdgeStackList +// @summary Fetches the list of EdgeStacks +// @description +// @tags edge_stacks +// @security jwt +// @accept json +// @produce json +// @success 200 {array} portainer.EdgeStack +// @failure 500 +// @failure 400 +// @failure 503 Edge compute features are disabled +// @router /edge_stacks [get] func (handler *Handler) edgeStackList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks() if err != nil { diff --git a/api/http/handler/edgestacks/edgestack_status_update.go b/api/http/handler/edgestacks/edgestack_status_update.go index f1d870d51..3eaea47b7 100644 --- a/api/http/handler/edgestacks/edgestack_status_update.go +++ b/api/http/handler/edgestacks/edgestack_status_update.go @@ -8,7 +8,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" ) @@ -31,6 +31,19 @@ func (payload *updateStatusPayload) Validate(r *http.Request) error { return nil } +// @id EdgeStackStatusUpdate +// @summary Update an EdgeStack status +// @description Authorized only if the request is done by an Edge Endpoint +// @tags edge_stacks +// @accept json +// @produce json +// @param id path string true "EdgeStack Id" +// @success 200 {object} portainer.EdgeStack +// @failure 500 +// @failure 400 +// @failure 404 +// @failure 403 +// @router /edge_stacks/{id}/status [put] func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { stackID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/edgestacks/edgestack_update.go b/api/http/handler/edgestacks/edgestack_update.go index ccc65bf44..695106bf6 100644 --- a/api/http/handler/edgestacks/edgestack_update.go +++ b/api/http/handler/edgestacks/edgestack_update.go @@ -9,7 +9,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/internal/edge" ) @@ -31,6 +31,20 @@ func (payload *updateEdgeStackPayload) Validate(r *http.Request) error { return nil } +// @id EdgeStackUpdate +// @summary Update an EdgeStack +// @description +// @tags edge_stacks +// @security jwt +// @accept json +// @produce json +// @param id path string true "EdgeStack Id" +// @param body body updateEdgeStackPayload true "EdgeStack data" +// @success 200 {object} portainer.EdgeStack +// @failure 500 +// @failure 400 +// @failure 503 Edge compute features are disabled +// @router /edge_stacks/{id} [put] func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { stackID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/edgetemplates/edgetemplate_list.go b/api/http/handler/edgetemplates/edgetemplate_list.go index 00f271dbb..0dfc766c9 100644 --- a/api/http/handler/edgetemplates/edgetemplate_list.go +++ b/api/http/handler/edgetemplates/edgetemplate_list.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/client" ) @@ -15,7 +15,16 @@ type templateFileFormat struct { Templates []portainer.Template `json:"templates"` } -// GET request on /api/edgetemplates +// @id EdgeTemplateList +// @summary Fetches the list of Edge Templates +// @description +// @tags edge_templates +// @security jwt +// @accept json +// @produce json +// @success 200 {array} portainer.Template +// @failure 500 +// @router /edge_templates [get] func (handler *Handler) edgeTemplateList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { settings, err := handler.DataStore.Settings().Settings() if err != nil { diff --git a/api/http/handler/endpointedge/endpoint_edgejob_logs.go b/api/http/handler/endpointedge/endpoint_edgejob_logs.go index d433f5789..59f55298d 100644 --- a/api/http/handler/endpointedge/endpoint_edgejob_logs.go +++ b/api/http/handler/endpointedge/endpoint_edgejob_logs.go @@ -7,7 +7,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" ) @@ -19,7 +19,18 @@ func (payload *logsPayload) Validate(r *http.Request) error { return nil } -// POST request on api/endpoints/:id/edge/jobs/:jobID/logs +// endpointEdgeJobsLogs +// @summary Inspect an EdgeJob Log +// @description +// @tags edge, endpoints +// @accept json +// @produce json +// @param id path string true "Endpoint Id" +// @param jobID path string true "Job Id" +// @success 200 +// @failure 500 +// @failure 400 +// @router /endpoints/{id}/edge/jobs/{jobID}/logs [post] func (handler *Handler) endpointEdgeJobsLogs(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/endpointedge/endpoint_edgestack_inspect.go b/api/http/handler/endpointedge/endpoint_edgestack_inspect.go index 9f57e115f..9e1057b20 100644 --- a/api/http/handler/endpointedge/endpoint_edgestack_inspect.go +++ b/api/http/handler/endpointedge/endpoint_edgestack_inspect.go @@ -17,7 +17,18 @@ type configResponse struct { Name string } -// GET request on api/endpoints/:id/edge/stacks/:stackId +// @summary Inspect an Edge Stack for an Endpoint +// @description +// @tags edge, endpoints, edge_stacks +// @accept json +// @produce json +// @param id path string true "Endpoint Id" +// @param stackID path string true "EdgeStack Id" +// @success 200 {object} configResponse +// @failure 500 +// @failure 400 +// @failure 404 +// @router /endpoints/{id}/edge/stacks/{stackId} [get] func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/endpointgroups/endpointgroup_create.go b/api/http/handler/endpointgroups/endpointgroup_create.go index f50bf9736..eb19b9d9e 100644 --- a/api/http/handler/endpointgroups/endpointgroup_create.go +++ b/api/http/handler/endpointgroups/endpointgroup_create.go @@ -8,14 +8,18 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" ) type endpointGroupCreatePayload struct { - Name string - Description string - AssociatedEndpoints []portainer.EndpointID - TagIDs []portainer.TagID + // Endpoint group name + Name string `validate:"required" example:"my-endpoint-group"` + // Endpoint group description + Description string `example:"description"` + // List of endpoint identifiers that will be part of this group + AssociatedEndpoints []portainer.EndpointID `example:"1,3"` + // List of tag identifiers to which this endpoint group is associated + TagIDs []portainer.TagID `example:"1,2"` } func (payload *endpointGroupCreatePayload) Validate(r *http.Request) error { @@ -28,7 +32,18 @@ func (payload *endpointGroupCreatePayload) Validate(r *http.Request) error { return nil } -// POST request on /api/endpoint_groups +// @summary Create an Endpoint Group +// @description Create a new endpoint group. +// @description **Access policy**: administrator +// @tags endpoint_groups +// @security jwt +// @accept json +// @produce json +// @param body body endpointGroupCreatePayload true "Endpoint Group details" +// @success 200 {object} portainer.EndpointGroup "Success" +// @failure 400 "Invalid request" +// @failure 500 "Server error" +// @router /endpoint_groups [post] func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { var payload endpointGroupCreatePayload err := request.DecodeAndValidateJSONPayload(r, &payload) diff --git a/api/http/handler/endpointgroups/endpointgroup_delete.go b/api/http/handler/endpointgroups/endpointgroup_delete.go index 2b845070c..4edf260a3 100644 --- a/api/http/handler/endpointgroups/endpointgroup_delete.go +++ b/api/http/handler/endpointgroups/endpointgroup_delete.go @@ -7,11 +7,24 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" ) -// DELETE request on /api/endpoint_groups/:id +// @id EndpointGroupDelete +// @summary Remove an endpoint group +// @description Remove an endpoint group. +// @description **Access policy**: administrator +// @tags endpoint_groups +// @security jwt +// @accept json +// @produce json +// @param id path int true "EndpointGroup identifier" +// @success 204 "Success" +// @failure 400 "Invalid request" +// @failure 404 "EndpointGroup not found" +// @failure 500 "Server error" +// @router /endpoint_groups/{id} [delete] func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/endpointgroups/endpointgroup_endpoint_add.go b/api/http/handler/endpointgroups/endpointgroup_endpoint_add.go index cce2e737a..b28370827 100644 --- a/api/http/handler/endpointgroups/endpointgroup_endpoint_add.go +++ b/api/http/handler/endpointgroups/endpointgroup_endpoint_add.go @@ -6,11 +6,23 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/errors" ) -// PUT request on /api/endpoint_groups/:id/endpoints/:endpointId +// @id EndpointGroupAddEndpoint +// @summary Add an endpoint to an endpoint group +// @description Add an endpoint to an endpoint group +// @description **Access policy**: administrator +// @tags endpoint_groups +// @security jwt +// @param id path int true "EndpointGroup identifier" +// @param endpointId path int true "Endpoint identifier" +// @success 204 "Success" +// @failure 400 "Invalid request" +// @failure 404 "EndpointGroup not found" +// @failure 500 "Server error" +// @router /endpoint_groups/{id}/endpoints/{endpointId} [put] func (handler *Handler) endpointGroupAddEndpoint(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/endpointgroups/endpointgroup_endpoint_delete.go b/api/http/handler/endpointgroups/endpointgroup_endpoint_delete.go index 595be6df9..ecba9adaa 100644 --- a/api/http/handler/endpointgroups/endpointgroup_endpoint_delete.go +++ b/api/http/handler/endpointgroups/endpointgroup_endpoint_delete.go @@ -6,11 +6,22 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/errors" ) -// DELETE request on /api/endpoint_groups/:id/endpoints/:endpointId +// @id EndpointGroupDeleteEndpoint +// @summary Removes endpoint from an endpoint group +// @description **Access policy**: administrator +// @tags endpoint_groups +// @security jwt +// @param id path int true "EndpointGroup identifier" +// @param endpointId path int true "Endpoint identifier" +// @success 204 "Success" +// @failure 400 "Invalid request" +// @failure 404 "EndpointGroup not found" +// @failure 500 "Server error" +// @router /endpoint_groups/{id}/endpoints/{endpointId} [delete] func (handler *Handler) endpointGroupDeleteEndpoint(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/endpointgroups/endpointgroup_inspect.go b/api/http/handler/endpointgroups/endpointgroup_inspect.go index fdf26a976..eb573c145 100644 --- a/api/http/handler/endpointgroups/endpointgroup_inspect.go +++ b/api/http/handler/endpointgroups/endpointgroup_inspect.go @@ -6,11 +6,23 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/errors" ) -// GET request on /api/endpoint_groups/:id +// @summary Inspect an Endpoint group +// @description Retrieve details abont an endpoint group. +// @description **Access policy**: administrator +// @tags endpoint_groups +// @security jwt +// @accept json +// @produce json +// @param id path int true "Endpoint group identifier" +// @success 200 {object} portainer.EndpointGroup "Success" +// @failure 400 "Invalid request" +// @failure 404 "EndpointGroup not found" +// @failure 500 "Server error" +// @router /endpoint_groups/:id [get] func (handler *Handler) endpointGroupInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/endpointgroups/endpointgroup_list.go b/api/http/handler/endpointgroups/endpointgroup_list.go index e4e1bb2da..b011d6a2c 100644 --- a/api/http/handler/endpointgroups/endpointgroup_list.go +++ b/api/http/handler/endpointgroups/endpointgroup_list.go @@ -8,7 +8,18 @@ import ( "github.com/portainer/portainer/api/http/security" ) -// GET request on /api/endpoint_groups +// @id EndpointGroupList +// @summary List Endpoint groups +// @description List all endpoint groups based on the current user authorizations. Will +// @description return all endpoint groups if using an administrator account otherwise it will +// @description only return authorized endpoint groups. +// @description **Access policy**: restricted +// @tags endpoint_groups +// @security jwt +// @produce json +// @success 200 {array} portainer.EndpointGroup "Endpoint group" +// @failure 500 "Server error" +// @router /endpoint_groups [get] func (handler *Handler) endpointGroupList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups() if err != nil { diff --git a/api/http/handler/endpointgroups/endpointgroup_update.go b/api/http/handler/endpointgroups/endpointgroup_update.go index 047bbb6b4..280c33494 100644 --- a/api/http/handler/endpointgroups/endpointgroup_update.go +++ b/api/http/handler/endpointgroups/endpointgroup_update.go @@ -7,15 +7,18 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/internal/tag" ) type endpointGroupUpdatePayload struct { - Name string - Description string - TagIDs []portainer.TagID + // Endpoint group name + Name string `example:"my-endpoint-group"` + // Endpoint group description + Description string `example:"description"` + // List of tag identifiers associated to the endpoint group + TagIDs []portainer.TagID `example:"3,4"` UserAccessPolicies portainer.UserAccessPolicies TeamAccessPolicies portainer.TeamAccessPolicies } @@ -24,7 +27,21 @@ func (payload *endpointGroupUpdatePayload) Validate(r *http.Request) error { return nil } -// PUT request on /api/endpoint_groups/:id +// @id EndpointGroupUpdate +// @summary Update an endpoint group +// @description Update an endpoint group. +// @description **Access policy**: administrator +// @tags endpoint_groups +// @security jwt +// @accept json +// @produce json +// @param id path int true "EndpointGroup identifier" +// @param body body endpointGroupUpdatePayload true "EndpointGroup details" +// @success 200 {object} portainer.EndpointGroup "Success" +// @failure 400 "Invalid request" +// @failure 404 "EndpointGroup not found" +// @failure 500 "Server error" +// @router /endpoint_groups/:id [put] func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/endpointproxy/proxy_kubernetes.go b/api/http/handler/endpointproxy/proxy_kubernetes.go index 7a4e9dc01..ed5b1a28b 100644 --- a/api/http/handler/endpointproxy/proxy_kubernetes.go +++ b/api/http/handler/endpointproxy/proxy_kubernetes.go @@ -3,11 +3,12 @@ package endpointproxy import ( "errors" "fmt" + "strings" "time" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" "net/http" @@ -66,9 +67,15 @@ func (handler *Handler) proxyRequestsToKubernetesAPI(w http.ResponseWriter, r *h requestPrefix := fmt.Sprintf("/%d/kubernetes", endpointID) if endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment { - requestPrefix = fmt.Sprintf("/%d", endpointID) + if isKubernetesRequest(strings.TrimPrefix(r.URL.String(), requestPrefix)) { + requestPrefix = fmt.Sprintf("/%d", endpointID) + } } http.StripPrefix(requestPrefix, proxy).ServeHTTP(w, r) return nil } + +func isKubernetesRequest(requestURL string) bool { + return strings.HasPrefix(requestURL, "/api") +} diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go index 728711d87..150391f38 100644 --- a/api/http/handler/endpoints/endpoint_create.go +++ b/api/http/handler/endpoints/endpoint_create.go @@ -14,7 +14,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/crypto" "github.com/portainer/portainer/api/http/client" "github.com/portainer/portainer/api/internal/edge" @@ -147,7 +147,34 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { return nil } -// POST request on /api/endpoints +// @id EndpointCreate +// @summary Create a new endpoint +// @description Create a new endpoint that will be used to manage an environment. +// @description **Access policy**: administrator +// @tags endpoints +// @security jwt +// @accept multipart/form-data +// @produce json +// @param Name formData string true "Name that will be used to identify this endpoint (example: my-endpoint)" +// @param EndpointType formData integer true "Environment type. Value must be one of: 1 (Local Docker environment), 2 (Agent environment), 3 (Azure environment), 4 (Edge agent environment) or 5 (Local Kubernetes Environment" Enum(1,2,3,4,5) +// @param URL formData string false "URL or IP address of a Docker host (example: docker.mydomain.tld:2375). Defaults to local if not specified (Linux: /var/run/docker.sock, Windows: //./pipe/docker_engine)" +// @param PublicURL formData string false "URL or IP address where exposed containers will be reachable. Defaults to URL if not specified (example: docker.mydomain.tld:2375)" +// @param GroupID formData int false "Endpoint group identifier. If not specified will default to 1 (unassigned)." +// @param TLS formData bool false "Require TLS to connect against this endpoint" +// @param TLSSkipVerify formData bool false "Skip server verification when using TLS" +// @param TLSSkipClientVerify formData bool false "Skip client verification when using TLS" +// @param TLSCACertFile formData file false "TLS CA certificate file" +// @param TLSCertFile formData file false "TLS client certificate file" +// @param TLSKeyFile formData file false "TLS client key file" +// @param AzureApplicationID formData string false "Azure application ID. Required if endpoint type is set to 3" +// @param AzureTenantID formData string false "Azure tenant ID. Required if endpoint type is set to 3" +// @param AzureAuthenticationKey formData string false "Azure authentication key. Required if endpoint type is set to 3" +// @param TagIDs formData []int false "List of tag identifiers to which this endpoint is associated" +// @param EdgeCheckinInterval formData int false "The check in interval for edge agent (in seconds)" +// @success 200 {object} portainer.Endpoint "Success" +// @failure 400 "Invalid request" +// @failure 500 "Server error" +// @router /endpoints [post] func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { payload := &endpointCreatePayload{} err := payload.Validate(r) @@ -440,6 +467,18 @@ func (handler *Handler) snapshotAndPersistEndpoint(endpoint *portainer.Endpoint) } func (handler *Handler) saveEndpointAndUpdateAuthorizations(endpoint *portainer.Endpoint) error { + endpoint.SecuritySettings = portainer.EndpointSecuritySettings{ + AllowVolumeBrowserForRegularUsers: false, + EnableHostManagementFeatures: false, + + AllowBindMountsForRegularUsers: true, + AllowPrivilegedModeForRegularUsers: true, + AllowHostNamespaceForRegularUsers: true, + AllowContainerCapabilitiesForRegularUsers: true, + AllowDeviceMappingForRegularUsers: true, + AllowStackManagementForRegularUsers: true, + } + err := handler.DataStore.Endpoint().CreateEndpoint(endpoint) if err != nil { return err diff --git a/api/http/handler/endpoints/endpoint_delete.go b/api/http/handler/endpoints/endpoint_delete.go index 875d4153e..9fda9b74c 100644 --- a/api/http/handler/endpoints/endpoint_delete.go +++ b/api/http/handler/endpoints/endpoint_delete.go @@ -7,11 +7,22 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/errors" ) -// DELETE request on /api/endpoints/:id +// @id EndpointDelete +// @summary Remove an endpoint +// @description Remove an endpoint. +// @description **Access policy**: administrator +// @tags endpoints +// @security jwt +// @param id path int true "Endpoint identifier" +// @success 204 "Success" +// @failure 400 "Invalid request" +// @failure 404 "Endpoint not found" +// @failure 500 "Server error" +// @router /endpoints/{id} [delete] func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/endpoints/endpoint_dockerhub_status.go b/api/http/handler/endpoints/endpoint_dockerhub_status.go new file mode 100644 index 000000000..793b85715 --- /dev/null +++ b/api/http/handler/endpoints/endpoint_dockerhub_status.go @@ -0,0 +1,140 @@ +package endpoints + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "strings" + + 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" + "github.com/portainer/portainer/api/http/client" + "github.com/portainer/portainer/api/internal/endpointutils" +) + +type dockerhubStatusResponse struct { + Remaining int `json:"remaining"` + Limit int `json:"limit"` +} + +// GET request on /api/endpoints/{id}/dockerhub/status +func (handler *Handler) endpointDockerhubStatus(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} + } + + if !endpointutils.IsLocalEndpoint(endpoint) { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment type", errors.New("Invalid environment type")} + } + + dockerhub, err := handler.DataStore.DockerHub().DockerHub() + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve DockerHub details from the database", err} + } + + httpClient := client.NewHTTPClient() + token, err := getDockerHubToken(httpClient, dockerhub) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve DockerHub token from DockerHub", err} + } + + resp, err := getDockerHubLimits(httpClient, token) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve DockerHub rate limits from DockerHub", err} + } + + return response.JSON(w, resp) +} + +func getDockerHubToken(httpClient *client.HTTPClient, dockerhub *portainer.DockerHub) (string, error) { + type dockerhubTokenResponse struct { + Token string `json:"token"` + } + + requestURL := "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" + + req, err := http.NewRequest(http.MethodGet, requestURL, nil) + if err != nil { + return "", err + } + + if dockerhub.Authentication { + req.SetBasicAuth(dockerhub.Username, dockerhub.Password) + } + + resp, err := httpClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", errors.New("failed fetching dockerhub token") + } + + var data dockerhubTokenResponse + err = json.NewDecoder(resp.Body).Decode(&data) + if err != nil { + return "", err + } + + return data.Token, nil +} + +func getDockerHubLimits(httpClient *client.HTTPClient, token string) (*dockerhubStatusResponse, error) { + + requestURL := "https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest" + + req, err := http.NewRequest(http.MethodHead, requestURL, nil) + if err != nil { + return nil, err + } + + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + + resp, err := httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, errors.New("failed fetching dockerhub limits") + } + + rateLimit, err := parseRateLimitHeader(resp.Header, "RateLimit-Limit") + rateLimitRemaining, err := parseRateLimitHeader(resp.Header, "RateLimit-Remaining") + + return &dockerhubStatusResponse{ + Limit: rateLimit, + Remaining: rateLimitRemaining, + }, nil +} + +func parseRateLimitHeader(headers http.Header, headerKey string) (int, error) { + headerValue := headers.Get(headerKey) + if headerValue == "" { + return 0, fmt.Errorf("Missing %s header", headerKey) + } + + matches := strings.Split(headerValue, ";") + value, err := strconv.Atoi(matches[0]) + if err != nil { + return 0, err + } + + return value, nil +} diff --git a/api/http/handler/endpoints/endpoint_extension_add.go b/api/http/handler/endpoints/endpoint_extension_add.go index e99e10caa..36112e5ea 100644 --- a/api/http/handler/endpoints/endpoint_extension_add.go +++ b/api/http/handler/endpoints/endpoint_extension_add.go @@ -10,7 +10,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" ) @@ -29,7 +29,6 @@ func (payload *endpointExtensionAddPayload) Validate(r *http.Request) error { return nil } -// POST request on /api/endpoints/:id/extensions func (handler *Handler) endpointExtensionAdd(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/endpoints/endpoint_extension_remove.go b/api/http/handler/endpoints/endpoint_extension_remove.go index 99edf1bc8..13e06a1f5 100644 --- a/api/http/handler/endpoints/endpoint_extension_remove.go +++ b/api/http/handler/endpoints/endpoint_extension_remove.go @@ -8,11 +8,10 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/errors" ) -// DELETE request on /api/endpoints/:id/extensions/:extensionType func (handler *Handler) endpointExtensionRemove(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/endpoints/endpoint_inspect.go b/api/http/handler/endpoints/endpoint_inspect.go index 1411e93cb..bab2452f5 100644 --- a/api/http/handler/endpoints/endpoint_inspect.go +++ b/api/http/handler/endpoints/endpoint_inspect.go @@ -6,11 +6,23 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/errors" ) -// GET request on /api/endpoints/:id +// @id EndpointInspect +// @summary Inspect an endpoint +// @description Retrieve details about an endpoint. +// @description **Access policy**: restricted +// @tags endpoints +// @security jwt +// @produce json +// @param id path int true "Endpoint identifier" +// @success 200 {object} portainer.Endpoint "Success" +// @failure 400 "Invalid request" +// @failure 404 "Endpoint not found" +// @failure 500 "Server error" +// @router /endpoints/{id} [get] func (handler *Handler) endpointInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { @@ -30,6 +42,7 @@ func (handler *Handler) endpointInspect(w http.ResponseWriter, r *http.Request) } hideFields(endpoint) + endpoint.ComposeSyntaxMaxVersion = handler.ComposeStackManager.ComposeSyntaxMaxVersion() return response.JSON(w, endpoint) } diff --git a/api/http/handler/endpoints/endpoint_list.go b/api/http/handler/endpoints/endpoint_list.go index fe7c489ae..c1dd76209 100644 --- a/api/http/handler/endpoints/endpoint_list.go +++ b/api/http/handler/endpoints/endpoint_list.go @@ -5,16 +5,34 @@ import ( "strconv" "strings" - "github.com/portainer/portainer/api" - "github.com/portainer/libhttp/request" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/response" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/security" ) -// GET request on /api/endpoints?(start=)&(limit=)&(search=)&(groupId=&method=&endpointId= +// @id StackCreate +// @summary Deploy a new stack +// @description Deploy a new stack into a Docker environment specified via the endpoint identifier. +// @description **Access policy**: restricted +// @tags stacks +// @security jwt +// @accept json, multipart/form-data +// @produce json +// @param type query int true "Stack deployment type. Possible values: 1 (Swarm stack) or 2 (Compose stack)." Enums(1,2) +// @param method query string true "Stack deployment method. Possible values: file, string or repository." Enums(string, file, repository) +// @param endpointId query int true "Identifier of the endpoint that will be used to deploy the stack" +// @param body_swarm_string body swarmStackFromFileContentPayload false "Required when using method=string and type=1" +// @param body_swarm_repository body swarmStackFromGitRepositoryPayload false "Required when using method=repository and type=1" +// @param body_compose_string body composeStackFromFileContentPayload false "Required when using method=string and type=2" +// @param body_compose_repository body composeStackFromGitRepositoryPayload false "Required when using method=repository and type=2" +// @param Name formData string false "Name of the stack. required when method is file" +// @param SwarmID formData string false "Swarm cluster identifier. Required when method equals file and type equals 1. required when method is file" +// @param Env formData string false "Environment variables passed during deployment, represented as a JSON array [{'name': 'name', 'value': 'value'}]. Optional, used when method equals file and type equals 1." +// @param file formData file false "Stack file. required when method is file" +// @success 200 {object} portainer.CustomTemplate +// @failure 400 "Invalid request" +// @failure 500 "Server error" +// @router /stacks [post] func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { stackType, err := request.RetrieveNumericQueryParameter(r, "type", false) if err != nil { @@ -46,12 +69,14 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err} } - settings, err := handler.DataStore.Settings().Settings() - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", 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} } - if !settings.AllowStackManagementForRegularUsers { + if !endpoint.SecuritySettings.AllowStackManagementForRegularUsers { securityContext, err := security.RetrieveRestrictedRequestContext(r) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err} @@ -69,13 +94,6 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt } } - 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.AuthorizedEndpointOperation(r, endpoint) if err != nil { return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} @@ -129,7 +147,7 @@ func (handler *Handler) createSwarmStack(w http.ResponseWriter, r *http.Request, return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", errors.New(request.ErrInvalidQueryParameter)} } -func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *portainer.Settings) error { +func (handler *Handler) isValidStackFile(stackFileContent []byte, securitySettings *portainer.EndpointSecuritySettings) error { composeConfigYAML, err := loader.ParseYAML(stackFileContent) if err != nil { return err @@ -154,7 +172,7 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *port for key := range composeConfig.Services { service := composeConfig.Services[key] - if !settings.AllowBindMountsForRegularUsers { + if !securitySettings.AllowBindMountsForRegularUsers { for _, volume := range service.Volumes { if volume.Type == "bind" { return errors.New("bind-mount disabled for non administrator users") @@ -162,19 +180,23 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *port } } - if !settings.AllowPrivilegedModeForRegularUsers && service.Privileged == true { + if !securitySettings.AllowPrivilegedModeForRegularUsers && service.Privileged == true { return errors.New("privileged mode disabled for non administrator users") } - if !settings.AllowHostNamespaceForRegularUsers && service.Pid == "host" { + if !securitySettings.AllowHostNamespaceForRegularUsers && service.Pid == "host" { return errors.New("pid host disabled for non administrator users") } - if !settings.AllowDeviceMappingForRegularUsers && service.Devices != nil && len(service.Devices) > 0 { + if !securitySettings.AllowDeviceMappingForRegularUsers && service.Devices != nil && len(service.Devices) > 0 { return errors.New("device mapping disabled for non administrator users") } - if !settings.AllowContainerCapabilitiesForRegularUsers && (len(service.CapAdd) > 0 || len(service.CapDrop) > 0) { + if !securitySettings.AllowSysctlSettingForRegularUsers && service.Sysctls != nil && len(service.Sysctls) > 0 { + return errors.New("sysctl setting disabled for non administrator users") + } + + if !securitySettings.AllowContainerCapabilitiesForRegularUsers && (len(service.CapAdd) > 0 || len(service.CapDrop) > 0) { return errors.New("container capabilities disabled for non administrator users") } } @@ -183,9 +205,20 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *port } func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *portainer.Stack, userID portainer.UserID) *httperror.HandlerError { - resourceControl := authorization.NewPrivateResourceControl(stack.Name, portainer.StackResourceControl, userID) + var resourceControl *portainer.ResourceControl - err := handler.DataStore.ResourceControl().CreateResourceControl(resourceControl) + isAdmin, err := handler.userIsAdmin(userID) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err} + } + + if isAdmin { + resourceControl = authorization.NewAdministratorsOnlyResourceControl(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl) + } else { + resourceControl = authorization.NewPrivateResourceControl(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl, userID) + } + + err = handler.DataStore.ResourceControl().CreateResourceControl(resourceControl) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist resource control inside the database", err} } diff --git a/api/http/handler/stacks/stack_delete.go b/api/http/handler/stacks/stack_delete.go index 6866c6809..dd33d38cc 100644 --- a/api/http/handler/stacks/stack_delete.go +++ b/api/http/handler/stacks/stack_delete.go @@ -12,11 +12,24 @@ import ( bolterrors "github.com/portainer/portainer/api/bolt/errors" httperrors "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/stackutils" ) -// DELETE request on /api/stacks/:id?external=&endpointId= -// If the external query parameter is set to true, the id route variable is expected to be -// the name of an external stack as a string. +// @id StackDelete +// @summary Remove a stack +// @description Remove a stack. +// @description **Access policy**: restricted +// @tags stacks +// @security jwt +// @param id path int true "Stack identifier" +// @param external query boolean false "Set to true to delete an external stack. Only external Swarm stacks are supported" +// @param endpointId query int false "Endpoint identifier used to remove an external stack (required when external is set to true)" +// @success 204 "Success" +// @failure 400 "Invalid request" +// @failure 403 "Permission denied" +// @failure 404 " not found" +// @failure 500 "Server error" +// @router /stacks/{id} [delete] func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { stackID, err := request.RetrieveRouteVariableValue(r, "id") if err != nil { @@ -70,7 +83,7 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} } - resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl) + resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err} } @@ -155,5 +168,6 @@ func (handler *Handler) deleteStack(stack *portainer.Stack, endpoint *portainer. if stack.Type == portainer.DockerSwarmStack { return handler.SwarmStackManager.Remove(stack, endpoint) } + return handler.ComposeStackManager.Down(stack, endpoint) } diff --git a/api/http/handler/stacks/stack_file.go b/api/http/handler/stacks/stack_file.go index 8024a72f2..fc11cecd0 100644 --- a/api/http/handler/stacks/stack_file.go +++ b/api/http/handler/stacks/stack_file.go @@ -7,17 +7,32 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/stackutils" ) type stackFileResponse struct { - StackFileContent string `json:"StackFileContent"` + // Content of the Stack file + StackFileContent string `json:"StackFileContent" example:"version: 3\n services:\n web:\n image:nginx"` } -// GET request on /api/stacks/:id/file +// @id StackFileInspect +// @summary Retrieve the content of the Stack file for the specified stack +// @description Get Stack file content. +// @description **Access policy**: restricted +// @tags stacks +// @security jwt +// @produce json +// @param id path int true "Stack identifier" +// @success 200 {object} stackFileResponse "Success" +// @failure 400 "Invalid request" +// @failure 403 "Permission denied" +// @failure 404 "Stack not found" +// @failure 500 "Server error" +// @router /stacks/{id}/file [get] func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { stackID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { @@ -43,7 +58,7 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} } - resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl) + resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err} } diff --git a/api/http/handler/stacks/stack_inspect.go b/api/http/handler/stacks/stack_inspect.go index df9bc279a..2891279cc 100644 --- a/api/http/handler/stacks/stack_inspect.go +++ b/api/http/handler/stacks/stack_inspect.go @@ -6,13 +6,27 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/stackutils" ) -// GET request on /api/stacks/:id +// @id StackInspect +// @summary Inspect a stack +// @description Retrieve details about a stack. +// @description **Access policy**: restricted +// @tags stacks +// @security jwt +// @produce json +// @param id path int true "Stack identifier" +// @success 200 {object} portainer.Stack "Success" +// @failure 400 "Invalid request" +// @failure 403 "Permission denied" +// @failure 404 "Stack not found" +// @failure 500 "Server error" +// @router /stacks/{id} [get] func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { stackID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { @@ -43,7 +57,7 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err} } - resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl) + resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err} } diff --git a/api/http/handler/stacks/stack_list.go b/api/http/handler/stacks/stack_list.go index f8be720da..ebc4de58d 100644 --- a/api/http/handler/stacks/stack_list.go +++ b/api/http/handler/stacks/stack_list.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" ) @@ -16,7 +16,20 @@ type stackListOperationFilters struct { EndpointID int `json:"EndpointID"` } -// GET request on /api/stacks?(filters=) +// @id StackList +// @summary List stacks +// @description List all stacks based on the current user authorizations. +// @description Will return all stacks if using an administrator account otherwise it +// @description will only return the list of stacks the user have access to. +// @description **Access policy**: restricted +// @tags stacks +// @security jwt +// @param filters query string false "Filters to process on the stack list. Encoded as JSON (a map[string]string). For example, {"SwarmID": "jpofkc0i9uo9wtx1zesuk649w"} will only return stacks that are part of the specified Swarm cluster. Available filters: EndpointID, SwarmID." +// @success 200 {array} portainer.Stack "Success" +// @success 204 "Success" +// @failure 400 "Invalid request" +// @failure 500 "Server error" +// @router /stacks [get] func (handler *Handler) stackList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { var filters stackListOperationFilters err := request.RetrieveJSONQueryParameter(r, "filters", &filters, true) diff --git a/api/http/handler/stacks/stack_migrate.go b/api/http/handler/stacks/stack_migrate.go index e84f7e5f7..9f13ef1f5 100644 --- a/api/http/handler/stacks/stack_migrate.go +++ b/api/http/handler/stacks/stack_migrate.go @@ -2,21 +2,26 @@ package stacks import ( "errors" + "fmt" "net/http" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" httperrors "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/stackutils" ) type stackMigratePayload struct { - EndpointID int - SwarmID string - Name string + // Endpoint identifier of the target endpoint where the stack will be relocated + EndpointID int `example:"2" validate:"required"` + // Swarm cluster identifier, must match the identifier of the cluster where the stack will be relocated + SwarmID string `example:"jpofkc0i9uo9wtx1zesuk649w"` + // If provided will rename the migrated stack + Name string `example:"new-stack"` } func (payload *stackMigratePayload) Validate(r *http.Request) error { @@ -26,7 +31,22 @@ func (payload *stackMigratePayload) Validate(r *http.Request) error { return nil } -// POST request on /api/stacks/:id/migrate?endpointId= +// @id StackMigrate +// @summary Migrate a stack to another endpoint +// @description Migrate a stack from an endpoint to another endpoint. It will re-create the stack inside the target endpoint before removing the original stack. +// @description **Access policy**: restricted +// @tags stacks +// @security jwt +// @produce json +// @param id path int true "Stack identifier" +// @param endpointId query int false "Stacks created before version 1.18.0 might not have an associated endpoint identifier. Use this optional parameter to set the endpoint identifier used by the stack." +// @param body body stackMigratePayload true "Stack migration details" +// @success 200 {object} portainer.Stack "Success" +// @failure 400 "Invalid request" +// @failure 403 "Permission denied" +// @failure 404 "Stack not found" +// @failure 500 "Server error" +// @router /stacks/{id}/migrate [post] func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { stackID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { @@ -58,7 +78,7 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} } - resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl) + resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err} } @@ -104,6 +124,16 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht stack.Name = payload.Name } + isUnique, err := handler.checkUniqueName(targetEndpoint, stack.Name, stack.ID, stack.SwarmID != "") + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err} + } + + if !isUnique { + errorMessage := fmt.Sprintf("A stack with the name '%s' is already running on endpoint '%s'", stack.Name, targetEndpoint.Name) + return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)} + } + migrationError := handler.migrateStack(r, stack, targetEndpoint) if migrationError != nil { return migrationError diff --git a/api/http/handler/stacks/stack_start.go b/api/http/handler/stacks/stack_start.go index 298a11d42..0a915600b 100644 --- a/api/http/handler/stacks/stack_start.go +++ b/api/http/handler/stacks/stack_start.go @@ -2,19 +2,33 @@ package stacks import ( "errors" + "fmt" "net/http" + portainer "github.com/portainer/portainer/api" httperrors "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/stackutils" 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/stacks/:id/start +// @id StackStart +// @summary Starts a stopped Stack +// @description Starts a stopped Stack. +// @description **Access policy**: restricted +// @tags stacks +// @security jwt +// @param id path int true "Stack identifier" +// @success 200 {object} portainer.Stack "Success" +// @failure 400 "Invalid request" +// @failure 403 "Permission denied" +// @failure 404 " not found" +// @failure 500 "Server error" +// @router /stacks/{id}/start [post] func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { stackID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { @@ -45,7 +59,16 @@ func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *http return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} } - resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl) + isUnique, err := handler.checkUniqueName(endpoint, stack.Name, stack.ID, stack.SwarmID != "") + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err} + } + if !isUnique { + errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", stack.Name) + return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)} + } + + resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err} } @@ -64,7 +87,7 @@ func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *http err = handler.startStack(stack, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to stop stack", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to start stack", err} } stack.Status = portainer.StackStatusActive diff --git a/api/http/handler/stacks/stack_stop.go b/api/http/handler/stacks/stack_stop.go index ee2c13f32..7dabfb265 100644 --- a/api/http/handler/stacks/stack_stop.go +++ b/api/http/handler/stacks/stack_stop.go @@ -4,18 +4,29 @@ import ( "errors" "net/http" - httperrors "github.com/portainer/portainer/api/http/errors" - - "github.com/portainer/portainer/api/http/security" - httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" + httperrors "github.com/portainer/portainer/api/http/errors" + "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/stackutils" ) -// POST request on /api/stacks/:id/stop +// @id StackStop +// @summary Stops a stopped Stack +// @description Stops a stopped Stack. +// @description **Access policy**: restricted +// @tags stacks +// @security jwt +// @param id path int true "Stack identifier" +// @success 200 {object} portainer.Stack "Success" +// @failure 400 "Invalid request" +// @failure 403 "Permission denied" +// @failure 404 " not found" +// @failure 500 "Server error" +// @router /stacks/{id}/stop [post] func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { stackID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { @@ -46,7 +57,7 @@ func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httpe return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} } - resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl) + resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err} } diff --git a/api/http/handler/stacks/stack_update.go b/api/http/handler/stacks/stack_update.go index df4178f8f..ea012382a 100644 --- a/api/http/handler/stacks/stack_update.go +++ b/api/http/handler/stacks/stack_update.go @@ -4,20 +4,24 @@ import ( "errors" "net/http" "strconv" + "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" + 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" + "github.com/portainer/portainer/api/internal/stackutils" ) type updateComposeStackPayload struct { - StackFileContent string - Env []portainer.Pair + // New content of the Stack file + StackFileContent string `example:"version: 3\n services:\n web:\n image:nginx"` + // A list of environment variables used during stack deployment + Env []portainer.Pair } func (payload *updateComposeStackPayload) Validate(r *http.Request) error { @@ -28,9 +32,12 @@ func (payload *updateComposeStackPayload) Validate(r *http.Request) error { } type updateSwarmStackPayload struct { - StackFileContent string - Env []portainer.Pair - Prune bool + // New content of the Stack file + StackFileContent string `example:"version: 3\n services:\n web:\n image:nginx"` + // A list of environment variables used during stack deployment + Env []portainer.Pair + // Prune services that are no longer referenced (only available for Swarm stacks) + Prune bool `example:"true"` } func (payload *updateSwarmStackPayload) Validate(r *http.Request) error { @@ -40,7 +47,23 @@ func (payload *updateSwarmStackPayload) Validate(r *http.Request) error { return nil } -// PUT request on /api/stacks/:id?endpointId= +// @id StackUpdate +// @summary Update a stack +// @description Update a stack. +// @description **Access policy**: restricted +// @tags stacks +// @security jwt +// @accept json +// @produce json +// @param id path int true "Stack identifier" +// @param endpointId query int false "Stacks created before version 1.18.0 might not have an associated endpoint identifier. Use this optional parameter to set the endpoint identifier used by the stack." +// @param body body updateSwarmStackPayload true "Stack details" +// @success 200 {object} portainer.Stack "Success" +// @failure 400 "Invalid request" +// @failure 403 "Permission denied" +// @failure 404 " not found" +// @failure 500 "Server error" +// @router /stacks/{id} [put] func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { stackID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { @@ -77,7 +100,7 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} } - resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl) + resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err} } @@ -135,6 +158,9 @@ func (handler *Handler) updateComposeStack(r *http.Request, stack *portainer.Sta return configErr } + stack.UpdateDate = time.Now().Unix() + stack.UpdatedBy = config.user.Username + err = handler.deployComposeStack(config) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err} @@ -163,6 +189,9 @@ func (handler *Handler) updateSwarmStack(r *http.Request, stack *portainer.Stack return configErr } + stack.UpdateDate = time.Now().Unix() + stack.UpdatedBy = config.user.Username + err = handler.deploySwarmStack(config) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err} diff --git a/api/http/handler/status/status_inspect.go b/api/http/handler/status/status_inspect.go index 1892ddd7d..d7b399ae5 100644 --- a/api/http/handler/status/status_inspect.go +++ b/api/http/handler/status/status_inspect.go @@ -7,7 +7,14 @@ import ( "github.com/portainer/libhttp/response" ) -// GET request on /api/status +// @id StatusInspect +// @summary Check Portainer status +// @description Retrieve Portainer status +// @description **Access policy**: public +// @tags status +// @produce json +// @success 200 {object} portainer.Status "Success" +// @router /status [get] func (handler *Handler) statusInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { return response.JSON(w, handler.Status) } diff --git a/api/http/handler/status/status_inspect_version.go b/api/http/handler/status/status_inspect_version.go index 054bd670e..58ef46a51 100644 --- a/api/http/handler/status/status_inspect_version.go +++ b/api/http/handler/status/status_inspect_version.go @@ -13,15 +13,25 @@ import ( ) type inspectVersionResponse struct { - UpdateAvailable bool `json:"UpdateAvailable"` - LatestVersion string `json:"LatestVersion"` + // Whether portainer has an update available + UpdateAvailable bool `json:"UpdateAvailable" example:"false"` + // The latest version available + LatestVersion string `json:"LatestVersion" example:"2.0.0"` } type githubData struct { TagName string `json:"tag_name"` } -// GET request on /api/status/version +// @id StatusInspectVersion +// @summary Check for portainer updates +// @description Check if portainer has an update available +// @description **Access policy**: authenticated +// @security jwt +// @tags status +// @produce json +// @success 200 {object} inspectVersionResponse "Success" +// @router /status/version [get] func (handler *Handler) statusInspectVersion(w http.ResponseWriter, r *http.Request) { motd, err := client.Get(portainer.VersionCheckURL, 5) if err != nil { diff --git a/api/http/handler/tags/tag_create.go b/api/http/handler/tags/tag_create.go index 5a2d1e400..d969d3d91 100644 --- a/api/http/handler/tags/tag_create.go +++ b/api/http/handler/tags/tag_create.go @@ -8,11 +8,12 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" ) type tagCreatePayload struct { - Name string + // Name + Name string `validate:"required" example:"org/acme"` } func (payload *tagCreatePayload) Validate(r *http.Request) error { @@ -22,7 +23,18 @@ func (payload *tagCreatePayload) Validate(r *http.Request) error { return nil } -// POST request on /api/tags +// @id TagCreate +// @summary Create a new tag +// @description Create a new tag. +// @description **Access policy**: administrator +// @tags tags +// @security jwt +// @produce json +// @param body body tagCreatePayload true "Tag details" +// @success 200 {object} portainer.Tag "Success" +// @failure 409 "Tag name exists" +// @failure 500 "Server error" +// @router /tags [post] func (handler *Handler) tagCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { var payload tagCreatePayload err := request.DecodeAndValidateJSONPayload(r, &payload) diff --git a/api/http/handler/tags/tag_delete.go b/api/http/handler/tags/tag_delete.go index 287af24bd..f1465dfce 100644 --- a/api/http/handler/tags/tag_delete.go +++ b/api/http/handler/tags/tag_delete.go @@ -6,12 +6,26 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/internal/edge" ) -// DELETE request on /api/tags/:id +// @id TagDelete +// @summary Remove a tag +// @description Remove a tag. +// @description **Access policy**: administrator +// @tags tags +// @security jwt +// @accept json +// @produce json +// @param id path int true "Tag identifier" +// @success 204 "Success" +// @failure 400 "Invalid request" +// @failure 403 "Permission denied" +// @failure 404 "Tag not found" +// @failure 500 "Server error" +// @router /tags/{id} [delete] func (handler *Handler) tagDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { id, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/tags/tag_list.go b/api/http/handler/tags/tag_list.go index e4d7f2afa..52852809b 100644 --- a/api/http/handler/tags/tag_list.go +++ b/api/http/handler/tags/tag_list.go @@ -7,7 +7,16 @@ import ( "github.com/portainer/libhttp/response" ) -// GET request on /api/tags +// @id TagList +// @summary List tags +// @description List tags. +// @description **Access policy**: administrator +// @tags tags +// @security jwt +// @produce json +// @success 200 {array} portainer.Tag "Success" +// @failure 500 "Server error" +// @router /tags [get] func (handler *Handler) tagList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { tags, err := handler.DataStore.Tag().Tags() if err != nil { diff --git a/api/http/handler/teammemberships/teammembership_create.go b/api/http/handler/teammemberships/teammembership_create.go index 361534d07..1b61e99a1 100644 --- a/api/http/handler/teammemberships/teammembership_create.go +++ b/api/http/handler/teammemberships/teammembership_create.go @@ -7,15 +7,18 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" httperrors "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) type teamMembershipCreatePayload struct { - UserID int - TeamID int - Role int + // User identifier + UserID int `validate:"required" example:"1"` + // Team identifier + TeamID int `validate:"required" example:"1"` + // Role for the user inside the team (1 for leader and 2 for regular member) + Role int `validate:"required" example:"1" enums:"1,2"` } func (payload *teamMembershipCreatePayload) Validate(r *http.Request) error { @@ -31,7 +34,22 @@ func (payload *teamMembershipCreatePayload) Validate(r *http.Request) error { return nil } -// POST request on /api/team_memberships +// @id TeamMembershipCreate +// @summary Create a new team membership +// @description Create a new team memberships. Access is only available to administrators leaders of the associated team. +// @description **Access policy**: admin +// @tags team_memberships +// @security jwt +// @accept json +// @produce json +// @param body body teamMembershipCreatePayload true "Team membership details" +// @success 200 {object} portainer.TeamMembership "Success" +// @success 204 "Success" +// @failure 400 "Invalid request" +// @failure 403 "Permission denied to manage memberships" +// @failure 409 "Team membership already registered" +// @failure 500 "Server error" +// @router /team_memberships [post] func (handler *Handler) teamMembershipCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { var payload teamMembershipCreatePayload err := request.DecodeAndValidateJSONPayload(r, &payload) diff --git a/api/http/handler/teammemberships/teammembership_delete.go b/api/http/handler/teammemberships/teammembership_delete.go index f4e71d3c5..bcc4ac1c9 100644 --- a/api/http/handler/teammemberships/teammembership_delete.go +++ b/api/http/handler/teammemberships/teammembership_delete.go @@ -6,13 +6,25 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) -// DELETE request on /api/team_memberships/:id +// @id TeamMembershipDelete +// @summary Remove a team membership +// @description Remove a team membership. Access is only available to administrators leaders of the associated team. +// @description **Access policy**: restricted +// @tags team_memberships +// @security jwt +// @param id path int true "TeamMembership identifier" +// @success 204 "Success" +// @failure 400 "Invalid request" +// @failure 403 "Permission denied" +// @failure 404 "TeamMembership not found" +// @failure 500 "Server error" +// @router /team_memberships/{id} [delete] func (handler *Handler) teamMembershipDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { membershipID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/teammemberships/teammembership_list.go b/api/http/handler/teammemberships/teammembership_list.go index 77a113776..7c69a9be9 100644 --- a/api/http/handler/teammemberships/teammembership_list.go +++ b/api/http/handler/teammemberships/teammembership_list.go @@ -9,7 +9,18 @@ import ( "github.com/portainer/portainer/api/http/security" ) -// GET request on /api/team_memberships +// @id TeamMembershipList +// @summary List team memberships +// @description List team memberships. Access is only available to administrators and team leaders. +// @description **Access policy**: admin +// @tags team_memberships +// @security jwt +// @produce json +// @success 200 {array} portainer.TeamMembership "Success" +// @failure 400 "Invalid request" +// @failure 403 "Permission denied" +// @failure 500 "Server error" +// @router /team_memberships [get] func (handler *Handler) teamMembershipList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { securityContext, err := security.RetrieveRestrictedRequestContext(r) if err != nil { diff --git a/api/http/handler/teammemberships/teammembership_update.go b/api/http/handler/teammemberships/teammembership_update.go index cf801a65d..b01130a40 100644 --- a/api/http/handler/teammemberships/teammembership_update.go +++ b/api/http/handler/teammemberships/teammembership_update.go @@ -7,16 +7,19 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" httperrors "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) type teamMembershipUpdatePayload struct { - UserID int - TeamID int - Role int + // User identifier + UserID int `validate:"required" example:"1"` + // Team identifier + TeamID int `validate:"required" example:"1"` + // Role for the user inside the team (1 for leader and 2 for regular member) + Role int `validate:"required" example:"1" enums:"1,2"` } func (payload *teamMembershipUpdatePayload) Validate(r *http.Request) error { @@ -32,7 +35,22 @@ func (payload *teamMembershipUpdatePayload) Validate(r *http.Request) error { return nil } -// PUT request on /api/team_memberships/:id +// @id TeamMembershipUpdate +// @summary Update a team membership +// @description Update a team membership. Access is only available to administrators leaders of the associated team. +// @description **Access policy**: restricted +// @tags team_memberships +// @security jwt +// @accept json +// @produce json +// @param id path int true "Team membership identifier" +// @param body body teamMembershipUpdatePayload true "Team membership details" +// @success 200 {object} portainer.TeamMembership "Success" +// @failure 400 "Invalid request" +// @failure 403 "Permission denied" +// @failure 404 "TeamMembership not found" +// @failure 500 "Server error" +// @router /team_memberships/{id} [put] func (handler *Handler) teamMembershipUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { membershipID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/teams/team_create.go b/api/http/handler/teams/team_create.go index 583087733..392b54ce3 100644 --- a/api/http/handler/teams/team_create.go +++ b/api/http/handler/teams/team_create.go @@ -8,12 +8,13 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" ) type teamCreatePayload struct { - Name string + // Name + Name string `example:"developers" validate:"required"` } func (payload *teamCreatePayload) Validate(r *http.Request) error { @@ -23,6 +24,20 @@ func (payload *teamCreatePayload) Validate(r *http.Request) error { return nil } +// @id TeamCreate +// @summary Create a new team +// @description Create a new team. +// @description **Access policy**: administrator +// @tags teams +// @security jwt +// @accept json +// @produce json +// @param body body teamCreatePayload true "details" +// @success 200 {object} portainer.Team "Success" +// @failure 400 "Invalid request" +// @failure 409 "Team already exists" +// @failure 500 "Server error" +// @router /team [post] func (handler *Handler) teamCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { var payload teamCreatePayload err := request.DecodeAndValidateJSONPayload(r, &payload) diff --git a/api/http/handler/teams/team_delete.go b/api/http/handler/teams/team_delete.go index b56551a7b..cc1246191 100644 --- a/api/http/handler/teams/team_delete.go +++ b/api/http/handler/teams/team_delete.go @@ -6,11 +6,22 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/errors" ) -// DELETE request on /api/teams/:id +// @id TeamDelete +// @summary Remove a team +// @description Remove a team. +// @description **Access policy**: administrator +// @tags teams +// @security jwt +// @success 204 "Success" +// @failure 400 "Invalid request" +// @failure 403 "Permission denied" +// @failure 404 "Team not found" +// @failure 500 "Server error" +// @router /teams/{id} [delete] func (handler *Handler) teamDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { teamID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/teams/team_inspect.go b/api/http/handler/teams/team_inspect.go index 81d739824..b4ac14a83 100644 --- a/api/http/handler/teams/team_inspect.go +++ b/api/http/handler/teams/team_inspect.go @@ -6,13 +6,27 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) -// GET request on /api/teams/:id +// @id TeamInspect +// @summary Inspect a team +// @description Retrieve details about a team. Access is only available for administrator and leaders of that team. +// @description **Access policy**: restricted +// @tags teams +// @security jwt +// @produce json +// @param id path int true "Team identifier" +// @success 200 {object} portainer.Team "Success" +// @success 204 "Success" +// @failure 400 "Invalid request" +// @failure 403 "Permission denied" +// @failure 404 "Team not found" +// @failure 500 "Server error" +// @router /teams/{id} [get] func (handler *Handler) teamInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { teamID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/teams/team_list.go b/api/http/handler/teams/team_list.go index 5056a69b6..f6b095d39 100644 --- a/api/http/handler/teams/team_list.go +++ b/api/http/handler/teams/team_list.go @@ -8,7 +8,16 @@ import ( "github.com/portainer/portainer/api/http/security" ) -// GET request on /api/teams +// @id TeamList +// @summary List teams +// @description List teams. For non-administrator users, will only list the teams they are member of. +// @description **Access policy**: restricted +// @tags teams +// @security jwt +// @produce json +// @success 200 {array} portainer.Team "Success" +// @failure 500 "Server error" +// @router /teams [get] func (handler *Handler) teamList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { teams, err := handler.DataStore.Team().Teams() if err != nil { diff --git a/api/http/handler/teams/team_memberships.go b/api/http/handler/teams/team_memberships.go index 75c6f1389..03b86ce62 100644 --- a/api/http/handler/teams/team_memberships.go +++ b/api/http/handler/teams/team_memberships.go @@ -6,12 +6,23 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) -// GET request on /api/teams/:id/memberships +// @id TeamMemberships +// @summary List team memberships +// @description List team memberships. Access is only available to administrators and team leaders. +// @description **Access policy**: restricted +// @tags team_memberships +// @security jwt +// @produce json +// @success 200 {array} portainer.TeamMembership "Success" +// @failure 400 "Invalid request" +// @failure 403 "Permission denied" +// @failure 500 "Server error" +// @router /teams/{id}/memberships [get] func (handler *Handler) teamMemberships(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { teamID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/teams/team_update.go b/api/http/handler/teams/team_update.go index eba25b11d..9ca8dd766 100644 --- a/api/http/handler/teams/team_update.go +++ b/api/http/handler/teams/team_update.go @@ -6,19 +6,36 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/errors" ) type teamUpdatePayload struct { - Name string + // Name + Name string `example:"developers"` } func (payload *teamUpdatePayload) Validate(r *http.Request) error { return nil } -// PUT request on /api/teams/:id +// @id TeamUpdate +// @summary Update a team +// @description Update a team. +// @description **Access policy**: administrator +// @tags +// @security jwt +// @accept json +// @produce json +// @param id path int true "Team identifier" +// @param body body teamUpdatePayload true "Team details" +// @success 200 {object} portainer.Team "Success" +// @success 204 "Success" +// @failure 400 "Invalid request" +// @failure 403 "Permission denied" +// @failure 404 "Team not found" +// @failure 500 "Server error" +// @router /team/{id} [put] func (handler *Handler) teamUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { teamID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/templates/template_file.go b/api/http/handler/templates/template_file.go index 614e3d170..359471ce6 100644 --- a/api/http/handler/templates/template_file.go +++ b/api/http/handler/templates/template_file.go @@ -13,12 +13,15 @@ import ( ) type filePayload struct { - RepositoryURL string - ComposeFilePathInRepository string + // URL of a git repository where the file is stored + RepositoryURL string `example:"https://github.com/portainer/portainer-compose" validate:"required"` + // Path to the file inside the git repository + ComposeFilePathInRepository string `example:"./subfolder/docker-compose.yml" validate:"required"` } type fileResponse struct { - FileContent string + // The requested file content + FileContent string `example: "version:2"` } func (payload *filePayload) Validate(r *http.Request) error { @@ -33,6 +36,19 @@ func (payload *filePayload) Validate(r *http.Request) error { return nil } +// @id TemplateFile +// @summary Get a template's file +// @description Get a template's file +// @description **Access policy**: restricted +// @tags templates +// @security jwt +// @accept json +// @produce json +// @param body body filePayload true "File details" +// @success 200 {object} fileResponse "Success" +// @failure 400 "Invalid request" +// @failure 500 "Server error" +// @router /templates/file [post] func (handler *Handler) templateFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { var payload filePayload err := request.DecodeAndValidateJSONPayload(r, &payload) diff --git a/api/http/handler/templates/template_list.go b/api/http/handler/templates/template_list.go index 236b5cfdd..62e3f48d7 100644 --- a/api/http/handler/templates/template_list.go +++ b/api/http/handler/templates/template_list.go @@ -5,9 +5,25 @@ import ( "net/http" httperror "github.com/portainer/libhttp/error" + portainer "github.com/portainer/portainer/api" ) -// GET request on /api/templates +// introduced for swagger +type listResponse struct { + Version string + Templates []portainer.Template +} + +// @id TemplateList +// @summary List available templates +// @description List available templates. +// @description **Access policy**: restricted +// @tags templates +// @security jwt +// @produce json +// @success 200 {object} listResponse "Success" +// @failure 500 "Server error" +// @router /templates [get] func (handler *Handler) templateList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { settings, err := handler.DataStore.Settings().Settings() if err != nil { diff --git a/api/http/handler/upload/upload_tls.go b/api/http/handler/upload/upload_tls.go index aa16544fb..a82d357db 100644 --- a/api/http/handler/upload/upload_tls.go +++ b/api/http/handler/upload/upload_tls.go @@ -6,11 +6,25 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/filesystem" ) -// POST request on /api/upload/tls/{certificate:(?:ca|cert|key)}?folder= +// @id UploadTLS +// @summary Upload TLS files +// @description Use this endpoint to upload TLS files. +// @description **Access policy**: administrator +// @tags upload +// @security jwt +// @accept multipart/form-data +// @produce json +// @param certificate path string true "TLS file type. Valid values are 'ca', 'cert' or 'key'." Enums(ca,cert,key) +// @param folder formData string true "Folder where the TLS file will be stored. Will be created if not existing" +// @param file formData file true "The file to upload" +// @success 204 "Success" +// @failure 400 "Invalid request" +// @failure 500 "Server error" +// @router /upload/tls/{certificate} [post] func (handler *Handler) uploadTLS(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { certificate, err := request.RetrieveRouteVariableValue(r, "certificate") if err != nil { diff --git a/api/http/handler/users/admin_check.go b/api/http/handler/users/admin_check.go index 0bf503f7c..10dcaac98 100644 --- a/api/http/handler/users/admin_check.go +++ b/api/http/handler/users/admin_check.go @@ -5,11 +5,18 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/errors" ) -// GET request on /api/users/admin/check +// @id UserAdminCheck +// @summary Check administrator account existence +// @description Check if an administrator account exists in the database. +// @description **Access policy**: public +// @tags users +// @success 204 "Success" +// @failure 404 "User not found" +// @router /users/admin/check [get] func (handler *Handler) adminCheck(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { users, err := handler.DataStore.User().UsersByRole(portainer.AdministratorRole) if err != nil { diff --git a/api/http/handler/users/admin_init.go b/api/http/handler/users/admin_init.go index ef6c5425c..184041cef 100644 --- a/api/http/handler/users/admin_init.go +++ b/api/http/handler/users/admin_init.go @@ -8,12 +8,14 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" ) type adminInitPayload struct { - Username string - Password string + // Username for the admin user + Username string `validate:"required" example:"admin"` + // Password for the admin user + Password string `validate:"required" example:"admin-password"` } func (payload *adminInitPayload) Validate(r *http.Request) error { @@ -26,7 +28,19 @@ func (payload *adminInitPayload) Validate(r *http.Request) error { return nil } -// POST request on /api/users/admin/init +// @id UserAdminInit +// @summary Initialize administrator account +// @description Initialize the 'admin' user account. +// @description **Access policy**: public +// @tags +// @accept json +// @produce json +// @param body body adminInitPayload true "User details" +// @success 200 {object} portainer.User "Success" +// @failure 400 "Invalid request" +// @failure 409 "Admin user already initialized" +// @failure 500 "Server error" +// @router /users/admin/init [post] func (handler *Handler) adminInit(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { var payload adminInitPayload err := request.DecodeAndValidateJSONPayload(r, &payload) diff --git a/api/http/handler/users/user_create.go b/api/http/handler/users/user_create.go index e9e1fa64a..e44328074 100644 --- a/api/http/handler/users/user_create.go +++ b/api/http/handler/users/user_create.go @@ -8,16 +8,17 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" httperrors "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) type userCreatePayload struct { - Username string - Password string - Role int + Username string `validate:"required" example:"bob"` + Password string `validate:"required" example:"cg9Wgky3"` + // User role (1 for administrator account and 2 for regular account) + Role int `validate:"required" enums:"1,2" example:"2"` } func (payload *userCreatePayload) Validate(r *http.Request) error { @@ -31,7 +32,23 @@ func (payload *userCreatePayload) Validate(r *http.Request) error { return nil } -// POST request on /api/users +// @id UserCreate +// @summary Create a new user +// @description Create a new Portainer user. +// @description Only team leaders and administrators can create users. +// @description Only administrators can create an administrator user account. +// @description **Access policy**: restricted +// @tags users +// @security jwt +// @accept json +// @produce json +// @param body body userCreatePayload true "User details" +// @success 200 {object} portainer.User "Success" +// @failure 400 "Invalid request" +// @failure 403 "Permission denied" +// @failure 409 "User already exists" +// @failure 500 "Server error" +// @router /users [post] func (handler *Handler) userCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { var payload userCreatePayload err := request.DecodeAndValidateJSONPayload(r, &payload) diff --git a/api/http/handler/users/user_delete.go b/api/http/handler/users/user_delete.go index 289b12303..488a94d7d 100644 --- a/api/http/handler/users/user_delete.go +++ b/api/http/handler/users/user_delete.go @@ -7,12 +7,26 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/http/security" ) -// DELETE request on /api/users/:id +// @id UserDelete +// @summary Remove a user +// @description Remove a user. +// @description **Access policy**: administrator +// @tags users +// @security jwt +// @accept json +// @produce json +// @param id path int true "User identifier" +// @success 204 "Success" +// @failure 400 "Invalid request" +// @failure 403 "Permission denied" +// @failure 404 "User not found" +// @failure 500 "Server error" +// @router /users/{id} [delete] func (handler *Handler) userDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { userID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/users/user_inspect.go b/api/http/handler/users/user_inspect.go index cfd87efd0..6e076dd0c 100644 --- a/api/http/handler/users/user_inspect.go +++ b/api/http/handler/users/user_inspect.go @@ -6,13 +6,26 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) -// GET request on /api/users/:id +// @id UserInspect +// @summary Inspect a user +// @description Retrieve details about a user. +// @description **Access policy**: administrator +// @tags users +// @security jwt +// @produce json +// @param id path int true "User identifier" +// @success 200 {object} portainer.User "Success" +// @failure 400 "Invalid request" +// @failure 403 "Permission denied" +// @failure 404 "User not found" +// @failure 500 "Server error" +// @router /users/{id} [get] func (handler *Handler) userInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { userID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/users/user_list.go b/api/http/handler/users/user_list.go index f79b65bb3..d85c835e6 100644 --- a/api/http/handler/users/user_list.go +++ b/api/http/handler/users/user_list.go @@ -8,7 +8,18 @@ import ( "github.com/portainer/portainer/api/http/security" ) -// GET request on /api/users +// @id UserList +// @summary List users +// @description List Portainer users. +// @description Non-administrator users will only be able to list other non-administrator user accounts. +// @description **Access policy**: restricted +// @tags users +// @security jwt +// @produce json +// @success 200 {array} portainer.User "Success" +// @failure 400 "Invalid request" +// @failure 500 "Server error" +// @router /users [get] func (handler *Handler) userList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { users, err := handler.DataStore.User().Users() if err != nil { diff --git a/api/http/handler/users/user_memberships.go b/api/http/handler/users/user_memberships.go index 283b6ee25..3b1d38ca2 100644 --- a/api/http/handler/users/user_memberships.go +++ b/api/http/handler/users/user_memberships.go @@ -6,12 +6,24 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) -// GET request on /api/users/:id/memberships +// @id UserMembershipsInspect +// @summary Inspect a user memberships +// @description Inspect a user memberships. +// @description **Access policy**: authenticated +// @tags users +// @security jwt +// @produce json +// @param id path int true "User identifier" +// @success 200 {object} portainer.TeamMembership "Success" +// @failure 400 "Invalid request" +// @failure 403 "Permission denied" +// @failure 500 "Server error" +// @router /users/{id}/memberships [get] func (handler *Handler) userMemberships(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { userID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/users/user_update.go b/api/http/handler/users/user_update.go index dd1f57519..fdd7e1a65 100644 --- a/api/http/handler/users/user_update.go +++ b/api/http/handler/users/user_update.go @@ -8,16 +8,17 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" httperrors "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) type userUpdatePayload struct { - Username string - Password string - Role int + Username string `validate:"required" example:"bob"` + Password string `validate:"required" example:"cg9Wgky3"` + // User role (1 for administrator account and 2 for regular account) + Role int `validate:"required" enums:"1,2" example:"2"` } func (payload *userUpdatePayload) Validate(r *http.Request) error { @@ -31,7 +32,23 @@ func (payload *userUpdatePayload) Validate(r *http.Request) error { return nil } -// PUT request on /api/users/:id +// @id UserUpdate +// @summary Update a user +// @description Update user details. A regular user account can only update his details. +// @description **Access policy**: authenticated +// @tags users +// @security jwt +// @accept json +// @produce json +// @param id path int true "User identifier" +// @param body body userUpdatePayload true "User details" +// @success 200 {object} portainer.User "Success" +// @failure 400 "Invalid request" +// @failure 403 "Permission denied" +// @failure 404 "User not found" +// @failure 409 "Username already exist" +// @failure 500 "Server error" +// @router /users/{id} [put] func (handler *Handler) userUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { userID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/users/user_update_password.go b/api/http/handler/users/user_update_password.go index c0556dfd1..c1e7d1e5e 100644 --- a/api/http/handler/users/user_update_password.go +++ b/api/http/handler/users/user_update_password.go @@ -8,15 +8,17 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" httperrors "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) type userUpdatePasswordPayload struct { - Password string - NewPassword string + // Current Password + Password string `example:"passwd" validate:"required"` + // New Password + NewPassword string `example:"new_passwd" validate:"required"` } func (payload *userUpdatePasswordPayload) Validate(r *http.Request) error { @@ -29,7 +31,22 @@ func (payload *userUpdatePasswordPayload) Validate(r *http.Request) error { return nil } -// PUT request on /api/users/:id/passwd +// @id UserUpdatePassword +// @summary Update password for a user +// @description Update password for the specified user. +// @description **Access policy**: authenticated +// @tags users +// @security jwt +// @accept json +// @produce json +// @param id path int true "identifier" +// @param body body userUpdatePasswordPayload true "details" +// @success 204 "Success" +// @failure 400 "Invalid request" +// @failure 403 "Permission denied" +// @failure 404 "User not found" +// @failure 500 "Server error" +// @router /users/{id}/passwd [put] func (handler *Handler) userUpdatePassword(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { userID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/webhooks/webhook_create.go b/api/http/handler/webhooks/webhook_create.go index dc00dcd38..f82f35651 100644 --- a/api/http/handler/webhooks/webhook_create.go +++ b/api/http/handler/webhooks/webhook_create.go @@ -9,7 +9,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" ) @@ -32,6 +32,18 @@ func (payload *webhookCreatePayload) Validate(r *http.Request) error { return nil } +// @summary Create a webhook +// @description +// @security jwt +// @tags webhooks +// @accept json +// @produce json +// @param body body webhookCreatePayload true "Webhook data" +// @success 200 {object} portainer.Webhook +// @failure 400 +// @failure 409 +// @failure 500 +// @router /webhooks [post] func (handler *Handler) webhookCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { var payload webhookCreatePayload err := request.DecodeAndValidateJSONPayload(r, &payload) diff --git a/api/http/handler/webhooks/webhook_delete.go b/api/http/handler/webhooks/webhook_delete.go index 305dbca6b..0cbaf944f 100644 --- a/api/http/handler/webhooks/webhook_delete.go +++ b/api/http/handler/webhooks/webhook_delete.go @@ -6,10 +6,20 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" ) -// DELETE request on /api/webhook/:serviceID +// @summary Delete a webhook +// @description +// @security jwt +// @tags webhooks +// @accept json +// @produce json +// @param id path int true "Webhook id" +// @success 202 "Webhook deleted" +// @failure 400 +// @failure 500 +// @router /webhooks/{id} [delete] func (handler *Handler) webhookDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { id, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { diff --git a/api/http/handler/webhooks/webhook_execute.go b/api/http/handler/webhooks/webhook_execute.go index e07ae8b2e..5449bd07a 100644 --- a/api/http/handler/webhooks/webhook_execute.go +++ b/api/http/handler/webhooks/webhook_execute.go @@ -10,11 +10,20 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" ) -// Acts on a passed in token UUID to restart the docker service +// @summary Execute a webhook +// @description Acts on a passed in token UUID to restart the docker service +// @tags webhooks +// @accept json +// @produce json +// @param token path string true "Webhook token" +// @success 202 "Webhook executed" +// @failure 400 +// @failure 500 +// @router /webhooks/{token} [post] func (handler *Handler) webhookExecute(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { webhookToken, err := request.RetrieveRouteVariableValue(r, "token") diff --git a/api/http/handler/webhooks/webhook_list.go b/api/http/handler/webhooks/webhook_list.go index df9f5550c..4acfac728 100644 --- a/api/http/handler/webhooks/webhook_list.go +++ b/api/http/handler/webhooks/webhook_list.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" ) type webhookListOperationFilters struct { @@ -14,7 +14,18 @@ type webhookListOperationFilters struct { EndpointID int `json:"EndpointID"` } -// GET request on /api/webhooks?(filters=) +// @summary List webhooks +// @description +// @security jwt +// @tags webhooks +// @accept json +// @produce json +// @param body body webhookCreatePayload true "Webhook data" +// @param filters query webhookListOperationFilters false "Filters" +// @success 200 {array} portainer.Webhook +// @failure 400 +// @failure 500 +// @router /webhooks [get] func (handler *Handler) webhookList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { var filters webhookListOperationFilters err := request.RetrieveJSONQueryParameter(r, "filters", &filters, true) diff --git a/api/http/handler/websocket/attach.go b/api/http/handler/websocket/attach.go index ea43cd9cd..7b28ec655 100644 --- a/api/http/handler/websocket/attach.go +++ b/api/http/handler/websocket/attach.go @@ -10,15 +10,28 @@ import ( "github.com/gorilla/websocket" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/errors" ) -// websocketAttach handles GET requests on /websocket/attach?id=&endpointId=&nodeName=&token= -// If the nodeName query parameter is present, the request will be proxied to the underlying agent endpoint. -// If the nodeName query parameter is not specified, the request will be upgraded to the websocket protocol and -// an AttachStart operation HTTP request will be created and hijacked. -// Authentication and access is controled via the mandatory token query parameter. +// @summary Attach a websocket +// @description If the nodeName query parameter is present, the request will be proxied to the underlying agent endpoint. +// @description If the nodeName query parameter is not specified, the request will be upgraded to the websocket protocol and +// @description an AttachStart operation HTTP request will be created and hijacked. +// @description Authentication and access is controlled via the mandatory token query parameter. +// @security jwt +// @tags websocket +// @accept json +// @produce json +// @param endpointId query int true "endpoint ID of the endpoint where the resource is located" +// @param nodeName query string false "node name" +// @param token query string true "JWT token used for authentication against this endpoint" +// @success 200 +// @failure 400 +// @failure 403 +// @failure 404 +// @failure 500 +// @router /websocket/attach [get] func (handler *Handler) websocketAttach(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { attachID, err := request.RetrieveQueryParameter(r, "id", false) if err != nil { diff --git a/api/http/handler/websocket/exec.go b/api/http/handler/websocket/exec.go index 836cf252c..7e451b252 100644 --- a/api/http/handler/websocket/exec.go +++ b/api/http/handler/websocket/exec.go @@ -3,17 +3,18 @@ package websocket import ( "bytes" "encoding/json" - "github.com/portainer/portainer/api/bolt/errors" "net" "net/http" "net/http/httputil" "time" + "github.com/portainer/portainer/api/bolt/errors" + "github.com/asaskevich/govalidator" "github.com/gorilla/websocket" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" ) type execStartOperationPayload struct { @@ -21,11 +22,23 @@ type execStartOperationPayload struct { Detach bool } -// websocketExec handles GET requests on /websocket/exec?id=&endpointId=&nodeName=&token= -// If the nodeName query parameter is present, the request will be proxied to the underlying agent endpoint. -// If the nodeName query parameter is not specified, the request will be upgraded to the websocket protocol and -// an ExecStart operation HTTP request will be created and hijacked. -// Authentication and access is controled via the mandatory token query parameter. +// @summary Execute a websocket +// @description If the nodeName query parameter is present, the request will be proxied to the underlying agent endpoint. +// @description If the nodeName query parameter is not specified, the request will be upgraded to the websocket protocol and +// @description an ExecStart operation HTTP request will be created and hijacked. +// @description Authentication and access is controlled via the mandatory token query parameter. +// @security jwt +// @tags websocket +// @accept json +// @produce json +// @param endpointId query int true "endpoint ID of the endpoint where the resource is located" +// @param nodeName query string false "node name" +// @param token query string true "JWT token used for authentication against this endpoint" +// @success 200 +// @failure 400 +// @failure 409 +// @failure 500 +// @router /websocket/exec [get] func (handler *Handler) websocketExec(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { execID, err := request.RetrieveQueryParameter(r, "id", false) if err != nil { diff --git a/api/http/handler/websocket/pod.go b/api/http/handler/websocket/pod.go index b90f096b3..3ae12750a 100644 --- a/api/http/handler/websocket/pod.go +++ b/api/http/handler/websocket/pod.go @@ -13,16 +13,25 @@ import ( bolterrors "github.com/portainer/portainer/api/bolt/errors" ) -// websocketPodExec handles GET requests on /websocket/pod?token=&endpointId=&namespace=&podName=&containerName=&command= -// The request will be upgraded to the websocket protocol. -// Authentication and access is controlled via the mandatory token query parameter. -// The following parameters query parameters are mandatory: -// * token: JWT token used for authentication against this endpoint -// * endpointId: endpoint ID of the endpoint where the resource is located -// * namespace: namespace where the container is located -// * podName: name of the pod containing the container -// * containerName: name of the container -// * command: command to execute in the container +// @summary Execute a websocket on pod +// @description The request will be upgraded to the websocket protocol. +// @description Authentication and access is controlled via the mandatory token query parameter. +// @security jwt +// @tags websocket +// @accept json +// @produce json +// @param endpointId query int true "endpoint ID of the endpoint where the resource is located" +// @param namespace query string true "namespace where the container is located" +// @param podName query string true "name of the pod containing the container" +// @param containerName query string true "name of the container" +// @param command query string true "command to execute in the container" +// @param token query string true "JWT token used for authentication against this endpoint" +// @success 200 +// @failure 400 +// @failure 403 +// @failure 404 +// @failure 500 +// @router /websocket/pod [get] func (handler *Handler) websocketPodExec(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false) if err != nil { diff --git a/api/http/proxy/factory/docker/access_control.go b/api/http/proxy/factory/docker/access_control.go index fda598cce..7f1cb4157 100644 --- a/api/http/proxy/factory/docker/access_control.go +++ b/api/http/proxy/factory/docker/access_control.go @@ -5,10 +5,12 @@ import ( "net/http" "strings" + "github.com/portainer/portainer/api/internal/stackutils" + "github.com/portainer/portainer/api/http/proxy/factory/responseutils" "github.com/portainer/portainer/api/internal/authorization" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" ) const ( @@ -29,6 +31,23 @@ type ( } ) +func getUniqueElements(items string) []string { + result := []string{} + seen := make(map[string]struct{}) + for _, item := range strings.Split(items, ",") { + v := strings.TrimSpace(item) + if v == "" { + continue + } + if _, ok := seen[v]; !ok { + result = append(result, v) + seen[v] = struct{}{} + } + } + + return result +} + func (transport *Transport) newResourceControlFromPortainerLabels(labelsObject map[string]interface{}, resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) { if labelsObject[resourceLabelForPortainerPublicResourceControl] != nil { resourceControl := authorization.NewPublicResourceControl(resourceID, resourceType) @@ -45,12 +64,12 @@ func (transport *Transport) newResourceControlFromPortainerLabels(labelsObject m userNames := make([]string, 0) if labelsObject[resourceLabelForPortainerTeamResourceControl] != nil { concatenatedTeamNames := labelsObject[resourceLabelForPortainerTeamResourceControl].(string) - teamNames = strings.Split(concatenatedTeamNames, ",") + teamNames = getUniqueElements(concatenatedTeamNames) } if labelsObject[resourceLabelForPortainerUserResourceControl] != nil { concatenatedUserNames := labelsObject[resourceLabelForPortainerUserResourceControl].(string) - userNames = strings.Split(concatenatedUserNames, ",") + userNames = getUniqueElements(concatenatedUserNames) } if len(teamNames) > 0 || len(userNames) > 0 { @@ -117,17 +136,17 @@ func (transport *Transport) getInheritedResourceControlFromServiceOrStack(resour switch resourceType { case portainer.ContainerResourceControl: - return getInheritedResourceControlFromContainerLabels(client, resourceIdentifier, resourceControls) + return getInheritedResourceControlFromContainerLabels(client, transport.endpoint.ID, resourceIdentifier, resourceControls) case portainer.NetworkResourceControl: - return getInheritedResourceControlFromNetworkLabels(client, resourceIdentifier, resourceControls) + return getInheritedResourceControlFromNetworkLabels(client, transport.endpoint.ID, resourceIdentifier, resourceControls) case portainer.VolumeResourceControl: - return getInheritedResourceControlFromVolumeLabels(client, resourceIdentifier, resourceControls) + return getInheritedResourceControlFromVolumeLabels(client, transport.endpoint.ID, resourceIdentifier, resourceControls) case portainer.ServiceResourceControl: - return getInheritedResourceControlFromServiceLabels(client, resourceIdentifier, resourceControls) + return getInheritedResourceControlFromServiceLabels(client, transport.endpoint.ID, resourceIdentifier, resourceControls) case portainer.ConfigResourceControl: - return getInheritedResourceControlFromConfigLabels(client, resourceIdentifier, resourceControls) + return getInheritedResourceControlFromConfigLabels(client, transport.endpoint.ID, resourceIdentifier, resourceControls) case portainer.SecretResourceControl: - return getInheritedResourceControlFromSecretLabels(client, resourceIdentifier, resourceControls) + return getInheritedResourceControlFromSecretLabels(client, transport.endpoint.ID, resourceIdentifier, resourceControls) } return nil, nil @@ -273,8 +292,9 @@ func (transport *Transport) findResourceControl(resourceIdentifier string, resou } if resourceLabelsObject[resourceLabelForDockerSwarmStackName] != nil { - inheritedSwarmStackIdentifier := resourceLabelsObject[resourceLabelForDockerSwarmStackName].(string) - resourceControl = authorization.GetResourceControlByResourceIDAndType(inheritedSwarmStackIdentifier, portainer.StackResourceControl, resourceControls) + stackName := resourceLabelsObject[resourceLabelForDockerSwarmStackName].(string) + stackResourceID := stackutils.ResourceControlID(transport.endpoint.ID, stackName) + resourceControl = authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls) if resourceControl != nil { return resourceControl, nil @@ -282,8 +302,9 @@ func (transport *Transport) findResourceControl(resourceIdentifier string, resou } if resourceLabelsObject[resourceLabelForDockerComposeStackName] != nil { - inheritedComposeStackIdentifier := resourceLabelsObject[resourceLabelForDockerComposeStackName].(string) - resourceControl = authorization.GetResourceControlByResourceIDAndType(inheritedComposeStackIdentifier, portainer.StackResourceControl, resourceControls) + stackName := resourceLabelsObject[resourceLabelForDockerComposeStackName].(string) + stackResourceID := stackutils.ResourceControlID(transport.endpoint.ID, stackName) + resourceControl = authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls) if resourceControl != nil { return resourceControl, nil @@ -296,6 +317,20 @@ func (transport *Transport) findResourceControl(resourceIdentifier string, resou return nil, nil } +func getStackResourceIDFromLabels(resourceLabelsObject map[string]string, endpointID portainer.EndpointID) string { + if resourceLabelsObject[resourceLabelForDockerSwarmStackName] != "" { + stackName := resourceLabelsObject[resourceLabelForDockerSwarmStackName] + return stackutils.ResourceControlID(endpointID, stackName) + } + + if resourceLabelsObject[resourceLabelForDockerComposeStackName] != "" { + stackName := resourceLabelsObject[resourceLabelForDockerComposeStackName] + return stackutils.ResourceControlID(endpointID, stackName) + } + + return "" +} + func decorateObject(object map[string]interface{}, resourceControl *portainer.ResourceControl) map[string]interface{} { if object["Portainer"] == nil { object["Portainer"] = make(map[string]interface{}) diff --git a/api/http/proxy/factory/docker/access_control_test.go b/api/http/proxy/factory/docker/access_control_test.go new file mode 100644 index 000000000..7fe0de46d --- /dev/null +++ b/api/http/proxy/factory/docker/access_control_test.go @@ -0,0 +1,63 @@ +package docker + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_getUniqueElements(t *testing.T) { + cases := []struct { + description string + input string + expected []string + }{ + { + description: "no items padded", + input: " ", + expected: []string{}, + }, + { + description: "single item", + input: "a", + expected: []string{"a"}, + }, + { + description: "single item padded", + input: " a ", + expected: []string{"a"}, + }, + { + description: "multiple items", + input: "a,b", + expected: []string{"a", "b"}, + }, + { + description: "multiple items padded", + input: " a , b ", + expected: []string{"a", "b"}, + }, + { + description: "multiple items with empty values", + input: " a , ,b ", + expected: []string{"a", "b"}, + }, + { + description: "duplicates", + input: " a , a ", + expected: []string{"a"}, + }, + { + description: "mix with duplicates", + input: " a ,b, a ", + expected: []string{"a", "b"}, + }, + } + + for _, tt := range cases { + t.Run(tt.description, func(t *testing.T) { + result := getUniqueElements(tt.input) + assert.ElementsMatch(t, result, tt.expected) + }) + } +} diff --git a/api/http/proxy/factory/docker/configs.go b/api/http/proxy/factory/docker/configs.go index 0fee44bbc..74d10759d 100644 --- a/api/http/proxy/factory/docker/configs.go +++ b/api/http/proxy/factory/docker/configs.go @@ -15,15 +15,15 @@ const ( configObjectIdentifier = "ID" ) -func getInheritedResourceControlFromConfigLabels(dockerClient *client.Client, configID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) { +func getInheritedResourceControlFromConfigLabels(dockerClient *client.Client, endpointID portainer.EndpointID, configID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) { config, _, err := dockerClient.ConfigInspectWithRaw(context.Background(), configID) if err != nil { return nil, err } - swarmStackName := config.Spec.Labels[resourceLabelForDockerSwarmStackName] - if swarmStackName != "" { - return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil + stackResourceID := getStackResourceIDFromLabels(config.Spec.Labels, endpointID) + if stackResourceID != "" { + return authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls), nil } return nil, nil @@ -58,7 +58,7 @@ func (transport *Transport) configListOperation(response *http.Response, executo func (transport *Transport) configInspectOperation(response *http.Response, executor *operationExecutor) error { // ConfigInspect response is a JSON object // https://docs.docker.com/engine/api/v1.30/#operation/ConfigInspect - responseObject, err := responseutils.GetResponseAsJSONOBject(response) + responseObject, err := responseutils.GetResponseAsJSONObject(response) if err != nil { return err } diff --git a/api/http/proxy/factory/docker/containers.go b/api/http/proxy/factory/docker/containers.go index 3f9ecf9a1..97108355e 100644 --- a/api/http/proxy/factory/docker/containers.go +++ b/api/http/proxy/factory/docker/containers.go @@ -9,7 +9,7 @@ import ( "net/http" "github.com/docker/docker/client" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/proxy/factory/responseutils" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" @@ -19,7 +19,7 @@ const ( containerObjectIdentifier = "Id" ) -func getInheritedResourceControlFromContainerLabels(dockerClient *client.Client, containerID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) { +func getInheritedResourceControlFromContainerLabels(dockerClient *client.Client, endpointID portainer.EndpointID, containerID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) { container, err := dockerClient.ContainerInspect(context.Background(), containerID) if err != nil { return nil, err @@ -33,14 +33,9 @@ func getInheritedResourceControlFromContainerLabels(dockerClient *client.Client, } } - swarmStackName := container.Config.Labels[resourceLabelForDockerSwarmStackName] - if swarmStackName != "" { - return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil - } - - composeStackName := container.Config.Labels[resourceLabelForDockerComposeStackName] - if composeStackName != "" { - return authorization.GetResourceControlByResourceIDAndType(composeStackName, portainer.StackResourceControl, resourceControls), nil + stackResourceID := getStackResourceIDFromLabels(container.Config.Labels, endpointID) + if stackResourceID != "" { + return authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls), nil } return nil, nil @@ -82,7 +77,7 @@ func (transport *Transport) containerListOperation(response *http.Response, exec func (transport *Transport) containerInspectOperation(response *http.Response, executor *operationExecutor) error { //ContainerInspect response is a JSON object // https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect - responseObject, err := responseutils.GetResponseAsJSONOBject(response) + responseObject, err := responseutils.GetResponseAsJSONObject(response) if err != nil { return err } @@ -157,12 +152,13 @@ func containerHasBlackListedLabel(containerLabels map[string]interface{}, labelB func (transport *Transport) decorateContainerCreationOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) { type PartialContainer struct { HostConfig struct { - Privileged bool `json:"Privileged"` - PidMode string `json:"PidMode"` - Devices []interface{} `json:"Devices"` - CapAdd []string `json:"CapAdd"` - CapDrop []string `json:"CapDrop"` - Binds []string `json:"Binds"` + Privileged bool `json:"Privileged"` + PidMode string `json:"PidMode"` + Devices []interface{} `json:"Devices"` + Sysctls map[string]interface{} `json:"Sysctls"` + CapAdd []string `json:"CapAdd"` + CapDrop []string `json:"CapDrop"` + Binds []string `json:"Binds"` } `json:"HostConfig"` } @@ -181,7 +177,7 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req } if !isAdminOrEndpointAdmin { - settings, err := transport.dataStore.Settings().Settings() + securitySettings, err := transport.fetchEndpointSecuritySettings() if err != nil { return nil, err } @@ -197,23 +193,27 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req return nil, err } - if !settings.AllowPrivilegedModeForRegularUsers && partialContainer.HostConfig.Privileged { + if !securitySettings.AllowPrivilegedModeForRegularUsers && partialContainer.HostConfig.Privileged { return forbiddenResponse, errors.New("forbidden to use privileged mode") } - if !settings.AllowHostNamespaceForRegularUsers && partialContainer.HostConfig.PidMode == "host" { + if !securitySettings.AllowHostNamespaceForRegularUsers && partialContainer.HostConfig.PidMode == "host" { return forbiddenResponse, errors.New("forbidden to use pid host namespace") } - if !settings.AllowDeviceMappingForRegularUsers && len(partialContainer.HostConfig.Devices) > 0 { + if !securitySettings.AllowDeviceMappingForRegularUsers && len(partialContainer.HostConfig.Devices) > 0 { return forbiddenResponse, errors.New("forbidden to use device mapping") } - if !settings.AllowContainerCapabilitiesForRegularUsers && (len(partialContainer.HostConfig.CapAdd) > 0 || len(partialContainer.HostConfig.CapDrop) > 0) { + if !securitySettings.AllowSysctlSettingForRegularUsers && len(partialContainer.HostConfig.Sysctls) > 0 { + return forbiddenResponse, errors.New("forbidden to use sysctl settings") + } + + if !securitySettings.AllowContainerCapabilitiesForRegularUsers && (len(partialContainer.HostConfig.CapAdd) > 0 || len(partialContainer.HostConfig.CapDrop) > 0) { return nil, errors.New("forbidden to use container capabilities") } - if !settings.AllowBindMountsForRegularUsers && (len(partialContainer.HostConfig.Binds) > 0) { + if !securitySettings.AllowBindMountsForRegularUsers && (len(partialContainer.HostConfig.Binds) > 0) { return forbiddenResponse, errors.New("forbidden to use bind mounts") } diff --git a/api/http/proxy/factory/docker/networks.go b/api/http/proxy/factory/docker/networks.go index 4c3c06d48..b38ce68ec 100644 --- a/api/http/proxy/factory/docker/networks.go +++ b/api/http/proxy/factory/docker/networks.go @@ -4,11 +4,12 @@ import ( "context" "net/http" + portainer "github.com/portainer/portainer/api" + "github.com/docker/docker/api/types" "github.com/docker/docker/client" - "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/proxy/factory/responseutils" "github.com/portainer/portainer/api/internal/authorization" ) @@ -18,15 +19,15 @@ const ( networkObjectName = "Name" ) -func getInheritedResourceControlFromNetworkLabels(dockerClient *client.Client, networkID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) { +func getInheritedResourceControlFromNetworkLabels(dockerClient *client.Client, endpointID portainer.EndpointID, networkID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) { network, err := dockerClient.NetworkInspect(context.Background(), networkID, types.NetworkInspectOptions{}) if err != nil { return nil, err } - swarmStackName := network.Labels[resourceLabelForDockerSwarmStackName] - if swarmStackName != "" { - return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil + stackResourceID := getStackResourceIDFromLabels(network.Labels, endpointID) + if stackResourceID != "" { + return authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls), nil } return nil, nil @@ -61,7 +62,7 @@ func (transport *Transport) networkListOperation(response *http.Response, execut func (transport *Transport) networkInspectOperation(response *http.Response, executor *operationExecutor) error { // NetworkInspect response is a JSON object // https://docs.docker.com/engine/api/v1.28/#operation/NetworkInspect - responseObject, err := responseutils.GetResponseAsJSONOBject(response) + responseObject, err := responseutils.GetResponseAsJSONObject(response) if err != nil { return err } diff --git a/api/http/proxy/factory/docker/secrets.go b/api/http/proxy/factory/docker/secrets.go index b57627d8d..148073c02 100644 --- a/api/http/proxy/factory/docker/secrets.go +++ b/api/http/proxy/factory/docker/secrets.go @@ -15,15 +15,15 @@ const ( secretObjectIdentifier = "ID" ) -func getInheritedResourceControlFromSecretLabels(dockerClient *client.Client, secretID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) { +func getInheritedResourceControlFromSecretLabels(dockerClient *client.Client, endpointID portainer.EndpointID, secretID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) { secret, _, err := dockerClient.SecretInspectWithRaw(context.Background(), secretID) if err != nil { return nil, err } - swarmStackName := secret.Spec.Labels[resourceLabelForDockerSwarmStackName] - if swarmStackName != "" { - return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil + stackResourceID := getStackResourceIDFromLabels(secret.Spec.Labels, endpointID) + if stackResourceID != "" { + return authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls), nil } return nil, nil @@ -58,7 +58,7 @@ func (transport *Transport) secretListOperation(response *http.Response, executo func (transport *Transport) secretInspectOperation(response *http.Response, executor *operationExecutor) error { // SecretInspect response is a JSON object // https://docs.docker.com/engine/api/v1.28/#operation/SecretInspect - responseObject, err := responseutils.GetResponseAsJSONOBject(response) + responseObject, err := responseutils.GetResponseAsJSONObject(response) if err != nil { return err } diff --git a/api/http/proxy/factory/docker/services.go b/api/http/proxy/factory/docker/services.go index 08f01a23c..683859f73 100644 --- a/api/http/proxy/factory/docker/services.go +++ b/api/http/proxy/factory/docker/services.go @@ -11,7 +11,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/client" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/proxy/factory/responseutils" "github.com/portainer/portainer/api/internal/authorization" ) @@ -20,15 +20,15 @@ const ( serviceObjectIdentifier = "ID" ) -func getInheritedResourceControlFromServiceLabels(dockerClient *client.Client, serviceID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) { +func getInheritedResourceControlFromServiceLabels(dockerClient *client.Client, endpointID portainer.EndpointID, serviceID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) { service, _, err := dockerClient.ServiceInspectWithRaw(context.Background(), serviceID, types.ServiceInspectOptions{}) if err != nil { return nil, err } - swarmStackName := service.Spec.Labels[resourceLabelForDockerSwarmStackName] - if swarmStackName != "" { - return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil + stackResourceID := getStackResourceIDFromLabels(service.Spec.Labels, endpointID) + if stackResourceID != "" { + return authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls), nil } return nil, nil @@ -63,7 +63,7 @@ func (transport *Transport) serviceListOperation(response *http.Response, execut func (transport *Transport) serviceInspectOperation(response *http.Response, executor *operationExecutor) error { //ServiceInspect response is a JSON object //https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect - responseObject, err := responseutils.GetResponseAsJSONOBject(response) + responseObject, err := responseutils.GetResponseAsJSONObject(response) if err != nil { return err } @@ -111,7 +111,7 @@ func (transport *Transport) decorateServiceCreationOperation(request *http.Reque } if !isAdminOrEndpointAdmin { - settings, err := transport.dataStore.Settings().Settings() + securitySettings, err := transport.fetchEndpointSecuritySettings() if err != nil { return nil, err } @@ -127,7 +127,7 @@ func (transport *Transport) decorateServiceCreationOperation(request *http.Reque return nil, err } - if !settings.AllowBindMountsForRegularUsers && (len(partialService.TaskTemplate.ContainerSpec.Mounts) > 0) { + if !securitySettings.AllowBindMountsForRegularUsers && (len(partialService.TaskTemplate.ContainerSpec.Mounts) > 0) { for _, mount := range partialService.TaskTemplate.ContainerSpec.Mounts { if mount.Type == "bind" { return forbiddenResponse, errors.New("forbidden to use bind mounts") diff --git a/api/http/proxy/factory/docker/swarm.go b/api/http/proxy/factory/docker/swarm.go index 57d27f8cc..bc3ff9c4d 100644 --- a/api/http/proxy/factory/docker/swarm.go +++ b/api/http/proxy/factory/docker/swarm.go @@ -11,7 +11,7 @@ import ( func swarmInspectOperation(response *http.Response, executor *operationExecutor) error { // SwarmInspect response is a JSON object // https://docs.docker.com/engine/api/v1.30/#operation/SwarmInspect - responseObject, err := responseutils.GetResponseAsJSONOBject(response) + responseObject, err := responseutils.GetResponseAsJSONObject(response) if err != nil { return err } diff --git a/api/http/proxy/factory/docker/transport.go b/api/http/proxy/factory/docker/transport.go index bb49bfa4b..803913ac5 100644 --- a/api/http/proxy/factory/docker/transport.go +++ b/api/http/proxy/factory/docker/transport.go @@ -1,9 +1,11 @@ package docker import ( + "bytes" "encoding/base64" "encoding/json" "errors" + "io/ioutil" "log" "net/http" "path" @@ -11,7 +13,7 @@ import ( "strings" "github.com/docker/docker/client" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/docker" "github.com/portainer/portainer/api/http/proxy/factory/responseutils" "github.com/portainer/portainer/api/http/security" @@ -163,6 +165,21 @@ func (transport *Transport) proxyAgentRequest(r *http.Request) (*http.Response, // volume browser request return transport.restrictedResourceOperation(r, resourceID, portainer.VolumeResourceControl, true) + case strings.HasPrefix(requestPath, "/dockerhub"): + dockerhub, err := transport.dataStore.DockerHub().DockerHub() + if err != nil { + return nil, err + } + + newBody, err := json.Marshal(dockerhub) + if err != nil { + return nil, err + } + + r.Method = http.MethodPost + + r.Body = ioutil.NopCloser(bytes.NewReader(newBody)) + r.ContentLength = int64(len(newBody)) } return transport.executeDockerRequest(r) @@ -407,12 +424,12 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r if tokenData.Role != portainer.AdministratorRole { if volumeBrowseRestrictionCheck { - settings, err := transport.dataStore.Settings().Settings() + securitySettings, err := transport.fetchEndpointSecuritySettings() if err != nil { return nil, err } - if !settings.AllowVolumeBrowserForRegularUsers { + if !securitySettings.AllowVolumeBrowserForRegularUsers { return responseutils.WriteAccessDeniedResponse() } } @@ -513,7 +530,7 @@ func (transport *Transport) interceptAndRewriteRequest(request *http.Request, op // https://docs.docker.com/engine/api/v1.37/#operation/SecretCreate // https://docs.docker.com/engine/api/v1.37/#operation/ConfigCreate func (transport *Transport) decorateGenericResourceCreationResponse(response *http.Response, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType, userID portainer.UserID) error { - responseObject, err := responseutils.GetResponseAsJSONOBject(response) + responseObject, err := responseutils.GetResponseAsJSONObject(response) if err != nil { return err } @@ -559,16 +576,18 @@ func (transport *Transport) executeGenericResourceDeletionOperation(request *htt return response, err } - resourceControl, err := transport.dataStore.ResourceControl().ResourceControlByResourceIDAndType(resourceIdentifierAttribute, resourceType) - if err != nil { - return response, err - } - - if resourceControl != nil { - err = transport.dataStore.ResourceControl().DeleteResourceControl(resourceControl.ID) + if response.StatusCode == http.StatusNoContent || response.StatusCode == http.StatusOK { + resourceControl, err := transport.dataStore.ResourceControl().ResourceControlByResourceIDAndType(resourceIdentifierAttribute, resourceType) if err != nil { return response, err } + + if resourceControl != nil { + err = transport.dataStore.ResourceControl().DeleteResourceControl(resourceControl.ID) + if err != nil { + return response, err + } + } } return response, err @@ -680,3 +699,12 @@ func (transport *Transport) isAdminOrEndpointAdmin(request *http.Request) (bool, return tokenData.Role == portainer.AdministratorRole, nil } + +func (transport *Transport) fetchEndpointSecuritySettings() (*portainer.EndpointSecuritySettings, error) { + endpoint, err := transport.dataStore.Endpoint().Endpoint(portainer.EndpointID(transport.endpoint.ID)) + if err != nil { + return nil, err + } + + return &endpoint.SecuritySettings, nil +} diff --git a/api/http/proxy/factory/docker/volumes.go b/api/http/proxy/factory/docker/volumes.go index 0d9705f82..c1bd3d993 100644 --- a/api/http/proxy/factory/docker/volumes.go +++ b/api/http/proxy/factory/docker/volumes.go @@ -8,7 +8,7 @@ import ( "github.com/docker/docker/client" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/proxy/factory/responseutils" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" @@ -18,15 +18,15 @@ const ( volumeObjectIdentifier = "ID" ) -func getInheritedResourceControlFromVolumeLabels(dockerClient *client.Client, volumeID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) { +func getInheritedResourceControlFromVolumeLabels(dockerClient *client.Client, endpointID portainer.EndpointID, volumeID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) { volume, err := dockerClient.VolumeInspect(context.Background(), volumeID) if err != nil { return nil, err } - swarmStackName := volume.Labels[resourceLabelForDockerSwarmStackName] - if swarmStackName != "" { - return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil + stackResourceID := getStackResourceIDFromLabels(volume.Labels, endpointID) + if stackResourceID != "" { + return authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls), nil } return nil, nil @@ -37,7 +37,7 @@ func getInheritedResourceControlFromVolumeLabels(dockerClient *client.Client, vo func (transport *Transport) volumeListOperation(response *http.Response, executor *operationExecutor) error { // VolumeList response is a JSON object // https://docs.docker.com/engine/api/v1.28/#operation/VolumeList - responseObject, err := responseutils.GetResponseAsJSONOBject(response) + responseObject, err := responseutils.GetResponseAsJSONObject(response) if err != nil { return err } @@ -76,7 +76,7 @@ func (transport *Transport) volumeListOperation(response *http.Response, executo func (transport *Transport) volumeInspectOperation(response *http.Response, executor *operationExecutor) error { // VolumeInspect response is a JSON object // https://docs.docker.com/engine/api/v1.28/#operation/VolumeInspect - responseObject, err := responseutils.GetResponseAsJSONOBject(response) + responseObject, err := responseutils.GetResponseAsJSONObject(response) if err != nil { return err } @@ -142,7 +142,7 @@ func (transport *Transport) decorateVolumeResourceCreationOperation(request *htt } func (transport *Transport) decorateVolumeCreationResponse(response *http.Response, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType, userID portainer.UserID) error { - responseObject, err := responseutils.GetResponseAsJSONOBject(response) + responseObject, err := responseutils.GetResponseAsJSONObject(response) if err != nil { return err } diff --git a/api/http/proxy/factory/docker_compose.go b/api/http/proxy/factory/docker_compose.go new file mode 100644 index 000000000..7da8d898f --- /dev/null +++ b/api/http/proxy/factory/docker_compose.go @@ -0,0 +1,88 @@ +package factory + +import ( + "fmt" + "log" + "net" + "net/http" + "net/url" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/crypto" + "github.com/portainer/portainer/api/http/proxy/factory/dockercompose" +) + +// ProxyServer provide an extedned proxy with a local server to forward requests +type ProxyServer struct { + server *http.Server + Port int +} + +func (factory *ProxyFactory) NewDockerComposeAgentProxy(endpoint *portainer.Endpoint) (*ProxyServer, error) { + + if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment { + return &ProxyServer{ + Port: factory.reverseTunnelService.GetTunnelDetails(endpoint.ID).Port, + }, nil + } + + endpointURL, err := url.Parse(endpoint.URL) + if err != nil { + return nil, err + } + + endpointURL.Scheme = "http" + httpTransport := &http.Transport{} + + if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify { + config, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify) + if err != nil { + return nil, err + } + + httpTransport.TLSClientConfig = config + endpointURL.Scheme = "https" + } + + proxy := newSingleHostReverseProxyWithHostHeader(endpointURL) + + proxy.Transport = dockercompose.NewAgentTransport(factory.signatureService, httpTransport) + + proxyServer := &ProxyServer{ + &http.Server{ + Handler: proxy, + }, + 0, + } + + return proxyServer, proxyServer.start() +} + +func (proxy *ProxyServer) start() error { + listener, err := net.Listen("tcp", ":0") + if err != nil { + return err + } + + proxy.Port = listener.Addr().(*net.TCPAddr).Port + go func() { + proxyHost := fmt.Sprintf("127.0.0.1:%d", proxy.Port) + log.Printf("Starting Proxy server on %s...\n", proxyHost) + + err := proxy.server.Serve(listener) + log.Printf("Exiting Proxy server %s\n", proxyHost) + + if err != http.ErrServerClosed { + log.Printf("Proxy server %s exited with an error: %s\n", proxyHost, err) + } + }() + + return nil +} + +// Close shuts down the server +func (proxy *ProxyServer) Close() { + if proxy.server != nil { + proxy.server.Close() + } +} diff --git a/api/http/proxy/factory/dockercompose/transport.go b/api/http/proxy/factory/dockercompose/transport.go new file mode 100644 index 000000000..b9be10e01 --- /dev/null +++ b/api/http/proxy/factory/dockercompose/transport.go @@ -0,0 +1,40 @@ +package dockercompose + +import ( + "net/http" + + portainer "github.com/portainer/portainer/api" +) + +type ( + // AgentTransport is an http.Transport wrapper that adds custom http headers to communicate to an Agent + AgentTransport struct { + httpTransport *http.Transport + signatureService portainer.DigitalSignatureService + endpointIdentifier portainer.EndpointID + } +) + +// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer agent +func NewAgentTransport(signatureService portainer.DigitalSignatureService, httpTransport *http.Transport) *AgentTransport { + transport := &AgentTransport{ + httpTransport: httpTransport, + signatureService: signatureService, + } + + return transport +} + +// RoundTrip is the implementation of the the http.RoundTripper interface +func (transport *AgentTransport) RoundTrip(request *http.Request) (*http.Response, error) { + + signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage) + if err != nil { + return nil, err + } + + request.Header.Set(portainer.PortainerAgentPublicKeyHeader, transport.signatureService.EncodedPublicKey()) + request.Header.Set(portainer.PortainerAgentSignatureHeader, signature) + + return transport.httpTransport.RoundTrip(request) +} diff --git a/api/http/proxy/factory/kubernetes.go b/api/http/proxy/factory/kubernetes.go index 2cb09dc62..a1774c0d2 100644 --- a/api/http/proxy/factory/kubernetes.go +++ b/api/http/proxy/factory/kubernetes.go @@ -72,7 +72,7 @@ func (factory *ProxyFactory) newKubernetesEdgeHTTPProxy(endpoint *portainer.Endp endpointURL.Scheme = "http" proxy := newSingleHostReverseProxyWithHostHeader(endpointURL) - proxy.Transport = kubernetes.NewEdgeTransport(factory.reverseTunnelService, endpoint.ID, tokenManager) + proxy.Transport = kubernetes.NewEdgeTransport(factory.dataStore, factory.reverseTunnelService, endpoint.ID, tokenManager) return proxy, nil } @@ -103,7 +103,7 @@ func (factory *ProxyFactory) newKubernetesAgentHTTPSProxy(endpoint *portainer.En } proxy := newSingleHostReverseProxyWithHostHeader(remoteURL) - proxy.Transport = kubernetes.NewAgentTransport(factory.signatureService, tlsConfig, tokenManager) + proxy.Transport = kubernetes.NewAgentTransport(factory.dataStore, factory.signatureService, tlsConfig, tokenManager) return proxy, nil } diff --git a/api/http/proxy/factory/kubernetes/transport.go b/api/http/proxy/factory/kubernetes/transport.go index 4fbacf590..377c7a3dc 100644 --- a/api/http/proxy/factory/kubernetes/transport.go +++ b/api/http/proxy/factory/kubernetes/transport.go @@ -1,9 +1,14 @@ package kubernetes import ( + "bytes" "crypto/tls" + "encoding/json" "fmt" + "io/ioutil" + "log" "net/http" + "strings" "github.com/portainer/portainer/api/http/security" @@ -13,17 +18,21 @@ import ( type ( localTransport struct { - httpTransport *http.Transport - tokenManager *tokenManager + httpTransport *http.Transport + tokenManager *tokenManager + endpointIdentifier portainer.EndpointID } agentTransport struct { - httpTransport *http.Transport - tokenManager *tokenManager - signatureService portainer.DigitalSignatureService + dataStore portainer.DataStore + httpTransport *http.Transport + tokenManager *tokenManager + signatureService portainer.DigitalSignatureService + endpointIdentifier portainer.EndpointID } edgeTransport struct { + dataStore portainer.DataStore httpTransport *http.Transport tokenManager *tokenManager reverseTunnelService portainer.ReverseTunnelService @@ -50,29 +59,20 @@ func NewLocalTransport(tokenManager *tokenManager) (*localTransport, error) { // RoundTrip is the implementation of the the http.RoundTripper interface func (transport *localTransport) RoundTrip(request *http.Request) (*http.Response, error) { - tokenData, err := security.RetrieveTokenData(request) + token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier) if err != nil { return nil, err } - var token string - if tokenData.Role == portainer.AdministratorRole { - token = transport.tokenManager.getAdminServiceAccountToken() - } else { - token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID)) - if err != nil { - return nil, err - } - } - request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) return transport.httpTransport.RoundTrip(request) } // NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer agent -func NewAgentTransport(signatureService portainer.DigitalSignatureService, tlsConfig *tls.Config, tokenManager *tokenManager) *agentTransport { +func NewAgentTransport(datastore portainer.DataStore, signatureService portainer.DigitalSignatureService, tlsConfig *tls.Config, tokenManager *tokenManager) *agentTransport { transport := &agentTransport{ + dataStore: datastore, httpTransport: &http.Transport{ TLSClientConfig: tlsConfig, }, @@ -85,23 +85,17 @@ func NewAgentTransport(signatureService portainer.DigitalSignatureService, tlsCo // RoundTrip is the implementation of the the http.RoundTripper interface func (transport *agentTransport) RoundTrip(request *http.Request) (*http.Response, error) { - tokenData, err := security.RetrieveTokenData(request) + token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier) if err != nil { return nil, err } - var token string - if tokenData.Role == portainer.AdministratorRole { - token = transport.tokenManager.getAdminServiceAccountToken() - } else { - token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID)) - if err != nil { - return nil, err - } - } - request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token) + if strings.HasPrefix(request.URL.Path, "/v2") { + decorateAgentRequest(request, transport.dataStore) + } + signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage) if err != nil { return nil, err @@ -113,9 +107,10 @@ func (transport *agentTransport) RoundTrip(request *http.Request) (*http.Respons return transport.httpTransport.RoundTrip(request) } -// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer Edge agent -func NewEdgeTransport(reverseTunnelService portainer.ReverseTunnelService, endpointIdentifier portainer.EndpointID, tokenManager *tokenManager) *edgeTransport { +// NewEdgeTransport returns a new transport that can be used to send signed requests to a Portainer Edge agent +func NewEdgeTransport(datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, endpointIdentifier portainer.EndpointID, tokenManager *tokenManager) *edgeTransport { transport := &edgeTransport{ + dataStore: datastore, httpTransport: &http.Transport{}, tokenManager: tokenManager, reverseTunnelService: reverseTunnelService, @@ -127,23 +122,17 @@ func NewEdgeTransport(reverseTunnelService portainer.ReverseTunnelService, endpo // RoundTrip is the implementation of the the http.RoundTripper interface func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response, error) { - tokenData, err := security.RetrieveTokenData(request) + token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier) if err != nil { return nil, err } - var token string - if tokenData.Role == portainer.AdministratorRole { - token = transport.tokenManager.getAdminServiceAccountToken() - } else { - token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID)) - if err != nil { - return nil, err - } - } - request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token) + if strings.HasPrefix(request.URL.Path, "/v2") { + decorateAgentRequest(request, transport.dataStore) + } + response, err := transport.httpTransport.RoundTrip(request) if err == nil { @@ -154,3 +143,57 @@ func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response return response, err } + +func getRoundTripToken( + request *http.Request, + tokenManager *tokenManager, + endpointIdentifier portainer.EndpointID, +) (string, error) { + tokenData, err := security.RetrieveTokenData(request) + if err != nil { + return "", err + } + + var token string + if tokenData.Role == portainer.AdministratorRole { + token = tokenManager.getAdminServiceAccountToken() + } else { + token, err = tokenManager.getUserServiceAccountToken(int(tokenData.ID)) + if err != nil { + log.Printf("Failed retrieving service account token: %v", err) + return "", err + } + } + + return token, nil +} + +func decorateAgentRequest(r *http.Request, dataStore portainer.DataStore) error { + requestPath := strings.TrimPrefix(r.URL.Path, "/v2") + + switch { + case strings.HasPrefix(requestPath, "/dockerhub"): + decorateAgentDockerHubRequest(r, dataStore) + } + + return nil +} + +func decorateAgentDockerHubRequest(r *http.Request, dataStore portainer.DataStore) error { + dockerhub, err := dataStore.DockerHub().DockerHub() + if err != nil { + return err + } + + newBody, err := json.Marshal(dockerhub) + if err != nil { + return err + } + + r.Method = http.MethodPost + + r.Body = ioutil.NopCloser(bytes.NewReader(newBody)) + r.ContentLength = int64(len(newBody)) + + return nil +} diff --git a/api/http/proxy/factory/responseutils/response.go b/api/http/proxy/factory/responseutils/response.go index 1ce1d39e9..9f5870810 100644 --- a/api/http/proxy/factory/responseutils/response.go +++ b/api/http/proxy/factory/responseutils/response.go @@ -10,8 +10,8 @@ import ( "strconv" ) -// GetResponseAsJSONOBject returns the response content as a generic JSON object -func GetResponseAsJSONOBject(response *http.Response) (map[string]interface{}, error) { +// GetResponseAsJSONObject returns the response content as a generic JSON object +func GetResponseAsJSONObject(response *http.Response) (map[string]interface{}, error) { responseData, err := getResponseBodyAsGenericJSON(response) if err != nil { return nil, err diff --git a/api/http/proxy/manager.go b/api/http/proxy/manager.go index e539d89c2..2013647d8 100644 --- a/api/http/proxy/manager.go +++ b/api/http/proxy/manager.go @@ -1,6 +1,7 @@ package proxy import ( + "fmt" "net/http" "github.com/portainer/portainer/api/http/proxy/factory/kubernetes" @@ -21,6 +22,7 @@ type ( proxyFactory *factory.ProxyFactory endpointProxies cmap.ConcurrentMap legacyExtensionProxies cmap.ConcurrentMap + k8sClientFactory *cli.ClientFactory } ) @@ -29,6 +31,7 @@ func NewManager(dataStore portainer.DataStore, signatureService portainer.Digita return &Manager{ endpointProxies: cmap.New(), legacyExtensionProxies: cmap.New(), + k8sClientFactory: kubernetesClientFactory, proxyFactory: factory.NewProxyFactory(dataStore, signatureService, tunnelService, clientFactory, kubernetesClientFactory, kubernetesTokenCacheManager), } } @@ -41,13 +44,19 @@ func (manager *Manager) CreateAndRegisterEndpointProxy(endpoint *portainer.Endpo return nil, err } - manager.endpointProxies.Set(string(endpoint.ID), proxy) + manager.endpointProxies.Set(fmt.Sprint(endpoint.ID), proxy) return proxy, nil } +// CreateComposeProxyServer creates a new HTTP reverse proxy based on endpoint properties and and adds it to the registered proxies. +// It can also be used to create a new HTTP reverse proxy and replace an already registered proxy. +func (manager *Manager) CreateComposeProxyServer(endpoint *portainer.Endpoint) (*factory.ProxyServer, error) { + return manager.proxyFactory.NewDockerComposeAgentProxy(endpoint) +} + // GetEndpointProxy returns the proxy associated to a key func (manager *Manager) GetEndpointProxy(endpoint *portainer.Endpoint) http.Handler { - proxy, ok := manager.endpointProxies.Get(string(endpoint.ID)) + proxy, ok := manager.endpointProxies.Get(fmt.Sprint(endpoint.ID)) if !ok { return nil } @@ -56,8 +65,11 @@ func (manager *Manager) GetEndpointProxy(endpoint *portainer.Endpoint) http.Hand } // DeleteEndpointProxy deletes the proxy associated to a key +// and cleans the k8s endpoint client cache. DeleteEndpointProxy +// is currently only called for edge connection clean up. func (manager *Manager) DeleteEndpointProxy(endpoint *portainer.Endpoint) { - manager.endpointProxies.Remove(string(endpoint.ID)) + manager.endpointProxies.Remove(fmt.Sprint(endpoint.ID)) + manager.k8sClientFactory.RemoveKubeClient(endpoint) } // CreateLegacyExtensionProxy creates a new HTTP reverse proxy for a legacy extension and adds it to the registered proxies diff --git a/api/http/server.go b/api/http/server.go index 8f83529f1..ad7826087 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -39,39 +39,41 @@ import ( "github.com/portainer/portainer/api/http/proxy" "github.com/portainer/portainer/api/http/proxy/factory/kubernetes" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/kubernetes/cli" ) // Server implements the portainer.Server interface type Server struct { - BindAddress string - AssetsPath string - Status *portainer.Status - ReverseTunnelService portainer.ReverseTunnelService - ComposeStackManager portainer.ComposeStackManager - CryptoService portainer.CryptoService - SignatureService portainer.DigitalSignatureService - SnapshotService portainer.SnapshotService - FileService portainer.FileService - DataStore portainer.DataStore - GitService portainer.GitService - JWTService portainer.JWTService - LDAPService portainer.LDAPService - OAuthService portainer.OAuthService - SwarmStackManager portainer.SwarmStackManager - Handler *handler.Handler - SSL bool - SSLCert string - SSLKey string - DockerClientFactory *docker.ClientFactory - KubernetesClientFactory *cli.ClientFactory - KubernetesDeployer portainer.KubernetesDeployer + BindAddress string + AssetsPath string + Status *portainer.Status + ReverseTunnelService portainer.ReverseTunnelService + ComposeStackManager portainer.ComposeStackManager + CryptoService portainer.CryptoService + SignatureService portainer.DigitalSignatureService + SnapshotService portainer.SnapshotService + FileService portainer.FileService + DataStore portainer.DataStore + GitService portainer.GitService + JWTService portainer.JWTService + LDAPService portainer.LDAPService + OAuthService portainer.OAuthService + SwarmStackManager portainer.SwarmStackManager + ProxyManager *proxy.Manager + KubernetesTokenCacheManager *kubernetes.TokenCacheManager + Handler *handler.Handler + SSL bool + SSLCert string + SSLKey string + DockerClientFactory *docker.ClientFactory + KubernetesClientFactory *cli.ClientFactory + KubernetesDeployer portainer.KubernetesDeployer } // Start starts the HTTP server func (server *Server) Start() error { - kubernetesTokenCacheManager := kubernetes.NewTokenCacheManager() - proxyManager := proxy.NewManager(server.DataStore, server.SignatureService, server.ReverseTunnelService, server.DockerClientFactory, server.KubernetesClientFactory, kubernetesTokenCacheManager) + kubernetesTokenCacheManager := server.KubernetesTokenCacheManager requestBouncer := security.NewRequestBouncer(server.DataStore, server.JWTService) @@ -82,7 +84,7 @@ func (server *Server) Start() error { authHandler.CryptoService = server.CryptoService authHandler.JWTService = server.JWTService authHandler.LDAPService = server.LDAPService - authHandler.ProxyManager = proxyManager + authHandler.ProxyManager = server.ProxyManager authHandler.KubernetesTokenCacheManager = kubernetesTokenCacheManager authHandler.OAuthService = server.OAuthService @@ -116,10 +118,10 @@ func (server *Server) Start() error { var endpointHandler = endpoints.NewHandler(requestBouncer) endpointHandler.DataStore = server.DataStore endpointHandler.FileService = server.FileService - endpointHandler.ProxyManager = proxyManager + endpointHandler.ProxyManager = server.ProxyManager endpointHandler.SnapshotService = server.SnapshotService - endpointHandler.ProxyManager = proxyManager endpointHandler.ReverseTunnelService = server.ReverseTunnelService + endpointHandler.ComposeStackManager = server.ComposeStackManager var endpointEdgeHandler = endpointedge.NewHandler(requestBouncer) endpointEdgeHandler.DataStore = server.DataStore @@ -131,7 +133,7 @@ func (server *Server) Start() error { var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer) endpointProxyHandler.DataStore = server.DataStore - endpointProxyHandler.ProxyManager = proxyManager + endpointProxyHandler.ProxyManager = server.ProxyManager endpointProxyHandler.ReverseTunnelService = server.ReverseTunnelService var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public")) @@ -141,7 +143,7 @@ func (server *Server) Start() error { var registryHandler = registries.NewHandler(requestBouncer) registryHandler.DataStore = server.DataStore registryHandler.FileService = server.FileService - registryHandler.ProxyManager = proxyManager + registryHandler.ProxyManager = server.ProxyManager var resourceControlHandler = resourcecontrols.NewHandler(requestBouncer) resourceControlHandler.DataStore = server.DataStore @@ -155,6 +157,7 @@ func (server *Server) Start() error { var stackHandler = stacks.NewHandler(requestBouncer) stackHandler.DataStore = server.DataStore + stackHandler.DockerClientFactory = server.DockerClientFactory stackHandler.FileService = server.FileService stackHandler.SwarmStackManager = server.SwarmStackManager stackHandler.ComposeStackManager = server.ComposeStackManager diff --git a/api/internal/authorization/access_control.go b/api/internal/authorization/access_control.go index 0e2d91ab7..4f533bffa 100644 --- a/api/internal/authorization/access_control.go +++ b/api/internal/authorization/access_control.go @@ -3,9 +3,25 @@ package authorization import ( "strconv" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/internal/stackutils" ) +// NewAdministratorsOnlyResourceControl will create a new administrators only resource control associated to the resource specified by the +// identifier and type parameters. +func NewAdministratorsOnlyResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType) *portainer.ResourceControl { + return &portainer.ResourceControl{ + Type: resourceType, + ResourceID: resourceIdentifier, + SubResourceIDs: []string{}, + UserAccesses: []portainer.UserResourceAccess{}, + TeamAccesses: []portainer.TeamResourceAccess{}, + AdministratorsOnly: true, + Public: false, + System: false, + } +} + // NewPrivateResourceControl will create a new private resource control associated to the resource specified by the // identifier and type parameters. It automatically assigns it to the user specified by the userID parameter. func NewPrivateResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType, userID portainer.UserID) *portainer.ResourceControl { @@ -95,7 +111,7 @@ func NewRestrictedResourceControl(resourceIdentifier string, resourceType portai func DecorateStacks(stacks []portainer.Stack, resourceControls []portainer.ResourceControl) []portainer.Stack { for idx, stack := range stacks { - resourceControl := GetResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl, resourceControls) + resourceControl := GetResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl, resourceControls) if resourceControl != nil { stacks[idx].ResourceControl = resourceControl } diff --git a/api/internal/endpointutils/endpointutils.go b/api/internal/endpointutils/endpointutils.go new file mode 100644 index 000000000..249ee11cb --- /dev/null +++ b/api/internal/endpointutils/endpointutils.go @@ -0,0 +1,11 @@ +package endpointutils + +import ( + "strings" + + portainer "github.com/portainer/portainer/api" +) + +func IsLocalEndpoint(endpoint *portainer.Endpoint) bool { + return strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") || endpoint.Type == 5 +} diff --git a/api/internal/snapshot/snapshot.go b/api/internal/snapshot/snapshot.go index da9565cce..2c2e5ef34 100644 --- a/api/internal/snapshot/snapshot.go +++ b/api/internal/snapshot/snapshot.go @@ -4,7 +4,7 @@ import ( "log" "time" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" ) // Service repesents a service to manage endpoint snapshots. @@ -48,7 +48,9 @@ func (service *Service) stop() { return } + // clear refreshSignal to mark the service as disabled close(service.refreshSignal) + service.refreshSignal = nil } // SetSnapshotInterval sets the snapshot interval and resets the service diff --git a/api/internal/stackutils/stackutils.go b/api/internal/stackutils/stackutils.go new file mode 100644 index 000000000..5b1e9bf43 --- /dev/null +++ b/api/internal/stackutils/stackutils.go @@ -0,0 +1,12 @@ +package stackutils + +import ( + "fmt" + + portainer "github.com/portainer/portainer/api" +) + +// ResourceControlID returns the stack resource control id +func ResourceControlID(endpointID portainer.EndpointID, name string) string { + return fmt.Sprintf("%d_%s", endpointID, name) +} diff --git a/api/kubernetes/cli/client.go b/api/kubernetes/cli/client.go index 707bf3924..a268150c9 100644 --- a/api/kubernetes/cli/client.go +++ b/api/kubernetes/cli/client.go @@ -40,6 +40,11 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService, revers } } +// Remove the cached kube client so a new one can be created +func (factory *ClientFactory) RemoveKubeClient(endpoint *portainer.Endpoint) { + factory.endpointClients.Remove(strconv.Itoa(int(endpoint.ID))) +} + // GetKubeClient checks if an existing client is already registered for the endpoint and returns it if one is found. // If no client is registered, it will create a new client, register it, and returns it. func (factory *ClientFactory) GetKubeClient(endpoint *portainer.Endpoint) (portainer.KubeClient, error) { diff --git a/api/libcompose/compose_stack.go b/api/libcompose/compose_stack.go index ec885b65b..41da7239d 100644 --- a/api/libcompose/compose_stack.go +++ b/api/libcompose/compose_stack.go @@ -5,6 +5,8 @@ import ( "fmt" "path" "path/filepath" + "regexp" + "strings" "github.com/portainer/libcompose/config" "github.com/portainer/libcompose/docker" @@ -13,11 +15,12 @@ import ( "github.com/portainer/libcompose/lookup" "github.com/portainer/libcompose/project" "github.com/portainer/libcompose/project/options" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" ) const ( - dockerClientVersion = "1.24" + dockerClientVersion = "1.24" + composeSyntaxMaxVersion = "2" ) // ComposeStackManager represents a service for managing compose stacks. @@ -58,6 +61,19 @@ func (manager *ComposeStackManager) createClient(endpoint *portainer.Endpoint) ( return client.NewDefaultFactory(clientOpts) } +// ComposeSyntaxMaxVersion returns the maximum supported version of the docker compose syntax +func (manager *ComposeStackManager) ComposeSyntaxMaxVersion() string { + return composeSyntaxMaxVersion +} + +// NormalizeStackName returns a new stack name with unsupported characters replaced +func (manager *ComposeStackManager) NormalizeStackName(name string) string { + // this is coming from libcompose + // https://github.com/portainer/libcompose/blob/master/project/context.go#L117-L120 + r := regexp.MustCompile("[^a-z0-9]+") + return r.ReplaceAllString(strings.ToLower(name), "") +} + // Up will deploy a compose stack (equivalent of docker-compose up) func (manager *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error { diff --git a/api/portainer.go b/api/portainer.go index e68e0c223..ea0711372 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -8,7 +8,8 @@ import ( type ( // AccessPolicy represent a policy that can be associated to a user or team AccessPolicy struct { - RoleID RoleID `json:"RoleId"` + // Role identifier. Reference the role that will be associated to this access policy + RoleID RoleID `json:"RoleId" example:"1"` } // AgentPlatform represents a platform type for an Agent @@ -26,9 +27,12 @@ type ( // AzureCredentials represents the credentials used to connect to an Azure // environment. AzureCredentials struct { - ApplicationID string `json:"ApplicationID"` - TenantID string `json:"TenantID"` - AuthenticationKey string `json:"AuthenticationKey"` + // Azure application ID + ApplicationID string `json:"ApplicationID" example:"eag7cdo9-o09l-9i83-9dO9-f0b23oe78db4"` + // Azure tenant ID + TenantID string `json:"TenantID" example:"34ddc78d-4fel-2358-8cc1-df84c8o839f5"` + // Azure authentication key + AuthenticationKey string `json:"AuthenticationKey" example:"cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk="` } // CLIFlags represents the available flags on the CLI @@ -59,17 +63,28 @@ type ( // CustomTemplate represents a custom template CustomTemplate struct { - ID CustomTemplateID `json:"Id"` - Title string `json:"Title"` - Description string `json:"Description"` - ProjectPath string `json:"ProjectPath"` - EntryPoint string `json:"EntryPoint"` - CreatedByUserID UserID `json:"CreatedByUserId"` - Note string `json:"Note"` - Platform CustomTemplatePlatform `json:"Platform"` - Logo string `json:"Logo"` - Type StackType `json:"Type"` - ResourceControl *ResourceControl `json:"ResourceControl"` + // CustomTemplate Identifier + ID CustomTemplateID `json:"Id" example:"1"` + // Title of the template + Title string `json:"Title" example:"Nginx"` + // Description of the template + Description string `json:"Description" example:"High performance web server"` + // Path on disk to the repository hosting the Stack file + ProjectPath string `json:"ProjectPath" example:"/data/custom_template/3"` + // Path to the Stack file + EntryPoint string `json:"EntryPoint" example:"docker-compose.yml"` + // User identifier who created this template + CreatedByUserID UserID `json:"CreatedByUserId" example:"3"` + // A note that will be displayed in the UI. Supports HTML content + Note string `json:"Note" example:"This is my custom template"` + // Platform associated to the template. + // Valid values are: 1 - 'linux', 2 - 'windows' + Platform CustomTemplatePlatform `json:"Platform" example:"1" enums:"1,2"` + // URL of the template's logo + Logo string `json:"Logo" example:"https://cloudinovasi.id/assets/img/logos/nginx.png"` + // Type of created stack (1 - swarm, 2 - compose) + Type StackType `json:"Type" example:"1"` + ResourceControl *ResourceControl `json:"ResourceControl"` } // CustomTemplateID represents a custom template identifier @@ -81,9 +96,12 @@ type ( // DockerHub represents all the required information to connect and use the // Docker Hub DockerHub struct { - Authentication bool `json:"Authentication"` - Username string `json:"Username"` - Password string `json:"Password,omitempty"` + // Is authentication against DockerHub enabled + Authentication bool `json:"Authentication" example:"true"` + // Username used to authenticate against the DockerHub + Username string `json:"Username" example:"user"` + // Password used to authenticate against the DockerHub + Password string `json:"Password,omitempty" example:"passwd"` } // DockerSnapshot represents a snapshot of a specific Docker endpoint at a specific time @@ -101,6 +119,7 @@ type ( ImageCount int `json:"ImageCount"` ServiceCount int `json:"ServiceCount"` StackCount int `json:"StackCount"` + NodeCount int `json:"NodeCount"` SnapshotRaw DockerSnapshotRaw `json:"DockerSnapshotRaw"` } @@ -116,7 +135,8 @@ type ( // EdgeGroup represents an Edge group EdgeGroup struct { - ID EdgeGroupID `json:"Id"` + // EdgeGroup Identifier + ID EdgeGroupID `json:"Id" example:"1"` Name string `json:"Name"` Dynamic bool `json:"Dynamic"` TagIDs []TagID `json:"TagIds"` @@ -129,7 +149,8 @@ type ( // EdgeJob represents a job that can run on Edge environments. EdgeJob struct { - ID EdgeJobID `json:"Id"` + // EdgeJob Identifier + ID EdgeJobID `json:"Id" example:"1"` Created int64 `json:"Created"` CronExpression string `json:"CronExpression"` Endpoints map[EndpointID]EdgeJobEndpointMeta `json:"Endpoints"` @@ -154,7 +175,8 @@ type ( // EdgeSchedule represents a scheduled job that can run on Edge environments. // Deprecated in favor of EdgeJob EdgeSchedule struct { - ID ScheduleID `json:"Id"` + // EdgeSchedule Identifier + ID ScheduleID `json:"Id" example:"1"` CronExpression string `json:"CronExpression"` Script string `json:"Script"` Version int `json:"Version"` @@ -163,7 +185,8 @@ type ( //EdgeStack represents an edge stack EdgeStack struct { - ID EdgeStackID `json:"Id"` + // EdgeStack Identifier + ID EdgeStackID `json:"Id" example:"1"` Name string `json:"Name"` Status map[EndpointID]EdgeStackStatus `json:"Status"` CreationDate int64 `json:"CreationDate"` @@ -190,24 +213,45 @@ type ( // Endpoint represents a Docker endpoint with all the info required // to connect to it Endpoint struct { - ID EndpointID `json:"Id"` - Name string `json:"Name"` - Type EndpointType `json:"Type"` - URL string `json:"URL"` - GroupID EndpointGroupID `json:"GroupId"` - PublicURL string `json:"PublicURL"` - TLSConfig TLSConfiguration `json:"TLSConfig"` - Extensions []EndpointExtension `json:"Extensions"` - AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"` - TagIDs []TagID `json:"TagIds"` - Status EndpointStatus `json:"Status"` - Snapshots []DockerSnapshot `json:"Snapshots"` - UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"` - TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"` - EdgeID string `json:"EdgeID,omitempty"` - EdgeKey string `json:"EdgeKey"` - EdgeCheckinInterval int `json:"EdgeCheckinInterval"` - Kubernetes KubernetesData `json:"Kubernetes"` + // Endpoint Identifier + ID EndpointID `json:"Id" example:"1"` + // Endpoint name + Name string `json:"Name" example:"my-endpoint"` + // Endpoint environment type. 1 for a Docker environment, 2 for an agent on Docker environment or 3 for an Azure environment. + Type EndpointType `json:"Type" example:"1"` + // URL or IP address of the Docker host associated to this endpoint + URL string `json:"URL" example:"docker.mydomain.tld:2375"` + // Endpoint group identifier + GroupID EndpointGroupID `json:"GroupId" example:"1"` + // URL or IP address where exposed containers will be reachable + PublicURL string `json:"PublicURL" example:"docker.mydomain.tld:2375"` + TLSConfig TLSConfiguration `json:"TLSConfig"` + Extensions []EndpointExtension `json:"Extensions" example:""` + AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty" example:""` + // List of tag identifiers to which this endpoint is associated + TagIDs []TagID `json:"TagIds"` + // The status of the endpoint (1 - up, 2 - down) + Status EndpointStatus `json:"Status" example:"1"` + // List of snapshots + Snapshots []DockerSnapshot `json:"Snapshots" example:""` + // List of user identifiers authorized to connect to this endpoint + UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"` + // List of team identifiers authorized to connect to this endpoint + TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies" example:""` + // The identifier of the edge agent associated with this endpoint + EdgeID string `json:"EdgeID,omitempty" example:""` + // The key which is used to map the agent to Portainer + EdgeKey string `json:"EdgeKey" example:""` + // The check in interval for edge agent (in seconds) + EdgeCheckinInterval int `json:"EdgeCheckinInterval" example:"5"` + // Associated Kubernetes data + Kubernetes KubernetesData `json:"Kubernetes" example:""` + // Maximum version of docker-compose + ComposeSyntaxMaxVersion string `json:"ComposeSyntaxMaxVersion" example:"3.8"` + // Endpoint specific security settings + SecuritySettings EndpointSecuritySettings + // LastCheckInDate mark last check-in date on checkin + LastCheckInDate int64 // Deprecated fields // Deprecated in DBVersion == 4 @@ -240,12 +284,16 @@ type ( // EndpointGroup represents a group of endpoints EndpointGroup struct { - ID EndpointGroupID `json:"Id"` - Name string `json:"Name"` - Description string `json:"Description"` - UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"` - TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"` - TagIDs []TagID `json:"TagIds"` + // Endpoint group Identifier + ID EndpointGroupID `json:"Id" example:"1"` + // Endpoint group name + Name string `json:"Name" example:"my-endpoint-group"` + // Description associated to the endpoint group + Description string `json:"Description" example:"Endpoint group description"` + UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies" example:""` + TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies" example:""` + // List of tags associated to this endpoint group + TagIDs []TagID `json:"TagIds"` // Deprecated fields Labels []Pair `json:"Labels"` @@ -271,10 +319,32 @@ type ( // Deprecated EndpointSyncJob struct{} + // EndpointSecuritySettings represents settings for an endpoint + EndpointSecuritySettings struct { + // Whether non-administrator should be able to use bind mounts when creating containers + AllowBindMountsForRegularUsers bool `json:"allowBindMountsForRegularUsers" example:"false"` + // Whether non-administrator should be able to use privileged mode when creating containers + AllowPrivilegedModeForRegularUsers bool `json:"allowPrivilegedModeForRegularUsers" example:"false"` + // Whether non-administrator should be able to browse volumes + AllowVolumeBrowserForRegularUsers bool `json:"allowVolumeBrowserForRegularUsers" example:"true"` + // Whether non-administrator should be able to use the host pid + AllowHostNamespaceForRegularUsers bool `json:"allowHostNamespaceForRegularUsers" example:"true"` + // Whether non-administrator should be able to use device mapping + AllowDeviceMappingForRegularUsers bool `json:"allowDeviceMappingForRegularUsers" example:"true"` + // Whether non-administrator should be able to manage stacks + AllowStackManagementForRegularUsers bool `json:"allowStackManagementForRegularUsers" example:"true"` + // Whether non-administrator should be able to use container capabilities + AllowContainerCapabilitiesForRegularUsers bool `json:"allowContainerCapabilitiesForRegularUsers" example:"true"` + // Whether non-administrator should be able to use sysctl settings + AllowSysctlSettingForRegularUsers bool `json:"AllowSysctlSettingForRegularUsers" example:"true"` + // Whether host management features are enabled + EnableHostManagementFeatures bool `json:"enableHostManagementFeatures" example:"true"` + } + // EndpointType represents the type of an endpoint EndpointType int - // EndpointRelation represnts a endpoint relation object + // EndpointRelation represents a endpoint relation object EndpointRelation struct { EndpointID EndpointID EdgeStacks map[EdgeStackID]bool @@ -282,7 +352,8 @@ type ( // Extension represents a deprecated Portainer extension Extension struct { - ID ExtensionID `json:"Id"` + // Extension Identifier + ID ExtensionID `json:"Id" example:"1"` Enabled bool `json:"Enabled"` Name string `json:"Name,omitempty"` ShortDescription string `json:"ShortDescription,omitempty"` @@ -310,6 +381,12 @@ type ( ProjectPath string `json:"ProjectPath"` } + // QuayRegistryData represents data required for Quay registry to work + QuayRegistryData struct { + UseOrganisation bool `json:"UseOrganisation"` + OrganisationName string `json:"OrganisationName"` + } + // JobType represents a job type JobType int @@ -352,29 +429,41 @@ type ( // LDAPGroupSearchSettings represents settings used to search for groups in a LDAP server LDAPGroupSearchSettings struct { - GroupBaseDN string `json:"GroupBaseDN"` - GroupFilter string `json:"GroupFilter"` - GroupAttribute string `json:"GroupAttribute"` + // The distinguished name of the element from which the LDAP server will search for groups + GroupBaseDN string `json:"GroupBaseDN" example:"dc=ldap,dc=domain,dc=tld"` + // The LDAP search filter used to select group elements, optional + GroupFilter string `json:"GroupFilter" example:"(objectClass=account"` + // LDAP attribute which denotes the group membership + GroupAttribute string `json:"GroupAttribute" example:"member"` } // LDAPSearchSettings represents settings used to search for users in a LDAP server LDAPSearchSettings struct { - BaseDN string `json:"BaseDN"` - Filter string `json:"Filter"` - UserNameAttribute string `json:"UserNameAttribute"` + // The distinguished name of the element from which the LDAP server will search for users + BaseDN string `json:"BaseDN" example:"dc=ldap,dc=domain,dc=tld"` + // Optional LDAP search filter used to select user elements + Filter string `json:"Filter" example:"(objectClass=account)"` + // LDAP attribute which denotes the username + UserNameAttribute string `json:"UserNameAttribute" example:"uid"` } // LDAPSettings represents the settings used to connect to a LDAP server LDAPSettings struct { - AnonymousMode bool `json:"AnonymousMode"` - ReaderDN string `json:"ReaderDN"` - Password string `json:"Password,omitempty"` - URL string `json:"URL"` - TLSConfig TLSConfiguration `json:"TLSConfig"` - StartTLS bool `json:"StartTLS"` + // Enable this option if the server is configured for Anonymous access. When enabled, ReaderDN and Password will not be used + AnonymousMode bool `json:"AnonymousMode" example:"true"` + // Account that will be used to search for users + ReaderDN string `json:"ReaderDN" example:"cn=readonly-account,dc=ldap,dc=domain,dc=tld"` + // Password of the account that will be used to search users + Password string `json:"Password,omitempty" example:"readonly-password"` + // URL or IP address of the LDAP server + URL string `json:"URL" example:"myldap.domain.tld:389"` + TLSConfig TLSConfiguration `json:"TLSConfig"` + // Whether LDAP connection should use StartTLS + StartTLS bool `json:"StartTLS" example:"true"` SearchSettings []LDAPSearchSettings `json:"SearchSettings"` GroupSearchSettings []LDAPGroupSearchSettings `json:"GroupSearchSettings"` - AutoCreateUsers bool `json:"AutoCreateUsers"` + // Automatically provision users and assign them to matching LDAP group names + AutoCreateUsers bool `json:"AutoCreateUsers" example:"true"` } // LicenseInformation represents information about an extension license @@ -404,22 +493,30 @@ type ( // Pair defines a key/value string pair Pair struct { - Name string `json:"name"` - Value string `json:"value"` + Name string `json:"name" example:"name"` + Value string `json:"value" example:"value"` } // Registry represents a Docker registry with all the info required // to connect to it Registry struct { - ID RegistryID `json:"Id"` - Type RegistryType `json:"Type"` - Name string `json:"Name"` - URL string `json:"URL"` - Authentication bool `json:"Authentication"` - Username string `json:"Username"` - Password string `json:"Password,omitempty"` + // Registry Identifier + ID RegistryID `json:"Id" example:"1"` + // Registry Type (1 - Quay, 2 - Azure, 3 - Custom, 4 - Gitlab) + Type RegistryType `json:"Type" enums:"1,2,3,4"` + // Registry Name + Name string `json:"Name" example:"my-registry"` + // URL or IP address of the Docker registry + URL string `json:"URL" example:"registry.mydomain.tld:2375"` + // Is authentication against this registry enabled + Authentication bool `json:"Authentication" example:"true"` + // Username used to authenticate against this registry + Username string `json:"Username" example:"registry user"` + // Password used to authenticate against this registry + Password string `json:"Password,omitempty" example:"registry_password"` ManagementConfiguration *RegistryManagementConfiguration `json:"ManagementConfiguration"` Gitlab GitlabRegistryData `json:"Gitlab"` + Quay QuayRegistryData `json:"Quay"` UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"` TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"` @@ -450,15 +547,23 @@ type ( // ResourceControl represent a reference to a Docker resource with specific access controls ResourceControl struct { - ID ResourceControlID `json:"Id"` - ResourceID string `json:"ResourceId"` - SubResourceIDs []string `json:"SubResourceIds"` - Type ResourceControlType `json:"Type"` - UserAccesses []UserResourceAccess `json:"UserAccesses"` - TeamAccesses []TeamResourceAccess `json:"TeamAccesses"` - Public bool `json:"Public"` - AdministratorsOnly bool `json:"AdministratorsOnly"` - System bool `json:"System"` + // ResourceControl Identifier + ID ResourceControlID `json:"Id" example:"1"` + // Docker resource identifier on which access control will be applied.\ + // In the case of a resource control applied to a stack, use the stack name as identifier + ResourceID string `json:"ResourceId" example:"617c5f22bb9b023d6daab7cba43a57576f83492867bc767d1c59416b065e5f08"` + // List of Docker resources that will inherit this access control + SubResourceIDs []string `json:"SubResourceIds" example:"617c5f22bb9b023d6daab7cba43a57576f83492867bc767d1c59416b065e5f08"` + // Type of Docker resource. Valid values are: 1- container, 2 -service + // 3 - volume, 4 - secret, 5 - stack, 6 - config or 7 - custom template + Type ResourceControlType `json:"Type" example:"1"` + UserAccesses []UserResourceAccess `json:"UserAccesses" example:""` + TeamAccesses []TeamResourceAccess `json:"TeamAccesses" example:""` + // Permit access to the associated resource to any user + Public bool `json:"Public" example:"true"` + // Permit access to resource only to admins + AdministratorsOnly bool `json:"AdministratorsOnly" example:"true"` + System bool `json:"System" example:""` // Deprecated fields // Deprecated in DBVersion == 2 @@ -475,9 +580,13 @@ type ( // Role represents a set of authorizations that can be associated to a user or // to a team. Role struct { - ID RoleID `json:"Id"` - Name string `json:"Name"` - Description string `json:"Description"` + // Role Identifier + ID RoleID `json:"Id" example:"1"` + // Role name + Name string `json:"Name" example:"HelpDesk"` + // Role description + Description string `json:"Description" example:"Read-only access of all resources in an endpoint"` + // Authorizations associated to a role Authorizations Authorizations `json:"Authorizations"` Priority int `json:"Priority"` } @@ -491,7 +600,8 @@ type ( // NOTE: The Recurring option is only used by ScriptExecutionJob at the moment // Deprecated in favor of EdgeJob Schedule struct { - ID ScheduleID `json:"Id"` + // Schedule Identifier + ID ScheduleID `json:"Id" example:"1"` Name string CronExpression string Recurring bool @@ -515,46 +625,78 @@ type ( // Settings represents the application settings Settings struct { - LogoURL string `json:"LogoURL"` - BlackListedLabels []Pair `json:"BlackListedLabels"` - AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"` - LDAPSettings LDAPSettings `json:"LDAPSettings"` - OAuthSettings OAuthSettings `json:"OAuthSettings"` - AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` - AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` - AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` - AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` - AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` - AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"` - AllowContainerCapabilitiesForRegularUsers bool `json:"AllowContainerCapabilitiesForRegularUsers"` - SnapshotInterval string `json:"SnapshotInterval"` - TemplatesURL string `json:"TemplatesURL"` - EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` - EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"` - EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` - UserSessionTimeout string `json:"UserSessionTimeout"` - EnableTelemetry bool `json:"EnableTelemetry"` + // URL to a logo that will be displayed on the login page as well as on top of the sidebar. Will use default Portainer logo when value is empty string + LogoURL string `json:"LogoURL" example:"https://mycompany.mydomain.tld/logo.png"` + // A list of label name & value that will be used to hide containers when querying containers + BlackListedLabels []Pair `json:"BlackListedLabels"` + // Active authentication method for the Portainer instance. Valid values are: 1 for internal, 2 for LDAP, or 3 for oauth + AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod" example:"1"` + LDAPSettings LDAPSettings `json:"LDAPSettings" example:""` + OAuthSettings OAuthSettings `json:"OAuthSettings" example:""` + // The interval in which endpoint snapshots are created + SnapshotInterval string `json:"SnapshotInterval" example:"5m"` + // URL to the templates that will be displayed in the UI when navigating to App Templates + TemplatesURL string `json:"TemplatesURL" example:"https://raw.githubusercontent.com/portainer/templates/master/templates.json"` + // The default check in interval for edge agent (in seconds) + EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval" example:"5"` + // Whether edge compute features are enabled + EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures" example:""` + // The duration of a user session + UserSessionTimeout string `json:"UserSessionTimeout" example:"5m"` + // Whether telemetry is enabled + EnableTelemetry bool `json:"EnableTelemetry" example:"false"` // Deprecated fields DisplayDonationHeader bool DisplayExternalContributors bool + + // Deprecated fields v26 + EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` + AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` + AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` + AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` + AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` + AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"` + AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` + AllowContainerCapabilitiesForRegularUsers bool `json:"AllowContainerCapabilitiesForRegularUsers"` } // SnapshotJob represents a scheduled job that can create endpoint snapshots SnapshotJob struct{} + // SoftwareEdition represents an edition of Portainer + SoftwareEdition int + // Stack represents a Docker stack created via docker stack deploy Stack struct { - ID StackID `json:"Id"` - Name string `json:"Name"` - Type StackType `json:"Type"` - EndpointID EndpointID `json:"EndpointId"` - SwarmID string `json:"SwarmId"` - EntryPoint string `json:"EntryPoint"` - Env []Pair `json:"Env"` - ResourceControl *ResourceControl `json:"ResourceControl"` - Status StackStatus `json:"Status"` - ProjectPath string + // Stack Identifier + ID StackID `json:"Id" example:"1"` + // Stack name + Name string `json:"Name" example:"myStack"` + // Stack type. 1 for a Swarm stack, 2 for a Compose stack + Type StackType `json:"Type" example:"2"` + // Endpoint identifier. Reference the endpoint that will be used for deployment + EndpointID EndpointID `json:"EndpointId" example:"1"` + // Cluster identifier of the Swarm cluster where the stack is deployed + SwarmID string `json:"SwarmId" example:"jpofkc0i9uo9wtx1zesuk649w"` + // Path to the Stack file + EntryPoint string `json:"EntryPoint" example:"docker-compose.yml"` + // A list of environment variables used during stack deployment + Env []Pair `json:"Env" example:""` + // + ResourceControl *ResourceControl `json:"ResourceControl" example:""` + // Stack status (1 - active, 2 - inactive) + Status StackStatus `json:"Status" example:"1"` + // Path on disk to the repository hosting the Stack file + ProjectPath string `example:"/data/compose/myStack_jpofkc0i9uo9wtx1zesuk649w"` + // The date in unix time when stack was created + CreationDate int64 `example:"1587399600"` + // The username which created this stack + CreatedBy string `example:"admin"` + // The date in unix time when stack was last updated + UpdateDate int64 `example:"1587399600"` + // The username which last updated this stack + UpdatedBy string `example:"bob"` } // StackID represents a stack identifier (it must be composed of Name + "_" + SwarmID to create a unique identifier) @@ -568,14 +710,19 @@ type ( // Status represents the application status Status struct { - Version string `json:"Version"` + // Portainer API version + Version string `json:"Version" example:"2.0.0"` } // Tag represents a tag that can be associated to a resource Tag struct { - ID TagID - Name string `json:"Name"` - Endpoints map[EndpointID]bool `json:"Endpoints"` + // Tag identifier + ID TagID `example:"1"` + // Tag name + Name string `json:"Name" example:"org/acme"` + // A set of endpoint ids that have this tag + Endpoints map[EndpointID]bool `json:"Endpoints"` + // A set of endpoint group ids that have this tag EndpointGroups map[EndpointGroupID]bool `json:"EndpointGroups"` } @@ -584,8 +731,10 @@ type ( // Team represents a list of user accounts Team struct { - ID TeamID `json:"Id"` - Name string `json:"Name"` + // Team Identifier + ID TeamID `json:"Id" example:"1"` + // Team name + Name string `json:"Name" example:"developers"` } // TeamAccessPolicies represent the association of an access policy and a team @@ -596,10 +745,14 @@ type ( // TeamMembership represents a membership association between a user and a team TeamMembership struct { - ID TeamMembershipID `json:"Id"` - UserID UserID `json:"UserID"` - TeamID TeamID `json:"TeamID"` - Role MembershipRole `json:"Role"` + // Membership Identifier + ID TeamMembershipID `json:"Id" example:"1"` + // User identifier + UserID UserID `json:"UserID" example:"1"` + // Team identifier + TeamID TeamID `json:"TeamID" example:"1"` + // Team role (1 for team leader and 2 for team member) + Role MembershipRole `json:"Role" example:"1"` } // TeamMembershipID represents a team membership identifier @@ -615,58 +768,92 @@ type ( // or an Edge template Template struct { // Mandatory container/stack fields - ID TemplateID `json:"Id"` - Type TemplateType `json:"type"` - Title string `json:"title"` - Description string `json:"description"` - AdministratorOnly bool `json:"administrator_only"` + // Template Identifier + ID TemplateID `json:"Id" example:"1"` + // Template type. Valid values are: 1 (container), 2 (Swarm stack) or 3 (Compose stack) + Type TemplateType `json:"type" example:"1"` + // Title of the template + Title string `json:"title" example:"Nginx"` + // Description of the template + Description string `json:"description" example:"High performance web server"` + // Whether the template should be available to administrators only + AdministratorOnly bool `json:"administrator_only" example:"true"` // Mandatory container fields - Image string `json:"image"` + // Image associated to a container template. Mandatory for a container template + Image string `json:"image" example:"nginx:latest"` // Mandatory stack fields Repository TemplateRepository `json:"repository"` // Mandatory Edge stack fields + // Stack file used for this template StackFile string `json:"stackFile"` // Optional stack/container fields - Name string `json:"name,omitempty"` - Logo string `json:"logo,omitempty"` - Env []TemplateEnv `json:"env,omitempty"` - Note string `json:"note,omitempty"` - Platform string `json:"platform,omitempty"` - Categories []string `json:"categories,omitempty"` + // Default name for the stack/container to be used on deployment + Name string `json:"name,omitempty" example:"mystackname"` + // URL of the template's logo + Logo string `json:"logo,omitempty" example:"https://cloudinovasi.id/assets/img/logos/nginx.png"` + // A list of environment variables used during the template deployment + Env []TemplateEnv `json:"env,omitempty"` + // A note that will be displayed in the UI. Supports HTML content + Note string `json:"note,omitempty" example:"This is my custom template"` + // Platform associated to the template. + // Valid values are: 'linux', 'windows' or leave empty for multi-platform + Platform string `json:"platform,omitempty" example:"linux"` + // A list of categories associated to the template + Categories []string `json:"categories,omitempty" example:"database"` // Optional container fields - Registry string `json:"registry,omitempty"` - Command string `json:"command,omitempty"` - Network string `json:"network,omitempty"` - Volumes []TemplateVolume `json:"volumes,omitempty"` - Ports []string `json:"ports,omitempty"` - Labels []Pair `json:"labels,omitempty"` - Privileged bool `json:"privileged,omitempty"` - Interactive bool `json:"interactive,omitempty"` - RestartPolicy string `json:"restart_policy,omitempty"` - Hostname string `json:"hostname,omitempty"` + // The URL of a registry associated to the image for a container template + Registry string `json:"registry,omitempty" example:"quay.io"` + // The command that will be executed in a container template + Command string `json:"command,omitempty" example:"ls -lah"` + // Name of a network that will be used on container deployment if it exists inside the environment + Network string `json:"network,omitempty" example:"mynet"` + // A list of volumes used during the container template deployment + Volumes []TemplateVolume `json:"volumes,omitempty"` + // A list of ports exposed by the container + Ports []string `json:"ports,omitempty" example:"8080:80/tcp"` + // Container labels + Labels []Pair `json:"labels,omitempty" example:""` + // Whether the container should be started in privileged mode + Privileged bool `json:"privileged,omitempty" example:"true"` + // Whether the container should be started in + // interactive mode (-i -t equivalent on the CLI) + Interactive bool `json:"interactive,omitempty" example:"true"` + // Container restart policy + RestartPolicy string `json:"restart_policy,omitempty" example:"on-failure"` + // Container hostname + Hostname string `json:"hostname,omitempty" example:"mycontainer"` } // TemplateEnv represents a template environment variable configuration TemplateEnv struct { - Name string `json:"name"` - Label string `json:"label,omitempty"` - Description string `json:"description,omitempty"` - Default string `json:"default,omitempty"` - Preset bool `json:"preset,omitempty"` - Select []TemplateEnvSelect `json:"select,omitempty"` + // name of the environment variable + Name string `json:"name" example:"MYSQL_ROOT_PASSWORD"` + // Text for the label that will be generated in the UI + Label string `json:"label,omitempty" example:"Root password"` + // Content of the tooltip that will be generated in the UI + Description string `json:"description,omitempty" example:"MySQL root account password"` + // Default value that will be set for the variable + Default string `json:"default,omitempty" example:"default_value"` + // If set to true, will not generate any input for this variable in the UI + Preset bool `json:"preset,omitempty" example:"false"` + // A list of name/value that will be used to generate a dropdown in the UI + Select []TemplateEnvSelect `json:"select,omitempty"` } // TemplateEnvSelect represents text/value pair that will be displayed as a choice for the // template user TemplateEnvSelect struct { - Text string `json:"text"` - Value string `json:"value"` - Default bool `json:"default"` + // Some text that will displayed as a choice + Text string `json:"text" example:"text value"` + // A value that will be associated to the choice + Value string `json:"value" example:"value"` + // Will set this choice as the default choice + Default bool `json:"default" example:"false"` } // TemplateID represents a template identifier @@ -674,8 +861,10 @@ type ( // TemplateRepository represents the git repository configuration for a template TemplateRepository struct { - URL string `json:"url"` - StackFile string `json:"stackfile"` + // URL of a git repository used to deploy a stack template. Mandatory for a Swarm/Compose stack template + URL string `json:"url" example:"https://github.com/portainer/portainer-compose"` + // Path to the stack file inside the git repository + StackFile string `json:"stackfile" example:"./subfolder/docker-compose.yml"` } // TemplateType represents the type of a template @@ -683,18 +872,26 @@ type ( // TemplateVolume represents a template volume configuration TemplateVolume struct { - Container string `json:"container"` - Bind string `json:"bind,omitempty"` - ReadOnly bool `json:"readonly,omitempty"` + // Path inside the container + Container string `json:"container" example:"/data"` + // Path on the host + Bind string `json:"bind,omitempty" example:"/tmp"` + // Whether the volume used should be readonly + ReadOnly bool `json:"readonly,omitempty" example:"true"` } // TLSConfiguration represents a TLS configuration TLSConfiguration struct { - TLS bool `json:"TLS"` - TLSSkipVerify bool `json:"TLSSkipVerify"` - TLSCACertPath string `json:"TLSCACert,omitempty"` - TLSCertPath string `json:"TLSCert,omitempty"` - TLSKeyPath string `json:"TLSKey,omitempty"` + // Use TLS + TLS bool `json:"TLS" example:"true"` + // Skip the verification of the server TLS certificate + TLSSkipVerify bool `json:"TLSSkipVerify" example:"false"` + // Path to the TLS CA certificate file + TLSCACertPath string `json:"TLSCACert,omitempty" example:"/data/tls/ca.pem"` + // Path to the TLS client certificate file + TLSCertPath string `json:"TLSCert,omitempty" example:"/data/tls/cert.pem"` + // Path to the TLS client key file + TLSKeyPath string `json:"TLSKey,omitempty" example:"/data/tls/key.pem"` } // TLSFileType represents a type of TLS file required to connect to a Docker endpoint. @@ -724,10 +921,12 @@ type ( // User represents a user account User struct { - ID UserID `json:"Id"` - Username string `json:"Username"` - Password string `json:"Password,omitempty"` - Role UserRole `json:"Role"` + // User Identifier + ID UserID `json:"Id" example:"1"` + Username string `json:"Username" example:"bob"` + Password string `json:"Password,omitempty" example:"passwd"` + // User role (1 for administrator account and 2 for regular account) + Role UserRole `json:"Role" example:"1"` // Deprecated fields // Deprecated in DBVersion == 25 @@ -753,7 +952,8 @@ type ( // Webhook represents a url webhook that can be used to update a service Webhook struct { - ID WebhookID `json:"Id"` + // Webhook Identifier + ID WebhookID `json:"Id" example:"1"` Token string `json:"Token"` ResourceID string `json:"ResourceId"` EndpointID EndpointID `json:"EndpointId"` @@ -774,6 +974,8 @@ type ( // ComposeStackManager represents a service to manage Compose stacks ComposeStackManager interface { + ComposeSyntaxMaxVersion() string + NormalizeStackName(name string) string Up(stack *Stack, endpoint *Endpoint) error Down(stack *Stack, endpoint *Endpoint) error } @@ -801,6 +1003,7 @@ type ( Close() error IsNew() bool MigrateData() error + CheckCurrentEdition() error DockerHub() DockerHubService CustomTemplate() CustomTemplateService @@ -992,7 +1195,7 @@ type ( DeleteResourceControl(ID ResourceControlID) error } - // ReverseTunnelService represensts a service used to manage reverse tunnel connections. + // ReverseTunnelService represents a service used to manage reverse tunnel connections. ReverseTunnelService interface { StartTunnelServer(addr, port string, snapshotService SnapshotService) error GenerateEdgeKey(url, host string, endpointIdentifier int) string @@ -1034,7 +1237,7 @@ type ( GetNextIdentifier() int } - // StackService represents a service for managing endpoint snapshots + // SnapshotService represents a service for managing endpoint snapshots SnapshotService interface { Start() SetSnapshotInterval(snapshotInterval string) error @@ -1101,8 +1304,9 @@ type ( // VersionService represents a service for managing version data VersionService interface { DBVersion() (int, error) - StoreDBVersion(version int) error + Edition() (SoftwareEdition, error) InstanceID() (string, error) + StoreDBVersion(version int) error StoreInstanceID(ID string) error } @@ -1119,9 +1323,11 @@ type ( const ( // APIVersion is the version number of the Portainer API - APIVersion = "2.0.0" + APIVersion = "2.2.0" // DBVersion is the version number of the Portainer database - DBVersion = 25 + DBVersion = 27 + // ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax + ComposeSyntaxMaxVersion = "3.9" // AssetsServerURL represents the URL of the Portainer asset server AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com" // MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved @@ -1245,6 +1451,16 @@ const ( TeamMember ) +const ( + _ SoftwareEdition = iota + // PortainerCE represents the community edition of Portainer + PortainerCE + // PortainerBE represents the business edition of Portainer + PortainerBE + // PortainerEE represents the business edition of Portainer + PortainerEE +) + const ( _ RegistryType = iota // QuayRegistry represents a Quay.io registry @@ -1279,7 +1495,7 @@ const ( StackResourceControl // ConfigResourceControl represents a resource control associated to a Docker config ConfigResourceControl - // CustomTemplateResourceControl represents a resource control associated to a custom template + // CustomTemplateResourceControl represents a resource control associated to a custom template CustomTemplateResourceControl ) @@ -1344,6 +1560,7 @@ const ( EdgeAgentActive string = "ACTIVE" ) +// represents an authorization type const ( OperationDockerContainerArchiveInfo Authorization = "DockerContainerArchiveInfo" OperationDockerContainerList Authorization = "DockerContainerList" diff --git a/api/swagger.yaml b/api/swagger.yaml index 87ba015f4..6a9f489e6 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -1,6 +1,2489 @@ ---- -swagger: '2.0' +basePath: /api +definitions: + auth.authenticatePayload: + properties: + password: + description: Password + example: mypassword + type: string + username: + description: Username + example: admin + type: string + required: + - password + - username + type: object + auth.authenticateResponse: + properties: + jwt: + description: JWT token used to authenticate against the API + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsImV4cCI6MTQ5OTM3NjE1NH0.NJ6vE8FY1WG6jsRQzfMqeatJ4vh2TWAeeYfDhP71YEE + type: string + type: object + auth.oauthPayload: + properties: + code: + description: OAuth code returned from OAuth Provided + type: string + type: object + customtemplates.customTemplateFromFileContentPayload: + properties: + description: + description: Description of the template + example: High performance web server + type: string + fileContent: + description: Content of stack file + type: string + logo: + description: URL of the template's logo + example: https://cloudinovasi.id/assets/img/logos/nginx.png + type: string + note: + description: A note that will be displayed in the UI. Supports HTML content + example: This is my custom template + type: string + platform: + description: |- + Platform associated to the template. + Valid values are: 1 - 'linux', 2 - 'windows' + enum: + - 1 + - 2 + example: 1 + type: integer + title: + description: Title of the template + example: Nginx + type: string + type: + description: Type of created stack (1 - swarm, 2 - compose) + enum: + - 1 + - 2 + example: 1 + type: integer + required: + - description + - fileContent + - platform + - title + - type + type: object + customtemplates.customTemplateFromGitRepositoryPayload: + properties: + composeFilePathInRepository: + default: docker-compose.yml + description: Path to the Stack file inside the Git repository + example: docker-compose.yml + type: string + description: + description: Description of the template + example: High performance web server + type: string + logo: + description: URL of the template's logo + example: https://cloudinovasi.id/assets/img/logos/nginx.png + type: string + note: + description: A note that will be displayed in the UI. Supports HTML content + example: This is my custom template + type: string + platform: + description: |- + Platform associated to the template. + Valid values are: 1 - 'linux', 2 - 'windows' + enum: + - 1 + - 2 + example: 1 + type: integer + repositoryAuthentication: + description: Use basic authentication to clone the Git repository + example: true + type: boolean + repositoryPassword: + description: Password used in basic authentication. Required when RepositoryAuthentication + is true. + example: myGitPassword + type: string + repositoryReferenceName: + description: Reference name of a Git repository hosting the Stack file + example: refs/heads/master + type: string + repositoryURL: + description: URL of a Git repository hosting the Stack file + example: https://github.com/openfaas/faas + type: string + repositoryUsername: + description: Username used in basic authentication. Required when RepositoryAuthentication + is true. + example: myGitUsername + type: string + title: + description: Title of the template + example: Nginx + type: string + type: + description: Type of created stack (1 - swarm, 2 - compose) + enum: + - 1 + - 2 + example: 1 + type: integer + required: + - description + - platform + - repositoryURL + - title + - type + type: object + customtemplates.customTemplateUpdatePayload: + properties: + description: + description: Description of the template + example: High performance web server + type: string + fileContent: + description: Content of stack file + type: string + logo: + description: URL of the template's logo + example: https://cloudinovasi.id/assets/img/logos/nginx.png + type: string + note: + description: A note that will be displayed in the UI. Supports HTML content + example: This is my custom template + type: string + platform: + description: |- + Platform associated to the template. + Valid values are: 1 - 'linux', 2 - 'windows' + enum: + - 1 + - 2 + example: 1 + type: integer + title: + description: Title of the template + example: Nginx + type: string + type: + description: Type of created stack (1 - swarm, 2 - compose) + enum: + - 1 + - 2 + example: 1 + type: integer + required: + - description + - fileContent + - platform + - title + - type + type: object + customtemplates.fileResponse: + properties: + fileContent: + type: string + type: object + dockerhub.dockerhubUpdatePayload: + properties: + authentication: + description: Enable authentication against DockerHub + example: false + type: boolean + password: + description: Password used to authenticate against the DockerHub + example: hub_password + type: string + username: + description: Username used to authenticate against the DockerHub + example: hub_user + type: string + required: + - authentication + - password + - username + type: object + edgegroups.edgeGroupCreatePayload: + properties: + dynamic: + type: boolean + endpoints: + items: + type: integer + type: array + name: + type: string + partialMatch: + type: boolean + tagIDs: + items: + description: Tag identifier + example: 1 + type: integer + type: array + type: object + edgegroups.edgeGroupUpdatePayload: + properties: + dynamic: + type: boolean + endpoints: + items: + type: integer + type: array + name: + type: string + partialMatch: + type: boolean + tagIDs: + items: + description: Tag identifier + example: 1 + type: integer + type: array + type: object + edgejobs.edgeJobCreateFromFileContentPayload: + properties: + cronExpression: + type: string + endpoints: + items: + type: integer + type: array + fileContent: + type: string + name: + type: string + recurring: + type: boolean + type: object + edgejobs.edgeJobCreateFromFilePayload: + properties: + cronExpression: + type: string + endpoints: + items: + type: integer + type: array + file: + items: + type: integer + type: array + name: + type: string + recurring: + type: boolean + type: object + edgejobs.edgeJobFileResponse: + properties: + FileContent: + type: string + type: object + edgejobs.edgeJobUpdatePayload: + properties: + cronExpression: + type: string + endpoints: + items: + type: integer + type: array + fileContent: + type: string + name: + type: string + recurring: + type: boolean + type: object + edgejobs.fileResponse: + properties: + FileContent: + type: string + type: object + edgejobs.taskContainer: + properties: + EndpointId: + type: integer + Id: + type: string + LogsStatus: + type: integer + type: object + edgestacks.stackFileResponse: + properties: + StackFileContent: + type: string + type: object + edgestacks.swarmStackFromFileContentPayload: + properties: + edgeGroups: + items: + description: EdgeGroup Identifier + example: 1 + type: integer + type: array + name: + type: string + stackFileContent: + type: string + type: object + edgestacks.swarmStackFromFileUploadPayload: + properties: + edgeGroups: + items: + description: EdgeGroup Identifier + example: 1 + type: integer + type: array + name: + type: string + stackFileContent: + items: + type: integer + type: array + type: object + edgestacks.swarmStackFromGitRepositoryPayload: + properties: + composeFilePathInRepository: + type: string + edgeGroups: + items: + description: EdgeGroup Identifier + example: 1 + type: integer + type: array + name: + type: string + repositoryAuthentication: + type: boolean + repositoryPassword: + type: string + repositoryReferenceName: + type: string + repositoryURL: + type: string + repositoryUsername: + type: string + type: object + edgestacks.updateEdgeStackPayload: + properties: + edgeGroups: + items: + description: EdgeGroup Identifier + example: 1 + type: integer + type: array + prune: + type: boolean + stackFileContent: + type: string + version: + type: integer + type: object + endpointedge.configResponse: + properties: + name: + type: string + prune: + type: boolean + stackFileContent: + type: string + type: object + endpointgroups.endpointGroupCreatePayload: + properties: + associatedEndpoints: + description: List of endpoint identifiers that will be part of this group + example: + - 1 + - 3 + items: + type: integer + type: array + description: + description: Endpoint group description + example: description + type: string + name: + description: Endpoint group name + example: my-endpoint-group + type: string + tagIDs: + description: List of tag identifiers to which this endpoint group is associated + example: + - 1 + - 2 + items: + description: Tag identifier + example: 1 + type: integer + type: array + required: + - name + type: object + endpointgroups.endpointGroupUpdatePayload: + properties: + description: + description: Endpoint group description + example: description + type: string + name: + description: Endpoint group name + example: my-endpoint-group + type: string + tagIDs: + description: List of tag identifiers associated to the endpoint group + example: + - 3 + - 4 + items: + description: Tag identifier + example: 1 + type: integer + type: array + teamAccessPolicies: + $ref: '#/definitions/portainer.TeamAccessPolicies' + userAccessPolicies: + $ref: '#/definitions/portainer.UserAccessPolicies' + type: object + endpoints.edgeJobResponse: + properties: + CollectLogs: + description: Whether to collect logs + example: true + type: boolean + CronExpression: + description: A cron expression to schedule this job + example: '* * * * *' + type: string + Id: + description: EdgeJob Identifier + example: 2 + type: integer + Script: + description: Script to run + example: echo hello + type: string + Version: + description: Version of this EdgeJob + example: 2 + type: integer + type: object + endpoints.endpointStatusInspectResponse: + properties: + checkin: + description: The current value of CheckinInterval + example: 5 + type: integer + credentials: + type: string + port: + description: The tunnel port + example: 8732 + type: integer + schedules: + description: List of requests for jobs to run on the endpoint + items: + $ref: '#/definitions/endpoints.edgeJobResponse' + type: array + stacks: + description: List of stacks to be deployed on the endpoints + items: + $ref: '#/definitions/endpoints.stackStatusResponse' + type: array + status: + description: Status represents the endpoint status + example: REQUIRED + type: string + type: object + endpoints.endpointUpdatePayload: + properties: + azureApplicationID: + description: Azure application ID + example: eag7cdo9-o09l-9i83-9dO9-f0b23oe78db4 + type: string + azureAuthenticationKey: + description: Azure authentication key + example: cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk= + type: string + azureTenantID: + description: Azure tenant ID + example: 34ddc78d-4fel-2358-8cc1-df84c8o839f5 + type: string + edgeCheckinInterval: + description: The check in interval for edge agent (in seconds) + example: 5 + type: integer + groupID: + description: Group identifier + example: 1 + type: integer + kubernetes: + $ref: '#/definitions/portainer.KubernetesData' + description: Associated Kubernetes data + name: + description: Name that will be used to identify this endpoint + example: my-endpoint + type: string + publicURL: + description: |- + URL or IP address where exposed containers will be reachable.\ + Defaults to URL if not specified + example: docker.mydomain.tld:2375 + type: string + status: + description: The status of the endpoint (1 - up, 2 - down) + example: 1 + type: integer + tagIDs: + description: List of tag identifiers to which this endpoint is associated + example: + - 1 + - 2 + items: + description: Tag identifier + example: 1 + type: integer + type: array + teamAccessPolicies: + $ref: '#/definitions/portainer.TeamAccessPolicies' + tls: + description: Require TLS to connect against this endpoint + example: true + type: boolean + tlsskipClientVerify: + description: Skip client verification when using TLS + example: false + type: boolean + tlsskipVerify: + description: Skip server verification when using TLS + example: false + type: boolean + url: + description: URL or IP address of a Docker host + example: docker.mydomain.tld:2375 + type: string + userAccessPolicies: + $ref: '#/definitions/portainer.UserAccessPolicies' + type: object + endpoints.stackStatusResponse: + properties: + id: + description: EdgeStack Identifier + example: 1 + type: integer + version: + description: Version of this stack + example: 3 + type: integer + type: object + motd.motdResponse: + properties: + ContentLayout: + additionalProperties: + type: string + type: object + Hash: + items: + type: integer + type: array + Message: + type: string + Style: + type: string + Title: + type: string + type: object + portainer.AccessPolicy: + properties: + RoleId: + description: Role identifier. Reference the role that will be associated to + this access policy + example: 1 + type: integer + type: object + portainer.Authorizations: + additionalProperties: + type: boolean + type: object + portainer.AzureCredentials: + properties: + ApplicationID: + description: Azure application ID + example: eag7cdo9-o09l-9i83-9dO9-f0b23oe78db4 + type: string + AuthenticationKey: + description: Azure authentication key + example: cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk= + type: string + TenantID: + description: Azure tenant ID + example: 34ddc78d-4fel-2358-8cc1-df84c8o839f5 + type: string + type: object + portainer.CustomTemplate: + properties: + CreatedByUserId: + description: User identifier who created this template + example: 3 + type: integer + Description: + description: Description of the template + example: High performance web server + type: string + EntryPoint: + description: Path to the Stack file + example: docker-compose.yml + type: string + Id: + description: CustomTemplate Identifier + example: 1 + type: integer + Logo: + description: URL of the template's logo + example: https://cloudinovasi.id/assets/img/logos/nginx.png + type: string + Note: + description: A note that will be displayed in the UI. Supports HTML content + example: This is my custom template + type: string + Platform: + description: |- + Platform associated to the template. + Valid values are: 1 - 'linux', 2 - 'windows' + enum: + - 1 + - 2 + example: 1 + type: integer + ProjectPath: + description: Path on disk to the repository hosting the Stack file + example: /data/custom_template/3 + type: string + ResourceControl: + $ref: '#/definitions/portainer.ResourceControl' + Title: + description: Title of the template + example: Nginx + type: string + Type: + description: Type of created stack (1 - swarm, 2 - compose) + example: 1 + type: integer + type: object + portainer.DockerHub: + properties: + Authentication: + description: Is authentication against DockerHub enabled + example: true + type: boolean + Password: + description: Password used to authenticate against the DockerHub + example: passwd + type: string + Username: + description: Username used to authenticate against the DockerHub + example: user + type: string + type: object + portainer.DockerSnapshot: + properties: + DockerSnapshotRaw: + $ref: '#/definitions/portainer.DockerSnapshotRaw' + DockerVersion: + type: string + HealthyContainerCount: + type: integer + ImageCount: + type: integer + RunningContainerCount: + type: integer + ServiceCount: + type: integer + StackCount: + type: integer + StoppedContainerCount: + type: integer + Swarm: + type: boolean + Time: + type: integer + TotalCPU: + type: integer + TotalMemory: + type: integer + UnhealthyContainerCount: + type: integer + VolumeCount: + type: integer + type: object + portainer.DockerSnapshotRaw: + properties: + Containers: + type: object + Images: + type: object + Info: + type: object + Networks: + type: object + Version: + type: object + Volumes: + type: object + type: object + portainer.EdgeGroup: + properties: + Dynamic: + type: boolean + Endpoints: + items: + type: integer + type: array + Id: + description: EdgeGroup Identifier + example: 1 + type: integer + Name: + type: string + PartialMatch: + type: boolean + TagIds: + items: + description: Tag identifier + example: 1 + type: integer + type: array + type: object + portainer.EdgeJob: + properties: + Created: + type: integer + CronExpression: + type: string + Endpoints: + additionalProperties: + $ref: '#/definitions/portainer.EdgeJobEndpointMeta' + type: object + Id: + description: EdgeJob Identifier + example: 1 + type: integer + Name: + type: string + Recurring: + type: boolean + ScriptPath: + type: string + Version: + type: integer + type: object + portainer.EdgeJobEndpointMeta: + properties: + collectLogs: + type: boolean + logsStatus: + type: integer + type: object + portainer.EdgeStack: + properties: + CreationDate: + type: integer + EdgeGroups: + items: + description: EdgeGroup Identifier + example: 1 + type: integer + type: array + EntryPoint: + type: string + Id: + description: EdgeStack Identifier + example: 1 + type: integer + Name: + type: string + ProjectPath: + type: string + Prune: + type: boolean + Status: + additionalProperties: + $ref: '#/definitions/portainer.EdgeStackStatus' + type: object + Version: + type: integer + type: object + portainer.EdgeStackStatus: + properties: + EndpointID: + type: integer + Error: + type: string + Type: + type: integer + type: object + portainer.Endpoint: + properties: + AuthorizedTeams: + items: + type: integer + type: array + AuthorizedUsers: + description: Deprecated in DBVersion == 18 + items: + description: User identifier who created this template + example: 3 + type: integer + type: array + AzureCredentials: + $ref: '#/definitions/portainer.AzureCredentials' + EdgeCheckinInterval: + description: The check in interval for edge agent (in seconds) + example: 5 + type: integer + EdgeID: + description: The identifier of the edge agent associated with this endpoint + type: string + EdgeKey: + description: The key which is used to map the agent to Portainer + type: string + Extensions: + items: + $ref: '#/definitions/portainer.EndpointExtension' + type: array + GroupId: + description: Endpoint group identifier + example: 1 + type: integer + Id: + description: Endpoint Identifier + example: 1 + type: integer + Kubernetes: + $ref: '#/definitions/portainer.KubernetesData' + description: Associated Kubernetes data + Name: + description: Endpoint name + example: my-endpoint + type: string + PublicURL: + description: URL or IP address where exposed containers will be reachable + example: docker.mydomain.tld:2375 + type: string + Snapshots: + description: List of snapshots + items: + $ref: '#/definitions/portainer.DockerSnapshot' + type: array + Status: + description: The status of the endpoint (1 - up, 2 - down) + example: 1 + type: integer + TLS: + description: |- + Deprecated fields + Deprecated in DBVersion == 4 + type: boolean + TLSCACert: + type: string + TLSCert: + type: string + TLSConfig: + $ref: '#/definitions/portainer.TLSConfiguration' + TLSKey: + type: string + TagIds: + description: List of tag identifiers to which this endpoint is associated + items: + description: Tag identifier + example: 1 + type: integer + type: array + Tags: + description: Deprecated in DBVersion == 22 + items: + type: string + type: array + TeamAccessPolicies: + $ref: '#/definitions/portainer.TeamAccessPolicies' + description: List of team identifiers authorized to connect to this endpoint + Type: + description: Endpoint environment type. 1 for a Docker environment, 2 for + an agent on Docker environment or 3 for an Azure environment. + example: 1 + type: integer + URL: + description: URL or IP address of the Docker host associated to this endpoint + example: docker.mydomain.tld:2375 + type: string + UserAccessPolicies: + $ref: '#/definitions/portainer.UserAccessPolicies' + description: List of user identifiers authorized to connect to this endpoint + type: object + portainer.EndpointAuthorizations: + additionalProperties: + $ref: '#/definitions/portainer.Authorizations' + type: object + portainer.EndpointExtension: + properties: + Type: + type: integer + URL: + type: string + type: object + portainer.EndpointGroup: + properties: + AuthorizedTeams: + items: + type: integer + type: array + AuthorizedUsers: + description: Deprecated in DBVersion == 18 + items: + description: User identifier who created this template + example: 3 + type: integer + type: array + Description: + description: Description associated to the endpoint group + example: Endpoint group description + type: string + Id: + description: Endpoint group Identifier + example: 1 + type: integer + Labels: + description: Deprecated fields + items: + $ref: '#/definitions/portainer.Pair' + type: array + Name: + description: Endpoint group name + example: my-endpoint-group + type: string + TagIds: + description: List of tags associated to this endpoint group + items: + description: Tag identifier + example: 1 + type: integer + type: array + Tags: + description: Deprecated in DBVersion == 22 + items: + type: string + type: array + TeamAccessPolicies: + $ref: '#/definitions/portainer.TeamAccessPolicies' + UserAccessPolicies: + $ref: '#/definitions/portainer.UserAccessPolicies' + type: object + portainer.GitlabRegistryData: + properties: + InstanceURL: + type: string + ProjectId: + type: integer + ProjectPath: + type: string + type: object + portainer.KubernetesConfiguration: + properties: + IngressClasses: + items: + $ref: '#/definitions/portainer.KubernetesIngressClassConfig' + type: array + StorageClasses: + items: + $ref: '#/definitions/portainer.KubernetesStorageClassConfig' + type: array + UseLoadBalancer: + type: boolean + UseServerMetrics: + type: boolean + type: object + portainer.KubernetesData: + properties: + Configuration: + $ref: '#/definitions/portainer.KubernetesConfiguration' + Snapshots: + items: + $ref: '#/definitions/portainer.KubernetesSnapshot' + type: array + type: object + portainer.KubernetesIngressClassConfig: + properties: + Name: + type: string + Type: + type: string + type: object + portainer.KubernetesSnapshot: + properties: + KubernetesVersion: + type: string + NodeCount: + type: integer + Time: + type: integer + TotalCPU: + type: integer + TotalMemory: + type: integer + type: object + portainer.KubernetesStorageClassConfig: + properties: + AccessModes: + items: + type: string + type: array + AllowVolumeExpansion: + type: boolean + Name: + type: string + Provisioner: + type: string + type: object + portainer.LDAPGroupSearchSettings: + properties: + GroupAttribute: + description: LDAP attribute which denotes the group membership + example: member + type: string + GroupBaseDN: + description: The distinguished name of the element from which the LDAP server + will search for groups + example: dc=ldap,dc=domain,dc=tld + type: string + GroupFilter: + description: The LDAP search filter used to select group elements, optional + example: (objectClass=account + type: string + type: object + portainer.LDAPSearchSettings: + properties: + BaseDN: + description: The distinguished name of the element from which the LDAP server + will search for users + example: dc=ldap,dc=domain,dc=tld + type: string + Filter: + description: Optional LDAP search filter used to select user elements + example: (objectClass=account) + type: string + UserNameAttribute: + description: LDAP attribute which denotes the username + example: uid + type: string + type: object + portainer.LDAPSettings: + properties: + AnonymousMode: + description: Enable this option if the server is configured for Anonymous + access. When enabled, ReaderDN and Password will not be used + example: true + type: boolean + AutoCreateUsers: + description: Automatically provision users and assign them to matching LDAP + group names + example: true + type: boolean + GroupSearchSettings: + items: + $ref: '#/definitions/portainer.LDAPGroupSearchSettings' + type: array + Password: + description: Password of the account that will be used to search users + example: readonly-password + type: string + ReaderDN: + description: Account that will be used to search for users + example: cn=readonly-account,dc=ldap,dc=domain,dc=tld + type: string + SearchSettings: + items: + $ref: '#/definitions/portainer.LDAPSearchSettings' + type: array + StartTLS: + description: Whether LDAP connection should use StartTLS + example: true + type: boolean + TLSConfig: + $ref: '#/definitions/portainer.TLSConfiguration' + URL: + description: URL or IP address of the LDAP server + example: myldap.domain.tld:389 + type: string + type: object + portainer.OAuthSettings: + properties: + AccessTokenURI: + type: string + AuthorizationURI: + type: string + ClientID: + type: string + ClientSecret: + type: string + DefaultTeamID: + type: integer + OAuthAutoCreateUsers: + type: boolean + RedirectURI: + type: string + ResourceURI: + type: string + Scopes: + type: string + UserIdentifier: + type: string + type: object + portainer.Pair: + properties: + name: + example: name + type: string + value: + example: value + type: string + type: object + portainer.Registry: + properties: + Authentication: + description: Is authentication against this registry enabled + example: true + type: boolean + AuthorizedTeams: + items: + type: integer + type: array + AuthorizedUsers: + description: |- + Deprecated fields + Deprecated in DBVersion == 18 + items: + description: User identifier who created this template + example: 3 + type: integer + type: array + Gitlab: + $ref: '#/definitions/portainer.GitlabRegistryData' + Id: + description: Registry Identifier + example: 1 + type: integer + ManagementConfiguration: + $ref: '#/definitions/portainer.RegistryManagementConfiguration' + Name: + description: Registry Name + example: my-registry + type: string + Password: + description: Password used to authenticate against this registry + example: registry_password + type: string + TeamAccessPolicies: + $ref: '#/definitions/portainer.TeamAccessPolicies' + Type: + description: Registry Type (1 - Quay, 2 - Azure, 3 - Custom, 4 - Gitlab) + enum: + - 1 + - 2 + - 3 + - 4 + type: integer + URL: + description: URL or IP address of the Docker registry + example: registry.mydomain.tld:2375 + type: string + UserAccessPolicies: + $ref: '#/definitions/portainer.UserAccessPolicies' + Username: + description: Username used to authenticate against this registry + example: registry user + type: string + type: object + portainer.RegistryManagementConfiguration: + properties: + Authentication: + type: boolean + Password: + type: string + TLSConfig: + $ref: '#/definitions/portainer.TLSConfiguration' + Type: + type: integer + Username: + type: string + type: object + portainer.ResourceControl: + properties: + AccessLevel: + type: integer + AdministratorsOnly: + description: Permit access to resource only to admins + example: true + type: boolean + Id: + description: ResourceControl Identifier + example: 1 + type: integer + OwnerId: + description: |- + Deprecated fields + Deprecated in DBVersion == 2 + type: integer + Public: + description: Permit access to the associated resource to any user + example: true + type: boolean + ResourceId: + description: |- + Docker resource identifier on which access control will be applied.\ + In the case of a resource control applied to a stack, use the stack name as identifier + example: 617c5f22bb9b023d6daab7cba43a57576f83492867bc767d1c59416b065e5f08 + type: string + SubResourceIds: + description: List of Docker resources that will inherit this access control + example: + - 617c5f22bb9b023d6daab7cba43a57576f83492867bc767d1c59416b065e5f08 + items: + type: string + type: array + System: + type: boolean + TeamAccesses: + items: + $ref: '#/definitions/portainer.TeamResourceAccess' + type: array + Type: + description: |- + Type of Docker resource. Valid values are: 1- container, 2 -service + 3 - volume, 4 - secret, 5 - stack, 6 - config or 7 - custom template + example: 1 + type: integer + UserAccesses: + items: + $ref: '#/definitions/portainer.UserResourceAccess' + type: array + type: object + portainer.Role: + properties: + Authorizations: + $ref: '#/definitions/portainer.Authorizations' + description: Authorizations associated to a role + Description: + description: Role description + example: Read-only access of all resources in an endpoint + type: string + Id: + description: Role Identifier + example: 1 + type: integer + Name: + description: Role name + example: HelpDesk + type: string + Priority: + type: integer + type: object + portainer.Settings: + properties: + AllowBindMountsForRegularUsers: + description: Whether non-administrator should be able to use bind mounts when + creating containers + example: false + type: boolean + AllowContainerCapabilitiesForRegularUsers: + description: Whether non-administrator should be able to use container capabilities + type: boolean + AllowDeviceMappingForRegularUsers: + description: Whether non-administrator should be able to use device mapping + type: boolean + AllowHostNamespaceForRegularUsers: + description: Whether non-administrator should be able to use the host pid + type: boolean + AllowPrivilegedModeForRegularUsers: + description: Whether non-administrator should be able to use privileged mode + when creating containers + example: false + type: boolean + AllowStackManagementForRegularUsers: + description: Whether non-administrator should be able to manage stacks + type: boolean + AllowVolumeBrowserForRegularUsers: + description: Whether non-administrator should be able to browse volumes + type: boolean + AuthenticationMethod: + description: 'Active authentication method for the Portainer instance. Valid + values are: 1 for internal, 2 for LDAP, or 3 for oauth' + example: 1 + type: integer + BlackListedLabels: + description: A list of label name & value that will be used to hide containers + when querying containers + items: + $ref: '#/definitions/portainer.Pair' + type: array + EdgeAgentCheckinInterval: + description: The default check in interval for edge agent (in seconds) + example: 5 + type: integer + EnableEdgeComputeFeatures: + description: Whether edge compute features are enabled + type: boolean + EnableHostManagementFeatures: + description: Whether host management features are enabled + type: boolean + EnableTelemetry: + description: Whether telemetry is enabled + example: false + type: boolean + LDAPSettings: + $ref: '#/definitions/portainer.LDAPSettings' + LogoURL: + description: URL to a logo that will be displayed on the login page as well + as on top of the sidebar. Will use default Portainer logo when value is + empty string + example: https://mycompany.mydomain.tld/logo.png + type: string + OAuthSettings: + $ref: '#/definitions/portainer.OAuthSettings' + SnapshotInterval: + description: The interval in which endpoint snapshots are created + example: 5m + type: string + TemplatesURL: + description: URL to the templates that will be displayed in the UI when navigating + to App Templates + example: https://raw.githubusercontent.com/portainer/templates/master/templates.json + type: string + UserSessionTimeout: + description: The duration of a user session + example: 5m + type: string + displayDonationHeader: + description: Deprecated fields + type: boolean + displayExternalContributors: + type: boolean + type: object + portainer.Stack: + properties: + EndpointId: + description: Endpoint identifier. Reference the endpoint that will be used + for deployment + example: 1 + type: integer + EntryPoint: + description: Path to the Stack file + example: docker-compose.yml + type: string + Env: + description: A list of environment variables used during stack deployment + items: + $ref: '#/definitions/portainer.Pair' + type: array + Id: + description: Stack Identifier + example: 1 + type: integer + Name: + description: Stack name + example: myStack + type: string + ResourceControl: + $ref: '#/definitions/portainer.ResourceControl' + Status: + description: Stack status (1 - active, 2 - inactive) + example: 1 + type: integer + SwarmId: + description: Cluster identifier of the Swarm cluster where the stack is deployed + example: jpofkc0i9uo9wtx1zesuk649w + type: string + Type: + description: Stack type. 1 for a Swarm stack, 2 for a Compose stack + example: 2 + type: integer + createdBy: + description: The username which created this stack + example: admin + type: string + creationDate: + description: The date in unix time when stack was created + example: 1587399600 + type: integer + projectPath: + description: Path on disk to the repository hosting the Stack file + example: /data/compose/myStack_jpofkc0i9uo9wtx1zesuk649w + type: string + updateDate: + description: The date in unix time when stack was last updated + example: 1587399600 + type: integer + updatedBy: + description: The username which last updated this stack + example: bob + type: string + type: object + portainer.Status: + properties: + Version: + description: Portainer API version + example: 2.0.0 + type: string + type: object + portainer.TLSConfiguration: + properties: + TLS: + description: Use TLS + example: true + type: boolean + TLSCACert: + description: Path to the TLS CA certificate file + example: /data/tls/ca.pem + type: string + TLSCert: + description: Path to the TLS client certificate file + example: /data/tls/cert.pem + type: string + TLSKey: + description: Path to the TLS client key file + example: /data/tls/key.pem + type: string + TLSSkipVerify: + description: Skip the verification of the server TLS certificate + example: false + type: boolean + type: object + portainer.Tag: + properties: + EndpointGroups: + additionalProperties: + type: boolean + description: A set of endpoint group ids that have this tag + type: object + Endpoints: + additionalProperties: + type: boolean + description: A set of endpoint ids that have this tag + type: object + Name: + description: Tag name + example: org/acme + type: string + id: + description: Tag identifier + example: 1 + type: integer + type: object + portainer.Team: + properties: + Id: + description: Team Identifier + example: 1 + type: integer + Name: + description: Team name + example: developers + type: string + type: object + portainer.TeamAccessPolicies: + additionalProperties: + $ref: '#/definitions/portainer.AccessPolicy' + type: object + portainer.TeamMembership: + properties: + Id: + description: Membership Identifier + example: 1 + type: integer + Role: + description: Team role (1 for team leader and 2 for team member) + example: 1 + type: integer + TeamID: + description: Team identifier + example: 1 + type: integer + UserID: + description: User identifier + example: 1 + type: integer + type: object + portainer.TeamResourceAccess: + properties: + AccessLevel: + type: integer + TeamId: + type: integer + type: object + portainer.Template: + properties: + Id: + description: |- + Mandatory container/stack fields + Template Identifier + example: 1 + type: integer + administrator_only: + description: Whether the template should be available to administrators only + example: true + type: boolean + categories: + description: A list of categories associated to the template + example: + - database + items: + type: string + type: array + command: + description: The command that will be executed in a container template + example: ls -lah + type: string + description: + description: Description of the template + example: High performance web server + type: string + env: + description: A list of environment variables used during the template deployment + items: + $ref: '#/definitions/portainer.TemplateEnv' + type: array + hostname: + description: Container hostname + example: mycontainer + type: string + image: + description: |- + Mandatory container fields + Image associated to a container template. Mandatory for a container template + example: nginx:latest + type: string + interactive: + description: |- + Whether the container should be started in + interactive mode (-i -t equivalent on the CLI) + example: true + type: boolean + labels: + description: Container labels + items: + $ref: '#/definitions/portainer.Pair' + type: array + logo: + description: URL of the template's logo + example: https://cloudinovasi.id/assets/img/logos/nginx.png + type: string + name: + description: |- + Optional stack/container fields + Default name for the stack/container to be used on deployment + example: mystackname + type: string + network: + description: Name of a network that will be used on container deployment if + it exists inside the environment + example: mynet + type: string + note: + description: A note that will be displayed in the UI. Supports HTML content + example: This is my custom template + type: string + platform: + description: |- + Platform associated to the template. + Valid values are: 'linux', 'windows' or leave empty for multi-platform + example: linux + type: string + ports: + description: A list of ports exposed by the container + example: + - 8080:80/tcp + items: + type: string + type: array + privileged: + description: Whether the container should be started in privileged mode + example: true + type: boolean + registry: + description: |- + Optional container fields + The URL of a registry associated to the image for a container template + example: quay.io + type: string + repository: + $ref: '#/definitions/portainer.TemplateRepository' + description: Mandatory stack fields + restart_policy: + description: Container restart policy + example: on-failure + type: string + stackFile: + description: |- + Mandatory Edge stack fields + Stack file used for this template + type: string + title: + description: Title of the template + example: Nginx + type: string + type: + description: 'Template type. Valid values are: 1 (container), 2 (Swarm stack) + or 3 (Compose stack)' + example: 1 + type: integer + volumes: + description: A list of volumes used during the container template deployment + items: + $ref: '#/definitions/portainer.TemplateVolume' + type: array + type: object + portainer.TemplateEnv: + properties: + default: + description: Default value that will be set for the variable + example: default_value + type: string + description: + description: Content of the tooltip that will be generated in the UI + example: MySQL root account password + type: string + label: + description: Text for the label that will be generated in the UI + example: Root password + type: string + name: + description: name of the environment variable + example: MYSQL_ROOT_PASSWORD + type: string + preset: + description: If set to true, will not generate any input for this variable + in the UI + example: false + type: boolean + select: + description: A list of name/value that will be used to generate a dropdown + in the UI + items: + $ref: '#/definitions/portainer.TemplateEnvSelect' + type: array + type: object + portainer.TemplateEnvSelect: + properties: + default: + description: Will set this choice as the default choice + example: false + type: boolean + text: + description: Some text that will displayed as a choice + example: text value + type: string + value: + description: A value that will be associated to the choice + example: value + type: string + type: object + portainer.TemplateRepository: + properties: + stackfile: + description: Path to the stack file inside the git repository + example: ./subfolder/docker-compose.yml + type: string + url: + description: URL of a git repository used to deploy a stack template. Mandatory + for a Swarm/Compose stack template + example: https://github.com/portainer/portainer-compose + type: string + type: object + portainer.TemplateVolume: + properties: + bind: + description: Path on the host + example: /tmp + type: string + container: + description: Path inside the container + example: /data + type: string + readonly: + description: Whether the volume used should be readonly + example: true + type: boolean + type: object + portainer.User: + properties: + EndpointAuthorizations: + $ref: '#/definitions/portainer.EndpointAuthorizations' + Id: + description: User Identifier + example: 1 + type: integer + Password: + example: passwd + type: string + PortainerAuthorizations: + $ref: '#/definitions/portainer.Authorizations' + description: |- + Deprecated fields + Deprecated in DBVersion == 25 + Role: + description: User role (1 for administrator account and 2 for regular account) + example: 1 + type: integer + Username: + example: bob + type: string + type: object + portainer.UserAccessPolicies: + additionalProperties: + $ref: '#/definitions/portainer.AccessPolicy' + type: object + portainer.UserResourceAccess: + properties: + AccessLevel: + type: integer + UserId: + type: integer + type: object + portainer.Webhook: + properties: + EndpointId: + type: integer + Id: + description: Webhook Identifier + example: 1 + type: integer + ResourceId: + type: string + Token: + type: string + Type: + type: integer + type: object + registries.registryConfigurePayload: + properties: + authentication: + description: Is authentication against this registry enabled + example: false + type: boolean + password: + description: Password used to authenticate against this registry. required + when Authentication is true + example: registry_password + type: string + tls: + description: Use TLS + example: true + type: boolean + tlscacertFile: + description: The TLS CA certificate file + items: + type: integer + type: array + tlscertFile: + description: The TLS client certificate file + items: + type: integer + type: array + tlskeyFile: + description: The TLS client key file + items: + type: integer + type: array + tlsskipVerify: + description: Skip the verification of the server TLS certificate + example: false + type: boolean + username: + description: Username used to authenticate against this registry. Required + when Authentication is true + example: registry_user + type: string + required: + - authentication + type: object + registries.registryCreatePayload: + properties: + authentication: + description: Is authentication against this registry enabled + example: false + type: boolean + gitlab: + $ref: '#/definitions/portainer.GitlabRegistryData' + description: Gitlab specific details, required when type = 4 + name: + description: Name that will be used to identify this registry + example: my-registry + type: string + password: + description: Password used to authenticate against this registry. required + when Authentication is true + example: registry_password + type: string + type: + description: 'Registry Type. Valid values are: 1 (Quay.io), 2 (Azure container + registry), 3 (custom registry) or 4 (Gitlab registry)' + enum: + - 1 + - 2 + - 3 + - 4 + example: 1 + type: integer + url: + description: URL or IP address of the Docker registry + example: registry.mydomain.tld:2375 + type: string + username: + description: Username used to authenticate against this registry. Required + when Authentication is true + example: registry_user + type: string + required: + - authentication + - name + - type + - url + type: object + registries.registryUpdatePayload: + properties: + authentication: + description: Is authentication against this registry enabled + example: false + type: boolean + name: + description: Name that will be used to identify this registry + example: my-registry + type: string + password: + description: Password used to authenticate against this registry. required + when Authentication is true + example: registry_password + type: string + teamAccessPolicies: + $ref: '#/definitions/portainer.TeamAccessPolicies' + url: + description: URL or IP address of the Docker registry + example: registry.mydomain.tld:2375 + type: string + userAccessPolicies: + $ref: '#/definitions/portainer.UserAccessPolicies' + username: + description: Username used to authenticate against this registry. Required + when Authentication is true + example: registry_user + type: string + required: + - authentication + - name + - url + type: object + resourcecontrols.resourceControlCreatePayload: + properties: + administratorsOnly: + description: Permit access to resource only to admins + example: true + type: boolean + public: + description: Permit access to the associated resource to any user + example: true + type: boolean + resourceID: + example: 617c5f22bb9b023d6daab7cba43a57576f83492867bc767d1c59416b065e5f08 + type: string + subResourceIDs: + description: List of Docker resources that will inherit this access control + example: + - 617c5f22bb9b023d6daab7cba43a57576f83492867bc767d1c59416b065e5f08 + items: + type: string + type: array + teams: + description: List of team identifiers with access to the associated resource + example: + - 56 + - 7 + items: + type: integer + type: array + type: + description: |- + Type of Docker resource. Valid values are: container, volume\ + service, secret, config or stack + example: container + type: string + users: + description: List of user identifiers with access to the associated resource + example: + - 1 + - 4 + items: + type: integer + type: array + required: + - resourceID + - type + type: object + resourcecontrols.resourceControlUpdatePayload: + properties: + administratorsOnly: + description: Permit access to resource only to admins + example: true + type: boolean + public: + description: Permit access to the associated resource to any user + example: true + type: boolean + teams: + description: List of team identifiers with access to the associated resource + example: + - 7 + items: + type: integer + type: array + users: + description: List of user identifiers with access to the associated resource + example: + - 4 + items: + type: integer + type: array + type: object + settings.publicSettingsResponse: + properties: + AllowBindMountsForRegularUsers: + description: Whether non-administrator should be able to use bind mounts when + creating containers + example: true + type: boolean + AllowContainerCapabilitiesForRegularUsers: + description: Whether non-administrator should be able to use container capabilities + example: true + type: boolean + AllowDeviceMappingForRegularUsers: + description: Whether non-administrator should be able to use device mapping + example: true + type: boolean + AllowHostNamespaceForRegularUsers: + description: Whether non-administrator should be able to use the host pid + example: true + type: boolean + AllowPrivilegedModeForRegularUsers: + description: Whether non-administrator should be able to use privileged mode + when creating containers + example: true + type: boolean + AllowStackManagementForRegularUsers: + description: Whether non-administrator should be able to manage stacks + example: true + type: boolean + AllowVolumeBrowserForRegularUsers: + description: Whether non-administrator should be able to browse volumes + example: true + type: boolean + AuthenticationMethod: + description: 'Active authentication method for the Portainer instance. Valid + values are: 1 for internal, 2 for LDAP, or 3 for oauth' + example: 1 + type: integer + EnableEdgeComputeFeatures: + description: Whether edge compute features are enabled + example: true + type: boolean + EnableHostManagementFeatures: + description: Whether host management features are enabled + example: true + type: boolean + EnableTelemetry: + description: Whether telemetry is enabled + example: true + type: boolean + LogoURL: + description: URL to a logo that will be displayed on the login page as well + as on top of the sidebar. Will use default Portainer logo when value is + empty string + example: https://mycompany.mydomain.tld/logo.png + type: string + OAuthLoginURI: + description: The URL used for oauth login + example: https://gitlab.com/oauth + type: string + type: object + settings.settingsLDAPCheckPayload: + properties: + ldapsettings: + $ref: '#/definitions/portainer.LDAPSettings' + type: object + settings.settingsUpdatePayload: + properties: + allowBindMountsForRegularUsers: + description: Whether non-administrator should be able to use bind mounts when + creating containers + example: false + type: boolean + allowContainerCapabilitiesForRegularUsers: + description: Whether non-administrator should be able to use container capabilities + example: true + type: boolean + allowDeviceMappingForRegularUsers: + description: Whether non-administrator should be able to use device mapping + example: true + type: boolean + allowHostNamespaceForRegularUsers: + description: Whether non-administrator should be able to use the host pid + example: true + type: boolean + allowPrivilegedModeForRegularUsers: + description: Whether non-administrator should be able to use privileged mode + when creating containers + example: false + type: boolean + allowStackManagementForRegularUsers: + description: Whether non-administrator should be able to manage stacks + example: true + type: boolean + allowVolumeBrowserForRegularUsers: + description: Whether non-administrator should be able to browse volumes + example: true + type: boolean + authenticationMethod: + description: 'Active authentication method for the Portainer instance. Valid + values are: 1 for internal, 2 for LDAP, or 3 for oauth' + example: 1 + type: integer + blackListedLabels: + description: A list of label name & value that will be used to hide containers + when querying containers + items: + $ref: '#/definitions/portainer.Pair' + type: array + edgeAgentCheckinInterval: + description: The default check in interval for edge agent (in seconds) + example: 5 + type: integer + enableEdgeComputeFeatures: + description: Whether edge compute features are enabled + example: true + type: boolean + enableHostManagementFeatures: + description: Whether host management features are enabled + example: true + type: boolean + enableTelemetry: + description: Whether telemetry is enabled + example: false + type: boolean + ldapsettings: + $ref: '#/definitions/portainer.LDAPSettings' + logoURL: + description: URL to a logo that will be displayed on the login page as well + as on top of the sidebar. Will use default Portainer logo when value is + empty string + example: https://mycompany.mydomain.tld/logo.png + type: string + oauthSettings: + $ref: '#/definitions/portainer.OAuthSettings' + snapshotInterval: + description: The interval in which endpoint snapshots are created + example: 5m + type: string + templatesURL: + description: URL to the templates that will be displayed in the UI when navigating + to App Templates + example: https://raw.githubusercontent.com/portainer/templates/master/templates.json + type: string + userSessionTimeout: + description: The duration of a user session + example: 5m + type: string + type: object + stacks.composeStackFromFileContentPayload: + properties: + env: + description: A list of environment variables used during stack deployment + items: + $ref: '#/definitions/portainer.Pair' + type: array + name: + description: Name of the stack + example: myStack + type: string + stackFileContent: + description: Content of the Stack file + example: |- + version: 3 + services: + web: + image:nginx + type: string + required: + - name + - stackFileContent + type: object + stacks.composeStackFromGitRepositoryPayload: + properties: + composeFilePathInRepository: + default: docker-compose.yml + description: Path to the Stack file inside the Git repository + example: docker-compose.yml + type: string + env: + description: A list of environment variables used during stack deployment + items: + $ref: '#/definitions/portainer.Pair' + type: array + name: + description: Name of the stack + example: myStack + type: string + repositoryAuthentication: + description: Use basic authentication to clone the Git repository + example: true + type: boolean + repositoryPassword: + description: Password used in basic authentication. Required when RepositoryAuthentication + is true. + example: myGitPassword + type: string + repositoryReferenceName: + description: Reference name of a Git repository hosting the Stack file + example: refs/heads/master + type: string + repositoryURL: + description: URL of a Git repository hosting the Stack file + example: https://github.com/openfaas/faas + type: string + repositoryUsername: + description: Username used in basic authentication. Required when RepositoryAuthentication + is true. + example: myGitUsername + type: string + required: + - name + - repositoryURL + type: object + stacks.stackFileResponse: + properties: + StackFileContent: + description: Content of the Stack file + example: |- + version: 3 + services: + web: + image:nginx + type: string + type: object + stacks.stackMigratePayload: + properties: + endpointID: + description: Endpoint identifier of the target endpoint where the stack will + be relocated + example: 2 + type: integer + name: + description: If provided will rename the migrated stack + example: new-stack + type: string + swarmID: + description: Swarm cluster identifier, must match the identifier of the cluster + where the stack will be relocated + example: jpofkc0i9uo9wtx1zesuk649w + type: string + required: + - endpointID + type: object + stacks.swarmStackFromFileContentPayload: + properties: + env: + description: A list of environment variables used during stack deployment + items: + $ref: '#/definitions/portainer.Pair' + type: array + name: + description: Name of the stack + example: myStack + type: string + stackFileContent: + description: Content of the Stack file + example: |- + version: 3 + services: + web: + image:nginx + type: string + swarmID: + description: Swarm cluster identifier + example: jpofkc0i9uo9wtx1zesuk649w + type: string + required: + - name + - stackFileContent + - swarmID + type: object + stacks.swarmStackFromGitRepositoryPayload: + properties: + composeFilePathInRepository: + default: docker-compose.yml + description: Path to the Stack file inside the Git repository + example: docker-compose.yml + type: string + env: + description: A list of environment variables used during stack deployment + items: + $ref: '#/definitions/portainer.Pair' + type: array + name: + description: Name of the stack + example: myStack + type: string + repositoryAuthentication: + description: Use basic authentication to clone the Git repository + example: true + type: boolean + repositoryPassword: + description: Password used in basic authentication. Required when RepositoryAuthentication + is true. + example: myGitPassword + type: string + repositoryReferenceName: + description: Reference name of a Git repository hosting the Stack file + example: refs/heads/master + type: string + repositoryURL: + description: URL of a Git repository hosting the Stack file + example: https://github.com/openfaas/faas + type: string + repositoryUsername: + description: Username used in basic authentication. Required when RepositoryAuthentication + is true. + example: myGitUsername + type: string + swarmID: + description: Swarm cluster identifier + example: jpofkc0i9uo9wtx1zesuk649w + type: string + required: + - name + - repositoryURL + - swarmID + type: object + stacks.updateSwarmStackPayload: + properties: + env: + description: A list of environment variables used during stack deployment + items: + $ref: '#/definitions/portainer.Pair' + type: array + prune: + description: Prune services that are no longer referenced (only available + for Swarm stacks) + example: true + type: boolean + stackFileContent: + description: New content of the Stack file + example: |- + version: 3 + services: + web: + image:nginx + type: string + type: object + status.inspectVersionResponse: + properties: + LatestVersion: + description: The latest version available + example: 2.0.0 + type: string + UpdateAvailable: + description: Whether portainer has an update available + example: false + type: boolean + type: object + tags.tagCreatePayload: + properties: + name: + description: Name + example: org/acme + type: string + required: + - name + type: object + teammemberships.teamMembershipCreatePayload: + properties: + role: + description: Role for the user inside the team (1 for leader and 2 for regular + member) + enum: + - 1 + - 2 + example: 1 + type: integer + teamID: + description: Team identifier + example: 1 + type: integer + userID: + description: User identifier + example: 1 + type: integer + required: + - role + - teamID + - userID + type: object + teammemberships.teamMembershipUpdatePayload: + properties: + role: + description: Role for the user inside the team (1 for leader and 2 for regular + member) + enum: + - 1 + - 2 + example: 1 + type: integer + teamID: + description: Team identifier + example: 1 + type: integer + userID: + description: User identifier + example: 1 + type: integer + required: + - role + - teamID + - userID + type: object + teams.teamCreatePayload: + properties: + name: + description: Name + example: developers + type: string + required: + - name + type: object + teams.teamUpdatePayload: + properties: + name: + description: Name + example: developers + type: string + type: object + templates.filePayload: + properties: + composeFilePathInRepository: + description: Path to the file inside the git repository + example: ./subfolder/docker-compose.yml + type: string + repositoryURL: + description: URL of a git repository where the file is stored + example: https://github.com/portainer/portainer-compose + type: string + required: + - composeFilePathInRepository + - repositoryURL + type: object + templates.fileResponse: + properties: + fileContent: + description: The requested file content + type: string + type: object + templates.listResponse: + properties: + templates: + items: + $ref: '#/definitions/portainer.Template' + type: array + version: + type: string + type: object + users.adminInitPayload: + properties: + password: + description: Password for the admin user + example: admin-password + type: string + username: + description: Username for the admin user + example: admin + type: string + required: + - password + - username + type: object + users.userCreatePayload: + properties: + password: + example: cg9Wgky3 + type: string + role: + description: User role (1 for administrator account and 2 for regular account) + enum: + - 1 + - 2 + example: 2 + type: integer + username: + example: bob + type: string + required: + - password + - role + - username + type: object + users.userUpdatePasswordPayload: + properties: + newPassword: + description: New Password + example: new_passwd + type: string + password: + description: Current Password + example: passwd + type: string + required: + - newPassword + - password + type: object + users.userUpdatePayload: + properties: + password: + example: cg9Wgky3 + type: string + role: + description: User role (1 for administrator account and 2 for regular account) + enum: + - 1 + - 2 + example: 2 + type: integer + username: + example: bob + type: string + required: + - password + - role + - username + type: object + webhooks.webhookCreatePayload: + properties: + endpointID: + type: integer + resourceID: + type: string + webhookType: + type: integer + type: object info: + contact: + email: info@portainer.io description: | Portainer API is an HTTP API served by Portainer. It is used by the Portainer UI and everything you can do with the UI can be done using the HTTP API. Examples are available at https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8 @@ -13,6 +2496,7 @@ info: with the **Bearer** authentication mechanism. Example: + ``` Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsImV4cCI6MTQ5OTM3NjE1NH0.NJ6vE8FY1WG6jsRQzfMqeatJ4vh2TWAeeYfDhP71YEE ``` @@ -22,10 +2506,11 @@ info: Each API endpoint has an associated access policy, it is documented in the description of each endpoint. Different access policies are available: - * Public access - * Authenticated access - * Restricted access - * Administrator access + + - Public access + - Authenticated access + - Restricted access + - Administrator access ### Public access @@ -53,4532 +2538,3551 @@ info: To do so, you can use the `/endpoints/{id}/docker` Portainer API endpoint (which is not documented below due to Swagger limitations). This endpoint has a restricted access policy so you still need to be authenticated to be able to query this endpoint. Any query on this endpoint will be proxied to the Docker API of the associated endpoint (requests and responses objects are the same as documented in the Docker API). **NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8). - - version: '2.0.0' - title: 'Portainer API' - contact: - email: 'info@portainer.io' -host: 'portainer.domain' -basePath: '/api' -tags: - - name: 'auth' - description: 'Authenticate against Portainer HTTP API' - - name: 'dockerhub' - description: 'Manage how Portainer connects to the DockerHub' - - name: 'endpoints' - description: 'Manage Docker environments' - - name: 'endpoint_groups' - description: 'Manage endpoint groups' - - name: 'extensions' - description: 'Manage extensions' - - name: 'registries' - description: 'Manage Docker registries' - - name: 'resource_controls' - description: 'Manage access control on Docker resources' - - name: 'roles' - description: 'Manage roles' - - name: 'settings' - description: 'Manage Portainer settings' - - name: 'status' - description: 'Information about the Portainer instance' - - name: 'stacks' - description: 'Manage Docker stacks' - - name: 'users' - description: 'Manage users' - - name: 'tags' - description: 'Manage tags' - - name: 'teams' - description: 'Manage teams' - - name: 'team_memberships' - description: 'Manage team memberships' - - name: 'templates' - description: 'Manage App Templates' - - name: 'stacks' - description: 'Manage stacks' - - name: 'upload' - description: 'Upload files' - - name: 'websocket' - description: 'Create exec sessions using websockets' -schemes: - - 'http' - - 'https' + license: {} + title: PortainerCE API + version: 2.0.0 paths: /auth: post: - tags: - - 'auth' - summary: 'Authenticate a user' - description: | - Use this endpoint to authenticate against Portainer using a username and password. - **Access policy**: public - operationId: 'AuthenticateUser' consumes: - - 'application/json' - produces: - - 'application/json' + - application/json + description: Use this endpoint to authenticate against Portainer using a username + and password. + operationId: AuthenticateUser parameters: - - in: 'body' - name: 'body' - description: 'Credentials used for authentication' - required: true - schema: - $ref: '#/definitions/AuthenticateUserRequest' + - description: Credentials used for authentication + in: body + name: body + required: true + schema: + $ref: '#/definitions/auth.authenticatePayload' + produces: + - application/json responses: - 200: - description: 'Success' + "200": + description: Success schema: - $ref: '#/definitions/AuthenticateUserResponse' - 400: - description: 'Invalid request' + $ref: '#/definitions/auth.authenticateResponse' + "400": + description: Invalid request + "422": + description: Invalid Credentials + "500": + description: Server error + summary: Authenticate + tags: + - auth + /auth/logout: + post: + consumes: + - application/json + operationId: logout + produces: + - application/json + responses: + "204": + description: "" + security: + - jwt: [] + summary: Logout + tags: + - auth + /auth/oauth/validate: + post: + consumes: + - application/json + operationId: authenticate_oauth + parameters: + - description: OAuth Credentials used for authentication + in: body + name: body + required: true + schema: + $ref: '#/definitions/auth.oauthPayload' + produces: + - application/json + responses: + "200": + description: Success schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid credentials' - 500: - description: 'Server error' + $ref: '#/definitions/auth.authenticateResponse' + "400": + description: Invalid request + "422": + description: Invalid Credentials + "500": + description: Server error + summary: Authenticate with OAuth + tags: + - auth + /custom_templates: + get: + description: |- + List available custom templates. + **Access policy**: authenticated + operationId: CustomTemplateList + produces: + - application/json + responses: + "200": + description: Success schema: - $ref: '#/definitions/GenericError' - 503: - description: 'Authentication disabled' + items: + $ref: '#/definitions/portainer.CustomTemplate' + type: array + "500": + description: Server error + security: + - jwt: [] + summary: List available custom templates + tags: + - custom_templates + post: + consumes: + - application/json + - ' multipart/form-data' + description: |- + Create a custom template. + **Access policy**: authenticated + operationId: CustomTemplateCreate + parameters: + - description: method for creating template + enum: + - string + - file + - repository + in: query + name: method + required: true + type: string + - description: Required when using method=string + in: body + name: body_string + schema: + $ref: '#/definitions/customtemplates.customTemplateFromFileContentPayload' + - description: Required when using method=repository + in: body + name: body_repository + schema: + $ref: '#/definitions/customtemplates.customTemplateFromGitRepositoryPayload' + - description: Title of the template. required when method is file + in: formData + name: Title + type: string + - description: Description of the template. required when method is file + in: formData + name: Description + type: string + - description: A note that will be displayed in the UI. Supports HTML content + in: formData + name: Note + type: string + - description: Platform associated to the template (1 - 'linux', 2 - 'windows'). + required when method is file + enum: + - 1 + - 2 + in: formData + name: Platform + type: integer + - description: Type of created stack (1 - swarm, 2 - compose), required when + method is file + enum: + - 1 + - 2 + in: formData + name: Type + type: integer + - description: required when method is file + in: formData + name: file + type: file + produces: + - application/json + responses: + "200": + description: OK schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Authentication is disabled' + $ref: '#/definitions/portainer.CustomTemplate' + "400": + description: Invalid request + "500": + description: Server error + security: + - jwt: [] + summary: Create a custom template + tags: + - custom_templates + /custom_templates/{id}: + delete: + description: |- + Remove a template. + **Access policy**: authorized + operationId: CustomTemplateDelete + parameters: + - description: Template identifier + in: path + name: id + required: true + type: integer + responses: + "204": + description: Success + "400": + description: Invalid request + "403": + description: Access denied to resource + "404": + description: Template not found + "500": + description: Server error + security: + - jwt: [] + summary: Remove a template + tags: + - custom_templates + get: + consumes: + - application/json + description: |- + Retrieve details about a template. + **Access policy**: authenticated + operationId: CustomTemplateInspect + parameters: + - description: Template identifier + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/portainer.CustomTemplate' + "400": + description: Invalid request + "404": + description: Template not found + "500": + description: Server error + security: + - jwt: [] + summary: Inspect a custom template + tags: + - custom_templates + put: + consumes: + - application/json + description: |- + Update a template. + **Access policy**: authenticated + operationId: CustomTemplateUpdate + parameters: + - description: Template identifier + in: path + name: id + required: true + type: integer + - description: Template details + in: body + name: body + required: true + schema: + $ref: '#/definitions/customtemplates.customTemplateUpdatePayload' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/portainer.CustomTemplate' + "400": + description: Invalid request + "403": + description: Permission denied to access template + "404": + description: Template not found + "500": + description: Server error + security: + - jwt: [] + summary: Update a template + tags: + - custom_templates + /custom_templates/{id}/file: + get: + description: |- + Retrieve the content of the Stack file for the specified custom template + **Access policy**: authorized + operationId: CustomTemplateFile + parameters: + - description: Template identifier + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/customtemplates.fileResponse' + "400": + description: Invalid request + "404": + description: Custom template not found + "500": + description: Server error + security: + - jwt: [] + summary: Get Template stack file content. + tags: + - custom_templates /dockerhub: get: - tags: - - 'dockerhub' - summary: 'Retrieve DockerHub information' - description: | + description: |- Use this endpoint to retrieve the information used to connect to the DockerHub **Access policy**: authenticated - operationId: 'DockerHubInspect' + operationId: DockerHubInspect produces: - - 'application/json' - security: - - jwt: [] - parameters: [] + - application/json responses: - 200: - description: 'Success' + "200": + description: OK schema: - $ref: '#/definitions/DockerHubSubset' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - put: + $ref: '#/definitions/portainer.DockerHub' + "500": + description: Server error + security: + - jwt: [] + summary: Retrieve DockerHub information tags: - - 'dockerhub' - summary: 'Update DockerHub information' - description: | + - dockerhub + put: + consumes: + - application/json + description: |- Use this endpoint to update the information used to connect to the DockerHub **Access policy**: administrator - operationId: 'DockerHubUpdate' - consumes: - - 'application/json' - produces: - - 'application/json' - security: - - jwt: [] + operationId: DockerHubUpdate parameters: - - in: 'body' - name: 'body' - description: 'DockerHub information' - required: true - schema: - $ref: '#/definitions/DockerHubUpdateRequest' - responses: - 200: - description: 'Success' - schema: - $ref: '#/definitions/DockerHub' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data format' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - /endpoints: - get: - tags: - - 'endpoints' - summary: 'List endpoints' - description: | - List all endpoints based on the current user authorizations. Will - return all endpoints if using an administrator account otherwise it will - only return authorized endpoints. - **Access policy**: restricted - operationId: 'EndpointList' + - description: DockerHub information + in: body + name: body + required: true + schema: + $ref: '#/definitions/dockerhub.dockerhubUpdatePayload' produces: - - 'application/json' - security: - - jwt: [] - parameters: [] + - application/json responses: - 200: - description: 'Success' + "204": + description: Success + "400": + description: Invalid request + "500": + description: Server error + security: + - jwt: [] + summary: Update DockerHub information + tags: + - dockerhub + /edge_groups: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: EdgeGroups schema: - $ref: '#/definitions/EndpointListResponse' - 500: - description: 'Server error' + items: + allOf: + - $ref: '#/definitions/portainer.EdgeGroup' + - properties: + HasEdgeStack: + type: boolean + type: object + type: array + "500": + description: "" + "503": + description: Service Unavailable schema: - $ref: '#/definitions/GenericError' + type: Edge + security: + - jwt: [] + summary: list EdgeGroups + tags: + - edge_groups post: - tags: - - 'endpoints' - summary: 'Create a new endpoint' - description: | - Create a new endpoint that will be used to manage a Docker environment. - **Access policy**: administrator - operationId: 'EndpointCreate' consumes: - - 'multipart/form-data' - produces: - - 'application/json' - security: - - jwt: [] + - application/json parameters: - - name: 'Name' - in: 'formData' - type: 'string' - description: 'Name that will be used to identify this endpoint (example: my-endpoint)' - required: true - - name: 'EndpointType' - in: 'formData' - type: 'integer' - description: 'Environment type. Value must be one of: 1 (Docker environment), 2 (Agent environment), 3 (Azure environment) or 4 (Edge agent environment)' - required: true - - name: 'URL' - in: 'formData' - type: 'string' - description: "URL or IP address of a Docker host (example: docker.mydomain.tld:2375).\ - \ Defaults to local if not specified (Linux: /var/run/docker.sock, Windows: //./pipe/docker_engine)" - - name: 'PublicURL' - in: 'formData' - type: 'string' - description: "URL or IP address where exposed containers will be reachable.\ - \ Defaults to URL if not specified (example: docker.mydomain.tld:2375)" - - name: 'GroupID' - in: 'formData' - type: 'string' - description: 'Endpoint group identifier. If not specified will default to 1 (unassigned).' - - name: 'TLS' - in: 'formData' - type: 'string' - description: 'Require TLS to connect against this endpoint (example: true)' - - name: 'TLSSkipVerify' - in: 'formData' - type: 'string' - description: 'Skip server verification when using TLS (example: false)' - - name: 'TLSSkipClientVerify' - in: 'formData' - type: 'string' - description: 'Skip client verification when using TLS (example: false)' - - name: 'TLSCACertFile' - in: 'formData' - type: 'file' - description: 'TLS CA certificate file' - - name: 'TLSCertFile' - in: 'formData' - type: 'file' - description: 'TLS client certificate file' - - name: 'TLSKeyFile' - in: 'formData' - type: 'file' - description: 'TLS client key file' - - name: 'AzureApplicationID' - in: 'formData' - type: 'string' - description: 'Azure application ID. Required if endpoint type is set to 3' - - name: 'AzureTenantID' - in: 'formData' - type: 'string' - description: 'Azure tenant ID. Required if endpoint type is set to 3' - - name: 'AzureAuthenticationKey' - in: 'formData' - type: 'string' - description: 'Azure authentication key. Required if endpoint type is set to 3' + - description: EdgeGroup data + in: body + name: body + required: true + schema: + $ref: '#/definitions/edgegroups.edgeGroupCreatePayload' + produces: + - application/json responses: - 200: - description: 'Success' + "200": + description: OK schema: - $ref: '#/definitions/Endpoint' - 400: - description: 'Invalid request' + $ref: '#/definitions/portainer.EdgeGroup' + "500": + description: "" + "503": + description: Service Unavailable schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data format' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - 503: - description: 'Endpoint management disabled' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Endpoint management is disabled' - /endpoints/{id}: - get: + type: Edge + security: + - jwt: [] + summary: Create an EdgeGroup tags: - - 'endpoints' - summary: 'Inspect an endpoint' - description: | - Retrieve details abount an endpoint. - **Access policy**: restricted - operationId: 'EndpointInspect' - produces: - - 'application/json' - security: - - jwt: [] - parameters: - - name: 'id' - in: 'path' - description: 'Endpoint identifier' - required: true - type: 'integer' - responses: - 200: - description: 'Success' - schema: - $ref: '#/definitions/Endpoint' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request' - 404: - description: 'Endpoint not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Endpoint not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - put: - tags: - - 'endpoints' - summary: 'Update an endpoint' - description: | - Update an endpoint. - **Access policy**: administrator - operationId: 'EndpointUpdate' - consumes: - - 'application/json' - produces: - - 'application/json' - security: - - jwt: [] - parameters: - - name: 'id' - in: 'path' - description: 'Endpoint identifier' - required: true - type: 'integer' - - in: 'body' - name: 'body' - description: 'Endpoint details' - required: true - schema: - $ref: '#/definitions/EndpointUpdateRequest' - responses: - 200: - description: 'Success' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data format' - 404: - description: 'Endpoint not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Endpoint not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - 503: - description: 'Endpoint management disabled' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Endpoint management is disabled' + - edge_groups + /edge_groups/{id}: delete: - tags: - - 'endpoints' - summary: 'Remove an endpoint' - description: | - Remove an endpoint. - **Access policy**: administrator - operationId: 'EndpointDelete' - security: - - jwt: [] - parameters: - - name: 'id' - in: 'path' - description: 'Endpoint identifier' - required: true - type: 'integer' - responses: - 204: - description: 'Success' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request' - 404: - description: 'Endpoint not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Endpoint not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - 503: - description: 'Endpoint management disabled' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Endpoint management is disabled' - /endpoints/{id}/job: - post: - tags: - - 'endpoints' - summary: 'Execute a job on the endpoint host' - description: | - Execute a job (script) on the underlying host of the endpoint. - **Access policy**: administrator - operationId: 'EndpointJob' consumes: - - 'multipart/form-data' - produces: - - 'application/json' - security: - - jwt: [] + - application/json parameters: - - name: 'id' - in: 'path' - description: 'Endpoint identifier' - required: true - type: 'integer' - - name: 'method' - in: 'query' - description: 'Job execution method. Possible values: file or string.' - required: true - type: 'string' - - name: 'nodeName' - in: 'query' - description: 'Optional. Hostname of a node when targeting a Portainer agent cluster.' - required: true - type: 'string' - - in: 'body' - name: 'body' - description: 'Job details. Required when method equals string.' - required: true - schema: - $ref: '#/definitions/EndpointJobRequest' - - name: 'Image' - in: 'formData' - type: 'string' - description: 'Container image which will be used to execute the job. Required when method equals file.' - - name: 'file' - in: 'formData' - type: 'file' - description: 'Job script file. Required when method equals file.' + - description: EdgeGroup Id + in: path + name: id + required: true + type: integer + produces: + - application/json responses: - 200: - description: 'Success' + "204": + description: "" + "500": + description: "" + "503": + description: Service Unavailable schema: - $ref: '#/definitions/Endpoint' - 400: - description: 'Invalid request' + type: Edge + security: + - jwt: [] + summary: Deletes an EdgeGroup + tags: + - edge_groups + get: + consumes: + - application/json + parameters: + - description: EdgeGroup Id + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data format' - 403: - description: 'Unauthorized' + $ref: '#/definitions/portainer.EdgeGroup' + "500": + description: "" + "503": + description: Service Unavailable schema: - $ref: '#/definitions/GenericError' - 404: - description: 'Endpoint not found' + type: Edge + security: + - jwt: [] + summary: Inspects an EdgeGroup + tags: + - edge_groups + put: + consumes: + - application/json + parameters: + - description: EdgeGroup Id + in: path + name: id + required: true + type: integer + - description: EdgeGroup data + in: body + name: body + required: true + schema: + $ref: '#/definitions/edgegroups.edgeGroupUpdatePayload' + produces: + - application/json + responses: + "200": + description: OK schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Endpoint not found' - 500: - description: 'Server error' + $ref: '#/definitions/portainer.EdgeGroup' + "500": + description: "" + "503": + description: Service Unavailable schema: - $ref: '#/definitions/GenericError' + type: Edge + security: + - jwt: [] + summary: Updates an EdgeGroup + tags: + - edge_groups + /edge_jobs: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/portainer.EdgeJob' + type: array + "400": + description: "" + "500": + description: "" + "503": + description: Service Unavailable + schema: + type: Edge + security: + - jwt: [] + summary: Fetch EdgeJobs list + tags: + - edge_jobs + post: + consumes: + - application/json + parameters: + - description: Creation Method + enum: + - file + - string + in: query + name: method + required: true + type: string + - description: EdgeGroup data when method is string + in: body + name: body + required: true + schema: + $ref: '#/definitions/edgejobs.edgeJobCreateFromFileContentPayload' + - description: EdgeGroup data when method is file + in: body + name: body + required: true + schema: + $ref: '#/definitions/edgejobs.edgeJobCreateFromFilePayload' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/portainer.EdgeGroup' + "500": + description: "" + "503": + description: Service Unavailable + schema: + type: Edge + security: + - jwt: [] + summary: Create an EdgeJob + tags: + - edge_jobs + /edge_jobs/{id}: + delete: + consumes: + - application/json + parameters: + - description: EdgeJob Id + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "204": + description: "" + "400": + description: "" + "500": + description: "" + "503": + description: Service Unavailable + schema: + type: Edge + security: + - jwt: [] + summary: Delete an EdgeJob + tags: + - edge_jobs + get: + consumes: + - application/json + parameters: + - description: EdgeJob Id + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/portainer.EdgeJob' + "400": + description: "" + "500": + description: "" + "503": + description: Service Unavailable + schema: + type: Edge + security: + - jwt: [] + summary: Inspect an EdgeJob + tags: + - edge_jobs + post: + consumes: + - application/json + parameters: + - description: EdgeJob Id + in: path + name: id + required: true + type: string + - description: EdgeGroup data + in: body + name: body + required: true + schema: + $ref: '#/definitions/edgejobs.edgeJobUpdatePayload' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/portainer.EdgeJob' + "400": + description: "" + "500": + description: "" + "503": + description: Service Unavailable + schema: + type: Edge + security: + - jwt: [] + summary: Update an EdgeJob + tags: + - edge_jobs + /edge_jobs/{id}/file: + get: + consumes: + - application/json + parameters: + - description: EdgeJob Id + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/edgejobs.edgeJobFileResponse' + "400": + description: "" + "500": + description: "" + "503": + description: Service Unavailable + schema: + type: Edge + security: + - jwt: [] + summary: Fetch a file of an EdgeJob + tags: + - edge_jobs + /edge_jobs/{id}/tasks: + get: + consumes: + - application/json + parameters: + - description: EdgeJob Id + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/edgejobs.taskContainer' + type: array + "400": + description: "" + "500": + description: "" + "503": + description: Service Unavailable + schema: + type: Edge + security: + - jwt: [] + summary: Fetch the list of tasks on an EdgeJob + tags: + - edge_jobs + /edge_jobs/{id}/tasks/{taskID}/logs: + delete: + consumes: + - application/json + parameters: + - description: EdgeJob Id + in: path + name: id + required: true + type: string + - description: Task Id + in: path + name: taskID + required: true + type: string + produces: + - application/json + responses: + "204": + description: "" + "400": + description: "" + "500": + description: "" + "503": + description: Service Unavailable + schema: + type: Edge + security: + - jwt: [] + summary: Clear the log for a specifc task on an EdgeJob + tags: + - edge_jobs + get: + consumes: + - application/json + parameters: + - description: EdgeJob Id + in: path + name: id + required: true + type: string + - description: Task Id + in: path + name: taskID + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/edgejobs.fileResponse' + "400": + description: "" + "500": + description: "" + "503": + description: Service Unavailable + schema: + type: Edge + security: + - jwt: [] + summary: Fetch the log for a specifc task on an EdgeJob + tags: + - edge_jobs + post: + consumes: + - application/json + parameters: + - description: EdgeJob Id + in: path + name: id + required: true + type: string + - description: Task Id + in: path + name: taskID + required: true + type: string + produces: + - application/json + responses: + "204": + description: "" + "400": + description: "" + "500": + description: "" + "503": + description: Service Unavailable + schema: + type: Edge + security: + - jwt: [] + summary: Collect the log for a specifc task on an EdgeJob + tags: + - edge_jobs + /edge_stacks: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/portainer.EdgeStack' + type: array + "400": + description: "" + "500": + description: "" + "503": + description: Service Unavailable + schema: + type: Edge + security: + - jwt: [] + summary: Fetches the list of EdgeStacks + tags: + - edge_stacks + post: + consumes: + - application/json + parameters: + - description: Creation Method + enum: + - file + - string + - repository + in: query + name: method + required: true + type: string + - description: Required when using method=string + in: body + name: body_string + required: true + schema: + $ref: '#/definitions/edgestacks.swarmStackFromFileContentPayload' + - description: Required when using method=file + in: body + name: body_file + required: true + schema: + $ref: '#/definitions/edgestacks.swarmStackFromFileUploadPayload' + - description: Required when using method=repository + in: body + name: body_repository + required: true + schema: + $ref: '#/definitions/edgestacks.swarmStackFromGitRepositoryPayload' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/portainer.EdgeStack' + "500": + description: "" + "503": + description: Service Unavailable + schema: + type: Edge + security: + - jwt: [] + summary: Create an EdgeStack + tags: + - edge_stacks + /edge_stacks/{id}: + delete: + consumes: + - application/json + parameters: + - description: EdgeStack Id + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "204": + description: "" + "400": + description: "" + "500": + description: "" + "503": + description: Service Unavailable + schema: + type: Edge + security: + - jwt: [] + summary: Delete an EdgeStack + tags: + - edge_stacks + get: + consumes: + - application/json + parameters: + - description: EdgeStack Id + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/portainer.EdgeStack' + "400": + description: "" + "500": + description: "" + "503": + description: Service Unavailable + schema: + type: Edge + security: + - jwt: [] + summary: Inspect an EdgeStack + tags: + - edge_stacks + put: + consumes: + - application/json + parameters: + - description: EdgeStack Id + in: path + name: id + required: true + type: string + - description: EdgeStack data + in: body + name: body + required: true + schema: + $ref: '#/definitions/edgestacks.updateEdgeStackPayload' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/portainer.EdgeStack' + "400": + description: "" + "500": + description: "" + "503": + description: Service Unavailable + schema: + type: Edge + security: + - jwt: [] + summary: Update an EdgeStack + tags: + - edge_stacks + /edge_stacks/{id}/file: + get: + consumes: + - application/json + parameters: + - description: EdgeStack Id + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/edgestacks.stackFileResponse' + "400": + description: "" + "500": + description: "" + "503": + description: Service Unavailable + schema: + type: Edge + security: + - jwt: [] + summary: Fetches the stack file for an EdgeStack + tags: + - edge_stacks + /edge_stacks/{id}/status: + put: + consumes: + - application/json + description: Authorized only if the request is done by an Edge Endpoint + parameters: + - description: EdgeStack Id + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/portainer.EdgeStack' + "400": + description: "" + "403": + description: "" + "404": + description: "" + "500": + description: "" + summary: Update an EdgeStack status + tags: + - edge_stacks + /edge_templates: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/portainer.Template' + type: array + "500": + description: "" + security: + - jwt: [] + summary: Fetches the list of Edge Templates + tags: + - edge_templates /endpoint_groups: get: - tags: - - 'endpoint_groups' - summary: 'List endpoint groups' - description: | + description: |- List all endpoint groups based on the current user authorizations. Will return all endpoint groups if using an administrator account otherwise it will only return authorized endpoint groups. **Access policy**: restricted - operationId: 'EndpointGroupList' + operationId: EndpointGroupList produces: - - 'application/json' - security: - - jwt: [] - parameters: [] + - application/json responses: - 200: - description: 'Success' + "200": + description: Endpoint group schema: - $ref: '#/definitions/EndpointGroupListResponse' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - post: + items: + $ref: '#/definitions/portainer.EndpointGroup' + type: array + "500": + description: Server error + security: + - jwt: [] + summary: List Endpoint groups tags: - - 'endpoint_groups' - summary: 'Create a new endpoint' - description: | + - endpoint_groups + post: + consumes: + - application/json + description: |- Create a new endpoint group. **Access policy**: administrator - operationId: 'EndpointGroupCreate' - consumes: - - 'application/json' - produces: - - 'application/json' - security: - - jwt: [] parameters: - - in: 'body' - name: 'body' - description: 'Registry details' - required: true - schema: - $ref: '#/definitions/EndpointGroupCreateRequest' + - description: Endpoint Group details + in: body + name: body + required: true + schema: + $ref: '#/definitions/endpointgroups.endpointGroupCreatePayload' + produces: + - application/json responses: - 200: - description: 'Success' + "200": + description: Success schema: - $ref: '#/definitions/EndpointGroup' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data format' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - /endpoint_groups/{id}: + $ref: '#/definitions/portainer.EndpointGroup' + "400": + description: Invalid request + "500": + description: Server error + security: + - jwt: [] + summary: Create an Endpoint Group + tags: + - endpoint_groups + /endpoint_groups/:id: get: - tags: - - 'endpoint_groups' - summary: 'Inspect an endpoint group' - description: | - Retrieve details abount an endpoint group. + consumes: + - application/json + description: |- + Retrieve details abont an endpoint group. **Access policy**: administrator - operationId: 'EndpointGroupInspect' - produces: - - 'application/json' - security: - - jwt: [] parameters: - - name: 'id' - in: 'path' - description: 'Endpoint group identifier' - required: true - type: 'integer' + - description: Endpoint group identifier + in: path + name: id + required: true + type: integer + produces: + - application/json responses: - 200: - description: 'Success' + "200": + description: Success schema: - $ref: '#/definitions/EndpointGroup' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request' - 404: - description: 'EndpointGroup not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'EndpointGroup not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - put: + $ref: '#/definitions/portainer.EndpointGroup' + "400": + description: Invalid request + "404": + description: EndpointGroup not found + "500": + description: Server error + security: + - jwt: [] + summary: Inspect an Endpoint group tags: - - 'endpoint_groups' - summary: 'Update an endpoint group' - description: | + - endpoint_groups + put: + consumes: + - application/json + description: |- Update an endpoint group. **Access policy**: administrator - operationId: 'EndpointGroupUpdate' - consumes: - - 'application/json' - produces: - - 'application/json' - security: - - jwt: [] + operationId: EndpointGroupUpdate parameters: - - name: 'id' - in: 'path' - description: 'EndpointGroup identifier' - required: true - type: 'integer' - - in: 'body' - name: 'body' - description: 'EndpointGroup details' - required: true - schema: - $ref: '#/definitions/EndpointGroupUpdateRequest' + - description: EndpointGroup identifier + in: path + name: id + required: true + type: integer + - description: EndpointGroup details + in: body + name: body + required: true + schema: + $ref: '#/definitions/endpointgroups.endpointGroupUpdatePayload' + produces: + - application/json responses: - 200: - description: 'Success' + "200": + description: Success schema: - $ref: '#/definitions/EndpointGroup' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data format' - 404: - description: 'EndpointGroup not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'EndpointGroup not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - 503: - description: 'EndpointGroup management disabled' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'EndpointGroup management is disabled' - delete: + $ref: '#/definitions/portainer.EndpointGroup' + "400": + description: Invalid request + "404": + description: EndpointGroup not found + "500": + description: Server error + security: + - jwt: [] + summary: Update an endpoint group tags: - - 'endpoint_groups' - summary: 'Remove an endpoint group' - description: | + - endpoint_groups + /endpoint_groups/{id}: + delete: + consumes: + - application/json + description: |- Remove an endpoint group. **Access policy**: administrator - operationId: 'EndpointGroupDelete' - security: - - jwt: [] + operationId: EndpointGroupDelete parameters: - - name: 'id' - in: 'path' - description: 'EndpointGroup identifier' - required: true - type: 'integer' + - description: EndpointGroup identifier + in: path + name: id + required: true + type: integer + produces: + - application/json responses: - 204: - description: 'Success' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request' - 404: - description: 'EndpointGroup not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'EndpointGroup not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - 503: - description: 'EndpointGroup management disabled' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'EndpointGroup management is disabled' - /endpoint_groups/{id}/endpoints/{endpointId}: - put: + "204": + description: Success + "400": + description: Invalid request + "404": + description: EndpointGroup not found + "500": + description: Server error + security: + - jwt: [] + summary: Remove an endpoint group tags: - - 'endpoint_groups' - summary: 'Add an endpoint to an endpoint group' - description: | + - endpoint_groups + /endpoint_groups/{id}/endpoints/{endpointId}: + delete: + description: '**Access policy**: administrator' + operationId: EndpointGroupDeleteEndpoint + parameters: + - description: EndpointGroup identifier + in: path + name: id + required: true + type: integer + - description: Endpoint identifier + in: path + name: endpointId + required: true + type: integer + responses: + "204": + description: Success + "400": + description: Invalid request + "404": + description: EndpointGroup not found + "500": + description: Server error + security: + - jwt: [] + summary: Removes endpoint from an endpoint group + tags: + - endpoint_groups + put: + description: |- Add an endpoint to an endpoint group **Access policy**: administrator - operationId: 'EndpointGroupAddEndpoint' - consumes: - - 'application/json' - produces: - - 'application/json' - security: - - jwt: [] + operationId: EndpointGroupAddEndpoint parameters: - - name: 'id' - in: 'path' - description: 'EndpointGroup identifier' - required: true - type: 'integer' - - name: 'endpointId' - in: 'path' - description: 'Endpoint identifier' - required: true - type: 'integer' + - description: EndpointGroup identifier + in: path + name: id + required: true + type: integer + - description: Endpoint identifier + in: path + name: endpointId + required: true + type: integer responses: - 204: - description: 'Success' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data format' - 404: - description: 'EndpointGroup not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'EndpointGroup not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - delete: + "204": + description: Success + "400": + description: Invalid request + "404": + description: EndpointGroup not found + "500": + description: Server error + security: + - jwt: [] + summary: Add an endpoint to an endpoint group tags: - - 'endpoint_groups' - summary: 'Remove an endpoint group' - description: | - Remove an endpoint group. - **Access policy**: administrator - operationId: 'EndpointGroupDeleteEndpoint' - security: - - jwt: [] + - endpoint_groups + /endpoints: + get: + description: |- + List all endpoints based on the current user authorizations. Will + return all endpoints if using an administrator account otherwise it will + only return authorized endpoints. + **Access policy**: restricted + operationId: EndpointList parameters: - - name: 'id' - in: 'path' - description: 'EndpointGroup identifier' - required: true - type: 'integer' - - name: 'endpointId' - in: 'path' - description: 'Endpoint identifier' - required: true - type: 'integer' + - description: Start searching from + in: query + name: start + type: integer + - description: Search query + in: query + name: search + type: string + - description: List endpoints of this group + in: query + name: groupId + type: integer + - description: Limit results to this value + in: query + name: limit + type: integer + - description: List endpoints of this type + in: query + name: type + type: integer + - description: search endpoints with these tags (depends on tagsPartialMatch) + in: query + items: + type: integer + name: tagIds + type: array + - description: If true, will return endpoint which has one of tagIds, if false + (or missing) will return only endpoints that has all the tags + in: query + name: tagsPartialMatch + type: boolean + - description: will return only these endpoints + in: query + items: + type: integer + name: endpointIds + type: array + produces: + - application/json responses: - 204: - description: 'Success' - 400: - description: 'Invalid request' + "200": + description: Endpoints schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request' - 404: - description: 'EndpointGroup not found' + items: + $ref: '#/definitions/portainer.Endpoint' + type: array + "500": + description: Internal Server Error schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'EndpointGroup not found' - 500: - description: 'Server error' + type: Server + security: + - jwt: [] + summary: List endpoints + tags: + - endpoints + post: + consumes: + - multipart/form-data + description: |- + Create a new endpoint that will be used to manage an environment. + **Access policy**: administrator + operationId: EndpointCreate + parameters: + - description: 'Name that will be used to identify this endpoint (example: my-endpoint)' + in: formData + name: Name + required: true + type: string + - description: 'Environment type. Value must be one of: 1 (Local Docker environment), + 2 (Agent environment), 3 (Azure environment), 4 (Edge agent environment) + or 5 (Local Kubernetes Environment' + in: formData + name: EndpointType + required: true + type: integer + - description: 'URL or IP address of a Docker host (example: docker.mydomain.tld:2375). + Defaults to local if not specified (Linux: /var/run/docker.sock, Windows: + //./pipe/docker_engine)' + in: formData + name: URL + type: string + - description: 'URL or IP address where exposed containers will be reachable. + Defaults to URL if not specified (example: docker.mydomain.tld:2375)' + in: formData + name: PublicURL + type: string + - description: Endpoint group identifier. If not specified will default to 1 + (unassigned). + in: formData + name: GroupID + type: integer + - description: Require TLS to connect against this endpoint + in: formData + name: TLS + type: boolean + - description: Skip server verification when using TLS + in: formData + name: TLSSkipVerify + type: boolean + - description: Skip client verification when using TLS + in: formData + name: TLSSkipClientVerify + type: boolean + - description: TLS CA certificate file + in: formData + name: TLSCACertFile + type: file + - description: TLS client certificate file + in: formData + name: TLSCertFile + type: file + - description: TLS client key file + in: formData + name: TLSKeyFile + type: file + - description: Azure application ID. Required if endpoint type is set to 3 + in: formData + name: AzureApplicationID + type: string + - description: Azure tenant ID. Required if endpoint type is set to 3 + in: formData + name: AzureTenantID + type: string + - description: Azure authentication key. Required if endpoint type is set to + 3 + in: formData + name: AzureAuthenticationKey + type: string + - description: List of tag identifiers to which this endpoint is associated + in: formData + items: + type: integer + name: TagIDs + type: array + - description: The check in interval for edge agent (in seconds) + in: formData + name: EdgeCheckinInterval + type: integer + produces: + - application/json + responses: + "200": + description: Success schema: - $ref: '#/definitions/GenericError' + $ref: '#/definitions/portainer.Endpoint' + "400": + description: Invalid request + "500": + description: Server error + security: + - jwt: [] + summary: Create a new endpoint + tags: + - endpoints + /endpoints/{id}: + delete: + description: |- + Remove an endpoint. + **Access policy**: administrator + operationId: EndpointDelete + parameters: + - description: Endpoint identifier + in: path + name: id + required: true + type: integer + responses: + "204": + description: Success + "400": + description: Invalid request + "404": + description: Endpoint not found + "500": + description: Server error + security: + - jwt: [] + summary: Remove an endpoint + tags: + - endpoints + get: + description: |- + Retrieve details about an endpoint. + **Access policy**: restricted + operationId: EndpointInspect + parameters: + - description: Endpoint identifier + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/portainer.Endpoint' + "400": + description: Invalid request + "404": + description: Endpoint not found + "500": + description: Server error + security: + - jwt: [] + summary: Inspect an endpoint + tags: + - endpoints + put: + consumes: + - application/json + description: |- + Update an endpoint. + **Access policy**: administrator + operationId: EndpointUpdate + parameters: + - description: Endpoint identifier + in: path + name: id + required: true + type: integer + - description: Endpoint details + in: body + name: body + required: true + schema: + $ref: '#/definitions/endpoints.endpointUpdatePayload' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/portainer.Endpoint' + "400": + description: Invalid request + "404": + description: Endpoint not found + "500": + description: Server error + security: + - jwt: [] + summary: Update an endpoint + tags: + - endpoints + /endpoints/{id}/edge/jobs/{jobID}/logs: + post: + consumes: + - application/json + parameters: + - description: Endpoint Id + in: path + name: id + required: true + type: string + - description: Job Id + in: path + name: jobID + required: true + type: string + produces: + - application/json + responses: + "200": + description: "" + "400": + description: "" + "500": + description: "" + summary: Inspect an EdgeJob Log + tags: + - edge + - endpoints + /endpoints/{id}/edge/stacks/{stackId}: + get: + consumes: + - application/json + parameters: + - description: Endpoint Id + in: path + name: id + required: true + type: string + - description: EdgeStack Id + in: path + name: stackID + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/endpointedge.configResponse' + "400": + description: "" + "404": + description: "" + "500": + description: "" + summary: Inspect an Edge Stack for an Endpoint + tags: + - edge + - endpoints + - edge_stacks + /endpoints/{id}/snapshot: + post: + description: |- + Snapshots an endpoint + **Access policy**: restricted + operationId: EndpointSnapshot + parameters: + - description: Endpoint identifier + in: path + name: id + required: true + type: integer + responses: + "204": + description: Success + "400": + description: Invalid request + "404": + description: Endpoint not found + "500": + description: Server error + security: + - jwt: [] + summary: Snapshots an endpoint + tags: + - endpoints + /endpoints/{id}/status: + get: + description: |- + Endpoint for edge agent to check status of environment + **Access policy**: restricted only to Edge endpoints + operationId: EndpointStatusInspect + parameters: + - description: Endpoint identifier + in: path + name: id + required: true + type: integer + responses: + "200": + description: Success + schema: + $ref: '#/definitions/endpoints.endpointStatusInspectResponse' + "400": + description: Invalid request + "403": + description: Permission denied to access endpoint + "404": + description: Endpoint not found + "500": + description: Server error + security: + - jwt: [] + summary: Get endpoint status + tags: + - endpoints + /endpoints/snapshot: + post: + description: |- + Snapshot all endpoints + **Access policy**: administrator + operationId: EndpointSnapshots + responses: + "204": + description: Success + "500": + description: Server Error + security: + - jwt: [] + summary: Snapshot all endpoints + tags: + - endpoints + /motd: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/motd.motdResponse' + security: + - jwt: [] + summary: fetches the message of the day + tags: + - motd /registries: get: - tags: - - 'registries' - summary: 'List registries' - description: | + description: |- List all registries based on the current user authorizations. Will return all registries if using an administrator account otherwise it will only return authorized registries. **Access policy**: restricted - operationId: 'RegistryList' + operationId: RegistryList produces: - - 'application/json' - security: - - jwt: [] - parameters: [] + - application/json responses: - 200: - description: 'Success' + "200": + description: Success schema: - $ref: '#/definitions/RegistryListResponse' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - post: + items: + $ref: '#/definitions/portainer.Registry' + type: array + "500": + description: Server error + security: + - jwt: [] + summary: List Registries tags: - - 'registries' - summary: 'Create a new registry' - description: | + - registries + post: + consumes: + - application/json + description: |- Create a new registry. **Access policy**: administrator - operationId: 'RegistryCreate' - consumes: - - 'application/json' - produces: - - 'application/json' - security: - - jwt: [] + operationId: RegistryCreate parameters: - - in: 'body' - name: 'body' - description: 'Registry details' - required: true - schema: - $ref: '#/definitions/RegistryCreateRequest' + - description: Registry details + in: body + name: body + required: true + schema: + $ref: '#/definitions/registries.registryCreatePayload' + produces: + - application/json responses: - 200: - description: 'Success' + "200": + description: Success schema: - $ref: '#/definitions/Registry' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data format' - 409: - description: 'Registry already exists' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'A registry is already defined for this URL' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - /registries/{id}: - get: + $ref: '#/definitions/portainer.Registry' + "400": + description: Invalid request + "500": + description: Server error + security: + - jwt: [] + summary: Create a new registry tags: - - 'registries' - summary: 'Inspect a registry' - description: | + - registries + /registries/{id}: + delete: + description: |- + Remove a registry + **Access policy**: administrator + operationId: RegistryDelete + parameters: + - description: Registry identifier + in: path + name: id + required: true + type: integer + responses: + "204": + description: Success + "400": + description: Invalid request + "404": + description: Registry not found + "500": + description: Server error + security: + - jwt: [] + summary: Remove a registry + tags: + - registries + get: + description: |- Retrieve details about a registry. **Access policy**: administrator - operationId: 'RegistryInspect' - produces: - - 'application/json' - security: - - jwt: [] + operationId: RegistryInspect parameters: - - name: 'id' - in: 'path' - description: 'Registry identifier' - required: true - type: 'integer' + - description: Registry identifier + in: path + name: id + required: true + type: integer + produces: + - application/json responses: - 200: - description: 'Success' + "200": + description: Success schema: - $ref: '#/definitions/Registry' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request' - 404: - description: 'Registry not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Registry not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' + $ref: '#/definitions/portainer.Registry' + "400": + description: Invalid request + "403": + description: Permission denied to access registry + "404": + description: Registry not found + "500": + description: Server error + security: + - jwt: [] + summary: Inspect a registry + tags: + - registries put: - tags: - - 'registries' - summary: 'Update a registry' - description: | - Update a registry. - **Access policy**: administrator - operationId: 'RegistryUpdate' consumes: - - 'application/json' - produces: - - 'application/json' - security: - - jwt: [] - parameters: - - name: 'id' - in: 'path' - description: 'Registry identifier' - required: true - type: 'integer' - - in: 'body' - name: 'body' - description: 'Registry details' - required: true - schema: - $ref: '#/definitions/RegistryUpdateRequest' - responses: - 200: - description: 'Success' - schema: - $ref: '#/definitions/Registry' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data format' - 404: - description: 'Registry not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Endpoint not found' - 409: - description: 'Registry already exists' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'A registry is already defined for this URL' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - delete: - tags: - - 'registries' - summary: 'Remove a registry' - description: | - Remove a registry. + - application/json + description: |- + Update a registry **Access policy**: administrator - operationId: 'RegistryDelete' - security: - - jwt: [] + operationId: RegistryUpdate parameters: - - name: 'id' - in: 'path' - description: 'Registry identifier' - required: true - type: 'integer' + - description: Registry identifier + in: path + name: id + required: true + type: integer + - description: Registry details + in: body + name: body + required: true + schema: + $ref: '#/definitions/registries.registryUpdatePayload' + produces: + - application/json responses: - 204: - description: 'Success' - 400: - description: 'Invalid request' + "200": + description: Success schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request' - 404: - description: 'Registry not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Registry not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' + $ref: '#/definitions/portainer.Registry' + "400": + description: Invalid request + "404": + description: Registry not found + "409": + description: Another registry with the same URL already exists + "500": + description: Server error + security: + - jwt: [] + summary: Update a registry + tags: + - registries + /registries/{id}/configure: + post: + consumes: + - application/json + description: |- + Configures a registry. + **Access policy**: admin + operationId: RegistryConfigure + parameters: + - description: Registry identifier + in: path + name: id + required: true + type: integer + - description: Registry configuration + in: body + name: body + required: true + schema: + $ref: '#/definitions/registries.registryConfigurePayload' + produces: + - application/json + responses: + "204": + description: Success + "400": + description: Invalid request + "403": + description: Permission denied + "404": + description: Registry not found + "500": + description: Server error + security: + - jwt: [] + summary: Configures a registry + tags: + - registries /resource_controls: post: - tags: - - 'resource_controls' - summary: 'Create a new resource control' - description: | + consumes: + - application/json + description: |- Create a new resource control to restrict access to a Docker resource. **Access policy**: administrator - operationId: 'ResourceControlCreate' - consumes: - - 'application/json' - produces: - - 'application/json' - security: - - jwt: [] + operationId: ResourceControlCreate parameters: - - in: 'body' - name: 'body' - description: 'Resource control details' - required: true - schema: - $ref: '#/definitions/ResourceControlCreateRequest' + - description: Resource control details + in: body + name: body + required: true + schema: + $ref: '#/definitions/resourcecontrols.resourceControlCreatePayload' + produces: + - application/json responses: - 200: - description: 'Success' + "200": + description: Success schema: - $ref: '#/definitions/ResourceControl' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data format' - 403: - description: 'Unauthorized' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Access denied to resource' - 409: - description: 'Resource control already exists' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'A resource control is already applied on this resource' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' + $ref: '#/definitions/portainer.ResourceControl' + "400": + description: Invalid request + "409": + description: Resource control already exists + "500": + description: Server error + security: + - jwt: [] + summary: Create a new resource control + tags: + - resource_controls /resource_controls/{id}: - put: - tags: - - 'resource_controls' - summary: 'Update a resource control' - description: | - Update a resource control. - **Access policy**: restricted - operationId: 'ResourceControlUpdate' - consumes: - - 'application/json' - produces: - - 'application/json' - security: - - jwt: [] - parameters: - - name: 'id' - in: 'path' - description: 'Resource control identifier' - required: true - type: 'integer' - - in: 'body' - name: 'body' - description: 'Resource control details' - required: true - schema: - $ref: '#/definitions/ResourceControlUpdateRequest' - responses: - 200: - description: 'Success' - schema: - $ref: '#/definitions/ResourceControl' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data format' - 403: - description: 'Unauthorized' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Access denied to resource' - 404: - description: 'Resource control not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Resource control not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' delete: - tags: - - 'resource_controls' - summary: 'Remove a resource control' - description: | + description: |- Remove a resource control. **Access policy**: administrator - operationId: 'ResourceControlDelete' - security: - - jwt: [] parameters: - - name: 'id' - in: 'path' - description: 'Resource control identifier' - required: true - type: 'integer' + - description: Resource control identifier + in: path + name: id + required: true + type: integer responses: - 204: - description: 'Success' - 400: - description: 'Invalid request' + "204": + description: Success + "400": + description: Invalid request + "404": + description: Resource control not found + "500": + description: Server error + security: + - jwt: [] + summary: Remove a resource control + tags: + - resource_controls + put: + consumes: + - application/json + description: |- + Update a resource control + **Access policy**: restricted + operationId: ResourceControlUpdate + parameters: + - description: Resource control identifier + in: path + name: id + required: true + type: integer + - description: Resource control details + in: body + name: body + required: true + schema: + $ref: '#/definitions/resourcecontrols.resourceControlUpdatePayload' + produces: + - application/json + responses: + "200": + description: Success schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request' - 403: - description: 'Unauthorized' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Access denied to resource' - 404: - description: 'Resource control not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Resource control not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' + $ref: '#/definitions/portainer.ResourceControl' + "400": + description: Invalid request + "403": + description: Unauthorized + "404": + description: Resource control not found + "500": + description: Server error + security: + - jwt: [] + summary: Update a resource control + tags: + - resource_controls /roles: get: - tags: - - 'roles' - summary: 'List roles' - description: | - List all roles available for use with the RBAC extension. + description: |- + List all roles available for use **Access policy**: administrator - operationId: 'RoleList' + operationId: RoleList produces: - - 'application/json' - security: - - jwt: [] - parameters: [] + - application/json responses: - 200: - description: 'Success' + "200": + description: Success schema: - $ref: '#/definitions/RoleListResponse' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' + items: + $ref: '#/definitions/portainer.Role' + type: array + "500": + description: Server error + security: + - jwt: [] + summary: List roles + tags: + - roles /settings: get: - tags: - - 'settings' - summary: 'Retrieve Portainer settings' - description: | + description: |- Retrieve Portainer settings. **Access policy**: administrator - operationId: 'SettingsInspect' + operationId: SettingsInspect produces: - - 'application/json' - security: - - jwt: [] - parameters: [] + - application/json responses: - 200: - description: 'Success' + "200": + description: Success schema: - $ref: '#/definitions/Settings' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - put: + $ref: '#/definitions/portainer.Settings' + "500": + description: Server error + security: + - jwt: [] + summary: Retrieve Portainer settings tags: - - 'settings' - summary: 'Update Portainer settings' - description: | + - settings + put: + consumes: + - application/json + description: |- Update Portainer settings. **Access policy**: administrator - operationId: 'SettingsUpdate' - consumes: - - 'application/json' - produces: - - 'application/json' - security: - - jwt: [] + operationId: SettingsUpdate parameters: - - in: 'body' - name: 'body' - description: 'New settings' - required: true - schema: - $ref: '#/definitions/SettingsUpdateRequest' + - description: New settings + in: body + name: body + required: true + schema: + $ref: '#/definitions/settings.settingsUpdatePayload' + produces: + - application/json responses: - 200: - description: 'Success' + "200": + description: Success schema: - $ref: '#/definitions/Settings' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data format' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' + $ref: '#/definitions/portainer.Settings' + "400": + description: Invalid request + "500": + description: Server error + security: + - jwt: [] + summary: Update Portainer settings + tags: + - settings + /settings/ldap/check: + put: + consumes: + - application/json + description: |- + Test LDAP connectivity using LDAP details + **Access policy**: administrator + operationId: SettingsLDAPCheck + parameters: + - description: details + in: body + name: body + required: true + schema: + $ref: '#/definitions/settings.settingsLDAPCheckPayload' + responses: + "204": + description: Success + "400": + description: Invalid request + "500": + description: Server error + security: + - jwt: [] + summary: Test LDAP connectivity + tags: + - settings /settings/public: get: - tags: - - 'settings' - summary: 'Retrieve Portainer public settings' - description: | + description: |- Retrieve public settings. Returns a small set of settings that are not reserved to administrators only. **Access policy**: public - operationId: 'PublicSettingsInspect' produces: - - 'application/json' - security: - - jwt: [] - parameters: [] + - application/json responses: - 200: - description: 'Success' + "200": + description: Success schema: - $ref: '#/definitions/PublicSettingsInspectResponse' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - /settings/authentication/checkLDAP: - put: + $ref: '#/definitions/settings.publicSettingsResponse' + "500": + description: Server error + summary: Retrieve Portainer public settings tags: - - 'settings' - summary: 'Test LDAP connectivity' - description: | - Test LDAP connectivity using LDAP details. - **Access policy**: administrator - operationId: 'SettingsLDAPCheck' - consumes: - - 'application/json' - produces: - - 'application/json' - security: - - jwt: [] - parameters: - - in: 'body' - name: 'body' - description: 'LDAP settings' - required: true - schema: - $ref: '#/definitions/SettingsLDAPCheckRequest' - responses: - 204: - description: 'Success' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data format' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - /status: - get: - tags: - - 'status' - summary: 'Check Portainer status' - description: | - Retrieve Portainer status. - **Access policy**: public - operationId: 'StatusInspect' - produces: - - 'application/json' - security: - - jwt: [] - parameters: [] - responses: - 200: - description: 'Success' - schema: - $ref: '#/definitions/Status' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' + - settings /stacks: get: - tags: - - 'stacks' - summary: 'List stacks' - description: | + description: |- List all stacks based on the current user authorizations. Will return all stacks if using an administrator account otherwise it will only return the list of stacks the user have access to. **Access policy**: restricted - operationId: 'StackList' - produces: - - 'application/json' - security: - - jwt: [] + operationId: StackList parameters: - - name: 'filters' - in: 'query' - description: | - Filters to process on the stack list. Encoded as JSON (a map[string]string). - For example, {"SwarmID": "jpofkc0i9uo9wtx1zesuk649w"} will only return stacks that are part - of the specified Swarm cluster. Available filters: EndpointID, SwarmID. - type: 'string' + - description: Filters to process on the stack list. Encoded as JSON (a map[string]string). + For example, { + in: query + name: filters + type: string responses: - 200: - description: 'Success' + "200": + description: Success schema: - $ref: '#/definitions/StackListResponse' - examples: - application/json: - err: 'Access denied to resource' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - post: + items: + $ref: '#/definitions/portainer.Stack' + type: array + "204": + description: Success + "400": + description: Invalid request + "500": + description: Server error + security: + - jwt: [] + summary: List stacks tags: - - 'stacks' - summary: 'Deploy a new stack' - description: | + - stacks + post: + consumes: + - application/json + - ' multipart/form-data' + description: |- Deploy a new stack into a Docker environment specified via the endpoint identifier. **Access policy**: restricted - operationId: 'StackCreate' - consumes: - - 'multipart/form-data' - produces: - - 'application/json' - security: - - jwt: [] + operationId: StackCreate parameters: - - name: 'type' - in: 'query' - description: 'Stack deployment type. Possible values: 1 (Swarm stack) or 2 (Compose stack).' - required: true - type: 'integer' - - name: 'method' - in: 'query' - description: 'Stack deployment method. Possible values: file, string or repository.' - required: true - type: 'string' - - name: 'endpointId' - in: 'query' - description: 'Identifier of the endpoint that will be used to deploy the stack.' - required: true - type: 'integer' - - in: 'body' - name: 'body' - description: 'Stack details. Required when method equals string or repository.' - schema: - $ref: '#/definitions/StackCreateRequest' - - name: 'Name' - in: 'formData' - type: 'string' - description: 'Name of the stack. Required when method equals file.' - - name: 'EndpointID' - in: 'formData' - type: 'string' - description: 'Endpoint identifier used to deploy the stack. Required when method equals file.' - - name: 'SwarmID' - in: 'formData' - type: 'string' - description: 'Swarm cluster identifier. Required when method equals file and type equals 1.' - - name: 'file' - in: 'formData' - type: 'file' - description: 'Stack file. Required when method equals file.' - - name: 'Env' - in: 'formData' - type: 'string' - description: "Environment variables passed during deployment, represented as a JSON array [{'name': 'name', 'value': 'value'}]. Optional, used when method equals file and type equals 1." + - description: 'Stack deployment type. Possible values: 1 (Swarm stack) or 2 + (Compose stack).' + enum: + - 1 + - 2 + in: query + name: type + required: true + type: integer + - description: 'Stack deployment method. Possible values: file, string or repository.' + enum: + - string + - file + - repository + in: query + name: method + required: true + type: string + - description: Identifier of the endpoint that will be used to deploy the stack + in: query + name: endpointId + required: true + type: integer + - description: Required when using method=string and type=1 + in: body + name: body_swarm_string + schema: + $ref: '#/definitions/stacks.swarmStackFromFileContentPayload' + - description: Required when using method=repository and type=1 + in: body + name: body_swarm_repository + schema: + $ref: '#/definitions/stacks.swarmStackFromGitRepositoryPayload' + - description: Required when using method=string and type=2 + in: body + name: body_compose_string + schema: + $ref: '#/definitions/stacks.composeStackFromFileContentPayload' + - description: Required when using method=repository and type=2 + in: body + name: body_compose_repository + schema: + $ref: '#/definitions/stacks.composeStackFromGitRepositoryPayload' + - description: Name of the stack. required when method is file + in: formData + name: Name + type: string + - description: Swarm cluster identifier. Required when method equals file and + type equals 1. required when method is file + in: formData + name: SwarmID + type: string + - description: 'Environment variables passed during deployment, represented + as a JSON array [{''name'': ''name'', ''value'': ''value''}]. Optional, + used when method equals file and type equals 1.' + in: formData + name: Env + type: string + - description: Stack file. required when method is file + in: formData + name: file + type: file + produces: + - application/json responses: - 200: - description: 'Success' + "200": + description: OK schema: - $ref: '#/definitions/Stack' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data format' - 403: - description: 'Unauthorized' - schema: - $ref: '#/definitions/GenericError' - 404: - description: 'Endpoint not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Endpoint not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' + $ref: '#/definitions/portainer.CustomTemplate' + "400": + description: Invalid request + "500": + description: Server error + security: + - jwt: [] + summary: Deploy a new stack + tags: + - stacks /stacks/{id}: - get: - tags: - - 'stacks' - summary: 'Inspect a stack' - description: | - Retrieve details about a stack. - **Access policy**: restricted - operationId: 'StackInspect' - produces: - - 'application/json' - security: - - jwt: [] - parameters: - - name: 'id' - in: 'path' - description: 'Stack identifier' - required: true - type: 'integer' - responses: - 200: - description: 'Success' - schema: - $ref: '#/definitions/Stack' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request' - 403: - description: 'Unauthorized' - schema: - $ref: '#/definitions/GenericError' - 404: - description: 'Stack not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Stack not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - put: - tags: - - 'stacks' - summary: 'Update a stack' - description: | - Update a stack. - **Access policy**: restricted - operationId: 'StackUpdate' - consumes: - - 'application/json' - produces: - - 'application/json' - security: - - jwt: [] - parameters: - - name: 'id' - in: 'path' - description: 'Stack identifier' - required: true - type: 'integer' - - name: 'endpointId' - in: 'query' - description: "Stacks created before version 1.18.0 might not have an associated endpoint identifier. Use this \ - optional parameter to set the endpoint identifier used by the stack." - type: 'integer' - - in: 'body' - name: 'body' - description: 'Stack details' - required: true - schema: - $ref: '#/definitions/StackUpdateRequest' - responses: - 200: - description: 'Success' - schema: - $ref: '#/definitions/Stack' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data format' - 403: - description: 'Unauthorized' - schema: - $ref: '#/definitions/GenericError' - 404: - description: 'Stack not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Stack not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' delete: - tags: - - 'stacks' - summary: 'Remove a stack' - description: | + description: |- Remove a stack. **Access policy**: restricted - operationId: 'StackDelete' - security: - - jwt: [] + operationId: StackDelete parameters: - - name: 'id' - in: 'path' - description: 'Stack identifier' - required: true - type: 'integer' - - name: 'external' - in: 'query' - description: 'Set to true to delete an external stack. Only external Swarm stacks are supported.' - type: 'boolean' - - name: 'endpointId' - in: 'query' - description: 'Endpoint identifier used to remove an external stack (required when external is set to true)' - type: 'string' + - description: Stack identifier + in: path + name: id + required: true + type: integer + - description: Set to true to delete an external stack. Only external Swarm + stacks are supported + in: query + name: external + type: boolean + - description: Endpoint identifier used to remove an external stack (required + when external is set to true) + in: query + name: endpointId + type: integer responses: - 204: - description: 'Success' - 400: - description: 'Invalid request' + "204": + description: Success + "400": + description: Invalid request + "403": + description: Permission denied + "404": + description: ' not found' + "500": + description: Server error + security: + - jwt: [] + summary: Remove a stack + tags: + - stacks + get: + description: |- + Retrieve details about a stack. + **Access policy**: restricted + operationId: StackInspect + parameters: + - description: Stack identifier + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Success schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request' - 403: - description: 'Unauthorized' + $ref: '#/definitions/portainer.Stack' + "400": + description: Invalid request + "403": + description: Permission denied + "404": + description: Stack not found + "500": + description: Server error + security: + - jwt: [] + summary: Inspect a stack + tags: + - stacks + put: + consumes: + - application/json + description: |- + Update a stack. + **Access policy**: restricted + operationId: StackUpdate + parameters: + - description: Stack identifier + in: path + name: id + required: true + type: integer + - description: Stacks created before version 1.18.0 might not have an associated + endpoint identifier. Use this optional parameter to set the endpoint identifier + used by the stack. + in: query + name: endpointId + type: integer + - description: Stack details + in: body + name: body + required: true + schema: + $ref: '#/definitions/stacks.updateSwarmStackPayload' + produces: + - application/json + responses: + "200": + description: Success schema: - $ref: '#/definitions/GenericError' - 404: - description: 'Stack not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Stack not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' + $ref: '#/definitions/portainer.Stack' + "400": + description: Invalid request + "403": + description: Permission denied + "404": + description: ' not found' + "500": + description: Server error + security: + - jwt: [] + summary: Update a stack + tags: + - stacks /stacks/{id}/file: get: - tags: - - 'stacks' - summary: 'Retrieve the content of the Stack file for the specified stack' - description: | + description: |- Get Stack file content. **Access policy**: restricted - operationId: 'StackFileInspect' - produces: - - 'application/json' - security: - - jwt: [] + operationId: StackFileInspect parameters: - - name: 'id' - in: 'path' - description: 'Stack identifier' - required: true - type: 'integer' + - description: Stack identifier + in: path + name: id + required: true + type: integer + produces: + - application/json responses: - 200: - description: 'Success' + "200": + description: Success schema: - $ref: '#/definitions/StackFileInspectResponse' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request' - 403: - description: 'Unauthorized' - schema: - $ref: '#/definitions/GenericError' - 404: - description: 'Stack not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Stack not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' + $ref: '#/definitions/stacks.stackFileResponse' + "400": + description: Invalid request + "403": + description: Permission denied + "404": + description: Stack not found + "500": + description: Server error + security: + - jwt: [] + summary: Retrieve the content of the Stack file for the specified stack + tags: + - stacks /stacks/{id}/migrate: post: - tags: - - 'stacks' - summary: 'Migrate a stack to another endpoint' - description: | - Migrate a stack from an endpoint to another endpoint. It will re-create - the stack inside the target endpoint before removing the original stack. + description: |- + Migrate a stack from an endpoint to another endpoint. It will re-create the stack inside the target endpoint before removing the original stack. **Access policy**: restricted - operationId: 'StackMigrate' - produces: - - 'application/json' - security: - - jwt: [] + operationId: StackMigrate parameters: - - name: 'id' - in: 'path' - description: 'Stack identifier' - required: true - type: 'integer' - - name: 'endpointId' - in: 'query' - description: "Stacks created before version 1.18.0 might not have an associated endpoint identifier. Use this \ - optional parameter to set the endpoint identifier used by the stack." - type: 'integer' - - in: 'body' - name: 'body' - description: 'Stack migration details.' - schema: - $ref: '#/definitions/StackMigrateRequest' - responses: - 200: - description: 'Success' - schema: - $ref: '#/definitions/Stack' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request' - 403: - description: 'Unauthorized' - schema: - $ref: '#/definitions/GenericError' - 404: - description: 'Stack not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Stack not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - /users: - get: - tags: - - 'users' - summary: 'List users' - description: | - List Portainer users. Non-administrator users will only be able to list other non-administrator user accounts. - **Access policy**: restricted - operationId: 'UserList' + - description: Stack identifier + in: path + name: id + required: true + type: integer + - description: Stacks created before version 1.18.0 might not have an associated + endpoint identifier. Use this optional parameter to set the endpoint identifier + used by the stack. + in: query + name: endpointId + type: integer + - description: Stack migration details + in: body + name: body + required: true + schema: + $ref: '#/definitions/stacks.stackMigratePayload' produces: - - 'application/json' - security: - - jwt: [] - parameters: [] + - application/json responses: - 200: - description: 'Success' + "200": + description: Success schema: - $ref: '#/definitions/UserListResponse' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' + $ref: '#/definitions/portainer.Stack' + "400": + description: Invalid request + "403": + description: Permission denied + "404": + description: Stack not found + "500": + description: Server error + security: + - jwt: [] + summary: Migrate a stack to another endpoint + tags: + - stacks + /stacks/{id}/start: post: - tags: - - 'users' - summary: 'Create a new user' - description: | - Create a new Portainer user. Only team leaders and administrators can create users. Only administrators can - create an administrator user account. + description: |- + Starts a stopped Stack. **Access policy**: restricted - operationId: 'UserCreate' - consumes: - - 'application/json' - produces: - - 'application/json' - security: - - jwt: [] + operationId: StackStart parameters: - - in: 'body' - name: 'body' - description: 'User details' - required: true - schema: - $ref: '#/definitions/UserCreateRequest' + - description: Stack identifier + in: path + name: id + required: true + type: integer responses: - 200: - description: 'Success' + "200": + description: Success schema: - $ref: '#/definitions/UserSubset' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data format' - 403: - description: 'Unauthorized' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Access denied to resource' - 409: - description: 'User already exists' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'User already exists' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - /users/{id}: - get: + $ref: '#/definitions/portainer.Stack' + "400": + description: Invalid request + "403": + description: Permission denied + "404": + description: ' not found' + "500": + description: Server error + security: + - jwt: [] + summary: Starts a stopped Stack tags: - - 'users' - summary: 'Inspect a user' - description: | - Retrieve details about a user. - **Access policy**: administrator - operationId: 'UserInspect' - produces: - - 'application/json' - security: - - jwt: [] - parameters: - - name: 'id' - in: 'path' - description: 'User identifier' - required: true - type: 'integer' - responses: - 200: - description: 'Success' - schema: - $ref: '#/definitions/User' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request' - 404: - description: 'User not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'User not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - put: - tags: - - 'users' - summary: 'Update a user' - description: | - Update user details. A regular user account can only update his details. - **Access policy**: authenticated - operationId: 'UserUpdate' - consumes: - - 'application/json' - produces: - - 'application/json' - security: - - jwt: [] - parameters: - - name: 'id' - in: 'path' - description: 'User identifier' - required: true - type: 'integer' - - in: 'body' - name: 'body' - description: 'User details' - required: true - schema: - $ref: '#/definitions/UserUpdateRequest' - responses: - 200: - description: 'Success' - schema: - $ref: '#/definitions/User' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data format' - 403: - description: 'Unauthorized' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Access denied to resource' - 404: - description: 'User not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'User not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - delete: - tags: - - 'users' - summary: 'Remove a user' - description: | - Remove a user. - **Access policy**: administrator - operationId: 'UserDelete' - security: - - jwt: [] - parameters: - - name: 'id' - in: 'path' - description: 'User identifier' - required: true - type: 'integer' - responses: - 204: - description: 'Success' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request' - 404: - description: 'User not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'User not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - /users/{id}/memberships: - get: - tags: - - 'users' - summary: 'Inspect a user memberships' - description: | - Inspect a user memberships. - **Access policy**: authenticated - operationId: 'UserMembershipsInspect' - produces: - - 'application/json' - security: - - jwt: [] - parameters: - - name: 'id' - in: 'path' - description: 'User identifier' - required: true - type: 'integer' - responses: - 200: - description: 'Success' - schema: - $ref: '#/definitions/UserMembershipsResponse' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request' - 403: - description: 'Unauthorized' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Access denied to resource' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - - /users/{id}/passwd: + - stacks + /stacks/{id}/stop: post: - tags: - - 'users' - summary: 'Check password validity for a user' - description: | - Check if the submitted password is valid for the specified user. - **Access policy**: authenticated - operationId: 'UserPasswordCheck' - consumes: - - 'application/json' - produces: - - 'application/json' - security: - - jwt: [] + description: |- + Stops a stopped Stack. + **Access policy**: restricted + operationId: StackStop parameters: - - name: 'id' - in: 'path' - description: 'User identifier' - required: true - type: 'integer' - - in: 'body' - name: 'body' - description: 'User details' - required: true - schema: - $ref: '#/definitions/UserPasswordCheckRequest' + - description: Stack identifier + in: path + name: id + required: true + type: integer responses: - 200: - description: 'Success' + "200": + description: Success schema: - $ref: '#/definitions/UserPasswordCheckResponse' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data format' - 404: - description: 'User not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'User not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - - /users/admin/check: - get: + $ref: '#/definitions/portainer.Stack' + "400": + description: Invalid request + "403": + description: Permission denied + "404": + description: ' not found' + "500": + description: Server error + security: + - jwt: [] + summary: Stops a stopped Stack tags: - - 'users' - summary: 'Check administrator account existence' - description: | - Check if an administrator account exists in the database. + - stacks + /status: + get: + description: |- + Retrieve Portainer status **Access policy**: public - operationId: 'UserAdminCheck' + operationId: StatusInspect produces: - - 'application/json' - security: - - jwt: [] - parameters: [] + - application/json responses: - 204: - description: 'Success' - 404: - description: 'User not found' + "200": + description: Success schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'User not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - - /users/admin/init: - post: + $ref: '#/definitions/portainer.Status' + summary: Check Portainer status tags: - - 'users' - summary: 'Initialize administrator account' - description: | - Initialize the 'admin' user account. - **Access policy**: public - operationId: 'UserAdminInit' - consumes: - - 'application/json' + - status + /status/version: + get: + description: |- + Check if portainer has an update available + **Access policy**: authenticated + operationId: StatusInspectVersion produces: - - 'application/json' - security: - - jwt: [] - parameters: - - in: 'body' - name: 'body' - description: 'User details' - required: true - schema: - $ref: '#/definitions/UserAdminInitRequest' + - application/json responses: - 200: - description: 'Success' + "200": + description: Success schema: - $ref: '#/definitions/User' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data format' - 409: - description: 'Admin user already initialized' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'User already exists' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - - /upload/tls/{certificate}: - post: + $ref: '#/definitions/status.inspectVersionResponse' + security: + - jwt: [] + summary: Check for portainer updates tags: - - 'upload' - summary: 'Upload TLS files' - description: | - Use this endpoint to upload TLS files. - **Access policy**: administrator - operationId: 'UploadTLS' - consumes: - - multipart/form-data - produces: - - 'application/json' - security: - - jwt: [] - parameters: - - in: 'path' - name: 'certificate' - description: "TLS file type. Valid values are 'ca', 'cert' or 'key'." - required: true - type: 'string' - - in: 'query' - name: 'folder' - description: 'Folder where the TLS file will be stored. Will be created if not existing.' - required: true - type: 'string' - - in: 'formData' - name: 'file' - type: 'file' - description: 'The file to upload.' - responses: - 200: - description: 'Success' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - + - status /tags: get: - tags: - - 'tags' - summary: 'List tags' - description: | + description: |- List tags. **Access policy**: administrator - operationId: 'TagList' + operationId: TagList produces: - - 'application/json' - security: - - jwt: [] - parameters: [] + - application/json responses: - 200: - description: 'Success' + "200": + description: Success schema: - $ref: '#/definitions/TagListResponse' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - post: + items: + $ref: '#/definitions/portainer.Tag' + type: array + "500": + description: Server error + security: + - jwt: [] + summary: List tags tags: - - 'tags' - summary: 'Create a new tag' - description: | + - tags + post: + description: |- Create a new tag. **Access policy**: administrator - operationId: 'TagCreate' - consumes: - - 'application/json' - produces: - - 'application/json' - security: - - jwt: [] + operationId: TagCreate parameters: - - in: 'body' - name: 'body' - description: 'Tag details' - required: true - schema: - $ref: '#/definitions/TagCreateRequest' + - description: Tag details + in: body + name: body + required: true + schema: + $ref: '#/definitions/tags.tagCreatePayload' + produces: + - application/json responses: - 200: - description: 'Success' + "200": + description: Success schema: - $ref: '#/definitions/Tag' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data format' - 409: - description: 'Conflict' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'A tag with the specified name already exists' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' + $ref: '#/definitions/portainer.Tag' + "409": + description: Tag name exists + "500": + description: Server error + security: + - jwt: [] + summary: Create a new tag + tags: + - tags /tags/{id}: delete: - tags: - - 'tags' - summary: 'Remove a tag' - description: | + consumes: + - application/json + description: |- Remove a tag. **Access policy**: administrator - operationId: 'TagDelete' - security: - - jwt: [] + operationId: TagDelete parameters: - - name: 'id' - in: 'path' - description: 'Tag identifier' - required: true - type: 'integer' - responses: - 204: - description: 'Success' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - - /teams: - get: - tags: - - 'teams' - summary: 'List teams' - description: | - List teams. For non-administrator users, will only list the teams they are member of. - **Access policy**: restricted - operationId: 'TeamList' + - description: Tag identifier + in: path + name: id + required: true + type: integer produces: - - 'application/json' - security: - - jwt: [] - parameters: [] + - application/json responses: - 200: - description: 'Success' - schema: - $ref: '#/definitions/TeamListResponse' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - post: + "204": + description: Success + "400": + description: Invalid request + "403": + description: Permission denied + "404": + description: Tag not found + "500": + description: Server error + security: + - jwt: [] + summary: Remove a tag tags: - - 'teams' - summary: 'Create a new team' - description: | + - tags + /team: + post: + consumes: + - application/json + description: |- Create a new team. **Access policy**: administrator - operationId: 'TeamCreate' - consumes: - - 'application/json' - produces: - - 'application/json' - security: - - jwt: [] + operationId: TeamCreate parameters: - - in: 'body' - name: 'body' - description: 'Team details' - required: true - schema: - $ref: '#/definitions/TeamCreateRequest' + - description: details + in: body + name: body + required: true + schema: + $ref: '#/definitions/teams.teamCreatePayload' + produces: + - application/json responses: - 200: - description: 'Success' + "200": + description: Success schema: - $ref: '#/definitions/Team' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data format' - 403: - description: 'Unauthorized' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Access denied to resource' - 409: - description: 'Team already exists' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Team already exists' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - /teams/{id}: - get: + $ref: '#/definitions/portainer.Team' + "400": + description: Invalid request + "409": + description: Team already exists + "500": + description: Server error + security: + - jwt: [] + summary: Create a new team tags: - - 'teams' - summary: 'Inspect a team' - description: | - Retrieve details about a team. Access is only available for administrator and leaders of that team. - **Access policy**: restricted - operationId: 'TeamInspect' - produces: - - 'application/json' - security: - - jwt: [] - parameters: - - name: 'id' - in: 'path' - description: 'Team identifier' - required: true - type: 'integer' - responses: - 200: - description: 'Success' - schema: - $ref: '#/definitions/Team' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request' - 403: - description: 'Unauthorized' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Access denied to resource' - 404: - description: 'Team not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Team not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' + - teams + /team/{id}: put: - tags: - - 'teams' - summary: 'Update a team' - description: | + consumes: + - application/json + description: |- Update a team. **Access policy**: administrator - operationId: 'TeamUpdate' - consumes: - - 'application/json' + operationId: TeamUpdate + parameters: + - description: Team identifier + in: path + name: id + required: true + type: integer + - description: Team details + in: body + name: body + required: true + schema: + $ref: '#/definitions/teams.teamUpdatePayload' produces: - - 'application/json' - security: - - jwt: [] - parameters: - - name: 'id' - in: 'path' - description: 'Team identifier' - required: true - type: 'integer' - - in: 'body' - name: 'body' - description: 'Team details' - required: true - schema: - $ref: '#/definitions/TeamUpdateRequest' + - application/json responses: - 200: - description: 'Success' - 400: - description: 'Invalid request' + "200": + description: Success schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data format' - 404: - description: 'Team not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Team not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - delete: + $ref: '#/definitions/portainer.Team' + "204": + description: Success + "400": + description: Invalid request + "403": + description: Permission denied + "404": + description: Team not found + "500": + description: Server error + security: + - jwt: [] + summary: Update a team tags: - - 'teams' - summary: 'Remove a team' - description: | - Remove a team. - **Access policy**: administrator - operationId: 'TeamDelete' - security: - - jwt: [] - parameters: - - name: 'id' - in: 'path' - description: 'Team identifier' - required: true - type: 'integer' - responses: - 204: - description: 'Success' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request' - 404: - description: 'Team not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Team not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - - /teams/{id}/memberships: - get: - tags: - - 'teams' - summary: 'Inspect a team memberships' - description: | - Inspect a team memberships. Access is only available for administrator and leaders of that team. - **Access policy**: restricted - operationId: 'TeamMembershipsInspect' - produces: - - 'application/json' - security: - - jwt: [] - parameters: - - name: 'id' - in: 'path' - description: 'Team identifier' - required: true - type: 'integer' - responses: - 200: - description: 'Success' - schema: - $ref: '#/definitions/TeamMembershipsResponse' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request' - 403: - description: 'Unauthorized' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Access denied to resource' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - + - "" /team_memberships: get: - tags: - - 'team_memberships' - summary: 'List team memberships' - description: | + description: |- List team memberships. Access is only available to administrators and team leaders. - **Access policy**: restricted - operationId: 'TeamMembershipList' + **Access policy**: admin + operationId: TeamMembershipList produces: - - 'application/json' - security: - - jwt: [] - parameters: [] + - application/json responses: - 200: - description: 'Success' + "200": + description: Success schema: - $ref: '#/definitions/TeamMembershipListResponse' - 403: - description: 'Unauthorized' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Access denied to resource' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' + items: + $ref: '#/definitions/portainer.TeamMembership' + type: array + "400": + description: Invalid request + "403": + description: Permission denied + "500": + description: Server error + security: + - jwt: [] + summary: List team memberships + tags: + - team_memberships post: - tags: - - 'team_memberships' - summary: 'Create a new team membership' - description: | + consumes: + - application/json + description: |- Create a new team memberships. Access is only available to administrators leaders of the associated team. - **Access policy**: restricted - operationId: 'TeamMembershipCreate' - consumes: - - 'application/json' - produces: - - 'application/json' - security: - - jwt: [] + **Access policy**: admin + operationId: TeamMembershipCreate parameters: - - in: 'body' - name: 'body' - description: 'Team membership details' - required: true - schema: - $ref: '#/definitions/TeamMembershipCreateRequest' + - description: Team membership details + in: body + name: body + required: true + schema: + $ref: '#/definitions/teammemberships.teamMembershipCreatePayload' + produces: + - application/json responses: - 200: - description: 'Success' + "200": + description: Success schema: - $ref: '#/definitions/TeamMembership' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data format' - 403: - description: 'Unauthorized' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Access denied to resource' - 409: - description: 'Team membership already exists' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Team membership already exists for this user and team.' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' + $ref: '#/definitions/portainer.TeamMembership' + "204": + description: Success + "400": + description: Invalid request + "403": + description: Permission denied to manage memberships + "409": + description: Team membership already registered + "500": + description: Server error + security: + - jwt: [] + summary: Create a new team membership + tags: + - team_memberships /team_memberships/{id}: - put: - tags: - - 'team_memberships' - summary: 'Update a team membership' - description: | - Update a team membership. Access is only available to administrators leaders of the associated team. - **Access policy**: restricted - operationId: 'TeamMembershipUpdate' - consumes: - - 'application/json' - produces: - - 'application/json' - security: - - jwt: [] - parameters: - - name: 'id' - in: 'path' - description: 'Team membership identifier' - required: true - type: 'integer' - - in: 'body' - name: 'body' - description: 'Team membership details' - required: true - schema: - $ref: '#/definitions/TeamMembershipUpdateRequest' - responses: - 200: - description: 'Success' - schema: - $ref: '#/definitions/TeamMembership' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data format' - 403: - description: 'Unauthorized' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Access denied to resource' - 404: - description: 'Team membership not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Team membership not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' delete: - tags: - - 'team_memberships' - summary: 'Remove a team membership' - description: | + description: |- Remove a team membership. Access is only available to administrators leaders of the associated team. **Access policy**: restricted - operationId: 'TeamMembershipDelete' - security: - - jwt: [] + operationId: TeamMembershipDelete parameters: - - name: 'id' - in: 'path' - description: 'TeamMembership identifier' - required: true - type: 'integer' + - description: TeamMembership identifier + in: path + name: id + required: true + type: integer responses: - 204: - description: 'Success' - 400: - description: 'Invalid request' + "204": + description: Success + "400": + description: Invalid request + "403": + description: Permission denied + "404": + description: TeamMembership not found + "500": + description: Server error + security: + - jwt: [] + summary: Remove a team membership + tags: + - team_memberships + put: + consumes: + - application/json + description: |- + Update a team membership. Access is only available to administrators leaders of the associated team. + **Access policy**: restricted + operationId: TeamMembershipUpdate + parameters: + - description: Team membership identifier + in: path + name: id + required: true + type: integer + - description: Team membership details + in: body + name: body + required: true + schema: + $ref: '#/definitions/teammemberships.teamMembershipUpdatePayload' + produces: + - application/json + responses: + "200": + description: Success schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request' - 403: - description: 'Unauthorized' + $ref: '#/definitions/portainer.TeamMembership' + "400": + description: Invalid request + "403": + description: Permission denied + "404": + description: TeamMembership not found + "500": + description: Server error + security: + - jwt: [] + summary: Update a team membership + tags: + - team_memberships + /teams: + get: + description: |- + List teams. For non-administrator users, will only list the teams they are member of. + **Access policy**: restricted + operationId: TeamList + produces: + - application/json + responses: + "200": + description: Success schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Access denied to resource' - 404: - description: 'Team membership not found' + items: + $ref: '#/definitions/portainer.Team' + type: array + "500": + description: Server error + security: + - jwt: [] + summary: List teams + tags: + - teams + /teams/{id}: + delete: + description: |- + Remove a team. + **Access policy**: administrator + operationId: TeamDelete + responses: + "204": + description: Success + "400": + description: Invalid request + "403": + description: Permission denied + "404": + description: Team not found + "500": + description: Server error + security: + - jwt: [] + summary: Remove a team + tags: + - teams + get: + description: |- + Retrieve details about a team. Access is only available for administrator and leaders of that team. + **Access policy**: restricted + operationId: TeamInspect + parameters: + - description: Team identifier + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Success schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Team membership not found' - 500: - description: 'Server error' + $ref: '#/definitions/portainer.Team' + "204": + description: Success + "400": + description: Invalid request + "403": + description: Permission denied + "404": + description: Team not found + "500": + description: Server error + security: + - jwt: [] + summary: Inspect a team + tags: + - teams + /teams/{id}/memberships: + get: + description: |- + List team memberships. Access is only available to administrators and team leaders. + **Access policy**: restricted + operationId: TeamMemberships + produces: + - application/json + responses: + "200": + description: Success schema: - $ref: '#/definitions/GenericError' + items: + $ref: '#/definitions/portainer.TeamMembership' + type: array + "400": + description: Invalid request + "403": + description: Permission denied + "500": + description: Server error + security: + - jwt: [] + summary: List team memberships + tags: + - team_memberships /templates: get: - tags: - - 'templates' - summary: 'List available templates' - description: | + description: |- List available templates. - Administrator templates will not be listed for non-administrator users. **Access policy**: restricted - operationId: 'TemplateList' + operationId: TemplateList produces: - - 'application/json' - security: - - jwt: [] - parameters: [] + - application/json responses: - 200: - description: 'Success' + "200": + description: Success schema: - $ref: '#/definitions/TemplateListResponse' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' + $ref: '#/definitions/templates.listResponse' + "500": + description: Server error + security: + - jwt: [] + summary: List available templates + tags: + - templates + /templates/file: post: - tags: - - 'templates' - summary: 'Create a new template' - description: | - Create a new template. - **Access policy**: administrator - operationId: 'TemplateCreate' consumes: - - 'application/json' - produces: - - 'application/json' - security: - - jwt: [] + - application/json + description: |- + Get a template's file + **Access policy**: restricted + operationId: TemplateFile parameters: - - in: 'body' - name: 'body' - description: 'Template details' - required: true - schema: - $ref: '#/definitions/TemplateCreateRequest' + - description: File details + in: body + name: body + required: true + schema: + $ref: '#/definitions/templates.filePayload' + produces: + - application/json responses: - 200: - description: 'Success' + "200": + description: Success schema: - $ref: '#/definitions/Template' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data format' - 403: - description: 'Unauthorized' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Access denied to resource' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - /templates/{id}: + $ref: '#/definitions/templates.fileResponse' + "400": + description: Invalid request + "500": + description: Server error + security: + - jwt: [] + summary: Get a template's file + tags: + - templates + /upload/tls/{certificate}: + post: + consumes: + - multipart/form-data + description: |- + Use this endpoint to upload TLS files. + **Access policy**: administrator + operationId: UploadTLS + parameters: + - description: TLS file type. Valid values are 'ca', 'cert' or 'key'. + enum: + - ca + - cert + - key + in: path + name: certificate + required: true + type: string + - description: Folder where the TLS file will be stored. Will be created if + not existing + in: formData + name: folder + required: true + type: string + - description: The file to upload + in: formData + name: file + required: true + type: file + produces: + - application/json + responses: + "204": + description: Success + "400": + description: Invalid request + "500": + description: Server error + security: + - jwt: [] + summary: Upload TLS files + tags: + - upload + /users: get: - tags: - - 'templates' - summary: 'Inspect a template' - description: | - Retrieve details about a template. - **Access policy**: administrator - operationId: 'TemplateInspect' + description: |- + List Portainer users. + Non-administrator users will only be able to list other non-administrator user accounts. + **Access policy**: restricted + operationId: UserList produces: - - 'application/json' - security: - - jwt: [] - parameters: - - name: 'id' - in: 'path' - description: 'Template identifier' - required: true - type: 'integer' + - application/json responses: - 200: - description: 'Success' + "200": + description: Success schema: - $ref: '#/definitions/Template' - 400: - description: 'Invalid request' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request' - 403: - description: 'Unauthorized' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Access denied to resource' - 404: - description: 'Template not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Template not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - put: + items: + $ref: '#/definitions/portainer.User' + type: array + "400": + description: Invalid request + "500": + description: Server error + security: + - jwt: [] + summary: List users tags: - - 'templates' - summary: 'Update a template' - description: | - Update a template. - **Access policy**: administrator - operationId: 'TemplateUpdate' + - users + post: consumes: - - 'application/json' + - application/json + description: |- + Create a new Portainer user. + Only team leaders and administrators can create users. + Only administrators can create an administrator user account. + **Access policy**: restricted + operationId: UserCreate + parameters: + - description: User details + in: body + name: body + required: true + schema: + $ref: '#/definitions/users.userCreatePayload' produces: - - 'application/json' - security: - - jwt: [] - parameters: - - name: 'id' - in: 'path' - description: 'Template identifier' - required: true - type: 'integer' - - in: 'body' - name: 'body' - description: 'Template details' - required: true - schema: - $ref: '#/definitions/TemplateUpdateRequest' + - application/json responses: - 200: - description: 'Success' - 400: - description: 'Invalid request' + "200": + description: Success schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request data format' - 403: - description: 'Unauthorized' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Access denied to resource' - 404: - description: 'Template not found' - schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Template not found' - 500: - description: 'Server error' - schema: - $ref: '#/definitions/GenericError' - delete: + $ref: '#/definitions/portainer.User' + "400": + description: Invalid request + "403": + description: Permission denied + "409": + description: User already exists + "500": + description: Server error + security: + - jwt: [] + summary: Create a new user tags: - - 'templates' - summary: 'Remove a template' - description: | - Remove a template. + - users + /users/{id}: + delete: + consumes: + - application/json + description: |- + Remove a user. **Access policy**: administrator - operationId: 'TemplateDelete' - security: - - jwt: [] + operationId: UserDelete parameters: - - name: 'id' - in: 'path' - description: 'Template identifier' - required: true - type: 'integer' + - description: User identifier + in: path + name: id + required: true + type: integer + produces: + - application/json responses: - 204: - description: 'Success' - 400: - description: 'Invalid request' + "204": + description: Success + "400": + description: Invalid request + "403": + description: Permission denied + "404": + description: User not found + "500": + description: Server error + security: + - jwt: [] + summary: Remove a user + tags: + - users + get: + description: |- + Retrieve details about a user. + **Access policy**: administrator + operationId: UserInspect + parameters: + - description: User identifier + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Success schema: - $ref: '#/definitions/GenericError' - examples: - application/json: - err: 'Invalid request' - 500: - description: 'Server error' + $ref: '#/definitions/portainer.User' + "400": + description: Invalid request + "403": + description: Permission denied + "404": + description: User not found + "500": + description: Server error + security: + - jwt: [] + summary: Inspect a user + tags: + - users + put: + consumes: + - application/json + description: |- + Update user details. A regular user account can only update his details. + **Access policy**: authenticated + operationId: UserUpdate + parameters: + - description: User identifier + in: path + name: id + required: true + type: integer + - description: User details + in: body + name: body + required: true + schema: + $ref: '#/definitions/users.userUpdatePayload' + produces: + - application/json + responses: + "200": + description: Success schema: - $ref: '#/definitions/GenericError' + $ref: '#/definitions/portainer.User' + "400": + description: Invalid request + "403": + description: Permission denied + "404": + description: User not found + "409": + description: Username already exist + "500": + description: Server error + security: + - jwt: [] + summary: Update a user + tags: + - users + /users/{id}/memberships: + get: + description: |- + Inspect a user memberships. + **Access policy**: authenticated + operationId: UserMembershipsInspect + parameters: + - description: User identifier + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/portainer.TeamMembership' + "400": + description: Invalid request + "403": + description: Permission denied + "500": + description: Server error + security: + - jwt: [] + summary: Inspect a user memberships + tags: + - users + /users/{id}/passwd: + put: + consumes: + - application/json + description: |- + Update password for the specified user. + **Access policy**: authenticated + operationId: UserUpdatePassword + parameters: + - description: identifier + in: path + name: id + required: true + type: integer + - description: details + in: body + name: body + required: true + schema: + $ref: '#/definitions/users.userUpdatePasswordPayload' + produces: + - application/json + responses: + "204": + description: Success + "400": + description: Invalid request + "403": + description: Permission denied + "404": + description: User not found + "500": + description: Server error + security: + - jwt: [] + summary: Update password for a user + tags: + - users + /users/admin/check: + get: + description: |- + Check if an administrator account exists in the database. + **Access policy**: public + operationId: UserAdminCheck + responses: + "204": + description: Success + "404": + description: User not found + summary: Check administrator account existence + tags: + - users + /users/admin/init: + post: + consumes: + - application/json + description: |- + Initialize the 'admin' user account. + **Access policy**: public + operationId: UserAdminInit + parameters: + - description: User details + in: body + name: body + required: true + schema: + $ref: '#/definitions/users.adminInitPayload' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/portainer.User' + "400": + description: Invalid request + "409": + description: Admin user already initialized + "500": + description: Server error + summary: Initialize administrator account + tags: + - "" + /webhooks: + get: + consumes: + - application/json + parameters: + - description: Webhook data + in: body + name: body + required: true + schema: + $ref: '#/definitions/webhooks.webhookCreatePayload' + - in: query + name: EndpointID + type: integer + - in: query + name: ResourceID + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/portainer.Webhook' + type: array + "400": + description: "" + "500": + description: "" + security: + - jwt: [] + summary: List webhooks + tags: + - webhooks + post: + consumes: + - application/json + parameters: + - description: Webhook data + in: body + name: body + required: true + schema: + $ref: '#/definitions/webhooks.webhookCreatePayload' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/portainer.Webhook' + "400": + description: "" + "409": + description: "" + "500": + description: "" + security: + - jwt: [] + summary: Create a webhook + tags: + - webhooks + /webhooks/{id}: + delete: + consumes: + - application/json + parameters: + - description: Webhook id + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "202": + description: Webhook deleted + "400": + description: "" + "500": + description: "" + security: + - jwt: [] + summary: Delete a webhook + tags: + - webhooks + /webhooks/{token}: + post: + consumes: + - application/json + description: Acts on a passed in token UUID to restart the docker service + parameters: + - description: Webhook token + in: path + name: token + required: true + type: string + produces: + - application/json + responses: + "202": + description: Webhook executed + "400": + description: "" + "500": + description: "" + summary: Execute a webhook + tags: + - webhooks + /websocket/attach: + get: + consumes: + - application/json + description: |- + If the nodeName query parameter is present, the request will be proxied to the underlying agent endpoint. + If the nodeName query parameter is not specified, the request will be upgraded to the websocket protocol and + an AttachStart operation HTTP request will be created and hijacked. + Authentication and access is controlled via the mandatory token query parameter. + parameters: + - description: endpoint ID of the endpoint where the resource is located + in: query + name: endpointId + required: true + type: integer + - description: node name + in: query + name: nodeName + type: string + - description: JWT token used for authentication against this endpoint + in: query + name: token + required: true + type: string + produces: + - application/json + responses: + "200": + description: "" + "400": + description: "" + "403": + description: "" + "404": + description: "" + "500": + description: "" + security: + - jwt: [] + summary: Attach a websocket + tags: + - websocket + /websocket/exec: + get: + consumes: + - application/json + description: |- + If the nodeName query parameter is present, the request will be proxied to the underlying agent endpoint. + If the nodeName query parameter is not specified, the request will be upgraded to the websocket protocol and + an ExecStart operation HTTP request will be created and hijacked. + Authentication and access is controlled via the mandatory token query parameter. + parameters: + - description: endpoint ID of the endpoint where the resource is located + in: query + name: endpointId + required: true + type: integer + - description: node name + in: query + name: nodeName + type: string + - description: JWT token used for authentication against this endpoint + in: query + name: token + required: true + type: string + produces: + - application/json + responses: + "200": + description: "" + "400": + description: "" + "409": + description: "" + "500": + description: "" + security: + - jwt: [] + summary: Execute a websocket + tags: + - websocket + /websocket/pod: + get: + consumes: + - application/json + description: |- + The request will be upgraded to the websocket protocol. + Authentication and access is controlled via the mandatory token query parameter. + parameters: + - description: endpoint ID of the endpoint where the resource is located + in: query + name: endpointId + required: true + type: integer + - description: namespace where the container is located + in: query + name: namespace + required: true + type: string + - description: name of the pod containing the container + in: query + name: podName + required: true + type: string + - description: name of the container + in: query + name: containerName + required: true + type: string + - description: command to execute in the container + in: query + name: command + required: true + type: string + - description: JWT token used for authentication against this endpoint + in: query + name: token + required: true + type: string + produces: + - application/json + responses: + "200": + description: "" + "400": + description: "" + "403": + description: "" + "404": + description: "" + "500": + description: "" + security: + - jwt: [] + summary: Execute a websocket on pod + tags: + - websocket +schemes: +- http +- https securityDefinitions: jwt: - type: 'apiKey' - name: 'Authorization' - in: 'header' -definitions: - Tag: - type: 'object' - properties: - Id: - type: 'integer' - example: 1 - description: 'Tag identifier' - Name: - type: 'string' - example: 'org/acme' - description: 'Tag name' - Team: - type: 'object' - properties: - Id: - type: 'integer' - example: 1 - description: 'Team identifier' - Name: - type: 'string' - example: 'developers' - description: 'Team name' - TeamMembership: - type: 'object' - properties: - Id: - type: 'integer' - example: 1 - description: 'Membership identifier' - UserID: - type: 'integer' - example: 1 - description: 'User identifier' - TeamID: - type: 'integer' - example: 1 - description: 'Team identifier' - Role: - type: 'integer' - example: 1 - description: 'Team role (1 for team leader and 2 for team member)' - UserSubset: - type: 'object' - properties: - Id: - type: 'integer' - example: 1 - description: 'User identifier' - Username: - type: 'string' - example: 'bob' - description: 'Username' - Role: - type: 'integer' - example: 1 - description: 'User role (1 for administrator account and 2 for regular account)' - User: - type: 'object' - properties: - Id: - type: 'integer' - example: 1 - description: 'User identifier' - Username: - type: 'string' - example: 'bob' - description: 'Username' - Password: - type: 'string' - example: 'passwd' - description: 'Password' - Role: - type: 'integer' - example: 1 - description: 'User role (1 for administrator account and 2 for regular account)' - Status: - type: 'object' - properties: - Authentication: - type: 'boolean' - example: true - description: 'Is authentication enabled' - Version: - type: 'string' - example: '2.0.0' - description: 'Portainer API version' - PublicSettingsInspectResponse: - type: 'object' - properties: - LogoURL: - type: 'string' - example: 'https://mycompany.mydomain.tld/logo.png' - description: "URL to a logo that will be displayed on the login page as well\ - \ as on top of the sidebar. Will use default Portainer logo when value is\ - \ empty string" - DisplayExternalContributors: - type: 'boolean' - example: false - description: "Whether to display or not external templates contributions as\ - \ sub-menus in the UI." - AuthenticationMethod: - type: 'integer' - example: 1 - description: 'Active authentication method for the Portainer instance. Valid values are: 1 for managed or 2 for LDAP.' - AllowBindMountsForRegularUsers: - type: 'boolean' - example: false - description: 'Whether non-administrator should be able to use bind mounts when creating containers' - AllowPrivilegedModeForRegularUsers: - type: 'boolean' - example: true - description: 'Whether non-administrator should be able to use privileged mode when creating containers' - TLSConfiguration: - type: 'object' - properties: - TLS: - type: 'boolean' - example: true - description: 'Use TLS' - TLSSkipVerify: - type: 'boolean' - example: false - description: 'Skip the verification of the server TLS certificate' - TLSCACertPath: - type: 'string' - example: '/data/tls/ca.pem' - description: 'Path to the TLS CA certificate file' - TLSCertPath: - type: 'string' - example: '/data/tls/cert.pem' - description: 'Path to the TLS client certificate file' - TLSKeyPath: - type: 'string' - example: '/data/tls/key.pem' - description: 'Path to the TLS client key file' - AzureCredentials: - type: 'object' - properties: - ApplicationID: - type: 'string' - example: 'eag7cdo9-o09l-9i83-9dO9-f0b23oe78db4' - description: 'Azure application ID' - TenantID: - type: 'string' - example: '34ddc78d-4fel-2358-8cc1-df84c8o839f5' - description: 'Azure tenant ID' - AuthenticationKey: - type: 'string' - example: 'cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk=' - description: 'Azure authentication key' - LDAPSearchSettings: - type: 'object' - properties: - BaseDN: - type: 'string' - example: 'dc=ldap,dc=domain,dc=tld' - description: 'The distinguished name of the element from which the LDAP server will search for users' - Filter: - type: 'string' - example: '(objectClass=account)' - description: 'Optional LDAP search filter used to select user elements' - UserNameAttribute: - type: 'string' - example: 'uid' - description: 'LDAP attribute which denotes the username' - LDAPGroupSearchSettings: - type: 'object' - properties: - GroupBaseDN: - type: 'string' - example: 'dc=ldap,dc=domain,dc=tld' - description: 'The distinguished name of the element from which the LDAP server will search for groups.' - GroupFilter: - type: 'string' - example: '(objectClass=account)' - description: 'The LDAP search filter used to select group elements, optional.' - GroupAttribute: - type: 'string' - example: 'member' - description: 'LDAP attribute which denotes the group membership.' - UserAccessPolicies: - type: 'object' - description: 'User access policies associated to a registry/endpoint/endpoint group. RoleID is not required for registry access policies and can be set to 0.' - additionalProperties: - $ref: '#/definitions/AccessPolicy' - example: - 1: { RoleID: 1 } - 2: { RoleID: 3 } - TeamAccessPolicies: - type: 'object' - description: 'Team access policies associated to a registry/endpoint/endpoint group. RoleID is not required for registry access policies and can be set to 0.' - additionalProperties: - $ref: '#/definitions/AccessPolicy' - example: - 1: { RoleID: 1 } - 2: { RoleID: 3 } - AccessPolicy: - type: 'object' - properties: - RoleID: - type: 'integer' - example: '1' - description: 'Role identifier. Reference the role that will be associated to this access policy' - LDAPSettings: - type: 'object' - properties: - AnonymousMode: - type: 'boolean' - example: true - description: 'Enable this option if the server is configured for Anonymous access. When enabled, ReaderDN and Password will not be used.' - ReaderDN: - type: 'string' - example: 'cn=readonly-account,dc=ldap,dc=domain,dc=tld' - description: 'Account that will be used to search for users' - Password: - type: 'string' - example: 'readonly-password' - description: 'Password of the account that will be used to search users' - URL: - type: 'string' - example: 'myldap.domain.tld:389' - description: 'URL or IP address of the LDAP server' - TLSConfig: - $ref: '#/definitions/TLSConfiguration' - StartTLS: - type: 'boolean' - example: true - description: 'Whether LDAP connection should use StartTLS' - SearchSettings: - type: 'array' - items: - $ref: '#/definitions/LDAPSearchSettings' - GroupSearchSettings: - type: 'array' - items: - $ref: '#/definitions/LDAPGroupSearchSettings' - AutoCreateUsers: - type: 'boolean' - example: true - description: 'Automatically provision users and assign them to matching LDAP group names' - - Settings: - type: 'object' - properties: - TemplatesURL: - type: 'string' - example: 'https://raw.githubusercontent.com/portainer/templates/master/templates.json' - description: "URL to the templates that will be displayed in the UI when navigating\ - \ to App Templates" - LogoURL: - type: 'string' - example: 'https://mycompany.mydomain.tld/logo.png' - description: "URL to a logo that will be displayed on the login page as well\ - \ as on top of the sidebar. Will use default Portainer logo when value is\ - \ empty string" - BlackListedLabels: - type: 'array' - description: "A list of label name & value that will be used to hide containers\ - \ when querying containers" - items: - $ref: '#/definitions/Settings_BlackListedLabels' - DisplayExternalContributors: - type: 'boolean' - example: false - description: "Whether to display or not external templates contributions as\ - \ sub-menus in the UI." - AuthenticationMethod: - type: 'integer' - example: 1 - description: 'Active authentication method for the Portainer instance. Valid values are: 1 for managed or 2 for LDAP.' - LDAPSettings: - $ref: '#/definitions/LDAPSettings' - AllowBindMountsForRegularUsers: - type: 'boolean' - example: false - description: 'Whether non-administrator should be able to use bind mounts when creating containers' - AllowPrivilegedModeForRegularUsers: - type: 'boolean' - example: true - description: 'Whether non-administrator should be able to use privileged mode when creating containers' - Settings_BlackListedLabels: - properties: - name: - type: 'string' - example: 'com.foo' - value: - type: 'string' - example: 'bar' - Pair: - properties: - name: - type: 'string' - example: 'name' - value: - type: 'string' - example: 'value' - Registry: - type: 'object' - properties: - Id: - type: 'integer' - example: 1 - description: 'Registry identifier' - Name: - type: 'string' - example: 'my-registry' - description: 'Registry name' - URL: - type: 'string' - example: 'registry.mydomain.tld:2375' - description: 'URL or IP address of the Docker registry' - Authentication: - type: 'boolean' - example: true - description: 'Is authentication against this registry enabled' - Username: - type: 'string' - example: 'registry_user' - description: 'Username used to authenticate against this registry' - Password: - type: 'string' - example: 'registry_password' - description: 'Password used to authenticate against this registry' - AuthorizedUsers: - type: 'array' - description: 'List of user identifiers authorized to use this registry' - items: - type: 'integer' - example: 1 - description: 'User identifier' - AuthorizedTeams: - type: 'array' - description: 'List of team identifiers authorized to use this registry' - items: - type: 'integer' - example: 1 - description: 'Team identifier' - RegistrySubset: - type: 'object' - properties: - Id: - type: 'integer' - example: 1 - description: 'Registry identifier' - Name: - type: 'string' - example: 'my-registry' - description: 'Registry name' - URL: - type: 'string' - example: 'registry.mydomain.tld:2375' - description: 'URL or IP address of the Docker registry' - Authentication: - type: 'boolean' - example: true - description: 'Is authentication against this registry enabled' - Username: - type: 'string' - example: 'registry_user' - description: 'Username used to authenticate against this registry' - AuthorizedUsers: - type: 'array' - description: 'List of user identifiers authorized to use this registry' - items: - type: 'integer' - example: 1 - description: 'User identifier' - AuthorizedTeams: - type: 'array' - description: 'List of team identifiers authorized to use this registry' - items: - type: 'integer' - example: 1 - description: 'Team identifier' - EndpointGroup: - type: 'object' - properties: - Id: - type: 'integer' - example: 1 - description: 'Endpoint group identifier' - Name: - type: 'string' - example: 'my-endpoint-group' - description: 'Endpoint group name' - Description: - type: 'string' - example: 'Description associated to the endpoint group' - description: 'Endpoint group description' - AuthorizedUsers: - type: 'array' - description: 'List of user identifiers authorized to connect to this endpoint group. Will be inherited by endpoints that are part of the group' - items: - type: 'integer' - example: 1 - description: 'User identifier' - AuthorizedTeams: - type: 'array' - description: 'List of team identifiers authorized to connect to this endpoint. Will be inherited by endpoints that are part of the group' - items: - type: 'integer' - example: 1 - description: 'Team identifier' - Labels: - type: 'array' - items: - $ref: '#/definitions/Pair' - Endpoint: - type: 'object' - properties: - Id: - type: 'integer' - example: 1 - description: 'Endpoint identifier' - Name: - type: 'string' - example: 'my-endpoint' - description: 'Endpoint name' - Type: - type: 'integer' - example: 1 - description: 'Endpoint environment type. 1 for a Docker environment, 2 for an agent on Docker environment or 3 for an Azure environment.' - URL: - type: 'string' - example: 'docker.mydomain.tld:2375' - description: 'URL or IP address of the Docker host associated to this endpoint' - PublicURL: - type: 'string' - example: 'docker.mydomain.tld:2375' - description: 'URL or IP address where exposed containers will be reachable' - GroupID: - type: 'integer' - example: 1 - description: 'Endpoint group identifier' - AuthorizedUsers: - type: 'array' - description: 'List of user identifiers authorized to connect to this endpoint' - items: - type: 'integer' - example: 1 - description: 'User identifier' - AuthorizedTeams: - type: 'array' - description: 'List of team identifiers authorized to connect to this endpoint' - items: - type: 'integer' - example: 1 - description: 'Team identifier' - TLSConfig: - $ref: '#/definitions/TLSConfiguration' - AzureCredentials: - $ref: '#/definitions/AzureCredentials' - EndpointSubset: - type: 'object' - properties: - Id: - type: 'integer' - example: 1 - description: 'Endpoint identifier' - Name: - type: 'string' - example: 'my-endpoint' - description: 'Endpoint name' - Type: - type: 'integer' - example: 1 - description: 'Endpoint environment type. 1 for a Docker environment, 2 for an agent on Docker environment, 3 for an Azure environment.' - URL: - type: 'string' - example: 'docker.mydomain.tld:2375' - description: 'URL or IP address of the Docker host associated to this endpoint' - PublicURL: - type: 'string' - example: 'docker.mydomain.tld:2375' - description: 'URL or IP address where exposed containers will be reachable' - GroupID: - type: 'integer' - example: 1 - description: 'Endpoint group identifier' - AuthorizedUsers: - type: 'array' - description: 'List of user identifiers authorized to connect to this endpoint' - items: - type: 'integer' - example: 1 - description: 'User identifier' - AuthorizedTeams: - type: 'array' - description: 'List of team identifiers authorized to connect to this endpoint' - items: - type: 'integer' - example: 1 - description: 'Team identifier' - TLSConfig: - $ref: '#/definitions/TLSConfiguration' - GenericError: - type: 'object' - properties: - err: - type: 'string' - example: 'Something bad happened' - description: 'Error message' - AuthenticateUserRequest: - type: 'object' - required: - - 'Password' - - 'Username' - properties: - Username: - type: 'string' - example: 'admin' - description: 'Username' - Password: - type: 'string' - example: 'mypassword' - description: 'Password' - AuthenticateUserResponse: - type: 'object' - properties: - jwt: - type: 'string' - example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsImV4cCI6MTQ5OTM3NjE1NH0.NJ6vE8FY1WG6jsRQzfMqeatJ4vh2TWAeeYfDhP71YEE' - description: 'JWT token used to authenticate against the API' - DockerHubSubset: - type: 'object' - properties: - Authentication: - type: 'boolean' - example: true - description: 'Is authentication against DockerHub enabled' - Username: - type: 'string' - example: 'hub_user' - description: 'Username used to authenticate against the DockerHub' - DockerHub: - type: 'object' - properties: - Authentication: - type: 'boolean' - example: true - description: 'Is authentication against DockerHub enabled' - Username: - type: 'string' - example: 'hub_user' - description: 'Username used to authenticate against the DockerHub' - Password: - type: 'string' - example: 'hub_password' - description: 'Password used to authenticate against the DockerHub' - ResourceControl: - type: 'object' - properties: - ResourceID: - type: 'string' - example: '617c5f22bb9b023d6daab7cba43a57576f83492867bc767d1c59416b065e5f08' - description: "Docker resource identifier on which access control will be applied.\ - \ In the case of a resource control applied to a stack, use the stack name as identifier" - Type: - type: 'string' - example: 'container' - description: "Type of Docker resource. Valid values are: container, volume\ - \ service, secret, config or stack" - Public: - type: 'boolean' - example: true - description: 'Permit access to the associated resource to any user' - Users: - type: 'array' - description: 'List of user identifiers with access to the associated resource' - items: - type: 'integer' - example: 1 - description: 'User identifier' - Teams: - type: 'array' - description: 'List of team identifiers with access to the associated resource' - items: - type: 'integer' - example: 1 - description: 'Team identifier' - SubResourceIDs: - type: 'array' - description: 'List of Docker resources that will inherit this access control' - items: - type: 'string' - example: '617c5f22bb9b023d6daab7cba43a57576f83492867bc767d1c59416b065e5f08' - description: 'Docker resource identifier' - DockerHubUpdateRequest: - type: 'object' - required: - - 'Authentication' - - 'Password' - - 'Username' - properties: - Authentication: - type: 'boolean' - example: true - description: 'Enable authentication against DockerHub' - Username: - type: 'string' - example: 'hub_user' - description: 'Username used to authenticate against the DockerHub' - Password: - type: 'string' - example: 'hub_password' - description: 'Password used to authenticate against the DockerHub' - EndpointListResponse: - type: 'array' - items: - $ref: '#/definitions/EndpointSubset' - EndpointGroupListResponse: - type: 'array' - items: - $ref: '#/definitions/EndpointGroup' - EndpointUpdateRequest: - type: 'object' - properties: - Name: - type: 'string' - example: 'my-endpoint' - description: 'Name that will be used to identify this endpoint' - URL: - type: 'string' - example: 'docker.mydomain.tld:2375' - description: 'URL or IP address of a Docker host' - PublicURL: - type: 'string' - example: 'docker.mydomain.tld:2375' - description: "URL or IP address where exposed containers will be reachable.\ - \ Defaults to URL if not specified" - GroupID: - type: 'integer' - example: '1' - description: 'Group identifier' - TLS: - type: 'boolean' - example: true - description: 'Require TLS to connect against this endpoint' - TLSSkipVerify: - type: 'boolean' - example: false - description: 'Skip server verification when using TLS' - TLSSkipClientVerify: - type: 'boolean' - example: false - description: 'Skip client verification when using TLS' - ApplicationID: - type: 'string' - example: 'eag7cdo9-o09l-9i83-9dO9-f0b23oe78db4' - description: 'Azure application ID' - TenantID: - type: 'string' - example: '34ddc78d-4fel-2358-8cc1-df84c8o839f5' - description: 'Azure tenant ID' - AuthenticationKey: - type: 'string' - example: 'cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk=' - description: 'Azure authentication key' - UserAccessPolicies: - $ref: '#/definitions/UserAccessPolicies' - TeamAccessPolicies: - $ref: '#/definitions/TeamAccessPolicies' - RegistryCreateRequest: - type: 'object' - required: - - 'Authentication' - - 'Name' - - 'Password' - - 'Type' - - 'URL' - - 'Username' - properties: - Name: - type: 'string' - example: 'my-registry' - description: 'Name that will be used to identify this registry' - Type: - type: 'integer' - example: 1 - description: 'Registry Type. Valid values are: 1 (Quay.io), 2 (Azure container registry) or 3 (custom registry)' - URL: - type: 'string' - example: 'registry.mydomain.tld:2375' - description: 'URL or IP address of the Docker registry' - Authentication: - type: 'boolean' - example: true - description: 'Is authentication against this registry enabled' - Username: - type: 'string' - example: 'registry_user' - description: 'Username used to authenticate against this registry' - Password: - type: 'string' - example: 'registry_password' - description: 'Password used to authenticate against this registry' - RegistryListResponse: - type: 'array' - items: - $ref: '#/definitions/RegistrySubset' - RegistryUpdateRequest: - type: 'object' - required: - - 'Name' - - 'URL' - properties: - Name: - type: 'string' - example: 'my-registry' - description: 'Name that will be used to identify this registry' - URL: - type: 'string' - example: 'registry.mydomain.tld:2375' - description: 'URL or IP address of the Docker registry' - Authentication: - type: 'boolean' - example: true - description: 'Is authentication against this registry enabled' - Username: - type: 'string' - example: 'registry_user' - description: 'Username used to authenticate against this registry' - Password: - type: 'string' - example: 'registry_password' - description: 'Password used to authenticate against this registry' - UserAccessPolicies: - $ref: '#/definitions/UserAccessPolicies' - TeamAccessPolicies: - $ref: '#/definitions/TeamAccessPolicies' - ResourceControlCreateRequest: - type: 'object' - required: - - 'ResourceID' - - 'Type' - properties: - ResourceID: - type: 'string' - example: '617c5f22bb9b023d6daab7cba43a57576f83492867bc767d1c59416b065e5f08' - description: "Docker resource identifier on which access control will be applied.\ - \ In the case of a resource control applied to a stack, use the stack name as identifier" - Type: - type: 'string' - example: 'container' - description: "Type of Docker resource. Valid values are: container, volume\ - \ service, secret, config or stack" - Public: - type: 'boolean' - example: true - description: 'Permit access to the associated resource to any user' - Users: - type: 'array' - description: 'List of user identifiers with access to the associated resource' - items: - type: 'integer' - example: 1 - description: 'User identifier' - Teams: - type: 'array' - description: 'List of team identifiers with access to the associated resource' - items: - type: 'integer' - example: 1 - description: 'Team identifier' - SubResourceIDs: - type: 'array' - description: 'List of Docker resources that will inherit this access control' - items: - type: 'string' - example: '617c5f22bb9b023d6daab7cba43a57576f83492867bc767d1c59416b065e5f08' - description: 'Docker resource identifier' - ResourceControlUpdateRequest: - type: 'object' - properties: - Public: - type: 'boolean' - example: false - description: 'Permit access to the associated resource to any user' - Users: - type: 'array' - description: 'List of user identifiers with access to the associated resource' - items: - type: 'integer' - example: 1 - description: 'User identifier' - Teams: - type: 'array' - description: 'List of team identifiers with access to the associated resource' - items: - type: 'integer' - example: 1 - description: 'Team identifier' - SettingsUpdateRequest: - type: 'object' - required: - - 'TemplatesURL' - - 'AuthenticationMethod' - properties: - TemplatesURL: - type: 'string' - example: 'https://raw.githubusercontent.com/portainer/templates/master/templates.json' - description: "URL to the templates that will be displayed in the UI when navigating\ - \ to App Templates" - LogoURL: - type: 'string' - example: 'https://mycompany.mydomain.tld/logo.png' - description: "URL to a logo that will be displayed on the login page as well\ - \ as on top of the sidebar. Will use default Portainer logo when value is\ - \ empty string" - BlackListedLabels: - type: 'array' - description: "A list of label name & value that will be used to hide containers\ - \ when querying containers" - items: - $ref: '#/definitions/Settings_BlackListedLabels' - DisplayExternalContributors: - type: 'boolean' - example: false - description: "Whether to display or not external templates contributions as\ - \ sub-menus in the UI." - AuthenticationMethod: - type: 'integer' - example: 1 - description: 'Active authentication method for the Portainer instance. Valid values are: 1 for managed or 2 for LDAP.' - LDAPSettings: - $ref: '#/definitions/LDAPSettings' - AllowBindMountsForRegularUsers: - type: 'boolean' - example: true - description: 'Whether non-administrator users should be able to use bind mounts when creating containers' - AllowPrivilegedModeForRegularUsers: - type: 'boolean' - example: true - description: 'Whether non-administrator users should be able to use privileged mode when creating containers' - EdgeAgentCheckinInterval: - type: 'integer' - example: '30' - description: 'Polling interval for Edge agent (in seconds)' - EndpointGroupCreateRequest: - type: 'object' - required: - - 'Name' - properties: - Name: - type: 'string' - example: 'my-endpoint-group' - description: 'Endpoint group name' - Description: - type: 'string' - example: 'Endpoint group description' - description: 'Endpoint group description' - Labels: - type: 'array' - items: - $ref: '#/definitions/Pair' - AssociatedEndpoints: - type: 'array' - description: 'List of endpoint identifiers that will be part of this group' - items: - type: 'integer' - example: 1 - description: 'Endpoint identifier' - EndpointGroupUpdateRequest: - type: 'object' - properties: - Name: - type: 'string' - example: 'my-endpoint-group' - description: 'Endpoint group name' - Description: - type: 'string' - example: 'Endpoint group description' - description: 'Endpoint group description' - Tags: - type: 'array' - description: 'List of tags associated to the endpoint group' - items: - type: 'string' - example: 'zone/east-coast' - description: 'Tag' - UserAccessPolicies: - $ref: '#/definitions/UserAccessPolicies' - TeamAccessPolicies: - $ref: '#/definitions/TeamAccessPolicies' - UserCreateRequest: - type: 'object' - required: - - 'Password' - - 'Role' - - 'Username' - properties: - Username: - type: 'string' - example: 'bob' - description: 'Username' - Password: - type: 'string' - example: 'cg9Wgky3' - description: 'Password' - Role: - type: 'integer' - example: 1 - description: 'User role (1 for administrator account and 2 for regular account)' - UserListResponse: - type: 'array' - items: - $ref: '#/definitions/UserSubset' - UserUpdateRequest: - type: 'object' - properties: - Password: - type: 'string' - example: 'cg9Wgky3' - description: 'Password' - Role: - type: 'integer' - example: 1 - description: 'User role (1 for administrator account and 2 for regular account)' - UserMembershipsResponse: - type: 'array' - items: - $ref: '#/definitions/TeamMembership' - UserPasswordCheckRequest: - type: 'object' - required: - - 'Password' - properties: - Password: - type: 'string' - example: 'cg9Wgky3' - description: 'Password' - UserPasswordCheckResponse: - type: 'object' - properties: - valid: - type: 'boolean' - example: true - description: 'Is the password valid' - TagListResponse: - type: 'array' - items: - $ref: '#/definitions/Tag' - TagCreateRequest: - type: 'object' - required: - - 'Name' - properties: - Name: - type: 'string' - example: 'org/acme' - description: 'Name' - TeamCreateRequest: - type: 'object' - required: - - 'Name' - properties: - Name: - type: 'string' - example: 'developers' - description: 'Name' - TeamListResponse: - type: 'array' - items: - $ref: '#/definitions/Team' - TeamUpdateRequest: - type: 'object' - required: - - 'Name' - properties: - Name: - type: 'string' - example: 'developers' - description: 'Name' - TeamMembershipsResponse: - type: 'array' - items: - $ref: '#/definitions/TeamMembership' - - TeamMembershipCreateRequest: - type: 'object' - required: - - 'UserID' - - 'TeamID' - - 'Role' - properties: - UserID: - type: 'integer' - example: 1 - description: 'User identifier' - TeamID: - type: 'integer' - example: 1 - description: 'Team identifier' - Role: - type: 'integer' - example: 1 - description: 'Role for the user inside the team (1 for leader and 2 for regular member)' - TeamMembershipListResponse: - type: 'array' - items: - $ref: '#/definitions/TeamMembership' - TeamMembershipUpdateRequest: - type: 'object' - required: - - 'UserID' - - 'TeamID' - - 'Role' - properties: - UserID: - type: 'integer' - example: 1 - description: 'User identifier' - TeamID: - type: 'integer' - example: 1 - description: 'Team identifier' - Role: - type: 'integer' - example: 1 - description: 'Role for the user inside the team (1 for leader and 2 for regular member)' - SettingsLDAPCheckRequest: - type: 'object' - properties: - LDAPSettings: - $ref: '#/definitions/LDAPSettings' - UserAdminInitRequest: - type: 'object' - properties: - Username: - type: 'string' - example: 'admin' - description: 'Username for the admin user' - Password: - type: 'string' - example: 'admin-password' - description: 'Password for the admin user' - TemplateListResponse: - type: 'array' - items: - $ref: '#/definitions/Template' - TemplateCreateRequest: - type: 'object' - required: - - 'type' - - 'title' - - 'description' - properties: - type: - type: 'integer' - example: 1 - description: 'Template type. Valid values are: 1 (container), 2 (Swarm stack) or 3 (Compose stack)' - title: - type: 'string' - example: 'Nginx' - description: 'Title of the template' - description: - type: 'string' - example: 'High performance web server' - description: 'Description of the template' - administrator_only: - type: 'boolean' - example: true - description: 'Whether the template should be available to administrators only' - image: - type: 'string' - example: 'nginx:latest' - description: 'Image associated to a container template. Mandatory for a container template' - repository: - $ref: '#/definitions/TemplateRepository' - name: - type: 'string' - example: 'mystackname' - description: 'Default name for the stack/container to be used on deployment' - logo: - type: 'string' - example: 'https://cloudinovasi.id/assets/img/logos/nginx.png' - description: "URL of the template's logo" - env: - type: 'array' - description: 'A list of environment variables used during the template deployment' - items: - $ref: '#/definitions/TemplateEnv' - note: - type: 'string' - example: 'This is my custom template' - description: 'A note that will be displayed in the UI. Supports HTML content' - platform: - type: 'string' - example: 'linux' - description: "Platform associated to the template. Valid values are: 'linux', 'windows' or leave empty for multi-platform" - categories: - type: 'array' - description: 'A list of categories associated to the template' - items: - type: 'string' - example: 'database' - registry: - type: 'string' - example: 'quay.io' - description: 'The URL of a registry associated to the image for a container template' - command: - type: 'string' - example: 'ls -lah' - description: 'The command that will be executed in a container template' - network: - type: 'string' - example: 'mynet' - description: 'Name of a network that will be used on container deployment if it exists inside the environment' - volumes: - type: 'array' - description: 'A list of volumes used during the container template deployment' - items: - $ref: '#/definitions/TemplateVolume' - ports: - type: 'array' - description: 'A list of ports exposed by the container' - items: - type: 'string' - example: '8080:80/tcp' - labels: - type: 'array' - description: 'Container labels' - items: - $ref: '#/definitions/Pair' - privileged: - type: 'boolean' - example: true - description: 'Whether the container should be started in privileged mode' - interactive: - type: 'boolean' - example: true - description: 'Whether the container should be started in interactive mode (-i -t equivalent on the CLI)' - restart_policy: - type: 'string' - example: 'on-failure' - description: 'Container restart policy' - hostname: - type: 'string' - example: 'mycontainer' - description: 'Container hostname' - TemplateUpdateRequest: - type: 'object' - properties: - type: - type: 'integer' - example: 1 - description: 'Template type. Valid values are: 1 (container), 2 (Swarm stack) or 3 (Compose stack)' - title: - type: 'string' - example: 'Nginx' - description: 'Title of the template' - description: - type: 'string' - example: 'High performance web server' - description: 'Description of the template' - administrator_only: - type: 'boolean' - example: true - description: 'Whether the template should be available to administrators only' - image: - type: 'string' - example: 'nginx:latest' - description: 'Image associated to a container template. Mandatory for a container template' - repository: - $ref: '#/definitions/TemplateRepository' - name: - type: 'string' - example: 'mystackname' - description: 'Default name for the stack/container to be used on deployment' - logo: - type: 'string' - example: 'https://cloudinovasi.id/assets/img/logos/nginx.png' - description: "URL of the template's logo" - env: - type: 'array' - description: 'A list of environment variables used during the template deployment' - items: - $ref: '#/definitions/TemplateEnv' - note: - type: 'string' - example: 'This is my custom template' - description: 'A note that will be displayed in the UI. Supports HTML content' - platform: - type: 'string' - example: 'linux' - description: "Platform associated to the template. Valid values are: 'linux', 'windows' or leave empty for multi-platform" - categories: - type: 'array' - description: 'A list of categories associated to the template' - items: - type: 'string' - example: 'database' - registry: - type: 'string' - example: 'quay.io' - description: 'The URL of a registry associated to the image for a container template' - command: - type: 'string' - example: 'ls -lah' - description: 'The command that will be executed in a container template' - network: - type: 'string' - example: 'mynet' - description: 'Name of a network that will be used on container deployment if it exists inside the environment' - volumes: - type: 'array' - description: 'A list of volumes used during the container template deployment' - items: - $ref: '#/definitions/TemplateVolume' - ports: - type: 'array' - description: 'A list of ports exposed by the container' - items: - type: 'string' - example: '8080:80/tcp' - labels: - type: 'array' - description: 'Container labels' - items: - $ref: '#/definitions/Pair' - privileged: - type: 'boolean' - example: true - description: 'Whether the container should be started in privileged mode' - interactive: - type: 'boolean' - example: true - description: 'Whether the container should be started in interactive mode (-i -t equivalent on the CLI)' - restart_policy: - type: 'string' - example: 'on-failure' - description: 'Container restart policy' - hostname: - type: 'string' - example: 'mycontainer' - description: 'Container hostname' - Template: - type: 'object' - properties: - id: - type: 'integer' - example: 1 - description: 'Template identifier' - type: - type: 'integer' - example: 1 - description: 'Template type. Valid values are: 1 (container), 2 (Swarm stack) or 3 (Compose stack)' - title: - type: 'string' - example: 'Nginx' - description: 'Title of the template' - description: - type: 'string' - example: 'High performance web server' - description: 'Description of the template' - administrator_only: - type: 'boolean' - example: true - description: 'Whether the template should be available to administrators only' - image: - type: 'string' - example: 'nginx:latest' - description: 'Image associated to a container template. Mandatory for a container template' - repository: - $ref: '#/definitions/TemplateRepository' - name: - type: 'string' - example: 'mystackname' - description: 'Default name for the stack/container to be used on deployment' - logo: - type: 'string' - example: 'https://cloudinovasi.id/assets/img/logos/nginx.png' - description: "URL of the template's logo" - env: - type: 'array' - description: 'A list of environment variables used during the template deployment' - items: - $ref: '#/definitions/TemplateEnv' - note: - type: 'string' - example: 'This is my custom template' - description: 'A note that will be displayed in the UI. Supports HTML content' - platform: - type: 'string' - example: 'linux' - description: "Platform associated to the template. Valid values are: 'linux', 'windows' or leave empty for multi-platform" - categories: - type: 'array' - description: 'A list of categories associated to the template' - items: - type: 'string' - example: 'database' - registry: - type: 'string' - example: 'quay.io' - description: 'The URL of a registry associated to the image for a container template' - command: - type: 'string' - example: 'ls -lah' - description: 'The command that will be executed in a container template' - network: - type: 'string' - example: 'mynet' - description: 'Name of a network that will be used on container deployment if it exists inside the environment' - volumes: - type: 'array' - description: 'A list of volumes used during the container template deployment' - items: - $ref: '#/definitions/TemplateVolume' - ports: - type: 'array' - description: 'A list of ports exposed by the container' - items: - type: 'string' - example: '8080:80/tcp' - labels: - type: 'array' - description: 'Container labels' - items: - $ref: '#/definitions/Pair' - privileged: - type: 'boolean' - example: true - description: 'Whether the container should be started in privileged mode' - interactive: - type: 'boolean' - example: true - description: 'Whether the container should be started in interactive mode (-i -t equivalent on the CLI)' - restart_policy: - type: 'string' - example: 'on-failure' - description: 'Container restart policy' - hostname: - type: 'string' - example: 'mycontainer' - description: 'Container hostname' - TemplateVolume: - type: 'object' - properties: - container: - type: 'string' - example: '/data' - description: 'Path inside the container' - bind: - type: 'string' - example: '/tmp' - description: 'Path on the host' - readonly: - type: 'boolean' - example: true - description: 'Whether the volume used should be readonly' - TemplateEnv: - type: 'object' - properties: - name: - type: 'string' - example: 'MYSQL_ROOT_PASSWORD' - description: 'name of the environment variable' - label: - type: 'string' - example: 'Root password' - description: 'Text for the label that will be generated in the UI' - description: - type: 'string' - example: 'MySQL root account password' - description: 'Content of the tooltip that will be generated in the UI' - default: - type: 'string' - example: 'default_value' - description: 'Default value that will be set for the variable' - preset: - type: 'boolean' - example: true - description: 'If set to true, will not generate any input for this variable in the UI' - select: - type: 'array' - description: 'A list of name/value that will be used to generate a dropdown in the UI' - items: - $ref: '#/definitions/TemplateEnvSelect' - TemplateEnvSelect: - type: 'object' - properties: - text: - type: 'string' - example: 'text value' - description: 'Some text that will displayed as a choice' - value: - type: 'string' - example: 'value' - description: 'A value that will be associated to the choice' - default: - type: 'boolean' - example: true - description: 'Will set this choice as the default choice' - TemplateRepository: - type: 'object' - required: - - 'URL' - properties: - URL: - type: 'string' - example: 'https://github.com/portainer/portainer-compose' - description: 'URL of a git repository used to deploy a stack template. Mandatory for a Swarm/Compose stack template' - stackfile: - type: 'string' - example: './subfolder/docker-compose.yml' - description: 'Path to the stack file inside the git repository' - StackMigrateRequest: - type: 'object' - required: - - 'EndpointID' - properties: - EndpointID: - type: 'integer' - example: 2 - description: 'Endpoint identifier of the target endpoint where the stack will be relocated' - SwarmID: - type: 'string' - example: 'jpofkc0i9uo9wtx1zesuk649w' - description: 'Swarm cluster identifier, must match the identifier of the cluster where the stack will be relocated' - Name: - type: 'string' - example: 'new-stack' - description: 'If provided will rename the migrated stack' - EndpointJobRequest: - type: 'object' - required: - - 'Image' - - 'FileContent' - properties: - Image: - type: 'string' - example: 'ubuntu:latest' - description: 'Container image which will be used to execute the job' - FileContent: - type: 'string' - example: 'ls -lah /host/tmp' - description: 'Content of the job script' - StackCreateRequest: - type: 'object' - required: - - 'Name' - properties: - Name: - type: 'string' - example: 'myStack' - description: 'Name of the stack' - SwarmID: - type: 'string' - example: 'jpofkc0i9uo9wtx1zesuk649w' - description: 'Swarm cluster identifier. Required when creating a Swarm stack (type 1).' - StackFileContent: - type: 'string' - example: "version: 3\n services:\n web:\n image:nginx" - description: "Content of the Stack file. Required when using the 'string' deployment method." - RepositoryURL: - type: 'string' - example: 'https://github.com/openfaas/faas' - description: "URL of a Git repository hosting the Stack file. Required when using the 'repository' deployment method." - RepositoryReferenceName: - type: 'string' - example: 'refs/heads/master' - description: "Reference name of a Git repository hosting the Stack file. Used in 'repository' deployment method." - ComposeFilePathInRepository: - type: 'string' - example: 'docker-compose.yml' - description: "Path to the Stack file inside the Git repository. Will default to 'docker-compose.yml' if not specified." - RepositoryAuthentication: - type: 'boolean' - example: true - description: 'Use basic authentication to clone the Git repository.' - RepositoryUsername: - type: 'string' - example: 'myGitUsername' - description: 'Username used in basic authentication. Required when RepositoryAuthentication is true.' - RepositoryPassword: - type: 'string' - example: 'myGitPassword' - description: 'Password used in basic authentication. Required when RepositoryAuthentication is true.' - Env: - type: 'array' - description: 'A list of environment variables used during stack deployment' - items: - $ref: '#/definitions/Stack_Env' - Stack_Env: - properties: - name: - type: 'string' - example: 'MYSQL_ROOT_PASSWORD' - value: - type: 'string' - example: 'password' - StackListResponse: - type: 'array' - items: - $ref: '#/definitions/Stack' - Stack: - type: 'object' - properties: - Id: - type: 'string' - example: 'myStack_jpofkc0i9uo9wtx1zesuk649w' - description: 'Stack identifier' - Name: - type: 'string' - example: 'myStack' - description: 'Stack name' - Type: - type: 'integer' - example: '1' - description: 'Stack type. 1 for a Swarm stack, 2 for a Compose stack' - EndpointID: - type: 'integer' - example: '1' - description: 'Endpoint identifier. Reference the endpoint that will be used for deployment ' - EntryPoint: - type: 'string' - example: 'docker-compose.yml' - description: 'Path to the Stack file' - SwarmID: - type: 'string' - example: 'jpofkc0i9uo9wtx1zesuk649w' - description: 'Cluster identifier of the Swarm cluster where the stack is deployed' - ProjectPath: - type: 'string' - example: '/data/compose/myStack_jpofkc0i9uo9wtx1zesuk649w' - description: 'Path on disk to the repository hosting the Stack file' - Env: - type: 'array' - description: 'A list of environment variables used during stack deployment' - items: - $ref: '#/definitions/Stack_Env' - StackUpdateRequest: - type: 'object' - properties: - StackFileContent: - type: 'string' - example: "version: 3\n services:\n web:\n image:nginx" - description: 'New content of the Stack file.' - Env: - type: 'array' - description: 'A list of environment variables used during stack deployment' - items: - $ref: '#/definitions/Stack_Env' - Prune: - type: 'boolean' - example: false - description: 'Prune services that are no longer referenced (only available for Swarm stacks)' - StackFileInspectResponse: - type: 'object' - properties: - StackFileContent: - type: 'string' - example: "version: 3\n services:\n web:\n image:nginx" - description: 'Content of the Stack file.' - LicenseInformation: - type: 'object' - properties: - LicenseKey: - type: 'string' - description: 'License key' - example: '1-uKmVwboSWVIZv5URmE0VRkpbPX0rrCVeDxJl97LZ0piltw2SU28DSrNwPZAHCEAwB2SeKm6BCFcVwzGMBEixKQ' - Company: - type: 'string' - description: 'Company associated to the license' - example: 'Portainer.io' - Expiration: - type: 'string' - description: 'License expiry date' - example: '2077-07-07' - Valid: - type: 'boolean' - description: 'Is the license valid' - example: 'true' - Extension: - type: 'object' - properties: - Id: - type: 'integer' - example: 1 - description: 'Extension identifier' - Name: - type: 'string' - example: 'Registry Manager' - description: 'Extension name' - Enabled: - type: 'boolean' - example: 'true' - description: 'Is the extension enabled' - ShortDescription: - type: 'string' - description: 'Short description about the extension' - example: 'Enable in-app registry management' - DescriptionURL: - type: 'string' - description: 'URL to the file containing the extension description' - example: https://portainer-io-assets.sfo2.digitaloceanspaces.com/description_registry_manager.html" - Available: - type: 'boolean' - description: 'Is the extension available for download and activation' - example: 'true' - Images: - type: 'array' - description: 'List of screenshot URLs' - items: - type: 'string' - example: 'https://portainer-io-assets.sfo2.digitaloceanspaces.com/screenshots/rm01.png' - description: 'Screenshot URL' - Logo: - type: 'string' - description: 'Icon associated to the extension' - example: 'fa-database' - Price: - type: 'string' - description: 'Extension price' - example: 'US$9.95' - PriceDescription: - type: 'string' - description: 'Details about extension pricing' - example: 'Price per instance per year' - ShopURL: - type: 'string' - description: 'URL used to buy the extension' - example: 'https://portainer.io/checkout/?add-to-cart=1164' - UpdateAvailable: - type: 'boolean' - description: 'Is an update available for this extension' - example: 'true' - Version: - type: 'string' - description: 'Extension version' - example: '1.0.0' - License: - $ref: '#/definitions/LicenseInformation' - ExtensionListResponse: - type: 'array' - items: - $ref: '#/definitions/Extension' - ExtensionCreateRequest: - type: 'object' - required: - - 'License' - properties: - License: - type: 'string' - example: '1-uKmVwboSWVIZv5URmE0VRkpbPX0rrCVeDxJl97LZ0piltw2SU28DSrNwPZAHCEAwB2SeKm6BCFcVwzGMBEixKQ' - description: 'License key' - ExtensionUpdateRequest: - type: 'object' - required: - - 'Version' - properties: - Version: - type: 'string' - example: '1.1.0' - description: 'New version of the extension' - RoleListResponse: - type: 'array' - items: - $ref: '#/definitions/Role' - Role: - type: 'object' - properties: - Id: - type: 'integer' - description: 'Role identifier' - example: 2 - Name: - type: 'string' - description: 'Role name' - example: 'HelpDesk' - Description: - type: 'string' - description: 'Role description' - example: 'Read-only access of all resources in an endpoint' - Authorizations: - $ref: '#/definitions/Authorizations' - Authorizations: - type: 'object' - description: 'Authorizations associated to a role' - additionalProperties: - type: 'object' - properties: - authorization: - type: 'string' - value: - type: 'boolean' - example: - 'DockerContainerList': true - 'DockerVolumeList': true + in: header + name: Authorization + type: apiKey +swagger: "2.0" +tags: +- description: Authenticate against Portainer HTTP API + name: auth +- description: Manage Custom Templates + name: custom_templates +- description: Manage how Portainer connects to the DockerHub + name: dockerhub +- description: Manage Edge Groups + name: edge_groups +- description: Manage Edge Jobs + name: edge_jobs +- description: Manage Edge Stacks + name: edge_stacks +- description: Manage Edge Templates + name: edge_templates +- description: Manage Edge related endpoint settings + name: edge +- description: Manage Docker environments + name: endpoints +- description: Manage endpoint groups + name: endpoint_groups +- description: Fetch the message of the day + name: motd +- description: Manage Docker registries + name: registries +- description: Manage access control on Docker resources + name: resource_controls +- description: Manage roles + name: roles +- description: Manage Portainer settings + name: settings +- description: Information about the Portainer instance + name: status +- description: Manage Docker stacks + name: stacks +- description: Manage users + name: users +- description: Manage tags + name: tags +- description: Manage teams + name: teams +- description: Manage team memberships + name: team_memberships +- description: Manage App Templates + name: templates +- description: Manage stacks + name: stacks +- description: Upload files + name: upload +- description: Manage webhooks + name: webhooks +- description: Create exec sessions using websockets + name: websocket diff --git a/app/agent/components/file-uploader/fileUploader.html b/app/agent/components/file-uploader/fileUploader.html index 5354b5c2f..27b741b0a 100644 --- a/app/agent/components/file-uploader/fileUploader.html +++ b/app/agent/components/file-uploader/fileUploader.html @@ -1,3 +1,3 @@ - diff --git a/app/agent/rest/dockerhub.js b/app/agent/rest/dockerhub.js new file mode 100644 index 000000000..b48481e8a --- /dev/null +++ b/app/agent/rest/dockerhub.js @@ -0,0 +1,13 @@ +import angular from 'angular'; + +angular.module('portainer.agent').factory('AgentDockerhub', AgentDockerhub); + +function AgentDockerhub($resource, API_ENDPOINT_ENDPOINTS) { + return $resource( + `${API_ENDPOINT_ENDPOINTS}/:endpointId/:endpointType/v2/dockerhub`, + {}, + { + limits: { method: 'GET' }, + } + ); +} diff --git a/app/assets/css/app.css b/app/assets/css/app.css index d42b10ee0..e41e1b01c 100644 --- a/app/assets/css/app.css +++ b/app/assets/css/app.css @@ -625,57 +625,6 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active { margin-left: 21px; } -/* switch box */ -:root { - --switch-size: 24px; -} - -.switch input { - display: none; -} - -.switch i, -.bootbox-form .checkbox i { - display: inline-block; - vertical-align: middle; - cursor: pointer; - padding-right: var(--switch-size); - transition: all ease 0.2s; - -webkit-transition: all ease 0.2s; - -moz-transition: all ease 0.2s; - -o-transition: all ease 0.2s; - border-radius: var(--switch-size); - box-shadow: inset 0 0 1px 1px rgba(0, 0, 0, 0.5); -} - -.switch i:before, -.bootbox-form .checkbox i:before { - display: block; - content: ''; - width: var(--switch-size); - height: var(--switch-size); - border-radius: var(--switch-size); - background: white; - box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.5); -} - -.switch :checked + i, -.bootbox-form .checkbox :checked ~ i { - padding-right: 0; - padding-left: var(--switch-size); - -webkit-box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7; - -moz-box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7; - box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7; -} -/* !switch box */ - -/* small switch box */ -.switch.small { - --switch-size: 12px; -} - -/* !small switch box */ - .boxselector_wrapper { display: flex; flex-flow: row wrap; @@ -927,6 +876,27 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active { z-index: 2; } +.striketext:before, +.striketext:after { + background-color: #777777; + content: ''; + display: inline-block; + height: 1px; + position: relative; + vertical-align: middle; + width: 50%; +} + +.striketext:before { + right: 0.5em; + margin-left: -50%; +} + +.striketext:after { + left: 0.5em; + margin-right: -50%; +} + /*bootbox override*/ .modal-open { padding-right: 0 !important; @@ -1042,3 +1012,14 @@ json-tree .branch-preview { background-color: #337ab7; } /* !spinkit override */ + +.w-full { + width: 100%; +} + +/* uib-typeahead override */ +#scrollable-dropdown-menu .dropdown-menu { + max-height: 300px; + overflow-y: auto; +} +/* !uib-typeahead override */ diff --git a/app/constants.js b/app/constants.js index 3dba966dc..547c7d518 100644 --- a/app/constants.js +++ b/app/constants.js @@ -27,4 +27,5 @@ angular .constant('APPLICATION_CACHE_VALIDITY', 3600) .constant('CONSOLE_COMMANDS_LABEL_PREFIX', 'io.portainer.commands.') .constant('PREDEFINED_NETWORKS', ['host', 'bridge', 'none']) + .constant('KUBERNETES_DEFAULT_NAMESPACE', 'default') .constant('KUBERNETES_SYSTEM_NAMESPACES', ['kube-system', 'kube-public', 'kube-node-lease', 'portainer']); diff --git a/app/docker/__module.js b/app/docker/__module.js index 33495ab47..516134d7d 100644 --- a/app/docker/__module.js +++ b/app/docker/__module.js @@ -581,6 +581,16 @@ angular.module('portainer.docker', ['portainer.app']).config([ }, }; + const dockerFeaturesConfiguration = { + name: 'docker.featuresConfiguration', + url: '/feat-config', + views: { + 'content@': { + component: 'dockerFeaturesConfigurationView', + }, + }, + }; + $stateRegistryProvider.register(configs); $stateRegistryProvider.register(config); $stateRegistryProvider.register(configCreation); @@ -630,5 +640,6 @@ angular.module('portainer.docker', ['portainer.app']).config([ $stateRegistryProvider.register(volume); $stateRegistryProvider.register(volumeBrowse); $stateRegistryProvider.register(volumeCreation); + $stateRegistryProvider.register(dockerFeaturesConfiguration); }, ]); diff --git a/app/docker/components/container-capabilities/containerCapabilities.html b/app/docker/components/container-capabilities/containerCapabilities.html index d6fe159b4..22d6b9aed 100644 --- a/app/docker/components/container-capabilities/containerCapabilities.html +++ b/app/docker/components/container-capabilities/containerCapabilities.html @@ -3,17 +3,18 @@ Container capabilities
-
-
- +
+
+
+ +
+
+ +
-
- -
-
diff --git a/app/docker/components/datatables/containers-datatable/containersDatatable.html b/app/docker/components/datatables/containers-datatable/containersDatatable.html index 9ba136c4c..12dbc9c3f 100644 --- a/app/docker/components/datatables/containers-datatable/containersDatatable.html +++ b/app/docker/components/datatables/containers-datatable/containersDatatable.html @@ -4,100 +4,8 @@
{{ $ctrl.titleText }}
- - Columns - - + + Settings