Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add profile support for cli #931

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ regtests/client/python/poetry.lock
/polaris-venv/
/pyproject.toml

# Polaris CLI profile
.polaris.json

# Notebook Checkpoints
**/.ipynb_checkpoints/

Expand Down
2 changes: 1 addition & 1 deletion polaris
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ if [ ! -d ${SCRIPT_DIR}/polaris-venv ]; then
fi

pushd $SCRIPT_DIR > /dev/null
PYTHONPATH=regtests/client/python ${SCRIPT_DIR}/polaris-venv/bin/python3 regtests/client/python/cli/polaris_cli.py "$@"
PYTHONPATH=regtests/client/python SCRIPT_DIR="$SCRIPT_DIR" ${SCRIPT_DIR}/polaris-venv/bin/python3 regtests/client/python/cli/polaris_cli.py "$@"
status=$?
popd > /dev/null

Expand Down
7 changes: 7 additions & 0 deletions regtests/client/python/cli/command/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,13 @@ def options_get(key, f=lambda x: x):
location=options_get(Arguments.LOCATION),
properties=properties
)
elif options.command == Commands.PROFILES:
from cli.command.profiles import ProfilesCommand
subcommand = options_get(f'{Commands.PROFILES}_subcommand')
command = ProfilesCommand(
subcommand,
profile_name=options_get(Arguments.PROFILE)
)
MonkeyCanCode marked this conversation as resolved.
Show resolved Hide resolved

if command is not None:
command.validate()
Expand Down
142 changes: 142 additions & 0 deletions regtests/client/python/cli/command/profiles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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 os
import sys
import json
from dataclasses import dataclass
from typing import Dict, Optional, List

from pydantic import StrictStr

from cli.command import Command
from cli.constants import Subcommands, DEFAULT_HOSTNAME, DEFAULT_PORT, CONFIG_DIR, CONFIG_FILE
from polaris.management import PolarisDefaultApi


@dataclass
class ProfilesCommand(Command):
MonkeyCanCode marked this conversation as resolved.
Show resolved Hide resolved
"""
A Command implementation to represent `polaris profiles`. The instance attributes correspond to parameters
that can be provided to various subcommands, except `profiles_subcommand` which represents the subcommand
itself.

Example commands:
* ./polaris profiles create dev
* ./polaris profiles delete dev
* ./polaris profiles update dev
MonkeyCanCode marked this conversation as resolved.
Show resolved Hide resolved
* ./polaris profiles get dev
* ./polaris profiles list
"""

profiles_subcommand: str
profile_name: str

def _load_profiles(self) -> Dict[str, Dict[str, str]]:
if not os.path.exists(CONFIG_FILE):
return {}
with open(CONFIG_FILE, "r") as f:
return json.load(f)

def _save_profiles(self, profiles: Dict[str, Dict[str, str]]) -> None:
if not os.path.exists(CONFIG_DIR):
os.makedirs(CONFIG_DIR)
with open(CONFIG_FILE, "w") as f:
json.dump(profiles, f, indent=2)

def _create_profile(self, name: str) -> None:
profiles = self._load_profiles()
if name not in profiles:
client_id = input("Polaris Client ID: ")
client_secret = input("Polaris Client Secret: ")
host = input(f"Polaris Host [{DEFAULT_HOSTNAME}]: ") or DEFAULT_HOSTNAME
port = input(f"Polaris Port [{DEFAULT_PORT}]: ") or DEFAULT_PORT
profiles[name] = {
"client_id": client_id,
"client_secret": client_secret,
"host": host,
"port": port
}
self._save_profiles(profiles)
else:
print(f"Profile {name} already exists.")
sys.exit(1)

def _get_profile(self, name: str) -> Optional[Dict[str, str]]:
profiles = self._load_profiles()
return profiles.get(name)

def _list_profiles(self) -> List[str]:
profiles = self._load_profiles()
return list(profiles.keys())

def _delete_profile(self, name: str) -> None:
profiles = self._load_profiles()
if name in profiles:
del profiles[name]
self._save_profiles(profiles)

def _update_profile(self, name: str) -> None:
profiles = self._load_profiles()
if name in profiles:
current_client_id = profiles[name].get("client_id")
current_client_secret = profiles[name].get("client_secret")
current_host = profiles[name].get("host")
current_port = profiles[name].get("port")

