store labels locally
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
@@ -7,7 +6,7 @@ import uuid
|
||||
import dcoscli
|
||||
import requests
|
||||
import rollbar
|
||||
from dcos.api import config, constants
|
||||
from dcos.api import config, constants, util
|
||||
from dcoscli.constants import (ROLLBAR_SERVER_POST_KEY,
|
||||
SEGMENT_IO_CLI_ERROR_EVENT,
|
||||
SEGMENT_IO_CLI_EVENT, SEGMENT_IO_WRITE_KEY_DEV,
|
||||
@@ -15,7 +14,7 @@ from dcoscli.constants import (ROLLBAR_SERVER_POST_KEY,
|
||||
from futures import ThreadPoolExecutor
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = util.get_logger(__name__)
|
||||
session_id = uuid.uuid4().hex
|
||||
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ import dcoscli
|
||||
import docopt
|
||||
import pkg_resources
|
||||
from dcos.api import (cmds, config, constants, emitting, errors, marathon,
|
||||
options, package, util)
|
||||
options, package, subcommand, util)
|
||||
|
||||
emitter = emitting.FlatEmitter()
|
||||
|
||||
@@ -256,13 +256,13 @@ def _describe(package_name):
|
||||
return 0
|
||||
|
||||
|
||||
def _install(package_name, options_file, app_id, cli, app):
|
||||
def _install(package_name, options_path, app_id, cli, app):
|
||||
"""Install the specified package.
|
||||
|
||||
:param package_name: the package to install
|
||||
:type package_name: str
|
||||
:param options_file: path to file containing option values
|
||||
:type options_file: str
|
||||
:param options_path: path to file containing option values
|
||||
:type options_path: str
|
||||
:param app_id: app ID for installation of this package
|
||||
:type app_id: str
|
||||
:param cli: indicates if the cli should be installed
|
||||
@@ -291,23 +291,26 @@ def _install(package_name, options_file, app_id, cli, app):
|
||||
"repositories"))
|
||||
return 1
|
||||
|
||||
options_json = {}
|
||||
|
||||
if options_file is not None:
|
||||
try:
|
||||
options_fd = open(options_file)
|
||||
options_json = json.load(options_fd)
|
||||
except Exception as e:
|
||||
emitter.publish(e.message)
|
||||
return 1
|
||||
|
||||
# TODO(CD): Make package version to install configurable
|
||||
pkg_version, version_error = pkg.latest_version()
|
||||
|
||||
if version_error is not None:
|
||||
emitter.publish(version_error)
|
||||
return 1
|
||||
|
||||
if options_path is None:
|
||||
options = {}
|
||||
else:
|
||||
try:
|
||||
with open(options_path) as options_file:
|
||||
user_options = json.load(options_file)
|
||||
options, err = pkg.options(pkg_version, user_options)
|
||||
if err is not None:
|
||||
emitter.publish(err)
|
||||
return 1
|
||||
except Exception as e:
|
||||
emitter.publish(errors.DefaultError(e.message))
|
||||
return 1
|
||||
|
||||
if app:
|
||||
# Install in Marathon
|
||||
version_map, version_error = pkg.software_versions()
|
||||
@@ -331,8 +334,9 @@ def _install(package_name, options_file, app_id, cli, app):
|
||||
pkg,
|
||||
pkg_version,
|
||||
init_client,
|
||||
options_json,
|
||||
options,
|
||||
app_id)
|
||||
|
||||
if install_error is not None:
|
||||
emitter.publish(install_error)
|
||||
return 1
|
||||
@@ -342,7 +346,7 @@ def _install(package_name, options_file, app_id, cli, app):
|
||||
emitter.publish('Installing CLI subcommand for package [{}]'.format(
|
||||
pkg.name()))
|
||||
|
||||
err = package.install_subcommand(pkg, pkg_version, options_json)
|
||||
err = subcommand.install(pkg, pkg_version, options)
|
||||
if err is not None:
|
||||
emitter.publish(err)
|
||||
return 1
|
||||
|
||||
@@ -129,11 +129,9 @@ def _install(package):
|
||||
|
||||
dcos_config = config.load_from_path(os.environ[constants.DCOS_CONFIG_ENV])
|
||||
|
||||
install_operation = {
|
||||
'pip': [package]
|
||||
}
|
||||
pip_operation = [package]
|
||||
if 'subcommand.pip_find_links' in dcos_config:
|
||||
install_operation['pip'].append(
|
||||
pip_operation.append(
|
||||
'--find-links {}'.format(dcos_config['subcommand.pip_find_links']))
|
||||
|
||||
distribution_name, err = _distribution_name(package)
|
||||
@@ -143,10 +141,13 @@ def _install(package):
|
||||
subcommand for a package, run `dcos package install <package> --cli` instead"))
|
||||
return 1
|
||||
|
||||
err = subcommand.install(
|
||||
env_dir = os.path.join(subcommand.package_dir(distribution_name),
|
||||
constants.DCOS_SUBCOMMAND_VIRTUALENV_SUBDIR)
|
||||
|
||||
err = subcommand.install_with_pip(
|
||||
distribution_name,
|
||||
install_operation,
|
||||
util.dcos_path())
|
||||
env_dir,
|
||||
pip_operation)
|
||||
if err is not None:
|
||||
emitter.publish(err)
|
||||
return 1
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import six
|
||||
from dcos.api import subcommand
|
||||
|
||||
from common import exec_command
|
||||
|
||||
@@ -149,7 +151,7 @@ def test_bad_install():
|
||||
'--options=tests/data/package/mesos-dns-config-bad.json'])
|
||||
|
||||
assert returncode == 1
|
||||
assert stdout == b'Installing package [mesos-dns] version [alpha]\n'
|
||||
assert stdout == b''
|
||||
|
||||
assert stderr == b"""\
|
||||
Error: 'mesos-dns/config-url' is a required property
|
||||
@@ -174,36 +176,81 @@ def test_install():
|
||||
assert stderr == b''
|
||||
|
||||
|
||||
def test_package_labels():
|
||||
app_labels = get_app_labels('mesos-dns')
|
||||
expected_metadata = b"""\
|
||||
eyJkZXNjcmlwdGlvbiI6ICJETlMtYmFzZWQgc2VydmljZSBkaXNjb3ZlcnkgZm9yIE1lc29zLiIsI\
|
||||
CJtYWludGFpbmVyIjogInN1cHBvcnRAbWVzb3NwaGVyZS5pbyIsICJuYW1lIjogIm1lc29zLWRucy\
|
||||
IsICJwb3N0SW5zdGFsbE5vdGVzIjogIlBsZWFzZSByZWZlciB0byB0aGUgdHV0b3JpYWwgaW5zdHJ\
|
||||
1Y3Rpb25zIGZvciBmdXJ0aGVyIHNldHVwIHJlcXVpcmVtZW50czogaHR0cDovL21lc29zcGhlcmUu\
|
||||
Z2l0aHViLmlvL21lc29zLWRucy9kb2NzL3R1dG9yaWFsLWdjZS5odG1sIiwgInNjbSI6ICJodHRwc\
|
||||
zovL2dpdGh1Yi5jb20vbWVzb3NwaGVyZS9tZXNvcy1kbnMuZ2l0IiwgInRhZ3MiOiBbIm1lc29zcG\
|
||||
hlcmUiXSwgInZlcnNpb24iOiAiYWxwaGEiLCAid2Vic2l0ZSI6ICJodHRwOi8vbWVzb3NwaGVyZS5\
|
||||
naXRodWIuaW8vbWVzb3MtZG5zIn0=\
|
||||
def test_package_metadata():
|
||||
returncode, stdout, stderr = exec_command(['dcos',
|
||||
'package',
|
||||
'install',
|
||||
'helloworld'])
|
||||
|
||||
assert returncode == 0
|
||||
assert stdout == b"""Installing package [helloworld] version [0.1.0]
|
||||
Installing CLI subcommand for package [helloworld]
|
||||
"""
|
||||
actual_metadata = app_labels.get('DCOS_PACKAGE_METADATA')
|
||||
assert(six.b(actual_metadata) == expected_metadata)
|
||||
assert stderr == b''
|
||||
|
||||
expected_registry_version = b'0.1.0-alpha'
|
||||
actual_registry_version = app_labels.get('DCOS_PACKAGE_REGISTRY_VERSION')
|
||||
assert(six.b(actual_registry_version) == expected_registry_version)
|
||||
# test marathon labels
|
||||
expected_metadata = b"""eyJkZXNjcmlwdGlvbiI6ICJFeGFtcGxlIERDT1MgYXBwbGljYX\
|
||||
Rpb24gcGFja2FnZSIsICJtYWludGFpbmVyIjogInN1cHBvcnRAbWVzb3NwaGVyZS5pbyIsICJuYW1l\
|
||||
IjogImhlbGxvd29ybGQiLCAidGFncyI6IFsibWVzb3NwaGVyZSIsICJleGFtcGxlIiwgInN1YmNvbW\
|
||||
1hbmQiXSwgInZlcnNpb24iOiAiMC4xLjAiLCAid2Vic2l0ZSI6ICJodHRwczovL2dpdGh1Yi5jb20v\
|
||||
bWVzb3NwaGVyZS9kY29zLWhlbGxvd29ybGQifQ=="""
|
||||
|
||||
expected_name = b'mesos-dns'
|
||||
actual_name = app_labels.get('DCOS_PACKAGE_NAME')
|
||||
assert(six.b(actual_name) == expected_name)
|
||||
|
||||
expected_version = b'alpha'
|
||||
actual_version = app_labels.get('DCOS_PACKAGE_VERSION')
|
||||
assert(six.b(actual_version) == expected_version)
|
||||
expected_command = b"""eyJwaXAiOiBbImh0dHA6Ly9kb3dubG9hZHMubWVzb3NwaGVyZS5\
|
||||
pby9kY29zLWNsaS9kY29zLTAuMS4wLXB5Mi5weTMtbm9uZS1hbnkud2hsIiwgImdpdCtodHRwczovL\
|
||||
2dpdGh1Yi5jb20vbWVzb3NwaGVyZS9kY29zLWhlbGxvd29ybGQuZ2l0I2Rjb3MtaGVsbG93b3JsZD0\
|
||||
wLjEuMCJdfQ=="""
|
||||
|
||||
expected_source = b'git://github.com/mesosphere/universe.git'
|
||||
actual_source = app_labels.get('DCOS_PACKAGE_SOURCE')
|
||||
assert(six.b(actual_source) == expected_source)
|
||||
|
||||
expected_labels = {
|
||||
'DCOS_PACKAGE_METADATA': expected_metadata,
|
||||
'DCOS_PACKAGE_COMMAND': expected_command,
|
||||
'DCOS_PACKAGE_REGISTRY_VERSION': b'0.1.0-alpha',
|
||||
'DCOS_PACKAGE_NAME': b'helloworld',
|
||||
'DCOS_PACKAGE_VERSION': b'0.1.0',
|
||||
'DCOS_PACKAGE_SOURCE': expected_source,
|
||||
'DCOS_PACKAGE_RELEASE': b'0',
|
||||
}
|
||||
|
||||
app_labels = get_app_labels('helloworld')
|
||||
|
||||
for label, value in expected_labels.items():
|
||||
assert value == six.b(app_labels.get(label))
|
||||
|
||||
# test local package.json
|
||||
package = {
|
||||
"website": "https://github.com/mesosphere/dcos-helloworld",
|
||||
"maintainer": "support@mesosphere.io",
|
||||
"name": "helloworld",
|
||||
"tags": ["mesosphere", "example", "subcommand"],
|
||||
"version": "0.1.0",
|
||||
"description": "Example DCOS application package"
|
||||
}
|
||||
|
||||
package_dir = subcommand.package_dir('helloworld')
|
||||
|
||||
# test local package.json
|
||||
package_path = os.path.join(package_dir, 'package.json')
|
||||
with open(package_path) as f:
|
||||
assert json.load(f) == package
|
||||
|
||||
# test local source
|
||||
source_path = os.path.join(package_dir, 'source')
|
||||
with open(source_path) as f:
|
||||
assert six.b(f.read()) == expected_source
|
||||
|
||||
# test local version
|
||||
version_path = os.path.join(package_dir, 'version')
|
||||
with open(version_path) as f:
|
||||
assert six.b(f.read()) == b'0'
|
||||
|
||||
# uninstall helloworld
|
||||
returncode, stdout, stderr = exec_command(
|
||||
['dcos', 'package', 'uninstall', 'helloworld'])
|
||||
|
||||
assert returncode == 0
|
||||
assert stdout == b''
|
||||
assert stderr == b''
|
||||
|
||||
|
||||
def test_install_with_id():
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
DCOS_DIR = ".dcos"
|
||||
"""DCOS data directory. Can store subcommands and the config file."""
|
||||
|
||||
DCOS_SUBCOMMAND_VIRTUALENV_SUBDIR = 'env'
|
||||
"""In a package's directory, this is the virtualenv subdirectory."""
|
||||
|
||||
DCOS_SUBCOMMAND_SUBDIR = 'subcommands'
|
||||
"""Name of the subdirectory that contains all of the subcommands. This is
|
||||
relative to the location of the executable."""
|
||||
|
||||
@@ -28,42 +28,11 @@ PACKAGE_NAME_KEY = 'DCOS_PACKAGE_NAME'
|
||||
PACKAGE_VERSION_KEY = 'DCOS_PACKAGE_VERSION'
|
||||
PACKAGE_SOURCE_KEY = 'DCOS_PACKAGE_SOURCE'
|
||||
PACKAGE_FRAMEWORK_KEY = 'DCOS_PACKAGE_IS_FRAMEWORK'
|
||||
PACKAGE_RELEASE_KEY = 'DCOS_PACKAGE_RELEASE'
|
||||
PACKAGE_COMMAND_KEY = 'DCOS_PACKAGE_COMMAND'
|
||||
|
||||
|
||||
def _merge_options(pkg, version, user_options):
|
||||
"""
|
||||
:param pkg: the package to install
|
||||
:type pkg: Package
|
||||
:param version: the package version to install
|
||||
:type version: str
|
||||
:param user_options: package parameters
|
||||
:type user_options: dict
|
||||
:returns: a dictionary with the user supplied options
|
||||
:rtype: (dict, dcos.api.errors.Error)
|
||||
"""
|
||||
if user_options is None:
|
||||
user_options = {}
|
||||
|
||||
config_schema, err = pkg.config_json(version)
|
||||
if err is not None:
|
||||
return (None, err)
|
||||
|
||||
default_options, err = _extract_default_values(config_schema)
|
||||
if err is not None:
|
||||
return (None, err)
|
||||
|
||||
# Merge option overrides
|
||||
options = dict(list(default_options.items()) + list(user_options.items()))
|
||||
|
||||
# Validate options with the config schema
|
||||
err = util.validate_json(options, config_schema)
|
||||
if err is not None:
|
||||
return (None, err)
|
||||
|
||||
return (options, None)
|
||||
|
||||
|
||||
def install_app(pkg, version, init_client, user_options, app_id):
|
||||
def install_app(pkg, version, init_client, options, app_id):
|
||||
"""Installs a package's application
|
||||
|
||||
:param pkg: the package to install
|
||||
@@ -72,29 +41,20 @@ def install_app(pkg, version, init_client, user_options, app_id):
|
||||
:type version: str
|
||||
:param init_client: the program to use to run the package
|
||||
:type init_client: object
|
||||
:param user_options: package parameters
|
||||
:type user_options: dict
|
||||
:param options: package parameters
|
||||
:type options: dict
|
||||
:param app_id: app ID for installation of this package
|
||||
:type app_id: str
|
||||
:rtype: Error
|
||||
"""
|
||||
|
||||
options, err = _merge_options(pkg, version, user_options)
|
||||
if err is not None:
|
||||
return err
|
||||
|
||||
# Insert option parameters into the init template
|
||||
template, err = pkg.marathon_template(version)
|
||||
if err is not None:
|
||||
return err
|
||||
|
||||
# Render the init template with the marshaled options
|
||||
init_desc, err = util.render_mustache_json(template, options)
|
||||
init_desc, err = pkg.marathon_json(version, options)
|
||||
if err is not None:
|
||||
return err
|
||||
|
||||
# Add package metadata
|
||||
package_labels, err = _make_package_labels(pkg, version)
|
||||
package_labels, err = _make_package_labels(pkg, version, options)
|
||||
if err is not None:
|
||||
return err
|
||||
|
||||
@@ -115,23 +75,24 @@ def install_app(pkg, version, init_client, user_options, app_id):
|
||||
return err
|
||||
|
||||
|
||||
def _make_package_labels(pkg, version):
|
||||
"""
|
||||
def _make_package_labels(pkg, version, options):
|
||||
"""Returns Marathon app labels for a package.
|
||||
|
||||
:param pkg: The package to install
|
||||
:type pkg: Package
|
||||
:param version: The package version to install
|
||||
:type version: str
|
||||
:param options: package parameters
|
||||
:type options: dict
|
||||
:returns: Marathon app labels
|
||||
:rtype: (dict, Error)
|
||||
"""
|
||||
|
||||
metadata, meta_error = pkg.package_json(version)
|
||||
|
||||
if meta_error is not None:
|
||||
return (None, meta_error)
|
||||
|
||||
metadata_json_string = json.dumps(metadata, sort_keys=True)
|
||||
metadata_bytes = six.b(metadata_json_string)
|
||||
encoded_metadata = base64.b64encode(metadata_bytes).decode('utf-8')
|
||||
encoded_metadata = _base64_encode(metadata)
|
||||
|
||||
is_framework = metadata.get('framework')
|
||||
if not is_framework:
|
||||
@@ -148,43 +109,32 @@ def _make_package_labels(pkg, version):
|
||||
PACKAGE_VERSION_KEY: metadata['version'],
|
||||
PACKAGE_SOURCE_KEY: pkg.registry.source.url,
|
||||
PACKAGE_FRAMEWORK_KEY: str(is_framework),
|
||||
PACKAGE_REGISTRY_VERSION_KEY: package_registry_version
|
||||
PACKAGE_REGISTRY_VERSION_KEY: package_registry_version,
|
||||
PACKAGE_RELEASE_KEY: str(version)
|
||||
}
|
||||
|
||||
if pkg.is_command_defined(version):
|
||||
command, cmd_error = pkg.command_json(version, options)
|
||||
if cmd_error is not None:
|
||||
return (None, cmd_error)
|
||||
|
||||
package_labels[PACKAGE_COMMAND_KEY] = _base64_encode(command)
|
||||
|
||||
return (package_labels, None)
|
||||
|
||||
|
||||
def install_subcommand(pkg, version, user_options):
|
||||
"""Installs a package's command line interface
|
||||
def _base64_encode(dictionary):
|
||||
"""Returns base64(json(dictionary)).
|
||||
|
||||
:param pkg: the package to install
|
||||
:type pkg: Package
|
||||
:param version: the package version to install
|
||||
:type version: str
|
||||
:param user_options: package parameters
|
||||
:type user_options: dict
|
||||
:rtype: dcos.api.errors.Error
|
||||
:param dictionary: dict to encode
|
||||
:type dictionary: dict
|
||||
:returns: base64 encoding
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
options, err = _merge_options(pkg, version, user_options)
|
||||
if err is not None:
|
||||
return err
|
||||
|
||||
# Insert option parameters into the init template
|
||||
init_template, err = pkg.command_template(version)
|
||||
if err is not None:
|
||||
return err
|
||||
|
||||
rendered_template = pystache.render(init_template, options)
|
||||
|
||||
install_operation, err = util.load_jsons(rendered_template)
|
||||
if err is not None:
|
||||
return err
|
||||
|
||||
return subcommand.install(
|
||||
pkg.name(),
|
||||
install_operation,
|
||||
util.dcos_path())
|
||||
json_str = json.dumps(dictionary, sort_keys=True)
|
||||
str_bytes = six.b(json_str)
|
||||
return base64.b64encode(str_bytes).decode('utf-8')
|
||||
|
||||
|
||||
def uninstall(package_name, remove_all, app_id, init_client):
|
||||
@@ -875,7 +825,7 @@ class Registry():
|
||||
def source(self):
|
||||
"""Returns the associated upstream package source for this registry.
|
||||
|
||||
:rtype: str
|
||||
:rtype: Source
|
||||
"""
|
||||
|
||||
return self._source
|
||||
@@ -972,7 +922,6 @@ class Package():
|
||||
"""
|
||||
|
||||
def __init__(self, registry, path):
|
||||
|
||||
assert os.path.isdir(path)
|
||||
self._registry = registry
|
||||
self.path = path
|
||||
@@ -986,6 +935,40 @@ class Package():
|
||||
|
||||
return os.path.basename(self.path)
|
||||
|
||||
def options(self, version, user_options):
|
||||
"""Merges package options with user supplied options, validates, and
|
||||
returns the result.
|
||||
|
||||
:param version: the package version to install
|
||||
:type version: str
|
||||
:param user_options: package parameters
|
||||
:type user_options: dict
|
||||
:returns: a dictionary with the user supplied options
|
||||
:rtype: (dict, dcos.api.errors.Error)
|
||||
"""
|
||||
|
||||
if user_options is None:
|
||||
user_options = {}
|
||||
|
||||
config_schema, err = self.config_json(version)
|
||||
if err is not None:
|
||||
return (None, err)
|
||||
|
||||
default_options, err = _extract_default_values(config_schema)
|
||||
if err is not None:
|
||||
return (None, err)
|
||||
|
||||
# Merge option overrides
|
||||
options = dict(list(default_options.items()) +
|
||||
list(user_options.items()))
|
||||
|
||||
# Validate options with the config schema
|
||||
err = util.validate_json(options, config_schema)
|
||||
if err is not None:
|
||||
return (None, err)
|
||||
|
||||
return (options, None)
|
||||
|
||||
@property
|
||||
def registry(self):
|
||||
"""Returns the containing registry for this package.
|
||||
@@ -1008,15 +991,6 @@ class Package():
|
||||
self.path,
|
||||
os.path.join(version, 'command.json')))
|
||||
|
||||
def command_template(self, version):
|
||||
"""Returns the JSON content of the command.json file.
|
||||
|
||||
:returns: Package command data
|
||||
:rtype: (str, Error)
|
||||
"""
|
||||
|
||||
return self._data(os.path.join(version, 'command.json'))
|
||||
|
||||
def config_json(self, version):
|
||||
"""Returns the JSON content of the config.json file.
|
||||
|
||||
@@ -1029,20 +1003,67 @@ class Package():
|
||||
def package_json(self, version):
|
||||
"""Returns the JSON content of the package.json file.
|
||||
|
||||
:param version: the package version
|
||||
:type version: str
|
||||
:returns: Package data
|
||||
:rtype: (dict, Error)
|
||||
"""
|
||||
|
||||
return self._json(os.path.join(version, 'package.json'))
|
||||
|
||||
def marathon_template(self, version):
|
||||
"""Returns the JSON content of the marathon.json file.
|
||||
def marathon_json(self, version, options):
|
||||
"""Returns the JSON content of the marathon.json template, after
|
||||
rendering it with options.
|
||||
|
||||
:returns: Package marathon data
|
||||
:rtype: (str, Error)
|
||||
:param version: the package version
|
||||
:type version: str
|
||||
:param options: the template options to use in rendering
|
||||
:type options: dict
|
||||
:rtype: (dict, Error)
|
||||
"""
|
||||
|
||||
return self._data(os.path.join(version, 'marathon.json'))
|
||||
return self._render_template('marathon.json', version, options)
|
||||
|
||||
def command_json(self, version, options):
|
||||
"""Returns the JSON content of the comand.json template, after
|
||||
rendering it with options.
|
||||
|
||||
:param version: the package version
|
||||
:type version: str
|
||||
:param options: the template options to use in rendering
|
||||
:type options: dict
|
||||
:returns: Package data
|
||||
:rtype: (dict, Error)
|
||||
"""
|
||||
|
||||
template, err = self._data(os.path.join(version, 'command.json'))
|
||||
if err is not None:
|
||||
return (None, err)
|
||||
|
||||
rendered = pystache.render(template, options)
|
||||
return (json.loads(rendered), None)
|
||||
|
||||
def _render_template(self, name, version, options):
|
||||
"""Render a template.
|
||||
|
||||
:param name: the file name of the template
|
||||
:type name: str
|
||||
:param version: the package version
|
||||
:type version: str
|
||||
:param options: the template options to use in rendering
|
||||
:type options: dict
|
||||
:rtype: (dict, Error)
|
||||
"""
|
||||
|
||||
template, err = self._data(os.path.join(version, name))
|
||||
if err is not None:
|
||||
return (None, err)
|
||||
|
||||
json, err = util.render_mustache_json(template, options)
|
||||
if err is not None:
|
||||
return (None, err)
|
||||
|
||||
return (json, None)
|
||||
|
||||
def _json(self, path):
|
||||
"""Returns the json content of the supplied file, relative to the
|
||||
|
||||
@@ -38,21 +38,6 @@ def command_executables(subcommand, dcos_path):
|
||||
return (executables[0], None)
|
||||
|
||||
|
||||
BIN_DIRECTORY = 'Scripts' if util.is_windows_platform() else 'bin'
|
||||
|
||||
|
||||
def _subcommand_dir():
|
||||
"""Returns path to the subcommand directory. This directory contains
|
||||
a virtualenv for each installed subcommand.
|
||||
|
||||
:returns: path to the subcommand directory
|
||||
:rtype: str
|
||||
"""
|
||||
return os.path.expanduser(os.path.join("~",
|
||||
constants.DCOS_DIR,
|
||||
constants.DCOS_SUBCOMMAND_SUBDIR))
|
||||
|
||||
|
||||
def list_paths(dcos_path):
|
||||
"""List the real path to executable dcos subcommand programs.
|
||||
|
||||
@@ -71,24 +56,19 @@ def list_paths(dcos_path):
|
||||
_is_executable(os.path.join(binpath, filename)))
|
||||
]
|
||||
|
||||
subcommand_directory = _subcommand_dir()
|
||||
subcommands = []
|
||||
for package in distributions(dcos_path):
|
||||
bin_dir = os.path.join(package_dir(package),
|
||||
constants.DCOS_SUBCOMMAND_VIRTUALENV_SUBDIR,
|
||||
BIN_DIRECTORY)
|
||||
|
||||
subcommands = [
|
||||
os.path.join(subcommand_directory, package, BIN_DIRECTORY, filename)
|
||||
for filename in os.listdir(bin_dir):
|
||||
path = os.path.join(bin_dir, filename)
|
||||
|
||||
for package in distributions(dcos_path)
|
||||
if (filename.startswith(constants.DCOS_COMMAND_PREFIX) and
|
||||
_is_executable(path)):
|
||||
|
||||
for filename in os.listdir(
|
||||
os.path.join(subcommand_directory, package, BIN_DIRECTORY))
|
||||
|
||||
if (filename.startswith(constants.DCOS_COMMAND_PREFIX) and
|
||||
_is_executable(
|
||||
os.path.join(
|
||||
subcommand_directory,
|
||||
package,
|
||||
BIN_DIRECTORY,
|
||||
filename)))
|
||||
]
|
||||
subcommands.append(path)
|
||||
|
||||
return commands + subcommands
|
||||
|
||||
@@ -114,10 +94,17 @@ def distributions(dcos_path):
|
||||
:rtype: list of str
|
||||
"""
|
||||
|
||||
subcommand_directory = _subcommand_dir()
|
||||
subcommand_dir = _subcommand_dir()
|
||||
|
||||
if os.path.isdir(subcommand_directory):
|
||||
return os.listdir(subcommand_directory)
|
||||
if os.path.isdir(subcommand_dir):
|
||||
return [
|
||||
subdir for subdir in os.listdir(subcommand_dir)
|
||||
if os.path.isdir(
|
||||
os.path.join(
|
||||
subcommand_dir,
|
||||
subdir,
|
||||
constants.DCOS_SUBCOMMAND_VIRTUALENV_SUBDIR))
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
@@ -182,32 +169,88 @@ def noun(executable_path):
|
||||
return noun
|
||||
|
||||
|
||||
def install(distribution_name, install_operation, dcos_path):
|
||||
"""Installs the dcos cli subcommand
|
||||
def _write_package_json(pkg, version):
|
||||
""" Write package.json locally.
|
||||
|
||||
:param distribution_name: the name of the package
|
||||
:type distribution_name: str
|
||||
:param install_operation: operation to use to install subcommand
|
||||
:type install_operation: dict
|
||||
:param dcos_path: path to the dcos cli directory
|
||||
:type dcos_path: str
|
||||
:param pkg: the package being installed
|
||||
:type pkg: Package
|
||||
:param version: the package version to install
|
||||
:type version: str
|
||||
:rtype: Error
|
||||
"""
|
||||
|
||||
pkg_dir = package_dir(pkg.name())
|
||||
|
||||
package_path = os.path.join(pkg_dir, 'package.json')
|
||||
|
||||
package_json, err = pkg.package_json(version)
|
||||
if err is not None:
|
||||
return err
|
||||
|
||||
with open(package_path, 'w') as package_file:
|
||||
json.dump(package_json, package_file)
|
||||
|
||||
|
||||
def _write_package_version(pkg, version):
|
||||
""" Write package version locally.
|
||||
|
||||
:param pkg: the package being installed
|
||||
:type pkg: Package
|
||||
:param version: the package version to install
|
||||
:type version: str
|
||||
:rtype: None
|
||||
"""
|
||||
|
||||
pkg_dir = package_dir(pkg.name())
|
||||
|
||||
version_path = os.path.join(pkg_dir, 'version')
|
||||
|
||||
with open(version_path, 'w') as version_file:
|
||||
version_file.write(version)
|
||||
|
||||
|
||||
def _write_package_source(pkg):
|
||||
""" Write package source locally.
|
||||
|
||||
:param pkg: the package being installed
|
||||
:type pkg: Package
|
||||
:rtype: None
|
||||
"""
|
||||
|
||||
pkg_dir = package_dir(pkg.name())
|
||||
|
||||
source_path = os.path.join(pkg_dir, 'source')
|
||||
|
||||
with open(source_path, 'w') as source_file:
|
||||
source_file.write(pkg.registry.source.url)
|
||||
|
||||
|
||||
def _install_env(pkg, version, options):
|
||||
""" Install subcommand virtual env.
|
||||
|
||||
:param pkg: the package to install
|
||||
:type pkg: Package
|
||||
:param version: the package version to install
|
||||
:type version: str
|
||||
:param options: package parameters
|
||||
:type options: dict
|
||||
:returns: an error if the subcommand failed; None otherwise
|
||||
:rtype: dcos.api.errors.Error
|
||||
"""
|
||||
|
||||
subcommand_directory = _subcommand_dir()
|
||||
pkg_dir = package_dir(pkg.name())
|
||||
|
||||
if not os.path.exists(subcommand_directory):
|
||||
logger.info('Creating directory: %r', subcommand_directory)
|
||||
os.makedirs(subcommand_directory, 0o775)
|
||||
install_operation, err = pkg.command_json(version, options)
|
||||
if err is not None:
|
||||
return err
|
||||
|
||||
package_directory = os.path.join(subcommand_directory, distribution_name)
|
||||
env_dir = os.path.join(pkg_dir,
|
||||
constants.DCOS_SUBCOMMAND_VIRTUALENV_SUBDIR)
|
||||
|
||||
if 'pip' in install_operation:
|
||||
return _install_with_pip(
|
||||
distribution_name,
|
||||
os.path.join(dcos_path, BIN_DIRECTORY),
|
||||
package_directory,
|
||||
return install_with_pip(
|
||||
pkg.name(),
|
||||
env_dir,
|
||||
install_operation['pip'])
|
||||
else:
|
||||
return errors.DefaultError(
|
||||
@@ -215,53 +258,100 @@ def install(distribution_name, install_operation, dcos_path):
|
||||
install_operation.keys()))
|
||||
|
||||
|
||||
def uninstall(distribution_name, dcos_path):
|
||||
def install(pkg, version, options):
|
||||
"""Installs the dcos cli subcommand
|
||||
|
||||
:param pkg: the package to install
|
||||
:type pkg: Package
|
||||
:param version: the package version to install
|
||||
:type version: str
|
||||
:param options: package parameters
|
||||
:type options: dict
|
||||
:returns: an error if the subcommand failed; None otherwise
|
||||
:rtype: dcos.api.errors.Error
|
||||
"""
|
||||
|
||||
pkg_dir = package_dir(pkg.name())
|
||||
util.ensure_dir(pkg_dir)
|
||||
|
||||
err = _write_package_json(pkg, version)
|
||||
if err is not None:
|
||||
return err
|
||||
|
||||
_write_package_version(pkg, version)
|
||||
|
||||
_write_package_source(pkg)
|
||||
|
||||
return _install_env(pkg, version, options)
|
||||
|
||||
|
||||
def _subcommand_dir():
|
||||
""" Returns ~/.dcos/subcommands """
|
||||
return os.path.expanduser(os.path.join("~",
|
||||
constants.DCOS_DIR,
|
||||
constants.DCOS_SUBCOMMAND_SUBDIR))
|
||||
|
||||
|
||||
# TODO(mgummelt): should be made private after "dcos subcommand" is removed
|
||||
def package_dir(name):
|
||||
""" Returns ~/.dcos/subcommands/<name>
|
||||
|
||||
:param name: package name
|
||||
:type name: str
|
||||
:rtype: str
|
||||
"""
|
||||
return os.path.join(_subcommand_dir(),
|
||||
name)
|
||||
|
||||
|
||||
def uninstall(package_name, dcos_path):
|
||||
"""Uninstall the dcos cli subcommand
|
||||
|
||||
:param distribution_name: the name of the package
|
||||
:type distribution_name: str
|
||||
:param package_name: the name of the package
|
||||
:type package_name: str
|
||||
:param dcos_path: the path to the dcos cli directory
|
||||
:type dcos_path: str
|
||||
:returns: True if the subcommand was uninstalled
|
||||
:rtype: bool
|
||||
"""
|
||||
|
||||
subcommand_directory = os.path.join(_subcommand_dir(), distribution_name)
|
||||
pkg_dir = package_dir(package_name)
|
||||
|
||||
if os.path.isdir(subcommand_directory):
|
||||
shutil.rmtree(subcommand_directory)
|
||||
if os.path.isdir(pkg_dir):
|
||||
shutil.rmtree(pkg_dir)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
BIN_DIRECTORY = 'Scripts' if util.is_windows_platform() else 'bin'
|
||||
|
||||
def _install_with_pip(
|
||||
distribution_name,
|
||||
bin_directory,
|
||||
package_directory,
|
||||
|
||||
# TODO (mgummelt): should be made private after "dcos subcommand" is removed
|
||||
def install_with_pip(
|
||||
package_name,
|
||||
env_directory,
|
||||
requirements):
|
||||
"""
|
||||
:param distribution_name: the name of the package
|
||||
:type distribution_name: str
|
||||
:param bin_directory: the path to the directory containing the
|
||||
executables (virtualenv, etc).
|
||||
:type bin_directory: str
|
||||
:param package_directory: the path to the directory for the package
|
||||
:type package_directory: str
|
||||
:param package_name: the name of the package
|
||||
:type package_name: str
|
||||
:param env_directory: the path to the directory in which to install the
|
||||
package's virtual env
|
||||
:type env_directory: str
|
||||
:param requirements: the list of pip requirements
|
||||
:type requirements: list of str
|
||||
:returns: an Error if it failed to install the package; None otherwise
|
||||
:rtype: dcos.api.errors.Error
|
||||
"""
|
||||
|
||||
new_package_dir = not os.path.exists(package_directory)
|
||||
bin_directory = os.path.join(util.dcos_path(), BIN_DIRECTORY)
|
||||
new_package_dir = not os.path.exists(env_directory)
|
||||
|
||||
pip_path = os.path.join(package_directory, BIN_DIRECTORY, 'pip')
|
||||
pip_path = os.path.join(env_directory, BIN_DIRECTORY, 'pip')
|
||||
if not os.path.exists(pip_path):
|
||||
cmd = [os.path.join(bin_directory, 'virtualenv'), package_directory]
|
||||
cmd = [os.path.join(bin_directory, 'virtualenv'), env_directory]
|
||||
|
||||
if _execute_command(cmd) != 0:
|
||||
return _generic_error(distribution_name)
|
||||
return _generic_error(package_name)
|
||||
|
||||
with util.temptext() as text_file:
|
||||
fd, requirement_path = text_file
|
||||
@@ -272,7 +362,7 @@ def _install_with_pip(
|
||||
print(line, file=requirements_file)
|
||||
|
||||
cmd = [
|
||||
os.path.join(package_directory, BIN_DIRECTORY, 'pip'),
|
||||
os.path.join(env_directory, BIN_DIRECTORY, 'pip'),
|
||||
'install',
|
||||
'--requirement',
|
||||
requirement_path,
|
||||
@@ -281,9 +371,9 @@ def _install_with_pip(
|
||||
if _execute_command(cmd) != 0:
|
||||
# We should remove the diretory that we just created
|
||||
if new_package_dir:
|
||||
shutil.rmtree(package_directory)
|
||||
shutil.rmtree(env_directory)
|
||||
|
||||
return _generic_error(distribution_name)
|
||||
return _generic_error(package_name)
|
||||
|
||||
return None
|
||||
|
||||
@@ -315,7 +405,7 @@ def _execute_command(command):
|
||||
return process.returncode
|
||||
|
||||
|
||||
def _generic_error(distribution_name):
|
||||
def _generic_error(package_name):
|
||||
"""
|
||||
:param package: package name
|
||||
:type: str
|
||||
@@ -324,4 +414,4 @@ def _generic_error(distribution_name):
|
||||
"""
|
||||
|
||||
return errors.DefaultError(
|
||||
'Error installing {!r} package'.format(distribution_name))
|
||||
'Error installing {!r} package'.format(package_name))
|
||||
|
||||
@@ -57,6 +57,19 @@ def temptext():
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
|
||||
|
||||
def ensure_dir(directory):
|
||||
"""If `directory` does not exist, create it.
|
||||
|
||||
:param directory: path to the directory
|
||||
:type directory: string
|
||||
:rtype: None
|
||||
"""
|
||||
|
||||
if not os.path.exists(directory):
|
||||
logger.info('Creating directory: %r', directory)
|
||||
os.makedirs(directory, 0o775)
|
||||
|
||||
|
||||
def which(program):
|
||||
"""Returns the path to the named executable program.
|
||||
|
||||
@@ -178,7 +191,6 @@ def load_jsons(value):
|
||||
return (json.loads(value), None)
|
||||
except:
|
||||
error = sys.exc_info()[0]
|
||||
logger = get_logger(__name__)
|
||||
logger.error(
|
||||
'Unhandled exception while loading JSON: %r -- %r',
|
||||
value,
|
||||
@@ -298,3 +310,5 @@ class CustomJsonRenderer(pystache.Renderer):
|
||||
:rtype: str
|
||||
"""
|
||||
return json.dumps(val)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
Reference in New Issue
Block a user