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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ __pycache__
*.py[cod]
*.py[oc]
*.swp
*.tsv
*.yaml
build/
dist/
Expand Down
32 changes: 29 additions & 3 deletions dev/test-cli-pypi-install.sh
Original file line number Diff line number Diff line change
@@ -1,24 +1,50 @@
#!/usr/bin/env bash
# Description: Tests CLI mode install

set -euo pipefail
set -euox pipefail

# Set the working directory
working_directory="${TMPDIR:-/tmp}/src-auth-perms-sync-pypi-install"
tmp_root="${TMPDIR:-/tmp}"
working_directory="${tmp_root%/}/src-auth-perms-sync-pypi-install"

# Delete, recreate, and cd to working directory
rm -rf "${working_directory}" && mkdir -p "${working_directory}" && cd "${working_directory}"

log_file="${working_directory}/test-cli-pypi-install.log"
exec > >(tee "${log_file}") 2>&1
echo "Writing output to ${log_file}"
echo ""
echo "Dir contents in ${working_directory} before"
ls -al

# Use python3.13 to create and activate a venv
# shellcheck disable=SC1091
echo ""
python3.13 -m venv .venv && source .venv/bin/activate
which python
python --version

# Ensure pip is up to date
echo ""
python -m pip install --upgrade pip

# pip install latest from https://pypi.org/project/src-auth-perms-sync
python3.13 -m pip install --upgrade pip src-auth-perms-sync
echo ""
python -m pip install src-auth-perms-sync

# Run commands
echo ""
src-auth-perms-sync --help
echo ""
src-auth-perms-sync get --help
echo ""
src-auth-perms-sync set --help
echo ""
src-auth-perms-sync restore --help
echo ""
src-auth-perms-sync sync-saml-orgs --help

echo ""
echo "Dir contents in ${working_directory} after"
ls -al
echo ""
84 changes: 61 additions & 23 deletions src/src_auth_perms_sync/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@

CommandName: TypeAlias = Literal["get", "set", "restore", "sync_saml_orgs"]
DEFAULT_MAPS_FILE_NAME = "maps.yaml"
COMMON_CONFIG_FIELDS = src.config_field_names(
COMMON_CONFIG_FIELDS_BEFORE = src.config_field_names(
src.SourcegraphClientConfig,
)
COMMON_CONFIG_FIELDS_AFTER = src.config_field_names(
src.LoggingConfig,
src.OpenTelemetryConfig,
"parallelism",
Expand All @@ -44,6 +46,7 @@
"fetch_sg_traces",
)
GET_CONFIG_FIELDS = src.config_field_names(
*COMMON_CONFIG_FIELDS_BEFORE,
"users",
"users_without_explicit_perms",
"created_after",
Expand All @@ -52,9 +55,10 @@
"repos_created_after",
"no_backup",
"explicit_permissions_batch_size",
*COMMON_CONFIG_FIELDS,
*COMMON_CONFIG_FIELDS_AFTER,
)
SET_CONFIG_FIELDS = src.config_field_names(
*COMMON_CONFIG_FIELDS_BEFORE,
"maps_path",
"full",
"users",
Expand All @@ -67,19 +71,22 @@
"apply",
"no_backup",
"explicit_permissions_batch_size",
*COMMON_CONFIG_FIELDS,
*COMMON_CONFIG_FIELDS_AFTER,
)
RESTORE_CONFIG_FIELDS = src.config_field_names(
*COMMON_CONFIG_FIELDS_BEFORE,
"restore_path",
"apply",
"no_backup",
"explicit_permissions_batch_size",
*COMMON_CONFIG_FIELDS,
*COMMON_CONFIG_FIELDS_AFTER,
)
SYNC_SAML_ORGS_CONFIG_FIELDS = src.config_field_names(
*COMMON_CONFIG_FIELDS_BEFORE,
"apply",
"no_backup",
*COMMON_CONFIG_FIELDS,
"parallelism",
*COMMON_CONFIG_FIELDS_AFTER,
)
LogCommandName: TypeAlias = Literal[
"get",
Expand Down Expand Up @@ -187,9 +194,9 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo
cli_flag="--maps-path",
metavar="FILE",
help=(
"Maps YAML file for the set command.\n"
"If omitted, set uses maps.yaml under src-auth-perms-sync-runs/<endpoint>/.\n"
"Relative paths are resolved from the current working directory."
"Maps YAML file for the set command\n"
"(default: ./src-auth-perms-sync-runs/<src-endpoint>/maps.yaml)\n"
"Relative paths are resolved from the current working directory"
),
help_group="Permission sync",
)
Expand All @@ -199,8 +206,8 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo
cli_flag="--restore-path",
metavar="FILE",
help=(
"Snapshot JSON file for the restore command.\n"
"Relative paths are resolved from the current working directory."
"Snapshot JSON file for the restore command\n"
"Relative paths are resolved from the current working directory"
),
help_group="Restore",
)
Expand All @@ -210,8 +217,8 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo
cli_flag="--full",
cli_action="store_true",
help=(
"With the set command: run full overwrite reconciliation "
"(default only when no user filter is set)"
"Full overwrite of all explicit perms for the repos in scope\n"
"Must be passed explicitly when no user or repo filter args are provided"
),
help_group="Permission sync",
)
Expand All @@ -220,7 +227,7 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo
env_var="SRC_AUTH_PERMS_SYNC_USERS",
cli_flag="--users",
metavar="USERS",
help="Process comma-delimited Sourcegraph usernames and/or email addresses",
help="Process a comma-delimited list of Sourcegraph usernames and/or email addresses",
help_group="User filters",
)
users_without_explicit_perms: bool = src.config_field(
Expand All @@ -245,15 +252,15 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo
env_var="SRC_AUTH_PERMS_SYNC_REPOS",
cli_flag="--repos",
metavar="REPOS",
help="Process comma-delimited Sourcegraph repository names",
help="Process a comma-delimited list of Sourcegraph repository names",
help_group="Repo filters",
)
repos_without_explicit_perms: bool = src.config_field(
default=False,
env_var="SRC_AUTH_PERMS_SYNC_REPOS_WITHOUT_EXPLICIT_PERMS",
cli_flag="--repos-without-explicit-perms",
cli_action="store_true",
help="Process Sourcegraph repositories without explicit permissions",
help="Process repositories without explicit permissions",
help_group="Repo filters",
)
repos_created_after: str | None = src.config_field(
Expand All @@ -262,7 +269,7 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo
cli_flag="--repos-created-after",
metavar="YYYY-MM-DD",
pattern=r"^\d{4}-\d{2}-\d{2}$",
help="Process Sourcegraph repositories created on or after this date",
help="Process repositories cloned to the Sourcegraph instance on or after this date",
help_group="Repo filters",
)
sync_saml_organizations: bool = src.config_field(
Expand All @@ -278,7 +285,7 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo
env_var="SRC_AUTH_PERMS_SYNC_APPLY",
cli_flag="--apply",
cli_action="store_true",
help="With mutating commands: actually mutate state. Default is dry-run",
help="Apply changes (default is dry run)",
help_group="Mutation",
)
no_backup: bool = src.config_field(
Expand All @@ -296,7 +303,7 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo
metavar="N",
ge=1,
help="Concurrent Sourcegraph API worker threads (default: 16)",
help_group="Runtime",
help_group="Performance",
)
explicit_permissions_batch_size: int = src.config_field(
default=25,
Expand All @@ -307,7 +314,7 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo
help=(
"Users per GraphQL request when capturing explicit repository permissions (default: 25)"
),
help_group="Runtime",
help_group="Performance",
)
max_attempts: int = src.config_field(
default=5,
Expand All @@ -316,7 +323,7 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo
metavar="N",
ge=1,
help="Max attempts per HTTP request before giving up (default: 5)",
help_group="Runtime",
help_group="Performance",
)
http_timeout_seconds: float = src.config_field(
default=60.0,
Expand All @@ -325,7 +332,7 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo
metavar="SECONDS",
gt=0,
help="HTTP read timeout per request in seconds (default: 60)",
help_group="Runtime",
help_group="Performance",
)
sample_interval: float = src.config_field(
default=10.0,
Expand All @@ -334,15 +341,15 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryCo
metavar="SECONDS",
ge=0,
help="Seconds between logging compute resource samples; set 0 to disable (default: 10)",
help_group="Runtime",
help_group="Performance",
)
fetch_sg_traces: bool = src.config_field(
default=False,
env_var="SRC_AUTH_PERMS_SYNC_FETCH_SG_TRACES",
cli_flag="--fetch-sg-traces",
cli_action="store_true",
help="Ask Sourcegraph to retain GraphQL traces and return debug trace metadata",
help_group="Runtime",
help_group="Performance",
)


