- All parameters are named

- Add create_stack method
- Add update_stack_acl method
- Add get_stack_id method
This commit is contained in:
Bastien Bonnefoy 2019-04-03 13:18:29 +00:00 committed by VALLEY Antoine (T0173847)
parent d526a783ec
commit 42cca1ad8d
2 changed files with 346 additions and 65 deletions

120
README.md
View file

@ -47,24 +47,47 @@ portainer-cli login username password
portainer-cli login douglas d1234
```
### update_stack command
### create_stack command
Update stack.
Create a stack.
```bash
portainer-cli update_stack id endpoint_id [stack_file] [-env-file]
portainer-cli create_stack -n stack_name -e endpoint_id -sf stack_file
```
**E.g:**
```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
```
#### 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 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 endpoint_id [stack_file] --env.var=value
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.
@ -73,10 +96,95 @@ Where `var` is the environment variable name and `value` is the environment vari
| Flag | Description |
|--|--|
| `-env-file` | Pass env file path, usually `.env` |
| `-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 |
| `-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.

View file

@ -29,19 +29,28 @@ class PortainerCLI(object):
COMMAND_CONFIGURE = 'configure'
COMMAND_LOGIN = 'login'
COMMAND_REQUEST = 'request'
COMMAND_CREATE_STACK = 'create_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 = [
COMMAND_CONFIGURE,
COMMAND_LOGIN,
COMMAND_REQUEST,
COMMAND_CREATE_STACK,
COMMAND_UPDATE_STACK,
COMMAND_UPDATE_STACK_ACL,
COMMAND_CREATE_OR_UPDATE_STACK,
COMMAND_GET_STACK_ID,
COMMAND_UPDATE_REGISTRY
]
METHOD_GET = 'GET'
METHOD_POST = 'POST'
METHOD_PUT = 'PUT'
METHOD_DELETE = 'DELETE'
local = False
_base_url = 'http://localhost:9000/'
@ -143,42 +152,109 @@ class PortainerCLI(object):
logger.info('logged with jwt: {}'.format(jwt))
self.jwt = jwt
def stack_exists(self, endpoint_id, stack_name):
stack_url = 'stacks'.format(endpoint_id)
result = self.request(
stack_url,
self.METHOD_GET
).json()
if not result:
return -1
else:
for stack in result:
if stack['Name'] == stack_name:
return stack['Id']
return -1
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()
def create_or_update_stack(self, endpoint_id, stack_file, stack_name, *args):
id = self.stack_exists(endpoint_id, stack_name)
if id == -1:
self.create_stack(endpoint_id, stack_file, stack_name)
else:
self.update_stack(id, endpoint_id, stack_file, *args)
# 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 create_stack(self, endpoint_id, stack_file, stack_name, env_file='', *args):
stack_url = 'stacks?type=1&method=string&endpointId={}'.format(
endpoint_id
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,
)
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()
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(
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):
if env_file:
env = {}
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:
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()
@ -188,10 +264,45 @@ class PortainerCLI(object):
lambda x: re.match(env_arg_regex, x),
args,
)
env = dict(map(
lambda x: env_arg_to_dict(x),
env_args,
))
env = dict(map(
lambda x: env_arg_to_dict(x),
env_args,
))
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'),
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]},
@ -210,49 +321,103 @@ class PortainerCLI(object):
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(
env_file=('Environment Variable file', 'option'),
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'),
clear_env=('Clear all env vars', 'flag', 'c'),
)
def update_stack(self, id, endpoint_id, stack_file='', env_file='',
def update_stack(self, stack_id, endpoint_id, stack_file='', env_file='',
prune=False, clear_env=False, *args):
logger.info('Updating stack id={}'.format(stack_id))
stack_url = 'stacks/{}?endpointId={}'.format(
id,
stack_id,
endpoint_id,
)
current = self.request(stack_url).json()
current = self.get_stack_by_id(stack_id, endpoint_id)
stack_file_content = ''
if stack_file:
stack_file_content = open(stack_file).read()
else:
stack_file_content = self.request(
'stacks/{}/file?endpointId={}'.format(
id,
stack_id,
endpoint_id,
)
).json().get('StackFileContent')
if env_file:
env = {}
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
else:
env_args = filter(
lambda x: re.match(env_arg_regex, x),
args,
)
env = dict(map(
lambda x: env_arg_to_dict(x),
env_args,
))
env = self.extract_env(env_file, *args)
if not clear_env:
current_env = dict(
map(
@ -269,7 +434,7 @@ class PortainerCLI(object):
),
)
data = {
'Id': id,
'Id': stack_id,
'StackFileContent': stack_file_content,
'Prune': prune,
'Env': final_env if len(final_env) > 0 else current.get('Env'),
@ -355,8 +520,16 @@ class PortainerCLI(object):
plac.call(self.configure, args)
elif command == self.COMMAND_LOGIN:
plac.call(self.login, args)
elif command == self.COMMAND_CREATE_STACK:
plac.call(self.create_stack, args)
elif command == self.COMMAND_UPDATE_STACK:
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: