Compare commits

...

19 commits

Author SHA1 Message Date
VALLEY Antoine (T0173847)
083e0e95f0 Environement variable can both comme from file and parameters 2019-07-24 09:44:03 +02:00
VALLEY Antoine (T0173847)
bec593bf40 Fix environement variable extraction 2019-07-24 09:32:29 +02:00
VALLEY Antoine (T0173847)
54c4cede32 Fix env file parameter 2019-07-24 09:24:11 +02:00
Bastien Bonnefoy
42cca1ad8d - All parameters are named
- Add create_stack method
- Add update_stack_acl method
- Add get_stack_id method
2019-07-24 09:04:15 +02:00
Bastien Bonnefoy
d526a783ec - Add create_or_update_stack method
- Add proxies property based on environment variables for HTTP requests
2019-07-24 09:02:55 +02:00
Douglas Paz
bb34e5ef5c
Merge pull request #2 from Ilhasoft/feature/env-file
Feature -env-file
2018-12-03 15:38:22 -03:00
Douglas Paz
1adea2c6d6 Bump version to 0.3.0 2018-12-03 13:44:43 -03:00
Douglas Paz
7101e255be Update README 2018-12-03 13:44:04 -03:00
Douglas Paz
ddcc8e7887 Fix update_stack request url mount, add env-file param in update_stack, fix clear-env in update_stack 2018-12-03 13:32:00 -03:00
Douglas Paz
5dae82b37d Update sdist task 2018-11-30 16:03:29 -03:00
Douglas Paz
bb27b927f6 Bump version to 0.2.2 2018-11-30 16:00:29 -03:00
Douglas Paz
ecd6728df2 Fix string formt compat python 2 2018-11-30 15:59:55 -03:00
Douglas Paz
fddf664cc2 Merge 2018-11-29 17:33:14 -03:00
Douglas Paz
eb867fd2a3 Update setup.py 2018-11-29 17:31:44 -03:00
Douglas Paz
38ff11d4db Add support to python 2 2018-11-29 17:31:01 -03:00
Douglas Paz
31d2cc126b
Merge pull request #1 from jonoid/fix-readme
Fix grammar and add missing article
2018-10-23 11:02:54 -03:00
Douglas Paz
cf8aabf233 Bump version to 0.2.0 2018-10-22 11:22:06 -03:00
Douglas Paz
89f5c1244e Add update_registry command 2018-10-22 11:21:06 -03:00
Erick Jones
62f6b5505e Fix grammar and add missing article 2018-10-22 10:08:14 -03:00
6 changed files with 527 additions and 177 deletions

View file

@ -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:

View file

@ -12,6 +12,3 @@ validators = ">=0.12.2"
"flake8" = "*" "flake8" = "*"
setuptools = "*" setuptools = "*"
wheel = "*" wheel = "*"
[requires]
python_version = "3.7"

124
Pipfile.lock generated
View file

@ -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
View file

@ -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.

View file

@ -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)

View file

@ -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',
) )