mirror of
https://github.com/Hopiu/portainer-cli.git
synced 2026-03-16 22:10:34 +00:00
Init portainer-cli project, add version 0.1.0
This commit is contained in:
parent
1d4caa8de7
commit
4b7ff1e1f7
11 changed files with 524 additions and 0 deletions
11
.editorconfig
Normal file
11
.editorconfig
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
insert_final_newline = true
|
||||
|
||||
[*.py]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -102,3 +102,9 @@ venv.bak/
|
|||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
|
||||
# ide
|
||||
.vscode/
|
||||
|
||||
# portainer cli
|
||||
.portainer-cli.json
|
||||
|
|
|
|||
19
LICENSE
Normal file
19
LICENSE
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2018 Ilhasoft
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
8
Makefile
Normal file
8
Makefile
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
sdist:
|
||||
pipenv run python setup.py sdist bdist_wheel
|
||||
|
||||
install:
|
||||
pipenv install --dev
|
||||
|
||||
lint:
|
||||
pipenv run flake8
|
||||
17
Pipfile
Normal file
17
Pipfile
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
plac = ">=1.0.0"
|
||||
requests = ">=2.20.0"
|
||||
validators = ">=0.12.2"
|
||||
|
||||
[dev-packages]
|
||||
"flake8" = "*"
|
||||
setuptools = "*"
|
||||
wheel = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
124
Pipfile.lock
generated
Normal file
124
Pipfile.lock
generated
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
107
README.md
Normal file
107
README.md
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
# Portainer CLI
|
||||
|
||||
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.
|
||||
|
||||
## Install
|
||||
|
||||
```
|
||||
pip install [--user] portainer-cli
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Global flags
|
||||
|
||||
| Flag | Description |
|
||||
|--|--|
|
||||
| `-l` or `--local` | Save and load configuration file (`.portainer-cli.json`) in current directory. |
|
||||
| `-d` or `--debug` | Enable DEBUG messages in stdout |
|
||||
|
||||
### configure command
|
||||
|
||||
Configure Portainer HTTP service base url.
|
||||
|
||||
```bash
|
||||
portainer-cli configure base_url
|
||||
```
|
||||
|
||||
**E.g:**
|
||||
|
||||
```bash
|
||||
portainer-cli configure http://10.0.0.1:9000/
|
||||
```
|
||||
|
||||
### login command
|
||||
|
||||
Identify yourself and take action.
|
||||
|
||||
```bash
|
||||
portainer-cli login username password
|
||||
```
|
||||
|
||||
**E.g:**
|
||||
|
||||
```bash
|
||||
portainer-cli login douglas d1234
|
||||
```
|
||||
|
||||
### update_stack command
|
||||
|
||||
Update stack.
|
||||
|
||||
```bash
|
||||
portainer-cli update_stack id endpoint_id [stack_file]
|
||||
```
|
||||
|
||||
**E.g:**
|
||||
|
||||
```bash
|
||||
portainer-cli update_stack 2 1 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
|
||||
|
||||
| Flag | Description |
|
||||
|--|--|
|
||||
| `-p` or `--prune` | Prune services |
|
||||
| `-c` or `--clear-env` | Clear all environment variables |
|
||||
|
||||
### request command
|
||||
|
||||
Make a request.
|
||||
|
||||
```bash
|
||||
portainer-cli request path [method=GET] [data]
|
||||
```
|
||||
|
||||
**E.g:**
|
||||
|
||||
```bash
|
||||
portainer-cli request status
|
||||
```
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|--|--|
|
||||
| `-p` or `--printc` | Print response content in stdout. |
|
||||
|
||||
## Development
|
||||
|
||||
This project use [Pipenv](https://pipenv.readthedocs.io/en/latest/) to manager Python packages.
|
||||
|
||||
With Pipenv installed, run `make install` to install all development packages dependencies.
|
||||
|
||||
Run `make lint` to run [flake8](http://flake8.pycqa.org/en/latest/) following PEP8 rules.
|
||||
|
||||
Run `make` or `make sdist` to create/update `dist` directory.
|
||||
6
portainer-cli
Normal file
6
portainer-cli
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/env python
|
||||
import plac
|
||||
from portainer_cli import PortainerCLI
|
||||
|
||||
p = PortainerCLI()
|
||||
plac.call(p.main)
|
||||
192
portainer_cli/__init__.py
Executable file
192
portainer_cli/__init__.py
Executable file
|
|
@ -0,0 +1,192 @@
|
|||
#!/usr/bin/env python
|
||||
import logging
|
||||
import plac
|
||||
import json
|
||||
import re
|
||||
import validators
|
||||
from pathlib import Path
|
||||
from requests import Request, Session
|
||||
|
||||
__version__ = '0.1.0'
|
||||
|
||||
logger = logging.getLogger('portainer-cli')
|
||||
|
||||
env_arg_regex = r'--env\.(.+)=(.+)'
|
||||
|
||||
|
||||
def env_arg_to_dict(s):
|
||||
split = re.split(env_arg_regex, s)
|
||||
return {
|
||||
'name': split[1],
|
||||
'value': split[2],
|
||||
}
|
||||
|
||||
|
||||
class PortainerCLI:
|
||||
COMMAND_CONFIGURE = 'configure'
|
||||
COMMAND_LOGIN = 'login'
|
||||
COMMAND_REQUEST = 'request'
|
||||
COMMAND_UPDATE_STACK = 'update_stack'
|
||||
COMMANDS = [
|
||||
COMMAND_CONFIGURE,
|
||||
COMMAND_LOGIN,
|
||||
COMMAND_REQUEST,
|
||||
COMMAND_UPDATE_STACK,
|
||||
]
|
||||
|
||||
METHOD_GET = 'GET'
|
||||
METHOD_POST = 'POST'
|
||||
METHOD_PUT = 'PUT'
|
||||
|
||||
local = False
|
||||
_base_url = 'http://localhost:9000/'
|
||||
_jwt = None
|
||||
|
||||
@property
|
||||
def base_url(self):
|
||||
return self._base_url
|
||||
|
||||
@base_url.setter
|
||||
def base_url(self, value):
|
||||
if not validators.url(value):
|
||||
raise Exception('Insert a valid base URL')
|
||||
self._base_url = value if value.endswith('/') else f'{value}/'
|
||||
self.persist()
|
||||
|
||||
@property
|
||||
def jwt(self):
|
||||
return self._jwt
|
||||
|
||||
@jwt.setter
|
||||
def jwt(self, value):
|
||||
self._jwt = value
|
||||
self.persist()
|
||||
|
||||
@property
|
||||
def data_path(self):
|
||||
if self.local:
|
||||
logger.debug('using local configuration file')
|
||||
return '.portainer-cli.json'
|
||||
logger.debug('using user configuration file')
|
||||
return Path.joinpath(Path.home(), '.portainer-cli.json')
|
||||
|
||||
def persist(self):
|
||||
data = {
|
||||
'base_url': self.base_url,
|
||||
'jwt': self.jwt,
|
||||
}
|
||||
logger.info(f'persisting configuration: {data}')
|
||||
data_file = open(self.data_path, 'w+')
|
||||
data_file.write(json.dumps(data))
|
||||
|
||||
def load(self):
|
||||
try:
|
||||
data_file = open(self.data_path)
|
||||
except FileNotFoundError:
|
||||
return
|
||||
data = json.loads(data_file.read())
|
||||
logger.info(f'configuration loaded: {data}')
|
||||
self._base_url = data.get('base_url')
|
||||
self._jwt = data.get('jwt')
|
||||
|
||||
def configure(self, base_url):
|
||||
self.base_url = base_url
|
||||
|
||||
def login(self, username, password):
|
||||
response = self.request(
|
||||
'auth',
|
||||
self.METHOD_POST,
|
||||
{
|
||||
'username': username,
|
||||
'password': password,
|
||||
}
|
||||
)
|
||||
r = response.json()
|
||||
jwt = r.get('jwt')
|
||||
logger.info(f'logged with jwt: {jwt}')
|
||||
self.jwt = jwt
|
||||
|
||||
@plac.annotations(
|
||||
prune=('Prune services', 'flag', 'p'),
|
||||
clear_env=('Clear all env vars', 'flag', 'c'),
|
||||
)
|
||||
def update_stack(self, id, endpoint_id, stack_file='', prune=False,
|
||||
clear_env=False, *args):
|
||||
stack_url = f'stacks/{id}?endpointId={endpoint_id}'
|
||||
current = self.request(stack_url).json()
|
||||
stack_file_content = ''
|
||||
if stack_file:
|
||||
stack_file_content = open(stack_file).read()
|
||||
else:
|
||||
stack_file_content = self.request(
|
||||
f'stacks/{id}/file?endpointId={endpoint_id}').json().get(
|
||||
'StackFileContent')
|
||||
env_args = filter(
|
||||
lambda x: re.match(env_arg_regex, x),
|
||||
args,
|
||||
)
|
||||
env = list(map(
|
||||
lambda x: env_arg_to_dict(x),
|
||||
env_args,
|
||||
))
|
||||
data = {
|
||||
'Id': id,
|
||||
'StackFileContent': stack_file_content,
|
||||
'Prune': prune,
|
||||
'Env': env if len(env) > 0 or clear_env else current.get('Env'),
|
||||
}
|
||||
self.request(
|
||||
stack_url,
|
||||
self.METHOD_PUT,
|
||||
data,
|
||||
)
|
||||
|
||||
@plac.annotations(
|
||||
printc=('Print response content', 'flag', 'p'),
|
||||
)
|
||||
def request(self, path, method=METHOD_GET, data='', printc=False):
|
||||
url = f'{self.base_url}api/{path}'
|
||||
session = Session()
|
||||
request = Request(method, url)
|
||||
prepped = request.prepare()
|
||||
if data:
|
||||
prepped.headers['Content-Type'] = 'application/json'
|
||||
try:
|
||||
json.loads(data)
|
||||
prepped.body = data
|
||||
except Exception as e:
|
||||
prepped.body = json.dumps(data)
|
||||
prepped.headers['Content-Length'] = len(prepped.body)
|
||||
if self.jwt:
|
||||
prepped.headers['Authorization'] = f'Bearer {self.jwt}'
|
||||
response = session.send(prepped)
|
||||
logger.debug(f'request response: {response.content}')
|
||||
response.raise_for_status()
|
||||
if printc:
|
||||
print(response.content.decode())
|
||||
return response
|
||||
|
||||
@plac.annotations(
|
||||
command=(
|
||||
'Command',
|
||||
'positional',
|
||||
None,
|
||||
str,
|
||||
COMMANDS,
|
||||
),
|
||||
debug=('Enable debug mode', 'flag', 'd'),
|
||||
local=('Use local/dir configuration', 'flag', 'l'),
|
||||
)
|
||||
def main(self, command, debug=False, local=local, *args):
|
||||
if debug:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
self.local = local
|
||||
self.load()
|
||||
if command == self.COMMAND_CONFIGURE:
|
||||
plac.call(self.configure, args)
|
||||
elif command == self.COMMAND_LOGIN:
|
||||
plac.call(self.login, args)
|
||||
elif command == self.COMMAND_UPDATE_STACK:
|
||||
plac.call(self.update_stack, args)
|
||||
elif command == self.COMMAND_REQUEST:
|
||||
plac.call(self.request, args)
|
||||
2
setup.cfg
Normal file
2
setup.cfg
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[bdist_wheel]
|
||||
universal=1
|
||||
32
setup.py
Normal file
32
setup.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import setuptools
|
||||
from portainer_cli import __version__
|
||||
|
||||
with open('README.md', 'r') as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
setuptools.setup(
|
||||
name='portainer-cli',
|
||||
version=__version__,
|
||||
author='Ilhasoft\'s Web Team',
|
||||
author_email='contato@ilhasoft.com.br',
|
||||
description='Command line interface to easy communicate to your ' +
|
||||
'Portainer application.',
|
||||
long_description=long_description,
|
||||
long_description_content_type='text/markdown',
|
||||
url='https://github.com/Ilhasoft/portainer-cli',
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: Console',
|
||||
'Programming Language :: Python :: 3',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: OS Independent',
|
||||
],
|
||||
packages=setuptools.find_packages(),
|
||||
scripts=['portainer-cli'],
|
||||
install_requires=[
|
||||
'plac>=1.0.0',
|
||||
'requests>=2.20.0',
|
||||
'validators>=0.12.2',
|
||||
],
|
||||
python_requires='>=3.6',
|
||||
)
|
||||
Loading…
Reference in a new issue