Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ It does not support:

It accepts the following parameters:

* `--stage` - [optional] flag to choose staging splunkbase to publish the app.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we planning to expose this for all UCC users, or is this for a specific use case?

* `--app-id` - [required] Splunkbase numerical app id listed in the URL of the app details page.
* `--package-path` - [required] path to the package file (.tar.gz) to be published (must be a valid UCC package).
* `--splunk-versions` - [required] comma-separated list of supported Splunk versions (e.g. "9.1,9.2").
Expand Down
22 changes: 17 additions & 5 deletions splunk_add_on_ucc_framework/commands/publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def encode_multipart_formdata(


def upload_package(
base_url: str,
app_id: int,
package_path: str,
splunk_versions: str,
Expand All @@ -70,7 +71,7 @@ def upload_package(
username: str,
password: str,
) -> str:
upload_url = f"https://splunkbase.splunk.com/api/v1/app/{app_id}/new_release/"
upload_url = f"{base_url}/app/{app_id}/new_release/"

fields = {
"filename": os.path.basename(package_path),
Expand Down Expand Up @@ -109,9 +110,9 @@ def upload_package(


def check_package_validation(
package_upload_id: str, username: str, password: str
base_url: str, package_upload_id: str, username: str, password: str
) -> None:
url = f"https://splunkbase.splunk.com/api/v1/package/{package_upload_id}/"
url = f"{base_url}/package/{package_upload_id}/"
auth_header = base64.b64encode(f"{username}:{password}".encode()).decode("utf-8")
context = ssl.create_default_context(cafile=certifi.where())

Expand All @@ -121,14 +122,20 @@ def check_package_validation(
try:
with urllib.request.urlopen(request, context=context) as response:
response_data = json.loads(response.read().decode("utf-8"))
logger.info("Validation status: {}".format(response_data.get("message")))
if response_data.get("result") == "pass":
logger.info(
"Validation status: {}".format(response_data.get("message"))
)
else:
raise Exception(response_data.get("message"))
except urllib.error.HTTPError as e:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Raising a bare Exception is too generic. Callers cannot distinguish a validation failure from other runtime errors. Consider using RuntimeError or a custom exception class like PackageValidationError.

error_msg = e.read().decode()
logger.error(f"Failed to retrieve package validation status. {error_msg}")
raise


def publish_package(
use_stage: bool,
app_id: int,
package_path: str,
splunk_versions: str,
Expand All @@ -137,7 +144,12 @@ def publish_package(
username: str,
password: str,
) -> None:
if use_stage:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The staging URL https://classic.stage.splunkbase.splunk.com/api/v1 is hardcoded inside the function body. Consider extracting both base URLs as module-level constants (e.g. SPLUNKBASE_API_URL and SPLUNKBASE_STAGE_API_URL) for better maintainability.

API_BASEURL = "https://classic.stage.splunkbase.splunk.com/api/v1"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API_BASEURL uses SCREAMING_SNAKE_CASE but this is a local variable, not a module-level constant. Per PEP 8 this should be api_base_url (snake_case).

else:
API_BASEURL = "https://splunkbase.splunk.com/api/v1"
package_upload_id = upload_package(
API_BASEURL,
app_id,
package_path,
splunk_versions,
Expand All @@ -147,4 +159,4 @@ def publish_package(
password,
)
if package_upload_id:
check_package_validation(package_upload_id, username, password)
check_package_validation(API_BASEURL, package_upload_id, username, password)
7 changes: 7 additions & 0 deletions splunk_add_on_ucc_framework/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,12 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
publish_parser = subparsers.add_parser(
"publish", description="Publish package to the Splunkbase"
)
publish_parser.add_argument(
"--stage",
dest="stage",
help="Whether to release the app on staging or production splunkbase.",
action="store_true",
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Help text says "Whether to release the app on staging or production splunkbase" but this is a boolean flag (present = staging, absent = production). Clearer wording: "Publish to staging Splunkbase instead of production."

publish_parser.add_argument(
"--app-id",
type=int,
Expand Down Expand Up @@ -334,6 +340,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
)
if args.command == "publish":
publish.publish_package(
use_stage=args.stage,
app_id=args.app_id,
package_path=args.package_path,
splunk_versions=args.splunk_versions,
Expand Down
27 changes: 22 additions & 5 deletions tests/unit/commands/test_publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def test_upload_package_success(self, mock_file, mock_urlopen, mock_logger):
mock_urlopen.return_value.__exit__ = MagicMock(return_value=None)

pkg_id = upload_package(
base_url="https://dummy_url",
app_id=1001,
package_path="tests/test_package.tgz",
splunk_versions="9.5",
Expand All @@ -48,7 +49,14 @@ def test_upload_package_no_id(self, mock_file, mock_urlopen, mock_logger):
mock_urlopen.return_value.__exit__ = MagicMock(return_value=None)

pkg_id = upload_package(
1001, "tests/test_package.tgz", "9.5", "6.x", True, "user", "pass"
"https://dummy_url",
1001,
"tests/test_package.tgz",
"9.5",
"6.x",
True,
"user",
"pass",
)
assert pkg_id == ""
mock_file.assert_called_once_with("tests/test_package.tgz", "rb")
Expand All @@ -75,7 +83,14 @@ def test_upload_package_http_error(self, mock_file, mock_urlopen, mock_logger):

with pytest.raises(urllib.error.HTTPError):
upload_package(
1001, "tests/test_package.tgz", "9.5", "6.x", True, "user", "pass"
"https://dummy_url",
1001,
"tests/test_package.tgz",
"9.5",
"6.x",
True,
"user",
"pass",
)
mock_file.assert_called_once_with("tests/test_package.tgz", "rb")
mock_logger.error.assert_called_with(
Expand All @@ -89,13 +104,15 @@ class TestPackageValidation:
def test_check_package_validation_success(self, mock_urlopen, mock_logger):
mock_response = MagicMock()
mock_response.read.return_value = json.dumps(
{"message": "Validation passed"}
{"message": "Validation passed", "result": "pass"}
).encode("utf-8")
mock_urlopen.return_value = mock_response
mock_urlopen.return_value.__enter__ = MagicMock(return_value=mock_response)
mock_urlopen.return_value.__exit__ = MagicMock(return_value=None)

check_package_validation("pkg123", "user", "pass") # should not raise
check_package_validation(
"https://dummy_url", "pkg123", "user", "pass"
) # should not raise
mock_logger.info.assert_called_with("Validation status: Validation passed")

@patch("splunk_add_on_ucc_framework.commands.publish.logger")
Expand All @@ -115,7 +132,7 @@ def test_check_package_validation_http_error(self, mock_urlopen, mock_logger):
mock_urlopen.return_value.__exit__ = MagicMock(return_value=None)

with pytest.raises(urllib.error.HTTPError):
check_package_validation("pkg123", "user", "pass")
check_package_validation("https://dummy_url", "pkg123", "user", "pass")
mock_logger.error.assert_called_with(
f"Failed to retrieve package validation status. {mock_error.read().decode()}"
)
3 changes: 3 additions & 0 deletions tests/unit/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,7 @@ def test_package_command(mock_package, args, expected_parameters):
(
[
"publish",
"--stage",
"--app-id",
"123",
"--package-path",
Expand All @@ -539,6 +540,7 @@ def test_package_command(mock_package, args, expected_parameters):
"pass",
],
{
"use_stage": True,
"app_id": 123,
"package_path": "dist/app.tar.gz",
"splunk_versions": "9.5",
Expand Down Expand Up @@ -566,6 +568,7 @@ def test_package_command(mock_package, args, expected_parameters):
"--make-visible",
],
{
"use_stage": False,
"app_id": 456,
"package_path": "/tmp/app.tar.gz",
"splunk_versions": "9.1,9.2",
Expand Down
Loading