diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e69de29 diff --git a/.scripts/prepare-repo.sh b/.scripts/prepare-repo.sh new file mode 100755 index 0000000..c650b72 --- /dev/null +++ b/.scripts/prepare-repo.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +PACK_DIRECTORY=$(find . -maxdepth 1 -type d -not -path '*/\.*' -not -path '\.' | head -1 | sed -e 's/^\.\///') + +if [[ -z "${PACK_DIRECTORY}" ]]; then + echo "Unable to find pack directory" + exit 1 +fi + +echo "Found pack directory: ${PACK_DIRECTORY}" + +PACK_NAME=$(cat ${PACK_DIRECTORY}/pack.yaml | grep "name:" | sed -e 's/^name:[ \t]*//') + +if [[ -z "${PACK_NAME}" ]]; then + echo "Unable to retrieve pack name from ${PACK_DIRECTORY}/pack.yaml" + exit 1 +fi + +echo "Found pack name: ${PACK_NAME}" + + +# Copy pack files in root dir so the checks work +echo "Exporting PACK_NAME and removing pack directory" + +cp -r ${PACK_DIRECTORY}/* . +rm -rf ${PACK_DIRECTORY} + +ls -la . + +echo "export PACK_NAME=$PACK_NAME" >> ~/.circlerc +echo "export FORCE_CHECK_ALL_FILES=true" >> ~/.circlerc diff --git a/.yo-rc.json b/.yo-rc.json new file mode 100644 index 0000000..71f3aee --- /dev/null +++ b/.yo-rc.json @@ -0,0 +1,90 @@ +{ + "generator-st2pack": { + "promptValues": { + "features": [ + "Sensors", + "Rules", + "Policies", + "Aliases" + ], + "pack_ref": "nexus3", + "pack_name": "Sontatype Nexus3", + "pack_desc": "Sontatype nexus3 stackstorm pack" + }, + "actions": {}, + "aliases": { + "greet": { + "name": "greet", + "pack": "nexus3", + "description": "Greet StackStorm", + "action_ref": "nexus3.greet", + "formats": [ + "greet {{greeting}}" + ] + } + }, + "rules": { + "on_hello_event1": { + "name": "on_hello_event1", + "pack": "nexus3", + "description": "Sample rule firing on nexus3.event1.", + "enabled": true, + "trigger": { + "type": "nexus3.event1" + }, + "action": { + "ref": "nexus3.greet", + "parameters": { + "greeting": "Yo" + } + } + } + }, + "sensors": { + "HelloSensor": { + "class_name": "HelloSensor", + "entry_point": "sensor1.py", + "description": "Test sensor that emits triggers.", + "trigger_types": [ + { + "name": "event1", + "description": "An example trigger.", + "payload_schema": { + "type": "object" + } + } + ] + } + }, + "policies": { + "http.retry": { + "name": "http.retry", + "description": "Retry core.http action on timeout.", + "enabled": true, + "resource_ref": "core.http", + "policy_type": "action.retry", + "parameters": { + "retry_on": "timeout", + "max_retry_count": 2, + "delay": 2 + } + }, + "greet.concurrency": { + "name": "greet.concurrency", + "pack": "nexus3", + "description": "Limits the concurrent executions of the greet action.", + "enabled": true, + "resource_ref": "nexus3.greet", + "policy_type": "action.concurrency", + "parameters": { + "threshold": 10 + } + } + }, + "pack": { + "ref": "nexus3", + "name": "Sontatype Nexus3", + "description": "Sontatype nexus3 stackstorm pack" + } + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..03ac651 --- /dev/null +++ b/README.md @@ -0,0 +1,106 @@ +# Stackstorm-Nexus3 + +[Sontatype nexus3](https://www.sonatype.com/nexus-repository-oss) stackstorm pack + +## Installation + +Install this pack with: `st2 pack install file://$PWD` + +Or if in remote repository: `st2 pack install https://github.com/MY/PACK` + +## Configuration + +Copy the example configuration in [nexus3.yaml.example](./nexus3.yaml.example) +to `/opt/stackstorm/configs/nexus3.yaml` and edit as required. + +add nexus3 server connection profile: + +* ``url`` - URL of the nexus3 server (e.g. ``http://localhost:8081``) +* ``user`` - username +* ``password`` - Password +* ``verify`` - https tls verify, only used if the `url` option specifies an `https` connection + +You can also add multiple profiles: + +When you add multiple profile, above parameters becomes defaults for any key missing in the profile. + +``` yaml +--- +url: "http://localhost:8081" +user: "admin" +password: "admin123" +verify: True +default_profile: "dev" # use this to make for missing config_profile parameter when calling any action +profiles: + "dev": + url: "http://localhost:8081" + user: "dev-user" + password: "mypas" + verify: True + "dev-2": + user: "dev2-user" + password: "mypass2" + # url : fallback to url: "http://localhost:8081" + # verify: fallback to verify: True +``` + +**Note** : When modifying the configuration in `/opt/stackstorm/configs/` please + remember to tell StackStorm to load these new values by running + `st2ctl reload --register-configs` + +## Actions + +While naming actions following convention is used: + + < VERB >_< RESOURCE(plural) > + eg. + list_repositories + get_scripts + +Supported Resources + +* Repositories +* Scripts + +### Available actions + +* **list_repositories** : List nexus3 repositories +* **get_repositories** : get nexus3 repositories +* **create_repositories** : create nexus3 repository +* **delete_repositories** : delete nexus3 repository + +* **list_scripts** : List nexus3 scripts +* **get_scripts** : get nexus3 scripts +* **create_scripts** : create(& upload) if missing, groovy script +* **delete_scripts** : delete nexus3 script + +## Policies + +* **http.retry** : Retry core.http action on timeout. + +## Aliases + +--NIL-- + +## Rules + +--NIL-- + +## Sensors + +--NIL-- + +## Testing + +While testing you can leverage [Makefile](./tests/Makefile) to ease writing and testing actions. Read the Makefile for the list of useful commands. + +## Contributing + +Before submitting make sure python linter is happy with your changes first + +``` bash +pip install flake8 + +#executing flake8 ./ +make -f tests/Makefile lint +``` \ No newline at end of file diff --git a/actions/create_repositories.yaml b/actions/create_repositories.yaml new file mode 100644 index 0000000..b73f5f3 --- /dev/null +++ b/actions/create_repositories.yaml @@ -0,0 +1,77 @@ +--- +name: create_repositories +runner_type: python-script +description: Create a repository +enabled: true +entry_point: run.py +parameters: + action: + default: create_repositories + immutable: true + type: string + config_profile: + type: string + description: Select which nexus config profile to use. + required: false + name: + type: string + description: A unique identifier for this repository + required: true + format: + type: string + description: The format of the repository (i.e. maven2, docker, raw, nuget...) + required: true + enum: + - bower + - docker + - gitlfs + - maven + - npm + - nuget + - pypi + - raw + - rubygems + - yum + type: + type: string + description: The type of repository (i.e. group, hosted, or proxy) + required: true + enum: + - hosted + - proxy + version_policy: + type: string + description: What type of artifacts does this repository store? + required: false + default: RELEASE + enum: + - RELEASE + - SNAPSHOT + - MIXED + layout_policy: + type: string + description: Validate that all paths are maven artifact or metadata paths + required: false + default: STRICT + enum: + - STRICT + - PERMISSIVE + blob_store_name: + type: string + description: Blob store used to store asset contents + required: true + default: "default" + strict_content_type_validation: + type: boolean + description: Validate that all content uploaded to this repository is of a MIME type appropriate for the repository format + required: true + default: false + write_policy: + type: string + description: Controls if deployments of and updates to artifacts are allowed + required: true + default: ALLOW + enum: + - ALLOW_ONCE + - ALLOW + - DENY \ No newline at end of file diff --git a/actions/create_scripts.yaml b/actions/create_scripts.yaml new file mode 100644 index 0000000..5e80f3e --- /dev/null +++ b/actions/create_scripts.yaml @@ -0,0 +1,19 @@ +--- +name: create_scripts +runner_type: python-script +description: create a script +enabled: true +entry_point: run.py +parameters: + action: + default: create_scripts + immutable: true + type: string + config_profile: + type: string + description: Select which nexus config profile to use. + required: false + data: + type: object + description: instance of script to be created. + required: false \ No newline at end of file diff --git a/actions/delete_repositories.yaml b/actions/delete_repositories.yaml new file mode 100644 index 0000000..43ef39f --- /dev/null +++ b/actions/delete_repositories.yaml @@ -0,0 +1,19 @@ +--- +name: delete_repositories +runner_type: python-script +description: delete all repositories of nexus3 +enabled: true +entry_point: run.py +parameters: + action: + default: delete_repositories + immutable: true + type: string + config_profile: + type: string + description: Select which nexus config profile to use. + required: false + name: + type: string + description: A unique identifier of the repository to delete + required: true \ No newline at end of file diff --git a/actions/delete_scripts.yaml b/actions/delete_scripts.yaml new file mode 100644 index 0000000..f3e5655 --- /dev/null +++ b/actions/delete_scripts.yaml @@ -0,0 +1,19 @@ +--- +name: delete_scripts +runner_type: python-script +description: delete a script +enabled: true +entry_point: run.py +parameters: + action: + default: delete_scripts + immutable: true + type: string + config_profile: + type: string + description: Select which nexus config profile to use. + required: false + name: + type: string + description: name of script to be deleted + required: true \ No newline at end of file diff --git a/actions/get_repositories.yaml b/actions/get_repositories.yaml new file mode 100644 index 0000000..d9d630a --- /dev/null +++ b/actions/get_repositories.yaml @@ -0,0 +1,19 @@ +--- +name: get_repositories +runner_type: python-script +description: get repository identified by a unique identifier +enabled: true +entry_point: run.py +parameters: + action: + default: get_repositories + immutable: true + type: string + config_profile: + type: string + description: Select which nexus config profile to use. + required: false + name: + type: string + description: A unique identifier of the repository to fetch + required: true \ No newline at end of file diff --git a/actions/get_scripts.yaml b/actions/get_scripts.yaml new file mode 100644 index 0000000..d527cb1 --- /dev/null +++ b/actions/get_scripts.yaml @@ -0,0 +1,19 @@ +--- +name: get_scripts +runner_type: python-script +description: Fetch script specified by name +enabled: true +entry_point: run.py +parameters: + action: + default: get_scripts + immutable: true + type: string + config_profile: + type: string + description: Select which nexus config profile to use. + required: false + name: + type: string + description: name of script to be fetched. + required: true \ No newline at end of file diff --git a/actions/lib/__init__.py b/actions/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/actions/lib/base.py b/actions/lib/base.py new file mode 100644 index 0000000..1e3b583 --- /dev/null +++ b/actions/lib/base.py @@ -0,0 +1,183 @@ +from st2common.runners.base_action import Action +from exception import MissingProfileError +from exception import ValidationFailError +from exception import NexusClientNotInstantiatedError +from nexuscli.repository import Repository +from nexuscli.nexus_client import NexusClient +from nexuscli.exception import NexusClientAPIError +from nexuscli.exception import NexusClientCreateRepositoryError + + +__all__ = [ + 'BaseAction' +] + +REQUIRED_FIELDS = ['url', 'user', 'password'] + + +class BaseAction(Action): + + def __init__(self, config): + super(BaseAction, self).__init__(config) + self.nexus_profiles = self.config.get('profiles', {}) + self._client = None + self.dial_config = None + + def validate_config(self, c_profile): + """ Validate connection configuration + This function returns a tuple of (is_valid, errors), where errors is + a list of all of the missing fields in a profile. This is avoid + making users having to correct each missing field at a time. + """ + validation_errors = [] + is_valid = True + for field in REQUIRED_FIELDS: + if c_profile.get(field) is None: + validation_errors.append( + "Required parameter %s is missing" % field) + is_valid = False + + return (is_valid, validation_errors) + + def get_connection_defaults(self): + """ Default properties + + """ + return { + "url": self.config.get('url', None), + "verify": self.config.get('verify', True), + "user": self.config.get('user', None), + "password": self.config.get('password', None) + } + + def init_config(self, profile=None): + """ Generate connection config and validate it + + """ + self.dial_config = self.get_connection_defaults() + profile = profile if profile and len(profile.strip()) > 0 else None + + # use 'default_profile' if 'config_profile' is empty + if profile is None: + profile = self.config.get('default_profile', None) + + # Use defaults options, if neither config_profile nor default_profile + # is provided + if profile is None: + self.logger.info("using defaults connection values") + else: + c_profile = self.nexus_profiles.get(profile, None) + if c_profile is None: + raise MissingProfileError( + "connection profile not found: '%s'" % profile) + + self.dial_config.update(c_profile) + + # Validate configuration + (is_valid, errors) = self.validate_config(self.dial_config) + if not is_valid: + raise ValidationFailError( + "Validation failed: %s" % errors) + + return self.dial_config + + def init_dialer(self): + try: + self.logger.debug("connection string: %s \n" % self.dial_config) + self._client = NexusClient(**self.dial_config) + except Exception as error: + self.logger.error("Couldn't instantiate nexus client %s" % error) + raise error + + def get_resource_dialer(self, resource): + """ return nexus client + + """ + if self._client is None: + raise NexusClientNotInstantiatedError( + "Instantiate nexus_client by calling \ + init_dialer() method first") + return getattr(self._client, resource) + + def intent_list(self, resource, **kwargs): + """ Intent: LIST + + """ + dialer = self.get_resource_dialer(resource) + is_success = True + response = {} + if resource == "repositories": + response = dialer.raw_list() + else: + response = dialer.list() + + return (is_success, response) + + def intent_get(self, resource, **kwargs): + """ Intent: GET + + """ + dialer = self.get_resource_dialer(resource) + is_success = True + response = {} + if resource == "repositories": + response = dialer.get_raw_by_name(kwargs.pop('name')) + else: + response = dialer.get(kwargs.pop('name')) + + return (is_success, response) + + def intent_create(self, resource, **kwargs): + """ Intent: CREATE + + """ + dialer = self.get_resource_dialer(resource) + is_success = True + response = {} + payload = kwargs.get('data', '') + + if resource == "repositories": + repo_type = kwargs.pop('type') + payload = Repository(repo_type, **kwargs) + + try: + if resource == "scripts": + response = dialer.create_if_missing(payload) + else: + response = dialer.create(payload) + except NexusClientCreateRepositoryError as error: + response = "Failed to create %s. Reason: %s" % (resource, error) + is_success = False + + return (is_success, response) + + def intent_run(self, resource, **kwargs): + """ Intent: RUN + + """ + dialer = self.get_resource_dialer(resource) + is_success = True + response = {} + if resource == "scripts": + response = dialer.run(kwargs.pop('name'), kwargs.pop('data')) + else: + is_success = False + response = "Invalid resource: %s\ + (supported only scripts)" % resource + + return (is_success, response) + + def intent_delete(self, resource, **kwargs): + """ Intent: DELETE + + """ + dialer = self.get_resource_dialer(resource) + is_success = True + response = {} + try: + response = dialer.delete(kwargs.pop('name')) + except NexusClientAPIError as error: + is_success = False + response = error + + return (is_success, response) diff --git a/actions/lib/exception.py b/actions/lib/exception.py new file mode 100644 index 0000000..9185b34 --- /dev/null +++ b/actions/lib/exception.py @@ -0,0 +1,43 @@ + +__all__ = [ + "InvalidParameterError", + "MissingParameterError", + "MissingProfileError", + "ValidationFailError", + "NexusClientNotInstantiatedError" +] + + +class Error(Exception): + """Base class for other exceptions""" + pass + + +class InvalidParameterError(Error): + """Raised when input parameter is not valid""" + pass + + +class MissingParameterError(Error): + """Raised when required parameter is missing""" + pass + + +class MissingProfileError(Error): + """Raised when connection profile is missing""" + pass + + +class ValidationFailError(Error): + """Raised when connection profile has missing required fields""" + pass + + +class UnsupportedActionOrResourceError(Error): + """Raised when resource or action is unsupported""" + pass + + +class NexusClientNotInstantiatedError(Error): + """Raised when nexus_client is not instantiated""" + pass diff --git a/actions/list_repositories.yaml b/actions/list_repositories.yaml new file mode 100644 index 0000000..3d9d287 --- /dev/null +++ b/actions/list_repositories.yaml @@ -0,0 +1,15 @@ +--- +name: list_repositories +runner_type: python-script +description: List all repositories of nexus3 +enabled: true +entry_point: run.py +parameters: + action: + default: list_repositories + immutable: true + type: string + config_profile: + type: string + description: Select which nexus config profile to use. + required: false \ No newline at end of file diff --git a/actions/list_scripts.yaml b/actions/list_scripts.yaml new file mode 100644 index 0000000..6623771 --- /dev/null +++ b/actions/list_scripts.yaml @@ -0,0 +1,15 @@ +--- +name: list_scripts +runner_type: python-script +description: List all scripts +enabled: true +entry_point: run.py +parameters: + action: + default: list_scripts + immutable: true + type: string + config_profile: + type: string + description: Select which nexus config profile to use. + required: false \ No newline at end of file diff --git a/actions/run.py b/actions/run.py new file mode 100644 index 0000000..9bce1ee --- /dev/null +++ b/actions/run.py @@ -0,0 +1,40 @@ +from lib import base +from lib.exception import MissingParameterError +from lib.exception import UnsupportedActionOrResourceError + + +class ActionManager(base.BaseAction): + + def run(self, **kwargs): + self.logger.debug(kwargs) + self.logger.debug(self.config) + try: + requested_action = kwargs.pop('action') + except KeyError as error: + raise MissingParameterError( + "'%s' required parameter is missing" % error) + + # Resolve connection config + self.init_config( + kwargs.pop('config_profile', None)) + + # Instantiating dialer + self.init_dialer() + + # Serve Request + try: + [intent, resource] = requested_action.split('_') + except ValueError: + raise UnsupportedActionOrResourceError( + "Requested action: '%s' \ + is not supported currently" % requested_action) + intent_method_name = "intent_%s" % intent + try: + (is_success, response) = getattr( + self, intent_method_name)(resource, **kwargs) + except AttributeError: + raise UnsupportedActionOrResourceError( + "Requested action: '%s' \ + is not supported currently" % (requested_action)) + + return (is_success, response) diff --git a/actions/run_scripts.yaml b/actions/run_scripts.yaml new file mode 100644 index 0000000..9f7f604 --- /dev/null +++ b/actions/run_scripts.yaml @@ -0,0 +1,23 @@ +--- +name: run_scripts +runner_type: python-script +description: Run script +enabled: true +entry_point: run.py +parameters: + action: + default: run_scripts + immutable: true + type: string + config_profile: + type: string + description: Select which nexus config profile to use. + required: false + name: + type: string + description: name of script to be run + required: true + data: + type: string + description: parameters to be passed to the script, via HTTP POST + required: false \ No newline at end of file diff --git a/aliases/get_nexus_repository.yaml b/aliases/get_nexus_repository.yaml new file mode 100644 index 0000000..95b1261 --- /dev/null +++ b/aliases/get_nexus_repository.yaml @@ -0,0 +1,8 @@ +--- +name: "nexus_get_repository" +pack: "nexus3" +action_ref: "nexus3.get_repositories" +description: "alias to nexus3.get_repositories" +formats: + - "get repository from {{name}}" + - "retrieve comments for {{issue_key}}" \ No newline at end of file diff --git a/config.schema.yaml b/config.schema.yaml new file mode 100644 index 0000000..c174106 --- /dev/null +++ b/config.schema.yaml @@ -0,0 +1,53 @@ +--- +url: + description: "URL of the nexus3 instance (e.g. ``http://localhost:8081``)" + type: "string" + secret: false + required: true + position: 0 +verify: + description: "verify TLS, only used if the ``url`` option specifies an ``HTTPS`` connection" + type: boolean + position: 1 + default: True +user: + description: "Username" + type: "string" + secret: false + required: false + position: 2 +password: + description: "Password" + type: "string" + secret: true + required: false + position: 3 +profiles: + description: "Nexus3 Connection profile name. This will use above options as default values." + type: object + required: false + properties: {} + additionalProperties: + type: object + additionalProperties: false + properties: + # TODO: Refactor to use YAML anchors and references + url: + description: "URL of the JIRA instance (e.g. ``https://myproject.atlassian.net``)" + type: "string" + secret: false + required: true + verify: + description: "verify TLS, only used if the ``url`` option specifies an ``HTTPS`` connection" + type: boolean + default: True + user: + description: "Username" + type: "string" + secret: false + required: false + password: + description: "Password" + type: "string" + secret: true + required: false diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..e9ebf2e Binary files /dev/null and b/icon.png differ diff --git a/nexus3.yaml.example b/nexus3.yaml.example new file mode 100644 index 0000000..762ab48 --- /dev/null +++ b/nexus3.yaml.example @@ -0,0 +1,12 @@ +--- +url: "http://localhost:8081" +user: "admin" +password: "admin123" +verify: True +default_profile: "dev" +profiles: + "dev": + url: "http://localhost:8081" + user: "dev-user" + password: "mypas" + verify: True \ No newline at end of file diff --git a/pack.yaml b/pack.yaml new file mode 100644 index 0000000..78dc401 --- /dev/null +++ b/pack.yaml @@ -0,0 +1,14 @@ +--- +ref: nexus3 +name: Sontatype nexus3 +description: Sontatype nexus3 stackstorm pack +keywords: + - nexus + - nexus3 + - sonatype +version: 0.1.0 +python_versions: + - "2" + - "3" +author: Sonu Meena +email: sonukr666@gmail.com diff --git a/policies/retry_core_http_on_timeout.yaml b/policies/retry_core_http_on_timeout.yaml new file mode 100644 index 0000000..47116d0 --- /dev/null +++ b/policies/retry_core_http_on_timeout.yaml @@ -0,0 +1,10 @@ +--- +name: http.retry +description: Retry core.http action on timeout. +enabled: true +resource_ref: core.http +policy_type: action.retry +parameters: + retry_on: timeout + max_retry_count: 2 + delay: 2 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..21bde5b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +eventlet +nexus3-cli \ No newline at end of file diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 0000000..cb27fdf --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,66 @@ + +ST2_EXEC_CMD = grep "execution get" | xargs -0 -I{} bash -c "sleep 2; {}"; + +run: sync list_repo create_repo get_repo del_repo get_repo list_scripts get_script run_script delete_script get_script test + +list_repo: + @echo "=================== List repository" + st2 action execute nexus3.list_repositories config_profile=dev | grep "execution get" | xargs -0 -I{} bash -c "sleep 2; {}"; + +create_repo: + @echo "=================== Create repository" + st2 action execute nexus3.create_repositories config_profile=dev type=hosted name=maven2-dumy format=maven write_policy=ALLOW | $(ST2_EXEC_CMD) + +get_repo: + @echo "=================== Get repository" + st2 action execute nexus3.get_repositories config_profile=dev name=maven2-dumy | $(ST2_EXEC_CMD) + +del_repo: + @echo "=================== Delete repository" + st2 action execute nexus3.delete_repositories config_profile=dev name=maven2-dumy | $(ST2_EXEC_CMD) + +list_scripts: + @echo "=================== List scripts" + st2 action execute nexus3.list_scripts config_profile=dev | $(ST2_EXEC_CMD) + +get_script: + @echo "=================== Get script" + st2 action execute nexus3.get_scripts config_profile=dev name=nexus3-cli-repository-create | $(ST2_EXEC_CMD) + +run_script: + @echo "=================== Run script" + st2 action execute nexus3.run_scripts config_profile=dev name=nexus3-cli-repository-create | $(ST2_EXEC_CMD) + +delete_script: + @echo "=================== Delete script" + st2 action execute nexus3.delete_scripts config_profile=dev name=nexus3-cli-repository-create | $(ST2_EXEC_CMD) + +install: + @echo "=================== Running st2 pack install" + st2 pack install file://$$PWD + +reload: + @echo "=================== Running st2ctl reload" + st2ctl reload --register-all + +restart: + @echo "=================== Running st2ctl restart" + st2ctl restart + +sync: + LOCAL_NEXUS_DIR=$$PWD; \ + ST2_PACK_DIR='/opt/stackstorm/packs'; \ + mkdir -p /opt/stackstorm/packs/nexus3; \ + cp -rf $$LOCAL_NEXUS_DIR/* $$ST2_PACK_DIR/nexus3/; \ + chmod -R --reference $$ST2_PACK_DIR/linux $$ST2_PACK_DIR/nexus3;\ + chown -R --reference $$ST2_PACK_DIR/linux/pack.yaml $$ST2_PACK_DIR/nexus3/*; + + +lint: + flake8 ./ + +validate: + st2-check-validate-yaml-file ./*.yaml + +test: + st2-run-pack-tests -cjtp ./ diff --git a/tests/fixtures/get_repositories.json b/tests/fixtures/get_repositories.json new file mode 100644 index 0000000..06a6c6b --- /dev/null +++ b/tests/fixtures/get_repositories.json @@ -0,0 +1,6 @@ +{ + "url": "http://nexus:8081/repository/maven2-dumy", + "type": "hosted", + "name": "maven2-repo", + "format": "maven2" +} \ No newline at end of file diff --git a/tests/fixtures/get_scripts.json b/tests/fixtures/get_scripts.json new file mode 100644 index 0000000..cab6ce5 --- /dev/null +++ b/tests/fixtures/get_scripts.json @@ -0,0 +1,5 @@ +{ + "content": "import groovy.json.JsonSlurper...xxx", + "type": "groovy", + "name": "nexus3-cli-repository-create" + } \ No newline at end of file diff --git a/tests/fixtures/list_repositories.json b/tests/fixtures/list_repositories.json new file mode 100644 index 0000000..af18145 --- /dev/null +++ b/tests/fixtures/list_repositories.json @@ -0,0 +1,44 @@ +[ + { + "url": "http://nexus:8081/repository/nuget-group", + "type": "group", + "name": "nuget-group", + "format": "nuget" + }, + { + "url": "http://nexus:8081/repository/maven-snapshots", + "type": "hosted", + "name": "maven-snapshots", + "format": "maven2" + }, + { + "url": "http://nexus:8081/repository/maven-central", + "type": "proxy", + "name": "maven-central", + "format": "maven2" + }, + { + "url": "http://nexus:8081/repository/nuget.org-proxy", + "type": "proxy", + "name": "nuget.org-proxy", + "format": "nuget" + }, + { + "url": "http://nexus:8081/repository/maven-releases", + "type": "hosted", + "name": "maven-releases", + "format": "maven2" + }, + { + "url": "http://nexus:8081/repository/nuget-hosted", + "type": "hosted", + "name": "nuget-hosted", + "format": "nuget" + }, + { + "url": "http://nexus:8081/repository/maven-public", + "type": "group", + "name": "maven-public", + "format": "maven2" + } + ] \ No newline at end of file diff --git a/tests/fixtures/list_scripts.json b/tests/fixtures/list_scripts.json new file mode 100644 index 0000000..b4d7c02 --- /dev/null +++ b/tests/fixtures/list_scripts.json @@ -0,0 +1,12 @@ +[ + { + "content": "xxxx", + "type": "groovy", + "name": "nexus3-cli-repository-delete" + }, + { + "content": "import groovy.json.JsonSlurper...xxxx", + "type": "groovy", + "name": "nexus3-cli-repository-create" + } +] \ No newline at end of file diff --git a/tests/nexus_base_test_case.py b/tests/nexus_base_test_case.py new file mode 100644 index 0000000..ffb235b --- /dev/null +++ b/tests/nexus_base_test_case.py @@ -0,0 +1,40 @@ +from st2tests.base import BaseActionTestCase +import json + +__all__ = [ + 'NexusBaseTestCase' +] + + +class NexusBaseTestCase(BaseActionTestCase): + __test__ = False + action_cls = None + + def setUp(self): + super(NexusBaseTestCase, self).setUp() + + self._configs = { + "action_config": { + "config_profile": None + }, + "pack_config": { + "url": "http://localhost:8081", + "user": "user", + "password": "supersecret" + } + } + + def get_action_config(self, action_name): + self._configs["action_config"]["action"] = action_name + + if action_name in ["get_repositories", "delete_repositories", + "get_scripts", "delete_scripts"]: + self._configs["action_config"]["name"] = "maven2-repo" + elif action_name == "create_repositories": + self._configs["action_config"].update(json.loads( + self.get_fixture_content('get_repositories.json'))) + + return self._configs["action_config"] + + def get_pack_config(self): + return self._configs['pack_config'] diff --git a/tests/test_resource_repositories.py b/tests/test_resource_repositories.py new file mode 100644 index 0000000..f5359be --- /dev/null +++ b/tests/test_resource_repositories.py @@ -0,0 +1,92 @@ +from run import ActionManager +import mock +import json +from nexus_base_test_case import NexusBaseTestCase + + +@mock.patch('lib.base.NexusClient') +class ResourceRepositoriesTest(NexusBaseTestCase): + action_cls = ActionManager + __test__ = True + + def test_action_list_repositories(self, mock_nexus): + """ (A)List Repositories: Should fetch an array of repositories + """ + + action_config = self.get_action_config("list_repositories") + pack_config = self._configs["pack_config"] + repo_list_fixture = json.loads( + self.get_fixture_content('list_repositories.json')) + + mock_client = mock_nexus.return_value + mock_client.repositories.raw_list.return_value = repo_list_fixture + + action = self.get_action_instance(pack_config) + action._client = mock_client + (is_success, response) = action.run(**action_config) + + mock_client.repositories.raw_list.assert_called_once() + self.assertEqual(response, repo_list_fixture) + + def test_action_get_repositories(self, mock_nexus): + """ (A)Get Repositories: Should fetch a repository identified by a name + """ + + action_config = self.get_action_config("get_repositories") + pack_config = self._configs["pack_config"] + repository_fixture = json.loads( + self.get_fixture_content('get_repositories.json')) + + def mock_get_raw_by_name(name): + return repository_fixture + + mock_client = mock_nexus.return_value + mock_client.repositories.get_raw_by_name = mock.Mock( + side_effect=mock_get_raw_by_name) + + action = self.get_action_instance(pack_config) + action._client = mock_client + (is_success, response) = action.run(**action_config) + + mock_client.repositories.get_raw_by_name.assert_called_once() + self.assertEqual((is_success, response), (True, repository_fixture)) + + def test_action_create_repositories(self, mock_nexus): + """ (A)Create Repositories: Should create a repository + """ + + action_config = self.get_action_config("create_repositories") + pack_config = self._configs["pack_config"] + + def mock_create(payload): + return None + + mock_client = mock_nexus.return_value + mock_client.repositories.create = mock.Mock(side_effect=mock_create) + + action = self.get_action_instance(pack_config) + action._client = mock_client + (is_success, response) = action.run(**action_config) + + mock_client.repositories.create.assert_called_once() + self.assertEqual((is_success, response), (True, None)) + + def test_action_delete_repositories(self, mock_nexus): + """ (A)Delete Repositories: Should delete a repository + """ + + action_config = self.get_action_config("delete_repositories") + pack_config = self._configs["pack_config"] + + def mock_delete(name): + return None + + mock_client = mock_nexus.return_value + mock_client.repositories.delete = mock.Mock(side_effect=mock_delete) + + action = self.get_action_instance(pack_config) + action._client = mock_client + (is_success, response) = action.run(**action_config) + + mock_client.repositories.delete.assert_called_once() + self.assertEqual((is_success, response), (True, None)) diff --git a/tests/test_resource_scripts.py b/tests/test_resource_scripts.py new file mode 100644 index 0000000..8acbb19 --- /dev/null +++ b/tests/test_resource_scripts.py @@ -0,0 +1,89 @@ +from run import ActionManager +import mock +from nexus_base_test_case import NexusBaseTestCase + + +@mock.patch('lib.base.NexusClient') +class ResourceScriptsTest(NexusBaseTestCase): + action_cls = ActionManager + __test__ = True + + def test_action_list_scripts(self, mock_nexus): + """ (A)List scripts: Should fetch an array of scripts + """ + + action_config = self.get_action_config("list_scripts") + pack_config = self._configs["pack_config"] + script_list_fixture = self.get_fixture_content('list_scripts.json') + + mock_client = mock_nexus.return_value + mock_client.scripts.list.return_value = script_list_fixture + + action = self.get_action_instance(pack_config) + action._client = mock_client + (is_success, response) = action.run(**action_config) + + mock_client.scripts.list.assert_called_once() + self.assertEqual(response, script_list_fixture) + + def test_action_get_scripts(self, mock_nexus): + """ (A)Get scripts: Should fetch a script identified by name + """ + + action_config = self.get_action_config("get_scripts") + pack_config = self._configs["pack_config"] + script_fixture = self.get_fixture_content('get_scripts.json') + + def mock_get(name): + return script_fixture + + mock_client = mock_nexus.return_value + mock_client.scripts.get = mock.Mock(side_effect=mock_get) + + action = self.get_action_instance(pack_config) + action._client = mock_client + (is_success, response) = action.run(**action_config) + + mock_client.scripts.get.assert_called_once() + self.assertEqual((is_success, response), (True, script_fixture)) + + def test_action_create_scripts(self, mock_nexus): + """ (A)Create scripts: Should create a script + """ + + action_config = self.get_action_config("create_scripts") + pack_config = self._configs["pack_config"] + + def mock_create_if_missing(payload): + return None + + mock_client = mock_nexus.return_value + mock_client.scripts.create_if_missing = mock.Mock( + side_effect=mock_create_if_missing) + + action = self.get_action_instance(pack_config) + action._client = mock_client + (is_success, response) = action.run(**action_config) + + mock_client.scripts.create_if_missing.assert_called_once() + self.assertEqual((is_success, response), (True, None)) + + def test_action_delete_scripts(self, mock_nexus): + """ (A)delete scripts: Should delete a script + """ + + action_config = self.get_action_config("delete_scripts") + pack_config = self._configs["pack_config"] + + def mock_delete(name): + return None + + mock_client = mock_nexus.return_value + mock_client.scripts.delete = mock.Mock(side_effect=mock_delete) + + action = self.get_action_instance(pack_config) + action._client = mock_client + (is_success, response) = action.run(**action_config) + + mock_client.scripts.delete.assert_called_once() + self.assertEqual((is_success, response), (True, None)) diff --git a/tests/test_resource_unsupported.py b/tests/test_resource_unsupported.py new file mode 100644 index 0000000..6efeafc --- /dev/null +++ b/tests/test_resource_unsupported.py @@ -0,0 +1,27 @@ +from run import ActionManager +import mock +from nexus_base_test_case import NexusBaseTestCase +from lib.exception import UnsupportedActionOrResourceError + + +@mock.patch('lib.base.NexusClient') +class ResourceUnsupportedTest(NexusBaseTestCase): + action_cls = ActionManager + __test__ = True + + def test_unsupported_resource(self, mock_nexus): + """ Resource Or Action unsupported: Should raise exception + """ + + action_config = self.get_action_config("unsupported_resource") + pack_config = self._configs["pack_config"] + + # aciton name in right format but still unsupported + action = self.get_action_instance(pack_config) + with self.assertRaises(UnsupportedActionOrResourceError): + action.run(**action_config) + + # action name in invalid format + action_config = self.get_action_config("unsupported") + with self.assertRaises(UnsupportedActionOrResourceError): + action.run(**action_config) diff --git a/tests/test_run.py b/tests/test_run.py new file mode 100644 index 0000000..f3896d1 --- /dev/null +++ b/tests/test_run.py @@ -0,0 +1,70 @@ +from st2tests.base import BaseActionTestCase + +from run import ActionManager +from lib.exception import MissingParameterError +from lib.exception import MissingProfileError +from lib.exception import ValidationFailError + + +class RunTestCase(BaseActionTestCase): + """ + Test action and pack configuration + """ + action_cls = ActionManager + + def test_missing_parameter(self): + """ + Test that MissingParameterError is raised when a required parameter is + not specified. + """ + configs = { + "action_config": { + "config_profile": None + }, + "pack_config": {} + } + # Action should have required 'action' parameter + action = self.get_action_instance(config=configs["pack_config"]) + with self.assertRaises(MissingParameterError): + action.run(**configs["action_config"]) + + def test_missing_profile(self): + """ + Test that MissingProfileError is raised when a non-existent profile is + specified. + """ + # Profile should exist, if defined in action + configs = { + "action_config": { + "action": "some_action", + "url": "http://localhost:8080", + "config_profile": "dev" + }, + "pack_config": {} + } + action = self.get_action_instance(config=configs["pack_config"]) + # Unsure why this works when assertRaises(MissingProfileError) doesn't + with self.assertRaises(Exception) as exc: + action.run(**configs["action_config"]) + + self.assertIsInstance(exc, MissingProfileError) + + def test_validation_failure(self): + """ + Test that ValidationFailError is raised when required connection + parameters are not configured. + """ + # Required connection parameters should exist: url, user, password + configs = { + "action_config": { + "action": "some_action", + "url": "http://localhost:8080" + }, + "pack_config": {} + } + action = self.get_action_instance(config=configs["pack_config"]) + # Unsure why this works when assertRaises(MissingProfileError) doesn't + with self.assertRaises(Exception) as exc: + action.run(**configs["action_config"]) + + self.assertIsInstance(exc, ValidationFailError)