diff --git a/cli/dcoscli/data/help/package.txt b/cli/dcoscli/data/help/package.txt new file mode 100644 index 0000000..fcacf1a --- /dev/null +++ b/cli/dcoscli/data/help/package.txt @@ -0,0 +1,48 @@ +Install and manage DCOS packages + +Usage: + dcos package --config-schema + dcos package --info + dcos package describe [--app --options= --cli] + dcos package install [--cli | [--app --app-id=]] + [--package-version=] + [--options=] [--yes] + dcos package list [--json --endpoints --app-id= ] + dcos package search [--json ] + dcos package sources + dcos package uninstall [--cli | [--app --app-id= --all]] + + dcos package update [--validate] + +Options: + --all Apply the operation to all matching packages + --app Apply the operation only to the package's + application + --app-id= The application id + --cli Apply the operation only to the package's CLI + -h, --help Show this screen + --info Show a short description of this subcommand + --options= Path to a JSON file containing package + installation options + --package-version= Package version to install + --validate Validate package content when updating sources + --version Show version + --yes Assume "yes" is the answer to all prompts and + run non-interactively + +Configuration: + [package] + # Path to the local package cache. + cache_dir = "/var/dcos/cache" + + # List of package sources, in search order. + # + # Three protocols are supported: + # - Local file + # - HTTPS + # - Git + sources = [ + "file:///Users/me/test-registry", + "https://my.org/registry", + "git://github.com/mesosphere/universe.git" + ] diff --git a/cli/dcoscli/package/main.py b/cli/dcoscli/package/main.py index f2c435b..e4f1f14 100644 --- a/cli/dcoscli/package/main.py +++ b/cli/dcoscli/package/main.py @@ -1,49 +1,3 @@ -"""Install and manage DCOS software packages - -Usage: - dcos package --config-schema - dcos package --info - dcos package describe [--app --options= --cli] - dcos package install [--cli | [--app --app-id=]] - [--options= --yes] - dcos package list [--json --endpoints --app-id= ] - dcos package search [--json ] - dcos package sources - dcos package uninstall [--cli | [--app --app-id= --all]] - - dcos package update [--validate] - -Options: - -h, --help Show this screen - --info Show a short description of this subcommand - --version Show version - --yes Assume "yes" is the answer to all prompts and run - non-interactively - --all Apply the operation to all matching packages - --app Apply the operation only to the package's application - --app-id= The application id - --cli Apply the operation only to the package's CLI - --options= Path to a JSON file containing package installation - options - --validate Validate package content when updating sources - -Configuration: - [package] - # Path to the local package cache. - cache_dir = "/var/dcos/cache" - - # List of package sources, in search order. - # - # Three protocols are supported: - # - Local file - # - HTTPS - # - Git - sources = [ - "file:///Users/me/test-registry", - "https://my.org/registry", - "git://github.com/mesosphere/universe.git" - ] -""" import json import os import sys @@ -68,11 +22,17 @@ def main(): return 1 +def _doc(): + return pkg_resources.resource_string( + 'dcoscli', + 'data/help/package.txt').decode('utf-8') + + def _main(): util.configure_process_from_environ() args = docopt.docopt( - __doc__, + _doc(), version='dcos-package version {}'.format(dcoscli.version)) http.silence_requests_warnings() @@ -103,8 +63,8 @@ def _cmds(): cmds.Command( hierarchy=['package', 'install'], - arg_keys=['', '--options', '--app-id', '--cli', - '--app', '--yes'], + arg_keys=['', '--package-version', '--options', + '--app-id', '--cli', '--app', '--yes'], function=_install), cmds.Command( @@ -148,7 +108,7 @@ def _package(config_schema, info): elif info: _info() else: - emitter.publish(options.make_generic_usage_message(__doc__)) + emitter.publish(options.make_generic_usage_message(_doc())) return 1 return 0 @@ -161,7 +121,7 @@ def _info(): :rtype: int """ - emitter.publish(__doc__.split('\n')[0]) + emitter.publish(_doc().split('\n')[0]) return 0 @@ -215,22 +175,22 @@ def _describe(package_name, cli, app, options_path): raise DCOSException("Package [{}] not found".format(package_name)) # TODO(CD): Make package version to describe configurable - pkg_version = pkg.latest_version() - pkg_json = pkg.package_json(pkg_version) - version_map = pkg.software_versions() - versions = [version_map[pkg_ver] for pkg_ver in version_map] + pkg_revision = pkg.latest_package_revision() + pkg_json = pkg.package_json(pkg_revision) + revision_map = pkg.package_revisions_map() + pkg_versions = list(revision_map.values()) del pkg_json['version'] - pkg_json['versions'] = versions + pkg_json['versions'] = pkg_versions if cli or app: user_options = _user_options(options_path) - options = pkg.options(pkg_version, user_options) + options = pkg.options(pkg_revision, user_options) if cli: - pkg_json['command'] = pkg.command_json(pkg_version, options) + pkg_json['command'] = pkg.command_json(pkg_revision, options) if app: - pkg_json['app'] = pkg.marathon_json(pkg_version, options) + pkg_json['app'] = pkg.marathon_json(pkg_revision, options) emitter.publish(pkg_json) return 0 @@ -277,11 +237,14 @@ def _confirm(prompt, yes): "'{}' is not a valid response.".format(response)) -def _install(package_name, options_path, app_id, cli, app, yes): +def _install(package_name, package_version, options_path, app_id, cli, app, + yes): """Install the specified package. :param package_name: the package to install :type package_name: str + :param package_version: package version to install + :type package_version: str :param options_path: path to file containing option values :type options_path: str :param app_id: app ID for installation of this package @@ -309,10 +272,15 @@ def _install(package_name, options_path, app_id, cli, app, yes): "repositories" raise DCOSException(msg) - # TODO(CD): Make package version to install configurable - pkg_version = pkg.latest_version() + pkg_revision = pkg.latest_package_revision(package_version) + if pkg_revision is None: + msg = "Package [{}] not available".format(package_name) + if package_version is not None: + msg += " with version {}".format(package_version) + raise DCOSException(msg) - pre_install_notes = pkg.package_json(pkg_version).get('preInstallNotes') + pkg_json = pkg.package_json(pkg_revision) + pre_install_notes = pkg_json.get('preInstallNotes') if pre_install_notes: emitter.publish(pre_install_notes) if not _confirm('Continue installing?', yes): @@ -321,35 +289,36 @@ def _install(package_name, options_path, app_id, cli, app, yes): user_options = _user_options(options_path) - options = pkg.options(pkg_version, user_options) + options = pkg.options(pkg_revision, user_options) - if app and pkg.has_marathon_definition(pkg_version): + revision_map = pkg.package_revisions_map() + package_version = revision_map.get(pkg_revision) + + if app and pkg.has_marathon_definition(pkg_revision): # Install in Marathon - version_map = pkg.software_versions() - sw_version = version_map.get(pkg_version, '?') - - message = 'Installing package [{}] version [{}]'.format( - pkg.name(), sw_version) + msg = 'Installing Marathon app for package [{}] version [{}]'.format( + pkg.name(), package_version) if app_id is not None: - message += ' with app id [{}]'.format(app_id) + msg += ' with app id [{}]'.format(app_id) - emitter.publish(message) + emitter.publish(msg) init_client = marathon.create_client(config) package.install_app( pkg, - pkg_version, + pkg_revision, init_client, options, app_id) - if cli and pkg.has_command_definition(pkg_version): + if cli and pkg.has_command_definition(pkg_revision): # Install subcommand - emitter.publish('Installing CLI subcommand for package [{}]'.format( - pkg.name())) + msg = 'Installing CLI subcommand for package [{}] version [{}]'.format( + pkg.name(), package_version) + emitter.publish(msg) - subcommand.install(pkg, pkg_version, options) + subcommand.install(pkg, pkg_revision, options) subcommand_paths = subcommand.get_package_commands(package_name) new_commands = [os.path.basename(p).replace('-', ' ', 1) @@ -361,7 +330,7 @@ def _install(package_name, options_path, app_id, cli, app, yes): emitter.publish("New command{} available: {}".format(plural, commands)) - post_install_notes = pkg.package_json(pkg_version).get('postInstallNotes') + post_install_notes = pkg_json.get('postInstallNotes') if post_install_notes: emitter.publish(post_install_notes) diff --git a/cli/dcoscli/tables.py b/cli/dcoscli/tables.py index d28a7f6..3d837c4 100644 --- a/cli/dcoscli/tables.py +++ b/cli/dcoscli/tables.py @@ -265,6 +265,7 @@ def package_table(packages): fields = OrderedDict([ ('NAME', lambda p: p['name']), + ('VERSION', lambda p: p['version']), ('APP', lambda p: '\n'.join(p['apps']) if p.get('apps') else EMPTY_ENTRY), ('COMMAND', @@ -274,6 +275,7 @@ def package_table(packages): tb = util.table(fields, packages, sortby="NAME") tb.align['NAME'] = 'l' + tb.align['VERSION'] = 'l' tb.align['APP'] = 'l' tb.align['COMMAND'] = 'l' tb.align['DESCRIPTION'] = 'l' diff --git a/cli/setup.py b/cli/setup.py index 873b7cf..1b39e7e 100644 --- a/cli/setup.py +++ b/cli/setup.py @@ -81,6 +81,7 @@ setup( package_data={ 'dcoscli': [ 'data/*.json', + 'data/help/*.txt', 'data/config-schema/*.json', ], }, diff --git a/cli/tests/data/package/help.txt b/cli/tests/data/package/help.txt new file mode 100644 index 0000000..fcacf1a --- /dev/null +++ b/cli/tests/data/package/help.txt @@ -0,0 +1,48 @@ +Install and manage DCOS packages + +Usage: + dcos package --config-schema + dcos package --info + dcos package describe [--app --options= --cli] + dcos package install [--cli | [--app --app-id=]] + [--package-version=] + [--options=] [--yes] + dcos package list [--json --endpoints --app-id= ] + dcos package search [--json ] + dcos package sources + dcos package uninstall [--cli | [--app --app-id= --all]] + + dcos package update [--validate] + +Options: + --all Apply the operation to all matching packages + --app Apply the operation only to the package's + application + --app-id= The application id + --cli Apply the operation only to the package's CLI + -h, --help Show this screen + --info Show a short description of this subcommand + --options= Path to a JSON file containing package + installation options + --package-version= Package version to install + --validate Validate package content when updating sources + --version Show version + --yes Assume "yes" is the answer to all prompts and + run non-interactively + +Configuration: + [package] + # Path to the local package cache. + cache_dir = "/var/dcos/cache" + + # List of package sources, in search order. + # + # Three protocols are supported: + # - Local file + # - HTTPS + # - Git + sources = [ + "file:///Users/me/test-registry", + "https://my.org/registry", + "git://github.com/mesosphere/universe.git" + ] diff --git a/cli/tests/data/package/json/test_describe_app_marathon.json b/cli/tests/data/package/json/test_describe_app_marathon.json index e46917c..a3a944c 100644 --- a/cli/tests/data/package/json/test_describe_app_marathon.json +++ b/cli/tests/data/package/json/test_describe_app_marathon.json @@ -61,7 +61,7 @@ "framework" ], "versions": [ - "0.8.1", - "0.9.0-RC3" + "0.9.0-RC3", + "0.8.1" ] } diff --git a/cli/tests/data/package/json/test_describe_marathon.json b/cli/tests/data/package/json/test_describe_marathon.json index d286d5f..48b960c 100644 --- a/cli/tests/data/package/json/test_describe_marathon.json +++ b/cli/tests/data/package/json/test_describe_marathon.json @@ -23,7 +23,7 @@ "framework" ], "versions": [ - "0.8.1", - "0.9.0-RC3" + "0.9.0-RC3", + "0.8.1" ] } diff --git a/cli/tests/integrations/test_dcos.py b/cli/tests/integrations/test_dcos.py index 5c794f3..9a4daa5 100644 --- a/cli/tests/integrations/test_dcos.py +++ b/cli/tests/integrations/test_dcos.py @@ -21,7 +21,7 @@ Available DCOS commands: \thelp \tDisplay command line usage information \tmarathon \tDeploy and manage applications on the DCOS \tnode \tManage DCOS nodes -\tpackage \tInstall and manage DCOS software packages +\tpackage \tInstall and manage DCOS packages \tservice \tManage DCOS services \ttask \tManage DCOS tasks diff --git a/cli/tests/integrations/test_help.py b/cli/tests/integrations/test_help.py index 92f70d8..94d1d28 100644 --- a/cli/tests/integrations/test_help.py +++ b/cli/tests/integrations/test_help.py @@ -40,7 +40,7 @@ Available DCOS commands: \thelp \tDisplay command line usage information \tmarathon \tDeploy and manage applications on the DCOS \tnode \tManage DCOS nodes -\tpackage \tInstall and manage DCOS software packages +\tpackage \tInstall and manage DCOS packages \tservice \tManage DCOS services \ttask \tManage DCOS tasks diff --git a/cli/tests/integrations/test_package.py b/cli/tests/integrations/test_package.py index 0a5da27..226506d 100644 --- a/cli/tests/integrations/test_package.py +++ b/cli/tests/integrations/test_package.py @@ -2,6 +2,7 @@ import contextlib import json import os +import pkg_resources import six from dcos import subcommand @@ -75,59 +76,16 @@ version-1.x.zip", def test_package(): - stdout = b"""Install and manage DCOS software packages - -Usage: - dcos package --config-schema - dcos package --info - dcos package describe [--app --options= --cli] - dcos package install [--cli | [--app --app-id=]] - [--options= --yes] - dcos package list [--json --endpoints --app-id= ] - dcos package search [--json ] - dcos package sources - dcos package uninstall [--cli | [--app --app-id= --all]] - - dcos package update [--validate] - -Options: - -h, --help Show this screen - --info Show a short description of this subcommand - --version Show version - --yes Assume "yes" is the answer to all prompts and run - non-interactively - --all Apply the operation to all matching packages - --app Apply the operation only to the package's application - --app-id= The application id - --cli Apply the operation only to the package's CLI - --options= Path to a JSON file containing package installation - options - --validate Validate package content when updating sources - -Configuration: - [package] - # Path to the local package cache. - cache_dir = "/var/dcos/cache" - - # List of package sources, in search order. - # - # Three protocols are supported: - # - Local file - # - HTTPS - # - Git - sources = [ - "file:///Users/me/test-registry", - "https://my.org/registry", - "git://github.com/mesosphere/universe.git" - ] -""" + stdout = pkg_resources.resource_string( + 'tests', + 'data/package/help.txt') assert_command(['dcos', 'package', '--help'], stdout=stdout) def test_info(): assert_command(['dcos', 'package', '--info'], - stdout=b'Install and manage DCOS software packages\n') + stdout=b'Install and manage DCOS packages\n') def test_version(): @@ -224,6 +182,35 @@ def test_install_missing_options_file(): stderr=b"Error opening file [asdf.json]: No such file or directory\n") +def test_install_specific_version(): + stdout = (b'We recommend a minimum of one node with at least 2 ' + b'CPU\'s and 1GB of RAM available for the Marathon Service.\n' + b'Installing Marathon app for package [marathon] ' + b'version [0.8.1]\n' + b'Marathon DCOS Service has been successfully installed!\n\n' + b'\tDocumentation: https://mesosphere.github.io/marathon\n' + b'\tIssues: https:/github.com/mesosphere/marathon/issues\n\n') + + with _package('marathon', + stdout=stdout, + args=['--yes', '--package-version=0.8.1']): + + returncode, stdout, stderr = exec_command( + ['dcos', 'package', 'list', 'marathon', '--json']) + assert returncode == 0 + assert stderr == b'' + assert json.loads(stdout.decode('utf-8'))[0]['version'] == "0.8.1" + + +def test_install_bad_package_version(): + stderr = b'Package [helloworld] not available with version a.b.c\n' + assert_command( + ['dcos', 'package', 'install', 'helloworld', + '--package-version=a.b.c'], + returncode=1, + stderr=stderr) + + def test_package_metadata(): _install_helloworld() @@ -293,13 +280,13 @@ version-1.x.zip' def test_install_with_id(zk_znode): args = ['--app-id=chronos-1', '--yes'] - stdout = (b'Installing package [chronos] version [2.3.4] with app id ' - b'[chronos-1]\n') + stdout = (b'Installing Marathon app for package [chronos] version [2.3.4] ' + b'with app id [chronos-1]\n') _install_chronos(args=args, stdout=stdout) args = ['--app-id=chronos-2', '--yes'] - stdout = (b'Installing package [chronos] version [2.3.4] with app id ' - b'[chronos-2]\n') + stdout = (b'Installing Marathon app for package [chronos] version [2.3.4] ' + b'with app id [chronos-2]\n') _install_chronos(args=args, stdout=stdout) @@ -371,9 +358,10 @@ version-1.x.zip", def test_uninstall_multiple_apps(): stdout = (b'A sample pre-installation message\n' - b'Installing package [helloworld] version [0.1.0] ' + - b'with app id [/helloworld-1]\n' - b'Installing CLI subcommand for package [helloworld]\n' + b'Installing Marathon app for package [helloworld] version ' + b'[0.1.0] with app id [/helloworld-1]\n' + b'Installing CLI subcommand for package [helloworld] ' + b'version [0.1.0]\n' b'New command available: dcos helloworld\n' b'A sample post-installation message\n') @@ -381,18 +369,19 @@ def test_uninstall_multiple_apps(): stdout=stdout) stdout = (b'A sample pre-installation message\n' - b'Installing package [helloworld] version [0.1.0] ' + - b'with app id [/helloworld-2]\n' - b'Installing CLI subcommand for package [helloworld]\n' + b'Installing Marathon app for package [helloworld] version ' + b'[0.1.0] with app id [/helloworld-2]\n' + b'Installing CLI subcommand for package [helloworld] ' + b'version [0.1.0]\n' b'New command available: dcos helloworld\n' b'A sample post-installation message\n') _install_helloworld(['--yes', '--app-id=/helloworld-2'], stdout=stdout) - stderr = (b"Multiple apps named [helloworld] are installed: " + - b"[/helloworld-1, /helloworld-2].\n" + - b"Please use --app-id to specify the ID of the app " + + stderr = (b"Multiple apps named [helloworld] are installed: " + b"[/helloworld-1, /helloworld-2].\n" + b"Please use --app-id to specify the ID of the app " b"to uninstall, or use --all to uninstall all apps.\n") _uninstall_helloworld(stderr=stderr, returncode=1) @@ -433,8 +422,10 @@ def test_install_yes(): stdin=yes_file, stdout=b'A sample pre-installation message\n' b'Continue installing? [yes/no] ' - b'Installing package [helloworld] version [0.1.0]\n' - b'Installing CLI subcommand for package [helloworld]\n' + b'Installing Marathon app for package [helloworld] version ' + b'[0.1.0]\n' + b'Installing CLI subcommand for package [helloworld] ' + b'version [0.1.0]\n' b'New command available: dcos helloworld\n' b'A sample post-installation message\n') _uninstall_helloworld() @@ -483,8 +474,9 @@ version-1.x.zip", _uninstall_helloworld() stdout = (b"A sample pre-installation message\n" - b"Installing CLI subcommand for package [helloworld]\n" - b'New command available: dcos helloworld\n' + b"Installing CLI subcommand for package [helloworld] " + + b"version [0.1.0]\n" + b"New command available: dcos helloworld\n" b"A sample post-installation message\n") _install_helloworld(args=['--cli', '--yes'], stdout=stdout) @@ -610,8 +602,10 @@ def _get_app_labels(app_id): def _install_helloworld( args=['--yes'], stdout=b'A sample pre-installation message\n' - b'Installing package [helloworld] version [0.1.0]\n' - b'Installing CLI subcommand for package [helloworld]\n' + b'Installing Marathon app for package [helloworld] ' + b'version [0.1.0]\n' + b'Installing CLI subcommand for package [helloworld] ' + b'version [0.1.0]\n' b'New command available: dcos helloworld\n' b'A sample post-installation message\n', returncode=0, @@ -646,7 +640,8 @@ def _uninstall_chronos(args=[], returncode=0, stdout=b'', stderr=''): def _install_chronos( args=['--yes'], returncode=0, - stdout=b'Installing package [chronos] version [2.3.4]\n', + stdout=b'Installing Marathon app for package [chronos] ' + b'version [2.3.4]\n', stderr=b'', preInstallNotes=b'We recommend a minimum of one node with at least 1 ' b'CPU and 2GB of RAM available for the Chronos ' @@ -675,8 +670,8 @@ def _list(args=['--json'], def _helloworld(): stdout = b'''A sample pre-installation message -Installing package [helloworld] version [0.1.0] -Installing CLI subcommand for package [helloworld] +Installing Marathon app for package [helloworld] version [0.1.0] +Installing CLI subcommand for package [helloworld] version [0.1.0] New command available: dcos helloworld A sample post-installation message ''' @@ -686,18 +681,21 @@ A sample post-installation message @contextlib.contextmanager def _package(name, - stdout=b''): - """Context manager that deploys an app on entrance, and removes it on + stdout=b'', + args=['--yes']): + """Context manager that installs a package on entrace, and uninstalls it on exit. - :param path: path to app's json definition: - :type path: str - :param app_id: app id - :type app_id: str + :param name: package name + :type name: str + :param stdout: Expected stdout + :type stdout: str + :param args: extra CLI args + :type args: [str] :rtype: None """ - assert_command(['dcos', 'package', 'install', name, '--yes'], + assert_command(['dcos', 'package', 'install', name] + args, stdout=stdout) try: yield diff --git a/cli/tests/unit/data/package.txt b/cli/tests/unit/data/package.txt index 97d0398..e4175d7 100644 --- a/cli/tests/unit/data/package.txt +++ b/cli/tests/unit/data/package.txt @@ -1,2 +1,2 @@ - NAME APP COMMAND DESCRIPTION - helloworld /helloworld helloworld Example DCOS application package \ No newline at end of file + NAME VERSION APP COMMAND DESCRIPTION + helloworld 0.1.0 /helloworld helloworld Example DCOS application package \ No newline at end of file diff --git a/dcos/package.py b/dcos/package.py index 39e856e..4b8a214 100644 --- a/dcos/package.py +++ b/dcos/package.py @@ -37,13 +37,13 @@ PACKAGE_REGISTRY_VERSION_KEY = 'DCOS_PACKAGE_REGISTRY_VERSION' PACKAGE_FRAMEWORK_NAME_KEY = 'DCOS_PACKAGE_FRAMEWORK_NAME' -def install_app(pkg, version, init_client, options, app_id): +def install_app(pkg, revision, init_client, options, app_id): """Installs a package's application :param pkg: the package to install :type pkg: Package - :param version: the package version to install - :type version: str + :param revision: the package revision to install + :type revision: str :param init_client: the program to use to run the package :type init_client: object :param options: package parameters @@ -54,7 +54,7 @@ def install_app(pkg, version, init_client, options, app_id): """ # Insert option parameters into the init template - init_desc = pkg.marathon_json(version, options) + init_desc = pkg.marathon_json(revision, options) if app_id is not None: logger.debug('Setting app ID to "%s" (was "%s")', @@ -66,20 +66,20 @@ def install_app(pkg, version, init_client, options, app_id): init_client.add_app(init_desc) -def _make_package_labels(pkg, version, options): +def _make_package_labels(pkg, revision, 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 revision: The package revision to install + :type revision: str :param options: package parameters :type options: dict :returns: Marathon app labels :rtype: dict """ - metadata = pkg.package_json(version) + metadata = pkg.package_json(revision) encoded_metadata = _base64_encode(metadata) @@ -96,11 +96,11 @@ def _make_package_labels(pkg, version, options): PACKAGE_SOURCE_KEY: pkg.registry.source.url, PACKAGE_FRAMEWORK_KEY: json.dumps(is_framework), PACKAGE_REGISTRY_VERSION_KEY: package_registry_version, - PACKAGE_RELEASE_KEY: str(version) + PACKAGE_RELEASE_KEY: revision } - if pkg.has_command_definition(version): - command = pkg.command_json(version, options) + if pkg.has_command_definition(revision): + command = pkg.command_json(revision, options) package_labels[PACKAGE_COMMAND_KEY] = _base64_encode(command) # Run a heuristic that determines the hint for the framework name @@ -319,7 +319,7 @@ class InstalledPackage(object): ret.update(package_json) ret['packageSource'] = self.subcommand.package_source() - ret['releaseVersion'] = self.subcommand.package_version() + ret['releaseVersion'] = self.subcommand.package_revision() else: ret.update(self.apps[0]) ret.pop('appId') @@ -357,9 +357,9 @@ def installed_packages(init_client, endpoints): dicts[key]['apps'].append(app) for subcmd in subcommands: - package_version = subcmd.package_version() + package_revision = subcmd.package_revision() package_source = subcmd.package_source() - key = (subcmd.name, package_version, package_source) + key = (subcmd.name, package_revision, package_source) dicts[key]['command'] = subcmd return [ @@ -709,7 +709,7 @@ def update_sources(config, validate=False): errors.append(e.message) continue - # check the version + # check version # TODO(jsancio): move this to the validation when it is forced Registry(source, stage_dir).check_version( LooseVersion('1.0'), @@ -1078,7 +1078,8 @@ class Registry(): """ version = LooseVersion(self.get_version()) - if not (version >= min_version and version < max_version): + if not (version >= min_version and + version < max_version): raise DCOSException(( 'Unable to update source [{}] because version {} is ' 'not supported. Supported versions are between {} and ' @@ -1194,12 +1195,12 @@ class Package(): return os.path.basename(self.path) - def options(self, version, user_options): + def options(self, revision, 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 revision: the package revision to install + :type revision: str :param user_options: package parameters :type user_options: dict :returns: a dictionary with the user supplied options @@ -1209,7 +1210,7 @@ class Package(): if user_options is None: user_options = {} - config_schema = self.config_json(version) + config_schema = self.config_json(revision) default_options = _extract_default_values(config_schema) logger.info('Generated default options: %r', default_options) @@ -1239,11 +1240,11 @@ class Package(): return self._registry - def has_definition(self, version, filename): + def has_definition(self, revision, filename): """Returns true if the package defines filename; false otherwise. - :param version: package version - :type version: str + :param revision: package revision + :type revision: str :param filename: file in package definition :type filename: str :returns: whether filename is defined @@ -1253,54 +1254,56 @@ class Package(): return os.path.isfile( os.path.join( self.path, - os.path.join(version, filename))) + os.path.join(revision, filename))) - def has_command_definition(self, version): + def has_command_definition(self, revision): """Returns true if the package defines a command; false otherwise. - :param version: package version - :type version: str + :param revision: package revision + :type revision: str :rtype: bool """ - return self.has_definition(version, 'command.json') + return self.has_definition(revision, 'command.json') - def has_marathon_definition(self, version): + def has_marathon_definition(self, revision): """Returns true if the package defines a Marathon json. false otherwise. - :param version: package version - :type version: str + :param revision: package revision + :type revision: str :rtype: bool """ - return self.has_definition(version, 'marathon.json') + return self.has_definition(revision, 'marathon.json') - def config_json(self, version): + def config_json(self, revision): """Returns the JSON content of the config.json file. + :param revision: package revision + :type revision: str :returns: Package config schema :rtype: dict """ - return self._json(os.path.join(version, 'config.json')) + return self._json(os.path.join(revision, 'config.json')) - def package_json(self, version): + def package_json(self, revision): """Returns the JSON content of the package.json file. - :param version: the package version - :type version: str + :param revision: the package revision + :type revision: str :returns: Package data :rtype: dict """ - return self._json(os.path.join(version, 'package.json')) + return self._json(os.path.join(revision, 'package.json')) - def marathon_json(self, version, options): + def marathon_json(self, revision, options): """Returns the JSON content of the marathon.json template, after rendering it with options. - :param version: the package version - :type version: str + :param revision: the package revision + :type revision: str :param options: the template options to use in rendering :type options: dict :rtype: dict @@ -1308,11 +1311,11 @@ class Package(): init_desc = self._render_template( 'marathon.json', - version, + revision, options) # Add package metadata - package_labels = _make_package_labels(self, version, options) + package_labels = _make_package_labels(self, revision, options) # Preserve existing labels labels = init_desc.get('labels', {}) @@ -1322,35 +1325,35 @@ class Package(): return init_desc - def command_json(self, version, options): + def command_json(self, revision, options): """Returns the JSON content of the comand.json template, after rendering it with options. - :param version: the package version - :type version: str + :param revision: the package revision + :type revision: str :param options: the template options to use in rendering :type options: dict :returns: Package data :rtype: dict """ - template = self._data(os.path.join(version, 'command.json')) + template = self._data(os.path.join(revision, 'command.json')) rendered = pystache.render(template, options) return json.loads(rendered) - def _render_template(self, name, version, options): + def _render_template(self, name, revision, options): """Render a template. :param name: the file name of the template :type name: str - :param version: the package version - :type version: str + :param revision: the package revision + :type revision: str :param options: the template options to use in rendering :type options: dict :rtype: dict """ - template = self._data(os.path.join(version, name)) + template = self._data(os.path.join(revision, name)) return util.render_mustache_json(template, options) def _json(self, path): @@ -1377,60 +1380,58 @@ class Package(): full_path = os.path.join(self.path, path) return util.read_file(full_path) - def package_versions(self): - """Returns all of the available package versions, most recent first. + def package_revisions(self): + """Returns all of the available package revisions, most recent first. - Note that the result does not describe versions of the package, not - the software described by the package. - - :returns: Available versions of this package + :returns: Available revisions of this package :rtype: [str] """ - vs = [f for f in os.listdir(self.path) if not f.startswith('.')] - vs.reverse() + vs = sorted((f for f in os.listdir(self.path) + if not f.startswith('.')), key=int, reverse=True) return vs - def software_versions(self): - """Returns a mapping from the package version to the version of the - software described by the package. + def package_revisions_map(self): + """Returns a mapping from the package revision to the package version. - :returns: Map from package versions to versions of the softwre. - :rtype: dict + :returns: Map from package revision to package version + :rtype: OrderedDict """ - software_package_map = collections.OrderedDict() - for v in self.package_versions(): - pkg_json = self.package_json(v) - software_package_map[v] = pkg_json['version'] - return software_package_map + package_version_map = collections.OrderedDict() + for rev in self.package_revisions(): + pkg_json = self.package_json(rev) + package_version_map[rev] = pkg_json['version'] + return package_version_map - def latest_version(self): - """Returns the latest package version. + def latest_package_revision(self, package_version=None): + """Returns the most recent package revision, for a + given package version if specified. - :returns: The latest version of this package - :rtype: str + :param package_version: a given package version + :type package_version: str + :returns: package revision + :rtype: str | None """ - pkg_versions = self.package_versions() + if package_version: + pkg_rev_map = self.package_revisions_map() + # depends on package_revisions() returning an OrderedDict + if package_version in pkg_rev_map.values(): + return next(pkg_rev for pkg_rev in reversed(pkg_rev_map) + if pkg_rev_map[pkg_rev] == package_version) + else: + return None + else: + pkg_revisions = self.package_revisions() + revision = pkg_revisions[0] - if len(pkg_versions) is 0: - raise DCOSException( - 'No versions found for package [{}]'.format(self.name())) - - pkg_versions.sort() - return pkg_versions[-1] + return revision def __repr__(self): - v, error = self.latest_version() - if error is not None: - return error.error() - - pkg_json, error = self.package_json(v) - - if error is not None: - return error.error() + rev = self.latest_package_revision() + pkg_json = self.package_json(rev) return json.dumps(pkg_json) diff --git a/dcos/subcommand.py b/dcos/subcommand.py index c01dcdf..28996ed 100644 --- a/dcos/subcommand.py +++ b/dcos/subcommand.py @@ -179,13 +179,13 @@ def noun(executable_path): return noun -def _write_package_json(pkg, version): +def _write_package_json(pkg, revision): """ Write package.json locally. :param pkg: the package being installed :type pkg: Package - :param version: the package version to install - :type version: str + :param revision: the package revision to install + :type revision: str :rtype: None """ @@ -193,28 +193,28 @@ def _write_package_json(pkg, version): package_path = os.path.join(pkg_dir, 'package.json') - package_json = pkg.package_json(version) + package_json = pkg.package_json(revision) with util.open_file(package_path, 'w') as package_file: json.dump(package_json, package_file) -def _write_package_version(pkg, version): - """ Write package version locally. +def _write_package_revision(pkg, revision): + """ Write package revision locally. :param pkg: the package being installed :type pkg: Package - :param version: the package version to install - :type version: str + :param revision: the package revision to install + :type revision: str :rtype: None """ pkg_dir = package_dir(pkg.name()) - version_path = os.path.join(pkg_dir, 'version') + revision_path = os.path.join(pkg_dir, 'version') - with util.open_file(version_path, 'w') as version_file: - version_file.write(version) + with util.open_file(revision_path, 'w') as revision_file: + revision_file.write(revision) def _write_package_source(pkg): @@ -233,13 +233,13 @@ def _write_package_source(pkg): source_file.write(pkg.registry.source.url) -def _install_env(pkg, version, options): +def _install_env(pkg, revision, 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 revision: the package revision to install + :type revision: str :param options: package parameters :type options: dict :rtype: None @@ -247,7 +247,7 @@ def _install_env(pkg, version, options): pkg_dir = package_dir(pkg.name()) - install_operation = pkg.command_json(version, options) + install_operation = pkg.command_json(revision, options) env_dir = os.path.join(pkg_dir, constants.DCOS_SUBCOMMAND_VIRTUALENV_SUBDIR) @@ -262,13 +262,13 @@ def _install_env(pkg, version, options): install_operation.keys())) -def install(pkg, version, options): +def install(pkg, revision, 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 revision: the package revision to install + :type revision: str :param options: package parameters :type options: dict :rtype: None @@ -277,11 +277,11 @@ def install(pkg, version, options): pkg_dir = package_dir(pkg.name()) util.ensure_dir(pkg_dir) - _write_package_json(pkg, version) - _write_package_version(pkg, version) + _write_package_json(pkg, revision) + _write_package_revision(pkg, revision) _write_package_source(pkg) - _install_env(pkg, version, options) + _install_env(pkg, revision, options) def _subcommand_dir(): @@ -432,14 +432,14 @@ class InstalledSubcommand(object): return package_dir(self.name) - def package_version(self): + def package_revision(self): """ - :returns: this subcommand's version. + :returns: this subcommand's revision. :rtype: str """ - version_path = os.path.join(self._dir(), 'version') - return util.read_file(version_path) + revision_path = os.path.join(self._dir(), 'version') + return util.read_file(revision_path) def package_source(self): """