Initial dcos cli commit
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
dcos.egg-info/
|
||||||
|
.ropeproject/
|
||||||
|
env/
|
||||||
|
*.pyc
|
0
DESCRIPTION.rst
Normal file
0
DESCRIPTION.rst
Normal file
68
README.rst
Normal file
68
README.rst
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
CLI-poc
|
||||||
|
=======
|
||||||
|
Proof-of-concept for a CLI with modular subcommands.
|
||||||
|
|
||||||
|
Setup
|
||||||
|
-----
|
||||||
|
|
||||||
|
#. Make sure you meet requirements for installing packages_
|
||||||
|
#. Install the "wheel" project::
|
||||||
|
|
||||||
|
pip install wheel
|
||||||
|
|
||||||
|
#. Clone git repo for the dcos cli::
|
||||||
|
|
||||||
|
git clone git@github.com:mesosphere/dcos-cli.git
|
||||||
|
|
||||||
|
#. Change directory to the repo directory::
|
||||||
|
|
||||||
|
cd dcos-cli
|
||||||
|
|
||||||
|
#. Create a virtualenv for the dcos cli project::
|
||||||
|
|
||||||
|
virtualenv --prompt='(dcos-cli) ' env
|
||||||
|
|
||||||
|
Configure Development Environment
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
#. Activate the virtualenv::
|
||||||
|
|
||||||
|
source env/bin/activate
|
||||||
|
|
||||||
|
#. Install project in develop mode::
|
||||||
|
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
|
#. Export DCOS_PATH::
|
||||||
|
|
||||||
|
export DCOS_PATH=<path-to-project>/env
|
||||||
|
|
||||||
|
#. Export DCOS_CONFIG::
|
||||||
|
|
||||||
|
mkdir $DCOS_PATH/config
|
||||||
|
touch $DCOS_PATH/config.Dcos.toml
|
||||||
|
export DCOS_CONFIG=$DCOS_PATH/env/config/Dcos.toml
|
||||||
|
|
||||||
|
Running POC
|
||||||
|
-----------
|
||||||
|
|
||||||
|
#. List command help::
|
||||||
|
|
||||||
|
dcos --help
|
||||||
|
|
||||||
|
#. Run subcommand::
|
||||||
|
|
||||||
|
dcos config --help
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
Submodule writing notes gathered so far:
|
||||||
|
|
||||||
|
#. Because we are using python's pip to install packages it looks like we can't install packages
|
||||||
|
that share the same python's package of other installed packages because they will conflict once
|
||||||
|
deployed to virtualenv directory structure.
|
||||||
|
|
||||||
|
#. Currently we require that subcommands implement an info command. For example dcos-subcommand
|
||||||
|
implements ``subcommand info``.
|
||||||
|
|
||||||
|
.. _packages: https://packaging.python.org/en/latest/installing.html#installing-requirements
|
0
data/data_file
Normal file
0
data/data_file
Normal file
69
dcos/__init__.py
Normal file
69
dcos/__init__.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
"""
|
||||||
|
Usage:
|
||||||
|
dcos [--mesos=<uri>] <command> [<args>...]
|
||||||
|
dcos -h | --help
|
||||||
|
dcos --version
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help Show this screen
|
||||||
|
--version Show version
|
||||||
|
--mesos=<uri> URI for the Mesos master
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import docopt
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
subcommands = _list_external_subcommands(os.environ['DCOS_PATH'])
|
||||||
|
subcommand_summaries = _external_command_documentation(subcommands)
|
||||||
|
|
||||||
|
args = docopt.docopt(
|
||||||
|
_extend_usage_docopt(__doc__, subcommand_summaries),
|
||||||
|
version='dcos version 0.1.0', # TODO: grab from setuptool
|
||||||
|
options_first=True)
|
||||||
|
|
||||||
|
argv = [args['<command>']] + args['<args>']
|
||||||
|
|
||||||
|
# TODO: We need to figure out a way to make subcommand's discoverable
|
||||||
|
if args['<command>'] in subcommands:
|
||||||
|
command = 'dcos-{}'.format(args['<command>'])
|
||||||
|
exit(subprocess.call([command] + argv))
|
||||||
|
else:
|
||||||
|
exit(
|
||||||
|
'{!r} is not a dcos command. See "dcos --help".'.format(
|
||||||
|
args['<command>']))
|
||||||
|
|
||||||
|
|
||||||
|
def _list_external_subcommands(dcos_path):
|
||||||
|
# TODO: check that dir_path is directory?
|
||||||
|
prefix = 'dcos-'
|
||||||
|
|
||||||
|
return [filename[len(prefix):]
|
||||||
|
|
||||||
|
for dirpath, _, filenames
|
||||||
|
in os.walk(os.path.join(dcos_path, "bin"))
|
||||||
|
|
||||||
|
for filename in filenames
|
||||||
|
|
||||||
|
if filename.startswith(prefix)
|
||||||
|
and os.access(os.path.join(dirpath, filename), os.X_OK)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _external_command_documentation(commands):
|
||||||
|
def info(commnand):
|
||||||
|
return subprocess.check_output(
|
||||||
|
['dcos-{}'.format(command), command, 'info'])
|
||||||
|
|
||||||
|
return [(command, info(command)) for command in commands]
|
||||||
|
|
||||||
|
|
||||||
|
def _extend_usage_docopt(doc, command_summaries):
|
||||||
|
# TODO: make sure that we deal with long commands
|
||||||
|
doc += '\nThe dcos commands are:'
|
||||||
|
for command, summary in command_summaries:
|
||||||
|
doc += '\n\t{:15}\t{}'.format(command, summary.strip())
|
||||||
|
|
||||||
|
return doc
|
75
dcos/config.py
Normal file
75
dcos/config.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
"""
|
||||||
|
Usage:
|
||||||
|
dcos config info
|
||||||
|
dcos config <name> [<value>]
|
||||||
|
dcos config --unset <name>
|
||||||
|
dcos config --help
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help Show this screen
|
||||||
|
--unset Remove property from the config file
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import docopt
|
||||||
|
import toml
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
config_path = os.environ['DCOS_CONFIG']
|
||||||
|
args = docopt.docopt(__doc__)
|
||||||
|
|
||||||
|
if args['config'] and args['info']:
|
||||||
|
print('Get and set DCOS command line options')
|
||||||
|
|
||||||
|
elif args['config'] and args['--unset']:
|
||||||
|
config = _load_config_file(config_path)
|
||||||
|
_unset_property(config, args['<name>'])
|
||||||
|
_save_config_file(config_path, config)
|
||||||
|
|
||||||
|
elif args['config'] and args['<value>'] is None:
|
||||||
|
config = _load_config_file(config_path)
|
||||||
|
print(_get_property(config, args['<name>']))
|
||||||
|
|
||||||
|
elif args['config']:
|
||||||
|
config = _load_config_file(config_path)
|
||||||
|
_set_property(config, args['<name>'], args['<value>'])
|
||||||
|
_save_config_file(config_path, config)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(args)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_config_file(config_path):
|
||||||
|
with open(config_path) as config_file:
|
||||||
|
return toml.loads(config_file.read())
|
||||||
|
|
||||||
|
|
||||||
|
def _save_config_file(config_path, config):
|
||||||
|
serial = toml.dumps(config)
|
||||||
|
with open(config_path, 'w') as config_file:
|
||||||
|
config_file.write(serial)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_property(config, name):
|
||||||
|
for section in name.split('.'):
|
||||||
|
config = config[section]
|
||||||
|
|
||||||
|
# TODO: Do we want to check that config is not a dictionary?
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def _set_property(config, name, value):
|
||||||
|
sections = name.split('.')
|
||||||
|
for section in sections[:-1]:
|
||||||
|
config = config[section]
|
||||||
|
|
||||||
|
config[sections[-1]] = value
|
||||||
|
|
||||||
|
|
||||||
|
def _unset_property(config, name):
|
||||||
|
sections = name.split('.')
|
||||||
|
for section in sections[:-1]:
|
||||||
|
config = config[section]
|
||||||
|
|
||||||
|
del config[sections[-1]]
|
31
dcos/subcommand.py
Normal file
31
dcos/subcommand.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
"""
|
||||||
|
Usage:
|
||||||
|
dcos subcommand info
|
||||||
|
dcos subcommand install python <uri>
|
||||||
|
dcos subcommand -h | --help
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help Show this screen
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import docopt
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = docopt.docopt(__doc__)
|
||||||
|
|
||||||
|
if args['subcommand'] and args['info']:
|
||||||
|
print('Manage DCOS external commands')
|
||||||
|
elif args['subcommand'] and args['install'] and args['python']:
|
||||||
|
print('Trying to install a python subcommand')
|
||||||
|
command = ['pip', 'install', args['<uri>']]
|
||||||
|
print('Running: {!r}'.format(command))
|
||||||
|
# For now we are just going to call pip and see if it works
|
||||||
|
exit_status = subprocess.call(command)
|
||||||
|
|
||||||
|
print(
|
||||||
|
'Using pip returned the following exit status: {!r}'.format(
|
||||||
|
exit_status))
|
||||||
|
else:
|
||||||
|
print(args)
|
5
setup.cfg
Normal file
5
setup.cfg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[bdist_wheel]
|
||||||
|
# This flag says that the code is written to work on both Python 2 and Python
|
||||||
|
# 3. If at all possible, it is good practice to do this. If you cannot, you
|
||||||
|
# will need to generate wheels for each Python version that you support.
|
||||||
|
universal=1
|
105
setup.py
Normal file
105
setup.py
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
from setuptools import setup, find_packages
|
||||||
|
from codecs import open
|
||||||
|
from os import path
|
||||||
|
|
||||||
|
# TODO: what license should we use?
|
||||||
|
|
||||||
|
here = path.abspath(path.dirname(__file__))
|
||||||
|
|
||||||
|
# Get the long description from the relevant file
|
||||||
|
with open(path.join(here, 'DESCRIPTION.rst'), encoding='utf-8') as f:
|
||||||
|
long_description = f.read()
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='dcos',
|
||||||
|
|
||||||
|
# Versions should comply with PEP440. For a discussion on single-sourcing
|
||||||
|
# the version across setup.py and the project code, see
|
||||||
|
# https://packaging.python.org/en/latest/single_source_version.html
|
||||||
|
version='0.1.0',
|
||||||
|
|
||||||
|
description='Dcos cli poc project',
|
||||||
|
long_description=long_description,
|
||||||
|
|
||||||
|
# The project's main homepage.
|
||||||
|
url='https://github.com/mesosphere/CLI-poc',
|
||||||
|
|
||||||
|
# Author details
|
||||||
|
author='Mesosphere, Inc.',
|
||||||
|
author_email='team@mesosphere.io',
|
||||||
|
|
||||||
|
# Choose your license
|
||||||
|
license='TODO',
|
||||||
|
|
||||||
|
# See https://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||||
|
classifiers=[
|
||||||
|
# How mature is this project? Common values are
|
||||||
|
# 3 - Alpha
|
||||||
|
# 4 - Beta
|
||||||
|
# 5 - Production/Stable
|
||||||
|
'Development Status :: 3 - Alpha',
|
||||||
|
|
||||||
|
# Indicate who your project is intended for
|
||||||
|
'Intended Audience :: Developers',
|
||||||
|
'Intended Audience :: Information Technology',
|
||||||
|
'Topic :: Software Development :: User Interfaces',
|
||||||
|
|
||||||
|
# Pick your license as you wish (should match "license" above)
|
||||||
|
'License :: OSI Approved :: TODO License',
|
||||||
|
|
||||||
|
# Specify the Python versions you support here. In particular, ensure
|
||||||
|
# that you indicate whether you support Python 2, Python 3 or both.
|
||||||
|
'Programming Language :: Python :: 2',
|
||||||
|
'Programming Language :: Python :: 2.6',
|
||||||
|
'Programming Language :: Python :: 2.7',
|
||||||
|
'Programming Language :: Python :: 3',
|
||||||
|
'Programming Language :: Python :: 3.2',
|
||||||
|
'Programming Language :: Python :: 3.3',
|
||||||
|
'Programming Language :: Python :: 3.4',
|
||||||
|
],
|
||||||
|
|
||||||
|
# What does your project relate to?
|
||||||
|
keywords='mesos apache marathon mesospehere command datacenter',
|
||||||
|
|
||||||
|
# You can just specify the packages manually here if your project is
|
||||||
|
# simple. Or you can use find_packages().
|
||||||
|
packages=find_packages(exclude=['contrib', 'docs', 'tests*']),
|
||||||
|
|
||||||
|
# List run-time dependencies here. These will be installed by pip when your
|
||||||
|
# project is installed. For an analysis of "install_requires" vs pip's
|
||||||
|
# requirements files see:
|
||||||
|
# https://packaging.python.org/en/latest/requirements.html
|
||||||
|
install_requires=['docopt', 'toml'],
|
||||||
|
|
||||||
|
# List additional groups of dependencies here (e.g. development
|
||||||
|
# dependencies). You can install these using the following syntax, for
|
||||||
|
# example:
|
||||||
|
# $ pip install -e .[dev,test]
|
||||||
|
extras_require={
|
||||||
|
'dev': ['check-manifest'],
|
||||||
|
'test': ['coverage'],
|
||||||
|
},
|
||||||
|
|
||||||
|
# If there are data files included in your packages that need to be
|
||||||
|
# installed, specify them here. If using Python 2.6 or less, then these
|
||||||
|
# have to be included in MANIFEST.in as well.
|
||||||
|
package_data={
|
||||||
|
'sample': ['package_data.dat'],
|
||||||
|
},
|
||||||
|
|
||||||
|
# Although 'package_data' is the preferred approach, in some case you may
|
||||||
|
# need to place data files outside of your packages.
|
||||||
|
# In this case, 'data_file' will be installed into '<sys.prefix>/my_data'
|
||||||
|
data_files=[('my_data', ['data/data_file'])],
|
||||||
|
|
||||||
|
# To provide executable scripts, use entry points in preference to the
|
||||||
|
# "scripts" keyword. Entry points provide cross-platform support and allow
|
||||||
|
# pip to create the appropriate form of executable for the target platform.
|
||||||
|
entry_points={
|
||||||
|
'console_scripts': [
|
||||||
|
'dcos=dcos:main',
|
||||||
|
'dcos-subcommand=dcos.subcommand:main',
|
||||||
|
'dcos-config=dcos.config:main',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
49
tests/test_config.py
Normal file
49
tests/test_config.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
from dcos import config
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def conf():
|
||||||
|
return {
|
||||||
|
'dcos': {
|
||||||
|
'user': 'principal',
|
||||||
|
'mesos_uri': 'zk://localhost/mesos'
|
||||||
|
},
|
||||||
|
'package': {
|
||||||
|
'repo_uri': 'git://localhost/mesosphere/package-repo.git'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_unset_property(conf):
|
||||||
|
expect = {
|
||||||
|
'dcos': {
|
||||||
|
'user': 'principal',
|
||||||
|
'mesos_uri': 'zk://localhost/mesos'
|
||||||
|
},
|
||||||
|
'package': {}
|
||||||
|
}
|
||||||
|
|
||||||
|
config._unset_property(conf, 'package.repo_uri')
|
||||||
|
|
||||||
|
assert conf == expect
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_property(conf):
|
||||||
|
expect = {
|
||||||
|
'dcos': {
|
||||||
|
'user': 'group',
|
||||||
|
'mesos_uri': 'zk://localhost/mesos'
|
||||||
|
},
|
||||||
|
'package': {
|
||||||
|
'repo_uri': 'git://localhost/mesosphere/package-repo.git'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config._set_property(conf, 'dcos.user', 'group')
|
||||||
|
|
||||||
|
assert conf == expect
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_property(conf):
|
||||||
|
config._get_property(conf, 'dcos.mesos_uri') == 'zk://localhost/mesos'
|
17
tests/test_dcos.py
Normal file
17
tests/test_dcos.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import dcos
|
||||||
|
|
||||||
|
|
||||||
|
def test_extend_usage_docopt():
|
||||||
|
command_summaries = [
|
||||||
|
('first', 'first summary'),
|
||||||
|
('second', ' second summary '),
|
||||||
|
('third', 'third summary\n')
|
||||||
|
]
|
||||||
|
|
||||||
|
expected = """
|
||||||
|
The dcos commands are:
|
||||||
|
\tfirst \tfirst summary
|
||||||
|
\tsecond \tsecond summary
|
||||||
|
\tthird \tthird summary"""
|
||||||
|
|
||||||
|
assert dcos._extend_usage_docopt('', command_summaries) == expected
|
Reference in New Issue
Block a user