Expand Down Expand Up @@ -479,6 +486,24 @@ def validate_set_mode_selection(command_name: CommandName, config: Config) -> No
"--repos-without-explicit-perms, or --repos-created-after"
)

set_mode_selected = any(
(
config.full,
bool(config.users),
config.users_without_explicit_perms,
config.created_after is not None,
bool(config.repos),
config.repos_without_explicit_perms,
config.repos_created_after is not None,
)
)
if not set_mode_selected:
config_error(
"set requires one of --full, --users, --users-without-explicit-perms, "
"--created-after, --repos, --repos-without-explicit-perms, or "
"--repos-created-after"
)


def set_command_options(config: Config) -> permission_types.SetCommandOptions:
"""Return the validated set mode options."""
Expand Down Expand Up @@ -600,6 +625,9 @@ def load_cli(argv: Sequence[str] | None = None) -> CliInput:
parser.error(str(exception))
command_name = cast(CommandName, arguments.command_name)
validate_config(command_name, config)
if command_name == "restore":
assert config.restore_path is not None
require_restore_input_file(config.restore_path)
return CliInput(command_name=command_name, config=config)


Expand Down Expand Up @@ -629,6 +657,15 @@ def require_set_input_file(maps_path: Path) -> None:
)


def require_restore_input_file(restore_path: Path) -> None:
"""Exit with a clear error if the selected restore snapshot is missing."""
if restore_path.is_file():
return
if restore_path.exists():
raise SystemExit(f"restore snapshot path is not a file: {restore_path}")
raise SystemExit(f"restore snapshot file does not exist: {restore_path}")


def run_fields(config: Config, command: ResolvedCommand, endpoint: str) -> dict[str, object]:
"""Return run-level fields for structured logging."""
fields: dict[str, object] = {
Expand Down Expand Up @@ -761,6 +798,7 @@ def run_restore(
) -> None:
"""Run the selected repo-permission restore command."""
assert config.restore_path is not None
require_restore_input_file(config.restore_path)
permissions_command.cmd_restore(
client,
config.restore_path,
Expand Down
Loading