client_id = input(f"Polaris Client ID [{current_client_id}]: ") or current_client_id
client_secret = input(f"Polaris Client Secret [{current_client_secret}]: ") or current_client_secret
host = input(f"Polaris Client ID [{current_host}]: ") or current_host
port = input(f"Polaris Client Secret [{current_port}]: ") or current_port
profiles[name] = {
"client_id": client_id,
"client_secret": client_secret,
"host": host,
"port": port
}
self._save_profiles(profiles)
else:
print(f"Profile {name} does not exist.")
sys.exit(1)

def validate(self):
pass

def execute(self, api: Optional[PolarisDefaultApi] = None) -> None:
if self.profiles_subcommand == Subcommands.CREATE:
self._create_profile(self.profile_name)
print(f"Polaris profile {self.profile_name} created successfully.")
elif self.profiles_subcommand == Subcommands.DELETE:
self._delete_profile(self.profile_name)
print(f"Polaris profile {self.profile_name} deleted successfully.")
elif self.profiles_subcommand == Subcommands.UPDATE:
self._update_profile(self.profile_name)
print(f"Polaris profile {self.profile_name} updated successfully.")
elif self.profiles_subcommand == Subcommands.GET:
profile = self._get_profile(self.profile_name)
if profile:
print(f"Polaris profile {self.profile_name}: {profile}")
else:
print(f"Polaris profile {self.profile_name} not found.")
elif self.profiles_subcommand == Subcommands.LIST:
profiles = self._list_profiles()
print("Polaris profiles:")
for profile in profiles:
print(f" - {profile}")
else:
raise Exception(f"{self.profiles_subcommand} is not supported in the CLI")
8 changes: 8 additions & 0 deletions regtests/client/python/cli/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# specific language governing permissions and limitations
# under the License.
#
import os
from enum import Enum


Expand Down Expand Up @@ -58,6 +59,7 @@ class Commands:
CATALOG_ROLES = 'catalog-roles'
PRIVILEGES = 'privileges'
NAMESPACES = 'namespaces'
PROFILES = 'profiles'


class Subcommands:
Expand Down Expand Up @@ -132,6 +134,7 @@ class Arguments:
PARENT = 'parent'
LOCATION = 'location'
REGION = 'region'
PROFILE = 'profile'


class Hints:
Expand Down Expand Up @@ -229,5 +232,10 @@ class Namespaces:
UNIT_SEPARATOR = chr(0x1F)
CLIENT_ID_ENV = 'CLIENT_ID'
CLIENT_SECRET_ENV = 'CLIENT_SECRET'
CLIENT_PROFILE_ENV = 'CLIENT_PROFILE'
DEFAULT_HOSTNAME = 'localhost'
DEFAULT_PORT = 8181
CONFIG_DIR = os.environ.get('SCRIPT_DIR')
if CONFIG_DIR is None:
raise Exception("The SCRIPT_DIR environment variable is not set. Please set it to the Polaris's script directory.")
CONFIG_FILE = os.path.join(CONFIG_DIR, '.polaris.json')
7 changes: 7 additions & 0 deletions regtests/client/python/cli/options/option_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,5 +229,12 @@ def get_tree() -> List[Option]:
Option(Subcommands.GET, args=[
Argument(Arguments.CATALOG, str, Hints.CatalogRoles.CATALOG_NAME)
], input_name=Arguments.NAMESPACE),
]),
Option(Commands.PROFILES, 'manage profiles', children=[
Option(Subcommands.CREATE, input_name=Arguments.PROFILE),
Option(Subcommands.DELETE, input_name=Arguments.PROFILE),
Option(Subcommands.UPDATE, input_name=Arguments.PROFILE),
Option(Subcommands.GET, input_name=Arguments.PROFILE),
Option(Subcommands.LIST),
])
]
1 change: 1 addition & 0 deletions regtests/client/python/cli/options/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class Parser(object):
Argument(Arguments.CLIENT_ID, str, hint='client ID for token-based authentication'),
Argument(Arguments.CLIENT_SECRET, str, hint='client secret for token-based authentication'),
Argument(Arguments.ACCESS_TOKEN, str, hint='access token for token-based authentication'),
Argument(Arguments.PROFILE, str, hint='profile for token-based authentication'),
]

