Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
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
67 changes: 38 additions & 29 deletions stacklet/client/platform/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,11 @@ def configure(
@cli.command()
@click.option(
"--url",
help="A Stacklet console URL",
)
@click.option(
"--prefix",
required=True,
help="A Stacklet console URL or deployment prefix",
help="A deployment prefix (assumes hosting at .stacklet.io)",
)
@click.option(
"--idp",
Expand All @@ -142,7 +144,7 @@ def configure(
type=click.Path(path_type=pathlib.Path, dir_okay=False),
show_default=True,
)
def auto_configure(url, idp, location):
def auto_configure(url, prefix, idp, location):
"""Automatically configure the stacklet-admin CLI

Fetch configuration details from a live Stacklet instance and use it
Expand All @@ -163,23 +165,36 @@ def auto_configure(url, idp, location):
# Using a deployment prefix (assumes hosting at .stacklet.io)
> stacklet-admin auto-configure --prefix myorg
"""
parts = urlsplit(url, scheme="https", allow_fragments=False)
host = parts.netloc or parts.path
# Validate mutual exclusion
if url and prefix:
raise click.ClickException(
"Cannot specify both --url and --prefix. Please provide one or the other."
)

if not url and not prefix:
raise click.ClickException("Must specify either --url or --prefix.")

if "." not in host:
# Assume that we have a prefix for a Stacklet instance
# hosted under stacklet.io
host = f"console.{host}.stacklet.io"
if not host.startswith("console"):
# Be forgiving if we get a base URL like customer.stacklet.io
host = f"console.{host}"
# Determine the host based on input type
if prefix:
# Handle prefix: always generate .stacklet.io URL
host = f"console.{prefix}.stacklet.io"
scheme = "https"
else:
# Handle URL: parse and potentially add console prefix
parts = urlsplit(url, scheme="https", allow_fragments=False)
host = parts.netloc or parts.path
scheme = parts.scheme

if not host.startswith("console"):
# Be forgiving if we get a base URL like customer.stacklet.io
host = f"console.{host}"

config = {}
try:
for config_path in ("config/cognito.json", "config/cubejs.json"):
config_url = urlunsplit(
(
parts.scheme,
scheme,
host,
config_path,
None,
Expand All @@ -190,17 +205,15 @@ def auto_configure(url, idp, location):
response.raise_for_status()
config.update(response.json())
except requests.exceptions.ConnectionError as err:
click.echo(f"Unable to connect to {config_url}")
click.echo(err)
return
raise click.ClickException(f"Unable to connect to {config_url}\n{err}")
except requests.exceptions.HTTPError as err:
click.echo(f"Unable to retrieve configuration details from {config_url}")
click.echo(err)
return
raise click.ClickException(
f"Unable to retrieve configuration details from {config_url}\n{err}"
)
except requests.exceptions.JSONDecodeError as err:
click.echo(f"Unable to parse configuration details from {config_url}")
click.echo(err)
return
raise click.ClickException(
f"Unable to parse configuration details from {config_url}\n{err}"
)

try:
auth_url = f"https://{config['cognito_install']}"
Expand All @@ -223,23 +236,19 @@ def auto_configure(url, idp, location):
_, idp_id = name_to_id.popitem()
else:
if not idp:
click.echo(
raise click.ClickException(
"Multiple identity providers available, specify one with --idp: "
+ ", ".join(sorted(name_to_id))
)
return
idp_id = name_to_id.get(idp)
if not idp_id:
click.echo(
raise click.ClickException(
f"Unknown identity provider '{idp}', known names: "
+ ", ".join(sorted(name_to_id))
)
return
formatted_config["idp_id"] = idp_id
except KeyError as err:
click.echo("The configuration details are missing a required key")
click.echo(err)
return
raise click.ClickException(f"The configuration details are missing a required key: {err}")

config_file = location.expanduser()
if not config_file.exists():
Expand Down
241 changes: 241 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import textwrap
from tempfile import NamedTemporaryFile

import requests
import requests_mock

from .utils import BaseCliTest


Expand Down Expand Up @@ -67,3 +70,241 @@ def test_admin_save_config_and_show(self):
)

os.unlink(file_location.name)

def test_auto_configure_url_only(self):
"""Test auto-configure with --url parameter"""
file_location = NamedTemporaryFile(mode="w+", delete=False)

# Mock the HTTP responses for config endpoints
mock_config = {
"cognito_install": "auth.console.dev.stacklet.dev",
"cognito_user_pool_region": "us-east-2",
"cognito_user_pool_id": "us-east-2_test123",
"cognito_user_pool_client_id": "test-client-id",
"cubejs_domain": "cubejs.dev.stacklet.dev",
"saml_providers": [{"name": "TestIDP", "idp_id": "test-idp-id"}],
}

with requests_mock.Mocker() as m:
# Mock both config endpoints
m.get("https://console.dev.stacklet.dev/config/cognito.json", json=mock_config)
m.get("https://console.dev.stacklet.dev/config/cubejs.json", json={})

res = self.runner.invoke(
self.cli,
[
"auto-configure",
"--url=console.dev.stacklet.dev",
f"--location={file_location.name}",
],
)

self.assertEqual(res.exit_code, 0)

# Verify the config was saved correctly
with open(file_location.name, "r") as f:
config = json.load(f)

self.assertEqual(config["api"], "https://api.dev.stacklet.dev")
self.assertEqual(config["auth_url"], "https://auth.console.dev.stacklet.dev")
self.assertEqual(config["cognito_client_id"], "test-client-id")
self.assertEqual(config["cognito_user_pool_id"], "us-east-2_test123")
self.assertEqual(config["region"], "us-east-2")
self.assertEqual(config["cubejs"], "https://cubejs.dev.stacklet.dev")
self.assertEqual(config["idp_id"], "test-idp-id")

os.unlink(file_location.name)

def test_auto_configure_prefix_only(self):
"""Test auto-configure with --prefix parameter"""
file_location = NamedTemporaryFile(mode="w+", delete=False)

mock_config = {
"cognito_install": "auth.console.myorg.stacklet.io",
"cognito_user_pool_region": "us-west-2",
"cognito_user_pool_id": "us-west-2_test456",
"cognito_user_pool_client_id": "test-client-id-2",
"cubejs_domain": "cubejs.myorg.stacklet.io",
"saml_providers": [{"name": "OrgIDP", "idp_id": "org-idp-id"}],
}

with requests_mock.Mocker() as m:
m.get("https://console.myorg.stacklet.io/config/cognito.json", json=mock_config)
m.get("https://console.myorg.stacklet.io/config/cubejs.json", json={})

res = self.runner.invoke(
self.cli,
[
"auto-configure",
"--prefix=myorg",
f"--location={file_location.name}",
],
)

self.assertEqual(res.exit_code, 0)

with open(file_location.name, "r") as f:
config = json.load(f)

self.assertEqual(config["api"], "https://api.myorg.stacklet.io")
self.assertEqual(config["auth_url"], "https://auth.console.myorg.stacklet.io")
self.assertEqual(config["cubejs"], "https://cubejs.myorg.stacklet.io")

os.unlink(file_location.name)

def test_auto_configure_url_without_console_prefix(self):
"""Test auto-configure with URL that needs console prefix added"""
file_location = NamedTemporaryFile(mode="w+", delete=False)

mock_config = {
"cognito_install": "auth.console.example.stacklet.io",
"cognito_user_pool_region": "eu-west-1",
"cognito_user_pool_id": "eu-west-1_test789",
"cognito_user_pool_client_id": "test-client-id-3",
"cubejs_domain": "cubejs.example.stacklet.io",
"saml_providers": [{"name": "ExampleIDP", "idp_id": "example-idp-id"}],
}

with requests_mock.Mocker() as m:
m.get("https://console.example.stacklet.io/config/cognito.json", json=mock_config)
m.get("https://console.example.stacklet.io/config/cubejs.json", json={})

res = self.runner.invoke(
self.cli,
[
"auto-configure",
"--url=example.stacklet.io", # Missing console prefix
f"--location={file_location.name}",
],
)

self.assertEqual(res.exit_code, 0)

# Verify it added the console prefix and connected correctly
with open(file_location.name, "r") as f:
config = json.load(f)

self.assertEqual(config["region"], "eu-west-1")
self.assertEqual(config["cognito_user_pool_id"], "eu-west-1_test789")

os.unlink(file_location.name)

def test_auto_configure_both_url_and_prefix_error(self):
"""Test that providing both --url and --prefix results in an error"""
res = self.runner.invoke(
self.cli,
[
"auto-configure",
"--url=console.dev.stacklet.dev",
"--prefix=dev",
],
)

self.assertEqual(res.exit_code, 1) # Command should exit with error code
self.assertIn("Cannot specify both --url and --prefix", res.output)

def test_auto_configure_neither_url_nor_prefix_error(self):
"""Test that providing neither --url nor --prefix results in an error"""
res = self.runner.invoke(
self.cli,
[
"auto-configure",
],
)

self.assertEqual(res.exit_code, 1) # Command should exit with error code
self.assertIn("Must specify either --url or --prefix", res.output)

def test_auto_configure_connection_error(self):
"""Test auto-configure behavior when connection fails"""
file_location = NamedTemporaryFile(mode="w+", delete=False)

with requests_mock.Mocker() as m:
# Mock connection error
m.get(
"https://console.nonexistent.stacklet.dev/config/cognito.json",
exc=requests.exceptions.ConnectionError,
)

res = self.runner.invoke(
self.cli,
[
"auto-configure",
"--url=console.nonexistent.stacklet.dev",
f"--location={file_location.name}",
],
)

self.assertEqual(res.exit_code, 1) # Command should exit with error code
self.assertIn("Unable to connect to", res.output)

os.unlink(file_location.name)

def test_auto_configure_multiple_idps_with_selection(self):
"""Test auto-configure with multiple IDPs and explicit selection"""
file_location = NamedTemporaryFile(mode="w+", delete=False)

mock_config = {
"cognito_install": "auth.console.multi.stacklet.dev",
"cognito_user_pool_region": "us-east-1",
"cognito_user_pool_id": "us-east-1_multi123",
"cognito_user_pool_client_id": "multi-client-id",
"cubejs_domain": "cubejs.multi.stacklet.dev",
"saml_providers": [
{"name": "IDP1", "idp_id": "idp-1"},
{"name": "IDP2", "idp_id": "idp-2"},
],
}

with requests_mock.Mocker() as m:
m.get("https://console.multi.stacklet.dev/config/cognito.json", json=mock_config)
m.get("https://console.multi.stacklet.dev/config/cubejs.json", json={})

res = self.runner.invoke(
self.cli,
[
"auto-configure",
"--url=console.multi.stacklet.dev",
"--idp=IDP2",
f"--location={file_location.name}",
],
)

self.assertEqual(res.exit_code, 0)

with open(file_location.name, "r") as f:
config = json.load(f)

self.assertEqual(config["idp_id"], "idp-2") # Should use the selected IDP

os.unlink(file_location.name)

def test_auto_configure_multiple_idps_no_selection_error(self):
"""Test auto-configure with multiple IDPs but no selection results in error"""
mock_config = {
"cognito_install": "auth.console.multi.stacklet.dev",
"cognito_user_pool_region": "us-east-1",
"cognito_user_pool_id": "us-east-1_multi123",
"cognito_user_pool_client_id": "multi-client-id",
"cubejs_domain": "cubejs.multi.stacklet.dev",
"saml_providers": [
{"name": "IDP1", "idp_id": "idp-1"},
{"name": "IDP2", "idp_id": "idp-2"},
],
}

with requests_mock.Mocker() as m:
m.get("https://console.multi.stacklet.dev/config/cognito.json", json=mock_config)
m.get("https://console.multi.stacklet.dev/config/cubejs.json", json={})

res = self.runner.invoke(
self.cli,
[
"auto-configure",
"--url=console.multi.stacklet.dev",
# No --idp specified
],
)

self.assertEqual(res.exit_code, 1) # Command should exit with error code
self.assertIn("Multiple identity providers available", res.output)