mirror of
https://github.com/Hopiu/portainer-cli.git
synced 2026-03-28 03:30:28 +00:00
Compare commits
19 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
083e0e95f0 | ||
|
|
bec593bf40 | ||
|
|
54c4cede32 | ||
|
|
42cca1ad8d | ||
|
|
d526a783ec | ||
|
|
bb34e5ef5c | ||
|
|
1adea2c6d6 | ||
|
|
7101e255be | ||
|
|
ddcc8e7887 | ||
|
|
5dae82b37d | ||
|
|
bb27b927f6 | ||
|
|
ecd6728df2 | ||
|
|
fddf664cc2 | ||
|
|
eb867fd2a3 | ||
|
|
38ff11d4db | ||
|
|
31d2cc126b | ||
|
|
cf8aabf233 | ||
|
|
89f5c1244e | ||
|
|
62f6b5505e |
6 changed files with 527 additions and 177 deletions
1
Makefile
1
Makefile
|
|
@ -1,4 +1,5 @@
|
||||||
sdist:
|
sdist:
|
||||||
|
rm -rf dist/*
|
||||||
pipenv run python setup.py sdist bdist_wheel
|
pipenv run python setup.py sdist bdist_wheel
|
||||||
|
|
||||||
install:
|
install:
|
||||||
|
|
|
||||||
3
Pipfile
3
Pipfile
|
|
@ -12,6 +12,3 @@ validators = ">=0.12.2"
|
||||||
"flake8" = "*"
|
"flake8" = "*"
|
||||||
setuptools = "*"
|
setuptools = "*"
|
||||||
wheel = "*"
|
wheel = "*"
|
||||||
|
|
||||||
[requires]
|
|
||||||
python_version = "3.7"
|
|
||||||
|
|
|
||||||
124
Pipfile.lock
generated
124
Pipfile.lock
generated
|
|
@ -1,124 +0,0 @@
|
||||||
{
|
|
||||||
"_meta": {
|
|
||||||
"hash": {
|
|
||||||
"sha256": "2e28e1ee11aba53785a7bee2ec9b68d004035f148a7655dbdc0e537eeab9c2e3"
|
|
||||||
},
|
|
||||||
"pipfile-spec": 6,
|
|
||||||
"requires": {
|
|
||||||
"python_version": "3.7"
|
|
||||||
},
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"name": "pypi",
|
|
||||||
"url": "https://pypi.org/simple",
|
|
||||||
"verify_ssl": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"default": {
|
|
||||||
"certifi": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c",
|
|
||||||
"sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a"
|
|
||||||
],
|
|
||||||
"version": "==2018.10.15"
|
|
||||||
},
|
|
||||||
"chardet": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
|
||||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
|
||||||
],
|
|
||||||
"version": "==3.0.4"
|
|
||||||
},
|
|
||||||
"decorator": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:2c51dff8ef3c447388fe5e4453d24a2bf128d3a4c32af3fabef1f01c6851ab82",
|
|
||||||
"sha256:c39efa13fbdeb4506c476c9b3babf6a718da943dab7811c206005a4a956c080c"
|
|
||||||
],
|
|
||||||
"version": "==4.3.0"
|
|
||||||
},
|
|
||||||
"idna": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
|
|
||||||
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
|
|
||||||
],
|
|
||||||
"version": "==2.7"
|
|
||||||
},
|
|
||||||
"plac": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:879d3009bee474cc96b5d7a4ebdf6fa0c4931008ecb858caf09eed9ca302c8da",
|
|
||||||
"sha256:b03f967f535b3bf5a71b191fa5eb09872a5cfb1e3b377efc4138995e10ba36d7"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==1.0.0"
|
|
||||||
},
|
|
||||||
"requests": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c",
|
|
||||||
"sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==2.20.0"
|
|
||||||
},
|
|
||||||
"six": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
|
|
||||||
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
|
|
||||||
],
|
|
||||||
"version": "==1.11.0"
|
|
||||||
},
|
|
||||||
"urllib3": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae",
|
|
||||||
"sha256:8819bba37a02d143296a4d032373c4dd4aca11f6d4c9973335ca75f9c8475f59"
|
|
||||||
],
|
|
||||||
"version": "==1.24"
|
|
||||||
},
|
|
||||||
"validators": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:172ac45f7d1944ce4beca3c5c53ca7c83e9759e39fd3fedc1cf28e2130268706"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==0.12.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"develop": {
|
|
||||||
"flake8": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0",
|
|
||||||
"sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==3.5.0"
|
|
||||||
},
|
|
||||||
"mccabe": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
|
|
||||||
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
|
|
||||||
],
|
|
||||||
"version": "==0.6.1"
|
|
||||||
},
|
|
||||||
"pycodestyle": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766",
|
|
||||||
"sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9"
|
|
||||||
],
|
|
||||||
"version": "==2.3.1"
|
|
||||||
},
|
|
||||||
"pyflakes": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f",
|
|
||||||
"sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805"
|
|
||||||
],
|
|
||||||
"version": "==1.6.0"
|
|
||||||
},
|
|
||||||
"wheel": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:9fa1f772f1a2df2bd00ddb4fa57e1cc349301e1facb98fbe62329803a9ff1196",
|
|
||||||
"sha256:d215f4520a1ba1851a3c00ba2b4122665cd3d6b0834d2ba2816198b1e3024a0e"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==0.32.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
157
README.md
157
README.md
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Powered by [Ilhasoft's Web Team](http://www.ilhasoft.com.br/en/).
|
Powered by [Ilhasoft's Web Team](http://www.ilhasoft.com.br/en/).
|
||||||
|
|
||||||
Portainer CLI is a Python software to use in command line. Use this command line interface to easy communicate to your [Portainer](https://portainer.io/) application, like in a continuous integration and continuous deploy environments.
|
Portainer CLI is a Python software to use in command line. Use this command line interface to easy communicate with your [Portainer](https://portainer.io/) application, like in a continuous integration and continuous deployment environments.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
|
@ -47,35 +47,166 @@ portainer-cli login username password
|
||||||
portainer-cli login douglas d1234
|
portainer-cli login douglas d1234
|
||||||
```
|
```
|
||||||
|
|
||||||
### update_stack command
|
### create_stack command
|
||||||
|
|
||||||
Update stack.
|
Create a stack.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
portainer-cli update_stack id endpoint_id [stack_file]
|
portainer-cli create_stack -n stack_name -e endpoint_id -sf stack_file
|
||||||
```
|
```
|
||||||
|
|
||||||
**E.g:**
|
**E.g:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
portainer-cli update_stack 2 1 docker-compose.yml
|
portainer-cli create_stack -n stack_name -e 1 stack-test -sf docker-compose.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
#### update_stack command environment variables arguments
|
|
||||||
|
|
||||||
```bash
|
|
||||||
portainer-cli update_stack id endpoint_id [stack_file] --env.var=value
|
|
||||||
```
|
|
||||||
|
|
||||||
Where `var` is environment variable name and `value` is the environment variable value.
|
|
||||||
|
|
||||||
#### Flags
|
#### Flags
|
||||||
|
|
||||||
| Flag | Description |
|
| Flag | Description |
|
||||||
|--|--|
|
|--|--|
|
||||||
|
| `-n` or `-stack-name` | Stack name |
|
||||||
|
| `-e` or `-endpoint-id` | Endpoint id (required) |
|
||||||
|
| `-sf` or `-stack-file` |Stack file |
|
||||||
|
| `-ef` or `-env-file` | Pass env file path, usually `.env` |
|
||||||
|
|
||||||
|
### update_stack command
|
||||||
|
|
||||||
|
Update a stack.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
portainer-cli update_stack -s stack_id -e endpoint_id -sf stack_file
|
||||||
|
```
|
||||||
|
|
||||||
|
**E.g:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
portainer-cli update_stack -s 18 -e 1 -sf docker-compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Environment variables arguments
|
||||||
|
|
||||||
|
```bash
|
||||||
|
portainer-cli update_stack id -s stack_id -e endpoint_id -sf stack_file --env.var=value
|
||||||
|
```
|
||||||
|
|
||||||
|
Where `var` is the environment variable name and `value` is the environment variable value.
|
||||||
|
|
||||||
|
#### Flags
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|--|--|
|
||||||
|
| `-s` or `-stack-id` | Stack id |
|
||||||
|
| `-e` or `-endpoint-id` | Endpoint id (required) |
|
||||||
|
| `-sf` or `-stack-file` |Stack file |
|
||||||
|
| `-ef` or `-env-file` | Pass env file path, usually `.env` |
|
||||||
| `-p` or `--prune` | Prune services |
|
| `-p` or `--prune` | Prune services |
|
||||||
| `-c` or `--clear-env` | Clear all environment variables |
|
| `-c` or `--clear-env` | Clear all environment variables |
|
||||||
|
|
||||||
|
### create_or_update_stack command
|
||||||
|
|
||||||
|
Create or update a stack based on it's name.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
portainer-cli create_or_update_stack -n stack_name -e endpoint_id -sf stack_file
|
||||||
|
```
|
||||||
|
|
||||||
|
**E.g:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
portainer-cli update_stack -s 18 -e 1 -sf docker-compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Environment variables arguments
|
||||||
|
|
||||||
|
```bash
|
||||||
|
portainer-cli create_or_update_stack -n stack_name -e endpoint_id -sf stack_file --env.var=value
|
||||||
|
```
|
||||||
|
|
||||||
|
Where `var` is the environment variable name and `value` is the environment variable value.
|
||||||
|
|
||||||
|
#### Flags
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|--|--|
|
||||||
|
| `-n` or `-stack-name` | Stack name |
|
||||||
|
| `-e` or `-endpoint-id` | Endpoint id (required) |
|
||||||
|
| `-sf` or `-stack-file` |Stack file |
|
||||||
|
| `-ef` or `-env-file` | Pass env file path, usually `.env` |
|
||||||
|
| `-p` or `--prune` | Prune services |
|
||||||
|
| `-c` or `--clear-env` | Clear all environment variables |
|
||||||
|
|
||||||
|
### update_stack_acl command
|
||||||
|
|
||||||
|
Update acl associated to a stack
|
||||||
|
|
||||||
|
```bash
|
||||||
|
portainer-cli update_stack_acl -s stack_id -e endpoint_id -o ownership_type
|
||||||
|
```
|
||||||
|
|
||||||
|
Remark : you can either update by stack_id or stack_name (`-s` or `-n`)
|
||||||
|
|
||||||
|
**E.g:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
portainer-cli update_stack_acl -n stack-test -e 1 -o restricted -u user1,user2 -t team1,team2
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Flags
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|--|--|
|
||||||
|
| `-s` or `-stack-id` | Stack id |
|
||||||
|
| `-n` or `-stack-name` | Stack name |
|
||||||
|
| `-e` or `-endpoint-id` | Endpoint id (required) |
|
||||||
|
| `-o` or `-ownership-type` | Ownership type (`admin`|`restricted`,`public`) (required) |
|
||||||
|
| `-u` or `-users` | Comma separated list of user names (when `restricted`) |
|
||||||
|
| `-t` or `-teams` | Comma separated list of team names (when `restricted`) |
|
||||||
|
| `-c` or `-clear` | Clear users and teams before updateing them (when `restricted`) |
|
||||||
|
|
||||||
|
### get_stack_id command
|
||||||
|
|
||||||
|
Get stack id by it's name. return -1 if the stack does not exist
|
||||||
|
|
||||||
|
```bash
|
||||||
|
portainer-cli get_stack_id -n stack_name -e endpoint_id
|
||||||
|
```
|
||||||
|
|
||||||
|
**E.g:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
portainer-cli get_stack_id -n stack-test -e 1
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Flags
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|--|--|
|
||||||
|
| `-n` or `-stack-name` | Stack name |
|
||||||
|
| `-e` or `-endpoint-id` | Endpoint id (required) |
|
||||||
|
|
||||||
|
### update_registry command
|
||||||
|
|
||||||
|
Update registry.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
portainer-cli update_registry id [-name] [-url]
|
||||||
|
```
|
||||||
|
|
||||||
|
**E.g:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
portainer-cli update_registry 1 -name="Some registry" -url="some.url.com/r"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Authentication
|
||||||
|
|
||||||
|
You can use authentication passing `-a` or `--authentication` flag, but you must pass the `-username` and `-password` options.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
portainer-cli update_registry 1 -a -username=douglas -password=d1234
|
||||||
|
```
|
||||||
|
|
||||||
### request command
|
### request command
|
||||||
|
|
||||||
Make a request.
|
Make a request.
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,19 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
import logging
|
import os
|
||||||
import plac
|
|
||||||
import json
|
|
||||||
import re
|
import re
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
import plac
|
||||||
import validators
|
import validators
|
||||||
from pathlib import Path
|
|
||||||
from requests import Request, Session
|
from requests import Request, Session
|
||||||
|
|
||||||
__version__ = '0.1.0'
|
|
||||||
|
try:
|
||||||
|
FileNotFoundError
|
||||||
|
except NameError:
|
||||||
|
FileNotFoundError = IOError
|
||||||
|
|
||||||
|
__version__ = '0.3.0'
|
||||||
|
|
||||||
logger = logging.getLogger('portainer-cli')
|
logger = logging.getLogger('portainer-cli')
|
||||||
|
|
||||||
|
|
@ -16,31 +22,41 @@ env_arg_regex = r'--env\.(.+)=(.+)'
|
||||||
|
|
||||||
def env_arg_to_dict(s):
|
def env_arg_to_dict(s):
|
||||||
split = re.split(env_arg_regex, s)
|
split = re.split(env_arg_regex, s)
|
||||||
return {
|
return (split[1], split[2],)
|
||||||
'name': split[1],
|
|
||||||
'value': split[2],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class PortainerCLI:
|
class PortainerCLI(object):
|
||||||
COMMAND_CONFIGURE = 'configure'
|
COMMAND_CONFIGURE = 'configure'
|
||||||
COMMAND_LOGIN = 'login'
|
COMMAND_LOGIN = 'login'
|
||||||
COMMAND_REQUEST = 'request'
|
COMMAND_REQUEST = 'request'
|
||||||
|
COMMAND_CREATE_STACK = 'create_stack'
|
||||||
COMMAND_UPDATE_STACK = 'update_stack'
|
COMMAND_UPDATE_STACK = 'update_stack'
|
||||||
|
COMMAND_UPDATE_STACK_ACL = 'update_stack_acl'
|
||||||
|
COMMAND_CREATE_OR_UPDATE_STACK = 'create_or_update_stack'
|
||||||
|
COMMAND_GET_STACK_ID = 'get_stack_id'
|
||||||
|
COMMAND_UPDATE_REGISTRY = 'update_registry'
|
||||||
COMMANDS = [
|
COMMANDS = [
|
||||||
COMMAND_CONFIGURE,
|
COMMAND_CONFIGURE,
|
||||||
COMMAND_LOGIN,
|
COMMAND_LOGIN,
|
||||||
COMMAND_REQUEST,
|
COMMAND_REQUEST,
|
||||||
|
COMMAND_CREATE_STACK,
|
||||||
COMMAND_UPDATE_STACK,
|
COMMAND_UPDATE_STACK,
|
||||||
|
COMMAND_UPDATE_STACK_ACL,
|
||||||
|
COMMAND_CREATE_OR_UPDATE_STACK,
|
||||||
|
COMMAND_GET_STACK_ID,
|
||||||
|
COMMAND_UPDATE_REGISTRY
|
||||||
]
|
]
|
||||||
|
|
||||||
METHOD_GET = 'GET'
|
METHOD_GET = 'GET'
|
||||||
METHOD_POST = 'POST'
|
METHOD_POST = 'POST'
|
||||||
METHOD_PUT = 'PUT'
|
METHOD_PUT = 'PUT'
|
||||||
|
METHOD_DELETE = 'DELETE'
|
||||||
|
|
||||||
local = False
|
local = False
|
||||||
_base_url = 'http://localhost:9000/'
|
_base_url = 'http://localhost:9000/'
|
||||||
_jwt = None
|
_jwt = None
|
||||||
|
_proxies = {}
|
||||||
|
_swarm_id = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def base_url(self):
|
def base_url(self):
|
||||||
|
|
@ -50,7 +66,7 @@ class PortainerCLI:
|
||||||
def base_url(self, value):
|
def base_url(self, value):
|
||||||
if not validators.url(value):
|
if not validators.url(value):
|
||||||
raise Exception('Insert a valid base URL')
|
raise Exception('Insert a valid base URL')
|
||||||
self._base_url = value if value.endswith('/') else f'{value}/'
|
self._base_url = value if value.endswith('/') else '{}/'.format(value)
|
||||||
self.persist()
|
self.persist()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -62,22 +78,52 @@ class PortainerCLI:
|
||||||
self._jwt = value
|
self._jwt = value
|
||||||
self.persist()
|
self.persist()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def proxies(self):
|
||||||
|
return self._proxies
|
||||||
|
|
||||||
|
@proxies.setter
|
||||||
|
def proxies(self, value):
|
||||||
|
try:
|
||||||
|
self._proxies['http'] = os.environ['HTTP_PROXY']
|
||||||
|
except KeyError:
|
||||||
|
self._proxies['http'] = ''
|
||||||
|
try:
|
||||||
|
self._proxies['https'] = os.environ['HTTPS_PROXY']
|
||||||
|
except KeyError:
|
||||||
|
self._proxies['https'] = ''
|
||||||
|
if self._proxies['http'] == '' and self._proxies['https'] == '':
|
||||||
|
self._proxies = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def swarm_id(self):
|
||||||
|
return self._swarm_id
|
||||||
|
|
||||||
|
@swarm_id.setter
|
||||||
|
def swarm_id(self, value):
|
||||||
|
self._swarm_id = value
|
||||||
|
self.persist()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def data_path(self):
|
def data_path(self):
|
||||||
if self.local:
|
if self.local:
|
||||||
logger.debug('using local configuration file')
|
logger.debug('using local configuration file')
|
||||||
return '.portainer-cli.json'
|
return '.portainer-cli.json'
|
||||||
logger.debug('using user configuration file')
|
logger.debug('using user configuration file')
|
||||||
return Path.joinpath(Path.home(), '.portainer-cli.json')
|
return os.path.join(
|
||||||
|
os.path.expanduser('~'),
|
||||||
|
'.portainer-cli.json',
|
||||||
|
)
|
||||||
|
|
||||||
def persist(self):
|
def persist(self):
|
||||||
data = {
|
data = {
|
||||||
'base_url': self.base_url,
|
'base_url': self.base_url,
|
||||||
'jwt': self.jwt,
|
'jwt': self.jwt,
|
||||||
}
|
}
|
||||||
logger.info(f'persisting configuration: {data}')
|
logger.info('persisting configuration: {}'.format(data))
|
||||||
data_file = open(self.data_path, 'w+')
|
data_file = open(self.data_path, 'w+')
|
||||||
data_file.write(json.dumps(data))
|
data_file.write(json.dumps(data))
|
||||||
|
logger.info('configuration persisted in: {}'.format(self.data_path))
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
try:
|
try:
|
||||||
|
|
@ -85,7 +131,7 @@ class PortainerCLI:
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
return
|
return
|
||||||
data = json.loads(data_file.read())
|
data = json.loads(data_file.read())
|
||||||
logger.info(f'configuration loaded: {data}')
|
logger.info('configuration loaded: {}'.format(data))
|
||||||
self._base_url = data.get('base_url')
|
self._base_url = data.get('base_url')
|
||||||
self._jwt = data.get('jwt')
|
self._jwt = data.get('jwt')
|
||||||
|
|
||||||
|
|
@ -103,49 +149,337 @@ class PortainerCLI:
|
||||||
)
|
)
|
||||||
r = response.json()
|
r = response.json()
|
||||||
jwt = r.get('jwt')
|
jwt = r.get('jwt')
|
||||||
logger.info(f'logged with jwt: {jwt}')
|
logger.info('logged with jwt: {}'.format(jwt))
|
||||||
self.jwt = jwt
|
self.jwt = jwt
|
||||||
|
|
||||||
|
def get_users(self):
|
||||||
|
users_url = 'users'
|
||||||
|
return self.request(users_url, self.METHOD_GET).json()
|
||||||
|
|
||||||
|
# retrieve users by their names
|
||||||
|
def get_users_by_name(self, names):
|
||||||
|
all_users = self.get_users()
|
||||||
|
if not all_users:
|
||||||
|
logger.debug('No users found')
|
||||||
|
return []
|
||||||
|
users=[]
|
||||||
|
for name in names:
|
||||||
|
# searching for user
|
||||||
|
user = next(u for u in all_users if u['Username'] == name)
|
||||||
|
if not user:
|
||||||
|
logger.warn('User with name \'{}\' not found'.format(name))
|
||||||
|
else:
|
||||||
|
logger.debug('User with name \'{}\' found'.format(name))
|
||||||
|
users.append(user)
|
||||||
|
return users
|
||||||
|
|
||||||
|
# retrieve users by their names
|
||||||
|
def get_users_by_name(self, names):
|
||||||
|
all_users = self.get_users()
|
||||||
|
all_users_by_name = dict(map(
|
||||||
|
lambda u: (u['Username'], u),
|
||||||
|
all_users,
|
||||||
|
))
|
||||||
|
users = []
|
||||||
|
for name in names:
|
||||||
|
user = all_users_by_name.get(name)
|
||||||
|
if not user:
|
||||||
|
logger.warn('User with name \'{}\' not found'.format(name))
|
||||||
|
else:
|
||||||
|
logger.debug('User with name \'{}\' found'.format(name))
|
||||||
|
users.append(user)
|
||||||
|
return users
|
||||||
|
|
||||||
|
def get_teams(self):
|
||||||
|
teams_url = 'teams'
|
||||||
|
return self.request(teams_url, self.METHOD_GET).json()
|
||||||
|
|
||||||
|
# retrieve teams by their names
|
||||||
|
def get_teams_by_name(self, names):
|
||||||
|
all_teams = self.get_teams()
|
||||||
|
all_teams_by_name = dict(map(
|
||||||
|
lambda u: (u['Name'], u),
|
||||||
|
all_teams,
|
||||||
|
))
|
||||||
|
teams = []
|
||||||
|
for name in names:
|
||||||
|
team = all_teams_by_name.get(name)
|
||||||
|
if not team:
|
||||||
|
logger.warn('Team with name \'{}\' not found'.format(name))
|
||||||
|
else:
|
||||||
|
logger.debug('Team with name \'{}\' found'.format(name))
|
||||||
|
teams.append(team)
|
||||||
|
return teams
|
||||||
|
|
||||||
|
def get_stacks(self):
|
||||||
|
stack_url = 'stacks'
|
||||||
|
return self.request(stack_url, self.METHOD_GET).json()
|
||||||
|
|
||||||
|
def get_stack_by_id(self, stack_id, endpoint_id):
|
||||||
|
stack_url = 'stacks/{}?endpointId={}'.format(
|
||||||
|
stack_id,
|
||||||
|
endpoint_id,
|
||||||
|
)
|
||||||
|
stack = self.request(stack_url).json()
|
||||||
|
if not stack:
|
||||||
|
raise Exception('Stack with id={} does not exist'.format(stack_id))
|
||||||
|
return stack
|
||||||
|
|
||||||
|
def get_stack_by_name(self, stack_name, endpoint_id, mandatory=False):
|
||||||
|
result = self.get_stacks()
|
||||||
|
if result:
|
||||||
|
for stack in result:
|
||||||
|
if stack['Name'] == stack_name and stack['EndpointId'] == endpoint_id:
|
||||||
|
return stack
|
||||||
|
if mandatory:
|
||||||
|
raise Exception('Stack with name={} and endpoint_id={} does not exist'.format(stack_name, endpoint_id))
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Retrieve the stack if. -1 if the stack does not exist
|
||||||
@plac.annotations(
|
@plac.annotations(
|
||||||
|
stack_name=('Stack name', 'option', 'n'),
|
||||||
|
endpoint_id=('Endpoint id', 'option', 'e', int)
|
||||||
|
)
|
||||||
|
def get_stack_id(self, stack_name, endpoint_id):
|
||||||
|
stack = self.get_stack_by_name(stack_name, endpoint_id)
|
||||||
|
if not stack:
|
||||||
|
logger.debug('Stack with name={} does not exist'.format(stack_name))
|
||||||
|
return -1
|
||||||
|
logger.debug('Stack with name={} -> id={}'.format(stack_name, stack['Id']))
|
||||||
|
return stack['Id']
|
||||||
|
|
||||||
|
def extract_env(self, env_file='', *args):
|
||||||
|
# Handle --env.PARAM=VALUE
|
||||||
|
env_args = filter(
|
||||||
|
lambda x: re.match(env_arg_regex, x),
|
||||||
|
args,
|
||||||
|
)
|
||||||
|
env = dict(map(
|
||||||
|
lambda x: env_arg_to_dict(x),
|
||||||
|
env_args,
|
||||||
|
))
|
||||||
|
# Hand environement file
|
||||||
|
if env_file:
|
||||||
|
for env_line in open(env_file).readlines():
|
||||||
|
env_line = env_line.strip()
|
||||||
|
if not env_line or env_line.startswith('#') or '=' not in env_line:
|
||||||
|
continue
|
||||||
|
k, v = env_line.split('=', 1)
|
||||||
|
k, v = k.strip(), v.strip()
|
||||||
|
env[k] = v
|
||||||
|
|
||||||
|
return env
|
||||||
|
|
||||||
|
@plac.annotations(
|
||||||
|
stack_name=('Stack name', 'option', 'n', str),
|
||||||
|
endpoint_id=('Endpoint id', 'option', 'e', int),
|
||||||
|
stack_file=('Stack file', 'option', 'sf'),
|
||||||
|
env_file=('Environment Variable file', 'option', 'ef'),
|
||||||
|
prune=('Prune services', 'flag', 'p'),
|
||||||
|
clear_env=('Clear all env vars', 'flag', 'c'),
|
||||||
|
)
|
||||||
|
def create_or_update_stack(self, stack_name, endpoint_id, stack_file='', env_file='', prune=False, clear_env=False, *args):
|
||||||
|
logger.debug('create_or_update_stack')
|
||||||
|
stack_id = self.get_stack_id(stack_name, endpoint_id)
|
||||||
|
if stack_id == -1:
|
||||||
|
self.create_stack(stack_name, endpoint_id, stack_file, env_file, *args)
|
||||||
|
else:
|
||||||
|
self.update_stack(stack_id, endpoint_id, stack_file, env_file, prune, clear_env, *args)
|
||||||
|
|
||||||
|
@plac.annotations(
|
||||||
|
stack_name=('Stack name', 'option', 'n'),
|
||||||
|
endpoint_id=('Endpoint id', 'option', 'e', int),
|
||||||
|
stack_file=('Environment Variable file', 'option', 'sf'),
|
||||||
|
env_file=('Environment Variable file', 'option', 'ef')
|
||||||
|
)
|
||||||
|
def create_stack(self, stack_name, endpoint_id, stack_file='', env_file='', *args):
|
||||||
|
logger.info('Creating stack name={}'.format(stack_name))
|
||||||
|
stack_url = 'stacks?type=1&method=string&endpointId={}'.format(
|
||||||
|
endpoint_id
|
||||||
|
)
|
||||||
|
swarm_url = 'endpoints/{}/docker/swarm'.format(endpoint_id)
|
||||||
|
swarm_id = self.request(swarm_url, self.METHOD_GET).json().get('ID')
|
||||||
|
self.swarm_id = swarm_id
|
||||||
|
stack_file_content = open(stack_file).read()
|
||||||
|
|
||||||
|
env = self.extract_env(env_file, *args)
|
||||||
|
final_env = list(
|
||||||
|
map(
|
||||||
|
lambda x: {'name': x[0], 'value': x[1]},
|
||||||
|
env.items()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
data = {
|
||||||
|
'StackFileContent': stack_file_content,
|
||||||
|
'SwarmID': self.swarm_id,
|
||||||
|
'Name': stack_name,
|
||||||
|
'Env': final_env if len(final_env) > 0 else []
|
||||||
|
}
|
||||||
|
logger.debug('create stack data: {}'.format(data))
|
||||||
|
self.request(
|
||||||
|
stack_url,
|
||||||
|
self.METHOD_POST,
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_or_update_resource_control(self, stack, public, users, teams):
|
||||||
|
resource_control = stack['ResourceControl']
|
||||||
|
if resource_control and resource_control['Id'] != 0:
|
||||||
|
resource_path = 'resource_controls/{}'.format(resource_control['Id'])
|
||||||
|
data = {
|
||||||
|
'Public': public,
|
||||||
|
'Users': users,
|
||||||
|
'Teams': teams
|
||||||
|
}
|
||||||
|
logger.debug('Updating stack acl {} for stack {}: {}'.format(resource_control['Id'], stack['Id'], data))
|
||||||
|
self.request(resource_path, self.METHOD_PUT, data)
|
||||||
|
else:
|
||||||
|
resource_path = 'resource_controls'
|
||||||
|
data = {
|
||||||
|
'Type': 'stack',
|
||||||
|
'ResourceID': stack['Name'],
|
||||||
|
'Public': public,
|
||||||
|
'Users': users,
|
||||||
|
'Teams': teams
|
||||||
|
}
|
||||||
|
logger.debug('Creating stack acl for stack {}: {}'.format(stack['Id'], data))
|
||||||
|
self.request(resource_path, self.METHOD_POST, data)
|
||||||
|
|
||||||
|
|
||||||
|
@plac.annotations(
|
||||||
|
stack_id=('Stack id', 'option', 's', int),
|
||||||
|
stack_name=('Stack name', 'option', 'n', str),
|
||||||
|
endpoint_id=('Endpoint id', 'option', 'e', int),
|
||||||
|
ownership_type=('Ownership type', 'option', 'o', str, ['admin', 'restricted', 'public']),
|
||||||
|
users=('Allowed usernames (comma separated - restricted ownership_type only)', 'option', 'u'),
|
||||||
|
teams=('Allowed teams (comma separated - restricted ownership_type only)', 'option', 't'),
|
||||||
|
clear=('Clear acl (restricted ownership_type only)', 'flag', 'c')
|
||||||
|
)
|
||||||
|
def update_stack_acl(self, stack_id, stack_name, endpoint_id, ownership_type, users, teams, clear=False):
|
||||||
|
stack = None
|
||||||
|
if stack_id:
|
||||||
|
stack = self.get_stack_by_id(stack_id, endpoint_id)
|
||||||
|
elif stack_name:
|
||||||
|
stack = self.get_stack_by_name(stack_name, endpoint_id, True)
|
||||||
|
else:
|
||||||
|
raise Exception('Please provide either stack_name or stack_id')
|
||||||
|
|
||||||
|
logger.info('Updating acl of stack name={} - type={}'.format(stack['Name'], ownership_type))
|
||||||
|
|
||||||
|
resource_control = stack['ResourceControl']
|
||||||
|
|
||||||
|
if ownership_type == 'admin':
|
||||||
|
if resource_control and resource_control['Id'] != 0:
|
||||||
|
logger.debug('Deleting resource control with id {}'.format(resource_control['Id']))
|
||||||
|
resource_path = 'resource_controls/{}'.format(resource_control['Id'])
|
||||||
|
logger.debug('resource_path : {}'.format(resource_path))
|
||||||
|
self.request(resource_path, self.METHOD_DELETE)
|
||||||
|
else:
|
||||||
|
logger.debug('Nothing to do')
|
||||||
|
elif ownership_type == 'public':
|
||||||
|
self.create_or_update_resource_control(stack, True, [], [])
|
||||||
|
elif ownership_type == 'restricted':
|
||||||
|
users = map(lambda u: u['Id'], self.get_users_by_name(users.split(',')))
|
||||||
|
teams = map(lambda t: t['Id'], self.get_teams_by_name(teams.split(',')))
|
||||||
|
|
||||||
|
if (not clear) and resource_control:
|
||||||
|
logger.debug('Merging existing users / teams')
|
||||||
|
users = list(set().union(users, map(lambda u: u['UserId'], resource_control['UserAccesses'])))
|
||||||
|
teams = list(set().union(teams, map(lambda t: t['TeamId'], resource_control['TeamAccesses'])))
|
||||||
|
|
||||||
|
self.create_or_update_resource_control(stack, False, users, teams)
|
||||||
|
|
||||||
|
@plac.annotations(
|
||||||
|
stack_id=('Stack id', 'option', 's', int),
|
||||||
|
endpoint_id=('Endpoint id', 'option', 'e', int),
|
||||||
|
stack_file=('Stack file', 'option', 'sf'),
|
||||||
|
env_file=('Environment Variable file', 'option', 'ef'),
|
||||||
prune=('Prune services', 'flag', 'p'),
|
prune=('Prune services', 'flag', 'p'),
|
||||||
clear_env=('Clear all env vars', 'flag', 'c'),
|
clear_env=('Clear all env vars', 'flag', 'c'),
|
||||||
)
|
)
|
||||||
def update_stack(self, id, endpoint_id, stack_file='', prune=False,
|
def update_stack(self, stack_id, endpoint_id, stack_file='', env_file='',
|
||||||
clear_env=False, *args):
|
prune=False, clear_env=False, *args):
|
||||||
stack_url = f'stacks/{id}?endpointId={endpoint_id}'
|
logger.info('Updating stack id={}'.format(stack_id))
|
||||||
current = self.request(stack_url).json()
|
stack_url = 'stacks/{}?endpointId={}'.format(
|
||||||
|
stack_id,
|
||||||
|
endpoint_id,
|
||||||
|
)
|
||||||
|
current = self.get_stack_by_id(stack_id, endpoint_id)
|
||||||
stack_file_content = ''
|
stack_file_content = ''
|
||||||
if stack_file:
|
if stack_file:
|
||||||
stack_file_content = open(stack_file).read()
|
stack_file_content = open(stack_file).read()
|
||||||
else:
|
else:
|
||||||
stack_file_content = self.request(
|
stack_file_content = self.request(
|
||||||
f'stacks/{id}/file?endpointId={endpoint_id}').json().get(
|
'stacks/{}/file?endpointId={}'.format(
|
||||||
'StackFileContent')
|
stack_id,
|
||||||
env_args = filter(
|
endpoint_id,
|
||||||
lambda x: re.match(env_arg_regex, x),
|
)
|
||||||
args,
|
).json().get('StackFileContent')
|
||||||
|
|
||||||
|
env = self.extract_env(env_file, *args)
|
||||||
|
|
||||||
|
if not clear_env:
|
||||||
|
current_env = dict(
|
||||||
|
map(
|
||||||
|
lambda x: (x.get('name'), x.get('value'),),
|
||||||
|
current.get('Env'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
current_env.update(env)
|
||||||
|
env = current_env
|
||||||
|
final_env = list(
|
||||||
|
map(
|
||||||
|
lambda x: {'name': x[0], 'value': x[1]},
|
||||||
|
env.items()
|
||||||
|
),
|
||||||
)
|
)
|
||||||
env = list(map(
|
|
||||||
lambda x: env_arg_to_dict(x),
|
|
||||||
env_args,
|
|
||||||
))
|
|
||||||
data = {
|
data = {
|
||||||
'Id': id,
|
'Id': stack_id,
|
||||||
'StackFileContent': stack_file_content,
|
'StackFileContent': stack_file_content,
|
||||||
'Prune': prune,
|
'Prune': prune,
|
||||||
'Env': env if len(env) > 0 or clear_env else current.get('Env'),
|
'Env': final_env if len(final_env) > 0 else current.get('Env'),
|
||||||
}
|
}
|
||||||
|
logger.debug('update stack data: {}'.format(data))
|
||||||
self.request(
|
self.request(
|
||||||
stack_url,
|
stack_url,
|
||||||
self.METHOD_PUT,
|
self.METHOD_PUT,
|
||||||
data,
|
data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@plac.annotations(
|
||||||
|
name=('Name', 'option'),
|
||||||
|
url=('URL', 'option'),
|
||||||
|
authentication=('Use authentication', 'flag', 'a'),
|
||||||
|
username=('Username', 'option'),
|
||||||
|
password=('Password', 'option'),
|
||||||
|
)
|
||||||
|
def update_registry(self, id, name='', url='', authentication=False,
|
||||||
|
username='', password=''):
|
||||||
|
assert not authentication or (authentication and username and password)
|
||||||
|
registry_url = 'registries/{}'.format(id)
|
||||||
|
current = self.request(registry_url).json()
|
||||||
|
data = {
|
||||||
|
'Name': name or current.get('Name'),
|
||||||
|
'URL': url or current.get('URL'),
|
||||||
|
'Authentication': authentication,
|
||||||
|
'Username': username or current.get('Username'),
|
||||||
|
'Password': password,
|
||||||
|
}
|
||||||
|
self.request(
|
||||||
|
registry_url,
|
||||||
|
self.METHOD_PUT,
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
|
||||||
@plac.annotations(
|
@plac.annotations(
|
||||||
printc=('Print response content', 'flag', 'p'),
|
printc=('Print response content', 'flag', 'p'),
|
||||||
)
|
)
|
||||||
def request(self, path, method=METHOD_GET, data='', printc=False):
|
def request(self, path, method=METHOD_GET, data='', printc=False):
|
||||||
url = f'{self.base_url}api/{path}'
|
url = '{}api/{}'.format(
|
||||||
|
self.base_url,
|
||||||
|
path,
|
||||||
|
)
|
||||||
session = Session()
|
session = Session()
|
||||||
request = Request(method, url)
|
request = Request(method, url)
|
||||||
prepped = request.prepare()
|
prepped = request.prepare()
|
||||||
|
|
@ -154,13 +488,13 @@ class PortainerCLI:
|
||||||
try:
|
try:
|
||||||
json.loads(data)
|
json.loads(data)
|
||||||
prepped.body = data
|
prepped.body = data
|
||||||
except Exception as e:
|
except Exception:
|
||||||
prepped.body = json.dumps(data)
|
prepped.body = json.dumps(data)
|
||||||
prepped.headers['Content-Length'] = len(prepped.body)
|
prepped.headers['Content-Length'] = len(prepped.body)
|
||||||
if self.jwt:
|
if self.jwt:
|
||||||
prepped.headers['Authorization'] = f'Bearer {self.jwt}'
|
prepped.headers['Authorization'] = 'Bearer {}'.format(self.jwt)
|
||||||
response = session.send(prepped)
|
response = session.send(prepped, proxies=self.proxies, verify=False)
|
||||||
logger.debug(f'request response: {response.content}')
|
logger.debug('request response: {}'.format(response.content))
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
if printc:
|
if printc:
|
||||||
print(response.content.decode())
|
print(response.content.decode())
|
||||||
|
|
@ -182,11 +516,22 @@ class PortainerCLI:
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
self.local = local
|
self.local = local
|
||||||
self.load()
|
self.load()
|
||||||
|
self.proxies = {}
|
||||||
if command == self.COMMAND_CONFIGURE:
|
if command == self.COMMAND_CONFIGURE:
|
||||||
plac.call(self.configure, args)
|
plac.call(self.configure, args)
|
||||||
elif command == self.COMMAND_LOGIN:
|
elif command == self.COMMAND_LOGIN:
|
||||||
plac.call(self.login, args)
|
plac.call(self.login, args)
|
||||||
|
elif command == self.COMMAND_CREATE_STACK:
|
||||||
|
plac.call(self.create_stack, args)
|
||||||
elif command == self.COMMAND_UPDATE_STACK:
|
elif command == self.COMMAND_UPDATE_STACK:
|
||||||
plac.call(self.update_stack, args)
|
plac.call(self.update_stack, args)
|
||||||
|
elif command == self.COMMAND_UPDATE_STACK_ACL:
|
||||||
|
plac.call(self.update_stack_acl, args)
|
||||||
|
elif command == self.COMMAND_CREATE_OR_UPDATE_STACK:
|
||||||
|
plac.call(self.create_or_update_stack, args)
|
||||||
|
elif command == self.COMMAND_GET_STACK_ID:
|
||||||
|
plac.call(self.get_stack_id, args)
|
||||||
|
elif command == self.COMMAND_UPDATE_REGISTRY:
|
||||||
|
plac.call(self.update_registry, args)
|
||||||
elif command == self.COMMAND_REQUEST:
|
elif command == self.COMMAND_REQUEST:
|
||||||
plac.call(self.request, args)
|
plac.call(self.request, args)
|
||||||
|
|
|
||||||
4
setup.py
4
setup.py
|
|
@ -1,6 +1,6 @@
|
||||||
import setuptools
|
import setuptools
|
||||||
from portainer_cli import __version__
|
|
||||||
|
|
||||||
|
__version__ = '0.3.0'
|
||||||
with open('README.md', 'r') as fh:
|
with open('README.md', 'r') as fh:
|
||||||
long_description = fh.read()
|
long_description = fh.read()
|
||||||
|
|
||||||
|
|
@ -28,5 +28,5 @@ setuptools.setup(
|
||||||
'requests>=2.20.0',
|
'requests>=2.20.0',
|
||||||
'validators>=0.12.2',
|
'validators>=0.12.2',
|
||||||
],
|
],
|
||||||
python_requires='>=3.6',
|
python_requires='>=2.7',
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue