Skip to content

Commit

Permalink
feat: introduce package command (#846)
Browse files Browse the repository at this point in the history
* feat: introduce package command

* test: fixing unit tests
  • Loading branch information
artemrys authored Sep 27, 2023
1 parent ccdba8d commit 266b28f
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 26 deletions.
26 changes: 22 additions & 4 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,19 +72,20 @@ pip install splunk-add-on-ucc-framework splunk-packaging-toolkit

> Note: `splunk-packaging-toolkit` does not work with Python 3.10+.
* Run `ucc-gen` and package it
> Note: `ucc-gen package` can be used instead of `slim` if UCC `v5.30.0+` is used (more details below).
* Run `ucc-gen build` and package it

> Provide `--ta-version=<version>` parameter if this repository is not version controlled.
```bash
ucc-gen
ucc-gen build
slim package output/<add-on-name>
```

> Please use `ucc-gen build` instead of `ucc-gen` if you are using UCC `v5.19.0` and higher.
Now you should see an archive created in the same level as your
`globalConfig.json` is located.
Now you should see an archive created in the same level as your `globalConfig.json` is located.

Now you can go to Splunk and install this add-on using the generated archive.

Expand Down Expand Up @@ -164,6 +165,23 @@ It takes the following parameters:

* `--addon-name` - [required] add-on name.

### `ucc-gen package`

Available from `v5.30.0`. Packages the add-on so it can be installed.
It mimics the basics of the `slim package` command. This command can be used for most of the simple cases.

It does not support:

* `.slimignore` file
* [dependencies section](https://dev.splunk.com/enterprise/docs/releaseapps/packageapps/packagingtoolkit/#Dependencies-section)

It takes the following parameters:

* `--path` - [required] path to the built add-on (should include `app.manifest` file).
* `-o` / `--output` - [optional] output folder to store packaged add-on in.
By default, it will be saved in the `current directory` folder.
Accepts absolute paths as well.

## What `ucc-gen build` does

* Cleans the output folder.
Expand Down
3 changes: 3 additions & 0 deletions splunk_add_on_ucc_framework/app_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ def __init__(self):
def get_addon_name(self) -> str:
return self._manifest["info"]["id"]["name"]

def get_addon_version(self) -> str:
return self._manifest["info"]["id"]["version"]

def get_title(self) -> str:
return self._manifest["info"]["title"]

Expand Down
64 changes: 64 additions & 0 deletions splunk_add_on_ucc_framework/commands/package.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#
# Copyright 2023 Splunk Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import logging
import os
import tarfile
from typing import Optional

from splunk_add_on_ucc_framework import app_manifest as app_manifest_lib

logger = logging.getLogger("ucc_gen")


def _get_package_output_path(output_directory: Optional[str] = None) -> str:
if output_directory is None:
return os.path.join(os.getcwd())
else:
if not os.path.isabs(output_directory):
return os.path.join(os.getcwd(), output_directory)
return output_directory


def package(path_to_built_addon: str, output_directory: Optional[str] = None) -> None:
"""
Archives a built add-on to the current directory with a specific add-on name
and version.
"""
logger.info(f"Packaging app at {path_to_built_addon}")
app_manifest = app_manifest_lib.AppManifest()
app_manifest_path = os.path.join(
path_to_built_addon, app_manifest_lib.APP_MANIFEST_FILE_NAME
)
if not os.path.exists(app_manifest_path):
logger.error(
f"No {app_manifest_lib.APP_MANIFEST_FILE_NAME} found @ {app_manifest_path}. "
f"Cannot package an add-on without a manifest file. "
f"Please check the --path provided."
)
exit(1)
with open(app_manifest_path) as _f:
app_manifest.read(_f.read())

addon_name = app_manifest.get_addon_name()
addon_version = app_manifest.get_addon_version()
output_directory = _get_package_output_path(output_directory)
archive_path = os.path.join(
output_directory, f"{addon_name}-{addon_version}.tar.gz"
)
with tarfile.open(archive_path, mode="w", encoding="utf-8") as archive_file:
logger.info(path_to_built_addon)
archive_file.add(path_to_built_addon, arcname=addon_name)
logger.info(f"Package exported to {archive_path}")
64 changes: 43 additions & 21 deletions splunk_add_on_ucc_framework/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from splunk_add_on_ucc_framework.commands import build
from splunk_add_on_ucc_framework.commands import init
from splunk_add_on_ucc_framework.commands import import_from_aob
from splunk_add_on_ucc_framework.commands import package

logger = logging.getLogger("ucc_gen")

Expand All @@ -45,9 +46,11 @@ def _parse_known_args(self, arg_strings, *args, **kwargs):
d_sp = self.__default_subparser
if d_sp is not None and not {"-h", "--help"}.intersection(in_args):
for x in self._subparsers._actions:
subparser_found = isinstance(
x, argparse._SubParsersAction
) and in_args.intersection(x._name_parser_map.keys())
subparser_found = (
isinstance(x, argparse._SubParsersAction)
and len(arg_strings) > 0
and arg_strings[0] in x._name_parser_map.keys()
)
if subparser_found:
break
else:
Expand All @@ -61,92 +64,109 @@ def _parse_known_args(self, arg_strings, *args, **kwargs):

def main(argv: Optional[Sequence[str]] = None):
argv = argv if argv is not None else sys.argv[1:]
parser = DefaultSubcommandArgumentParser()
parser = DefaultSubcommandArgumentParser(prog="ucc-gen")
parser.set_default_subparser("build")
subparsers = parser.add_subparsers(dest="command", description="Build an add-on")
subparsers = parser.add_subparsers(dest="command")

build_parser = subparsers.add_parser("build")
build_parser = subparsers.add_parser("build", description="Build an add-on")
build_parser.add_argument(
"--source",
type=str,
nargs="?",
help="Folder containing the app.manifest and app source.",
help="folder containing the app.manifest and app source",
default="package",
)
build_parser.add_argument(
"--config",
type=str,
nargs="?",
help="Path to configuration file, defaults to globalConfig file in parent directory of source provided.",
help="path to configuration file, defaults to globalConfig file in parent directory of source provided",
default=None,
)
build_parser.add_argument(
"--ta-version",
type=str,
help="Version of TA, default version is version specified in the "
"package such as app.manifest, app.conf, and globalConfig file.",
help="version of add-on, default version is version specified in the "
"package such as app.manifest, app.conf, and globalConfig file",
default=None,
)
build_parser.add_argument(
"-o",
"--output",
type=str,
help="Output path to store built add-on.",
help="output path to store built add-on",
default=None,
)
build_parser.add_argument(
"--python-binary-name",
type=str,
help="Python binary name to use to install requirements.",
help="Python binary name to use to install requirements",
default="python3",
)

init_parser = subparsers.add_parser("init", description="Bootstrap an add-on.")
package_parser = subparsers.add_parser("package", description="Package an add-on")
package_parser.add_argument(
"--path",
required=True,
type=str,
help="path to the built add-on (should include app.manifest file)",
)
package_parser.add_argument(
"-o",
"--output",
type=str,
help="output path to store archived add-on",
default=None,
)

init_parser = subparsers.add_parser(
"init", description="Initialize an empty add-on"
)
init_parser.add_argument(
"--addon-name",
type=str,
help="Add-on name.",
help="add-on name",
required=True,
)
init_parser.add_argument(
"--addon-rest-root",
type=str,
help="Add-on REST root.",
help="add-on REST root",
required=False,
default=None,
)
init_parser.add_argument(
"--addon-display-name",
type=str,
help="Add-on display name.",
help="add-on display name",
required=True,
)
init_parser.add_argument(
"--addon-input-name",
type=str,
help="Add-on input name.",
help="add-on input name",
required=True,
)
init_parser.add_argument(
"--addon-version",
type=str,
help="Add-on version.",
help="add-on version",
default="0.0.1",
)
init_parser.add_argument(
"--overwrite",
action="store_true",
default=False,
help="Overwrite already generated add-on folder.",
help="overwrite already generated add-on folder",
)

import_from_aob_parser = subparsers.add_parser(
"import-from-aob", description="[Experimental] Import from AoB."
"import-from-aob", description="[Experimental] Import from AoB"
)
import_from_aob_parser.add_argument(
"--addon-name",
type=str,
help="Add-on name.",
help="add-on name",
required=True,
)

Expand All @@ -159,6 +179,8 @@ def main(argv: Optional[Sequence[str]] = None):
output_directory=args.output,
python_binary_name=args.python_binary_name,
)
if args.command == "package":
package.package(path_to_built_addon=args.path, output_directory=args.output)
if args.command == "init":
init.init(
addon_name=args.addon_name,
Expand Down
38 changes: 38 additions & 0 deletions tests/smoke/test_ucc_package.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import os
import tempfile

from splunk_add_on_ucc_framework.commands import build
from splunk_add_on_ucc_framework.commands import init
from splunk_add_on_ucc_framework.commands import package


def test_ucc_package():
"""
Checks the complete from initializing the add-on, building it and then packaging it.
"""
addon_name = "init_addon_for_ucc_package"
generated_addon_path = init.init(
addon_name,
"Demo Add-on for Splunk",
"demo_input",
"1.0.0",
overwrite=True,
)
with tempfile.TemporaryDirectory() as temp_dir_for_build:
build.generate(
os.path.join(generated_addon_path, "package"),
os.path.join(generated_addon_path, "globalConfig.json"),
"1.0.0",
output_directory=temp_dir_for_build,
)

path_to_built_addon = os.path.join(
temp_dir_for_build,
addon_name,
)
with tempfile.TemporaryDirectory() as temp_dir_for_package:
package.package(path_to_built_addon, output_directory=temp_dir_for_package)

found_files = os.listdir(temp_dir_for_package)
if "init_addon_for_ucc_package-1.0.0.tar.gz" not in found_files:
assert False, "No archive found where it should be"
Original file line number Diff line number Diff line change
Expand Up @@ -1131,7 +1131,7 @@
"meta": {
"name": "Splunk_TA_UCCExample",
"restRoot": "splunk_ta_uccexample",
"version": "5.29.0Rafd1fe38",
"version": "5.29.0R87e714a5",
"displayName": "Splunk UCC test Add-on",
"schemaVersion": "0.0.3"
}
Expand Down
5 changes: 5 additions & 0 deletions tests/unit/test_app_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ def test_get_addon_name(app_manifest_correct):
assert expected_addon_name == app_manifest_correct.get_addon_name()


def test_get_addon_version(app_manifest_correct):
expected_addon_version = "7.0.1"
assert expected_addon_version == app_manifest_correct.get_addon_version()


def test_get_title(app_manifest_correct):
expected_title = "Splunk Add-on for UCC Example"
assert expected_title == app_manifest_correct.get_title()
Expand Down
26 changes: 26 additions & 0 deletions tests/unit/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,29 @@ def test_import_from_aob_command(mock_import_from_aob, args, expected_parameters
main.main(args)

mock_import_from_aob.assert_called_with(**expected_parameters)


@pytest.mark.parametrize(
"args,expected_parameters",
[
(
["package", "--path", "output/foo"],
{
"path_to_built_addon": "output/foo",
"output_directory": None,
},
),
(
["package", "--path", "output/foo", "--output", "bar"],
{
"path_to_built_addon": "output/foo",
"output_directory": "bar",
},
),
],
)
@mock.patch("splunk_add_on_ucc_framework.commands.package.package")
def test_package_command(mock_package, args, expected_parameters):
main.main(args)

mock_package.assert_called_with(**expected_parameters)

0 comments on commit 266b28f

Please sign in to comment.