Compare commits

..

No commits in common. "master" and "0.1.0" have entirely different histories.

6 changed files with 170 additions and 520 deletions

View file

@ -1,5 +1,4 @@
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,3 +12,6 @@ validators = ">=0.12.2"
"flake8" = "*" "flake8" = "*"
setuptools = "*" setuptools = "*"
wheel = "*" wheel = "*"
[requires]
python_version = "3.7"

124
Pipfile.lock generated Normal file
View file

@ -0,0 +1,124 @@
{
"_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"
}
}
}

145
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 with your [Portainer](https://portainer.io/) application, like in a continuous integration and continuous deployment environments. 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.
## Install ## Install
@ -47,166 +47,35 @@ portainer-cli login username password
portainer-cli login douglas d1234 portainer-cli login douglas d1234
``` ```
### create_stack command
Create a stack.
```bash
portainer-cli create_stack -n stack_name -e endpoint_id -sf stack_file
```
**E.g:**
```bash
portainer-cli create_stack -n stack_name -e 1 stack-test -sf docker-compose.yml
```
#### 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` |
### update_stack command ### update_stack command
Update a stack. Update stack.
```bash ```bash
portainer-cli update_stack -s stack_id -e endpoint_id -sf stack_file portainer-cli update_stack id endpoint_id [stack_file]
``` ```
**E.g:** **E.g:**
```bash ```bash
portainer-cli update_stack -s 18 -e 1 -sf docker-compose.yml portainer-cli update_stack 2 1 docker-compose.yml
``` ```
#### Environment variables arguments #### update_stack command environment variables arguments
```bash ```bash
portainer-cli update_stack id -s stack_id -e endpoint_id -sf stack_file --env.var=value portainer-cli update_stack id endpoint_id [stack_file] --env.var=value
``` ```
Where `var` is the environment variable name and `value` is the environment variable value. Where `var` is environment variable name and `value` is the environment variable value.
#### Flags #### Flags
| Flag | Description | | 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,19 +1,13 @@
#!/usr/bin/env python #!/usr/bin/env python
import os
import re
import logging import logging
import json
import plac import plac
import json
import re
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')
@ -22,41 +16,31 @@ 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 (split[1], split[2],) return {
'name': split[1],
'value': split[2],
}
class PortainerCLI(object): class PortainerCLI:
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):
@ -66,7 +50,7 @@ class PortainerCLI(object):
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 '{}/'.format(value) self._base_url = value if value.endswith('/') else f'{value}/'
self.persist() self.persist()
@property @property
@ -78,52 +62,22 @@ class PortainerCLI(object):
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 os.path.join( return Path.joinpath(Path.home(), '.portainer-cli.json')
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('persisting configuration: {}'.format(data)) logger.info(f'persisting configuration: {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:
@ -131,7 +85,7 @@ class PortainerCLI(object):
except FileNotFoundError: except FileNotFoundError:
return return
data = json.loads(data_file.read()) data = json.loads(data_file.read())
logger.info('configuration loaded: {}'.format(data)) logger.info(f'configuration loaded: {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')
@ -149,337 +103,49 @@ class PortainerCLI(object):
) )
r = response.json() r = response.json()
jwt = r.get('jwt') jwt = r.get('jwt')
logger.info('logged with jwt: {}'.format(jwt)) logger.info(f'logged with jwt: {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, stack_id, endpoint_id, stack_file='', env_file='', def update_stack(self, id, endpoint_id, stack_file='', prune=False,
prune=False, clear_env=False, *args): clear_env=False, *args):
logger.info('Updating stack id={}'.format(stack_id)) stack_url = f'stacks/{id}?endpointId={endpoint_id}'
stack_url = 'stacks/{}?endpointId={}'.format( current = self.request(stack_url).json()
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(
'stacks/{}/file?endpointId={}'.format( f'stacks/{id}/file?endpointId={endpoint_id}').json().get(
stack_id, 'StackFileContent')
endpoint_id, env_args = filter(
) lambda x: re.match(env_arg_regex, x),
).json().get('StackFileContent') args,
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': stack_id, 'Id': id,
'StackFileContent': stack_file_content, 'StackFileContent': stack_file_content,
'Prune': prune, 'Prune': prune,
'Env': final_env if len(final_env) > 0 else current.get('Env'), 'Env': env if len(env) > 0 or clear_env 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 = '{}api/{}'.format( url = f'{self.base_url}api/{path}'
self.base_url,
path,
)
session = Session() session = Session()
request = Request(method, url) request = Request(method, url)
prepped = request.prepare() prepped = request.prepare()
@ -488,13 +154,13 @@ class PortainerCLI(object):
try: try:
json.loads(data) json.loads(data)
prepped.body = data prepped.body = data
except Exception: except Exception as e:
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'] = 'Bearer {}'.format(self.jwt) prepped.headers['Authorization'] = f'Bearer {self.jwt}'
response = session.send(prepped, proxies=self.proxies, verify=False) response = session.send(prepped)
logger.debug('request response: {}'.format(response.content)) logger.debug(f'request response: {response.content}')
response.raise_for_status() response.raise_for_status()
if printc: if printc:
print(response.content.decode()) print(response.content.decode())
@ -516,22 +182,11 @@ class PortainerCLI(object):
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='>=2.7', python_requires='>=3.6',
) )