@staticmethod
Expand Down
51 changes: 36 additions & 15 deletions regtests/client/python/cli/polaris_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
import sys
from json import JSONDecodeError

from cli.constants import Arguments, CLIENT_ID_ENV, CLIENT_SECRET_ENV, DEFAULT_HOSTNAME, DEFAULT_PORT
from typing import Dict

from cli.constants import Arguments, Commands, CLIENT_ID_ENV, CLIENT_SECRET_ENV, CLIENT_PROFILE_ENV, DEFAULT_HOSTNAME, DEFAULT_PORT, CONFIG_FILE
from cli.options.option_tree import Argument
from cli.options.parser import Parser
from polaris.management import ApiClient, Configuration
Expand All @@ -47,16 +49,21 @@ class PolarisCli:
@staticmethod
def execute(args=None):
options = Parser.parse(args)
client_builder = PolarisCli._get_client_builder(options)
with client_builder() as api_client:
try:
from cli.command import Command
admin_api = PolarisDefaultApi(api_client)
command = Command.from_options(options)
command.execute(admin_api)
except Exception as e:
PolarisCli._try_print_exception(e)
sys.exit(1)
if options.command == Commands.PROFILES:
from cli.command import Command
command = Command.from_options(options)
command.execute()
else:
client_builder = PolarisCli._get_client_builder(options)
with client_builder() as api_client:
try:
from cli.command import Command
admin_api = PolarisDefaultApi(api_client)
command = Command.from_options(options)
command.execute(admin_api)
except Exception as e:
PolarisCli._try_print_exception(e)
sys.exit(1)

@staticmethod
def _try_print_exception(e):
Expand All @@ -71,6 +78,13 @@ def _try_print_exception(e):
sys.stderr.write(f'Exception when communicating with the Polaris server.'
f' {e}{os.linesep}')

@staticmethod
def _load_profiles() -> Dict[str, Dict[str, str]]:
if not os.path.exists(CONFIG_FILE):
return {}
with open(CONFIG_FILE, "r") as f:
return json.load(f)

@staticmethod
def _get_token(api_client: ApiClient, catalog_url, client_id, client_secret) -> str:
response = api_client.call_api(
Expand All @@ -90,9 +104,16 @@ def _get_token(api_client: ApiClient, catalog_url, client_id, client_secret) ->

@staticmethod
def _get_client_builder(options):
profile = {}
client_profile = options.profile or os.getenv(CLIENT_PROFILE_ENV)
if client_profile:
profiles = PolarisCli._load_profiles()
profile = profiles.get(client_profile)
if not profile:
raise Exception(f'Polaris profile {client_profile} not found')
# Determine which credentials to use
client_id = options.client_id or os.getenv(CLIENT_ID_ENV)
client_secret = options.client_secret or os.getenv(CLIENT_SECRET_ENV)
client_id = options.client_id or os.getenv(CLIENT_ID_ENV) or profile.get('client_id')
client_secret = options.client_secret or os.getenv(CLIENT_SECRET_ENV) or profile.get('client_secret')

# Validates
has_access_token = options.access_token is not None
Expand All @@ -117,8 +138,8 @@ def _get_client_builder(options):
polaris_management_url = f'{options.base_url}/api/management/v1'
polaris_catalog_url = f'{options.base_url}/api/catalog/v1'
else:
host = options.host or DEFAULT_HOSTNAME
port = options.port or DEFAULT_PORT
host = options.host or profile.get('host') or DEFAULT_HOSTNAME
port = options.port or profile.get('port') or DEFAULT_PORT
polaris_management_url = f'http://{host}:{port}/api/management/v1'
polaris_catalog_url = f'http://{host}:{port}/api/catalog/v1'

Expand Down
2 changes: 1 addition & 1 deletion regtests/polaris-reg-test
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ fi
# Save the current directory
CURRENT_DIR=$(pwd)
cd $SCRIPT_DIR > /dev/null
PYTHONPATH=regtests/client/python ${SCRIPT_DIR}/polaris-venv/bin/python3 regtests/client/python/cli/polaris_cli.py "$@"
PYTHONPATH=regtests/client/python SCRIPT_DIR="$SCRIPT_DIR" ${SCRIPT_DIR}/polaris-venv/bin/python3 regtests/client/python/cli/polaris_cli.py "$@"
status=$?
cd $CURRENT_DIR > /dev/null

Expand Down
Loading