blob: 78d8728e341f167f70f99b81b45e30c6ff869237 [file] [log] [blame]
#
# 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 functools
import json
import os
import sys
from json import JSONDecodeError
import urllib3
from cli.api_client_builder import ApiClientBuilder
from cli.constants import Commands
from cli.options.parser import Parser
from polaris.management import PolarisDefaultApi
class PolarisCli:
"""
Implements a basic Command-Line Interface (CLI) for interacting with a Polaris service. The CLI can be used to
manage entities like catalogs, principals, and grants within Polaris and can perform most operations that are
available in the Python client API.
Example usage:
* ./polaris --client-id ${id} --client-secret ${secret} --host ${hostname} --port ${port} principals create example_user
* ./polaris --client-id ${id} --client-secret ${secret} --host ${hostname} --port ${port} principal-roles create example_role
* ./polaris --client-id ${id} --client-secret ${secret} --host ${hostname} --port ${port} catalog-roles list
* ./polaris --client-id ${id} --client-secret ${secret} --base-url https://custom-polaris-domain.example.com/service-prefix catalogs list
"""
# Can be enabled if the client is able to authenticate directly without first fetching a token
DIRECT_AUTHENTICATION_ENABLED = False
@staticmethod
def _patch_generated_models() -> None:
"""
The OpenAPI generator creates an `api_client` that dynamically looks up
model classes from the `polaris.catalog.models` module using `getattr()`.
For example, when a response for a `create_policy` call is received, the
deserializer tries to find the `LoadPolicyResponse` class by looking for
`polaris.catalog.models.LoadPolicyResponse`.
However, the generator fails to add the necessary `import` statements
to the `polaris/catalog/models/__init__.py` file. This means that even
though the model files exist (e.g., `load_policy_response.py`), the classes
are not part of the `polaris.catalog.models` namespace.
This method works around the bug in the generated code without modifying
the source files. It runs once per CLI execution, before any commands, and
manually injects the missing response-side model classes into the
`polaris.catalog.models` namespace, allowing the deserializer to find them.
"""
import polaris.catalog.models
from polaris.catalog.models.applicable_policy import ApplicablePolicy
from polaris.catalog.models.get_applicable_policies_response import GetApplicablePoliciesResponse
from polaris.catalog.models.list_policies_response import ListPoliciesResponse
from polaris.catalog.models.load_policy_response import LoadPolicyResponse
from polaris.catalog.models.policy import Policy
from polaris.catalog.models.policy_attachment_target import PolicyAttachmentTarget
from polaris.catalog.models.policy_identifier import PolicyIdentifier
models_to_patch = {
"ApplicablePolicy": ApplicablePolicy,
"GetApplicablePoliciesResponse": GetApplicablePoliciesResponse,
"ListPoliciesResponse": ListPoliciesResponse,
"LoadPolicyResponse": LoadPolicyResponse,
"Policy": Policy,
"PolicyAttachmentTarget": PolicyAttachmentTarget,
"PolicyIdentifier": PolicyIdentifier,
}
for name, model_class in models_to_patch.items():
setattr(polaris.catalog.models, name, model_class)
@staticmethod
def execute(args=None):
PolarisCli._patch_generated_models()
options = Parser.parse(args)
if options.command == Commands.PROFILES:
from cli.command import Command
command = Command.from_options(options)
command.execute()
else:
api_client = ApiClientBuilder(
options, direct_authentication=PolarisCli.DIRECT_AUTHENTICATION_ENABLED
).get_api_client()
try:
from cli.command import Command
admin_api = PolarisDefaultApi(api_client)
command = Command.from_options(options)
if options.debug:
PolarisCli._enable_api_request_logging()
command.execute(admin_api)
except Exception as e:
PolarisCli._try_print_exception(e)
sys.exit(1)
@staticmethod
def _enable_api_request_logging():
# Store the original urlopen method
if not hasattr(urllib3.PoolManager, "original_urlopen"):
urllib3.PoolManager.original_urlopen = urllib3.PoolManager.urlopen
# Define the wrapper function
@functools.wraps(urllib3.PoolManager.original_urlopen)
def urlopen_wrapper(self, method, url, **kwargs):
sys.stderr.write(f"Request: {method} {url}\n")
if "headers" in kwargs:
sys.stderr.write(f"Headers: {kwargs['headers']}\n")
if "body" in kwargs:
sys.stderr.write(f"Body: {kwargs['body']}\n")
sys.stderr.write("\n")
# Call the original urlopen method
return urllib3.PoolManager.original_urlopen(self, method, url, **kwargs)
urllib3.PoolManager.urlopen = urlopen_wrapper
@staticmethod
def _try_print_exception(e):
try:
error = json.loads(e.body)["error"]
sys.stderr.write(
f"Exception when communicating with the Polaris server."
f" {error['type']}: {error['message']}{os.linesep}"
)
except JSONDecodeError as _:
sys.stderr.write(
f"Exception when communicating with the Polaris server."
f" {e.status}: {e.reason}{os.linesep}"
)
except Exception as _:
sys.stderr.write(
f"Exception when communicating with the Polaris server. {e}{os.linesep}"
)
def main():
PolarisCli.execute()
if __name__ == "__main__":
main()