def delete( ctx: typer.Context, id: int = typer.Option( ..., help=f"the specific id of the application to delete. {ID_NOTE}", ), ): """ Delete an existing application. """ jg_ctx: JobbergateContext = ctx.obj # Make static type checkers happy assert jg_ctx.client is not None # Delete the upload. The API will also remove the application data files make_request( jg_ctx.client, f"/applications/{id}", "DELETE", expected_status=204, expect_response=False, abort_message="Request to delete application was not accepted by the API", support=True, ) terminal_message( """ The application was successfully deleted. """, subject="Application delete succeeded", )
def delete( ctx: typer.Context, id: int = typer.Option( ..., help="The id of the job submission to delete", ), ): """ Delete an existing job submission. """ jg_ctx: JobbergateContext = ctx.obj # Make static type checkers happy assert jg_ctx.client is not None, "Client is uninitialized" make_request( jg_ctx.client, f"/job-submissions/{id}", "DELETE", expected_status=204, abort_message="Request to delete job submission was not accepted by the API", support=True, ) terminal_message( "The job submission was successfully deleted.", subject="Job submission delete succeeded", )
def logout(): """ Logs out of the jobbergate-cli. Clears the saved user credentials. """ clear_token_cache() terminal_message( "User was logged out.", subject="Logged out", )
def login(ctx: typer.Context): """ Log in to the jobbergate-cli by storing the supplied token argument in the cache. """ token_set: TokenSet = fetch_auth_tokens(ctx.obj) persona: Persona = init_persona(ctx.obj, token_set) terminal_message( f"User was logged in with email '{persona.identity_data.user_email}'", subject="Logged in!", )
def main( ctx: typer.Context, verbose: bool = typer.Option( False, help="Enable verbose logging to the terminal"), full: bool = typer.Option(False, help="Print all fields from CRUD commands"), raw: bool = typer.Option( False, help="Print output from CRUD commands as raw json"), version: bool = typer.Option( False, help="Print the version of jobbergate-cli and exit"), ): """ Welcome to the Jobbergate CLI! More information can be shown for each command listed below by running it with the --help option. """ if version: typer.echo(importlib_metadata.version("jobbergate-cli")) raise typer.Exit() if ctx.invoked_subcommand is None: terminal_message( conjoin( "No command provided. Please check the [bold magenta]usage[/bold magenta] and add a command", "", f"[yellow]{ctx.get_help()}[/yellow]", ), subject="Need a jobbergate command", ) raise typer.Exit() init_logs(verbose=verbose) init_sentry() persona = None client = httpx.Client( base_url=f"https://{settings.AUTH0_LOGIN_DOMAIN}", headers={"content-type": "application/x-www-form-urlencoded"}, ) context = JobbergateContext(persona=None, client=client) if ctx.invoked_subcommand not in ("login", "logout"): persona = init_persona(context) context.client = httpx.Client( base_url=settings.JOBBERGATE_API_ENDPOINT, headers=dict( Authorization=f"Bearer {persona.token_set.access_token}"), ) context.persona = persona context.full_output = full context.raw_output = raw ctx.obj = context
def build_settings(*args, **kwargs): """ Return a Setting object and handle ValidationError with a message to the user. """ try: return Settings(*args, **kwargs) except ValidationError: terminal_message( conjoin( "A configuration error was detected.", "", f"[yellow]Please contact [bold]{OV_CONTACT}[/bold] for support and trouble-shooting[/yellow]", ), subject="Configuration Error", ) exit(1)
def update( ctx: typer.Context, id: int = typer.Option( ..., help=f"The specific id of the application to update. {ID_NOTE}", ), application_path: Optional[pathlib.Path] = typer.Option( None, help="The path to the directory where the application files are located", ), identifier: Optional[str] = typer.Option( None, help="Optional new application identifier to be set", ), application_desc: Optional[str] = typer.Option( None, help="Optional new application description to be set", ), ): """ Update an existing application. """ req_data = dict() if identifier: req_data["application_identifier"] = identifier if application_desc: req_data["application_description"] = application_desc if application_path is not None: validate_application_files(application_path) req_data["application_config"] = dump_full_config(application_path) req_data["application_file"] = read_application_module(application_path) jg_ctx: JobbergateContext = ctx.obj # Make static type checkers happy assert jg_ctx.client is not None result = cast( Dict[str, Any], make_request( jg_ctx.client, f"/applications/{id}", "PUT", expected_status=200, abort_message="Request to update application was not accepted by the API", support=True, json=req_data, ), ) if application_path is not None: successful_upload = _upload_application(jg_ctx, application_path, id) if not successful_upload: terminal_message( f""" The zipped application files could not be uploaded. [yellow]If the problem persists, please contact [bold]{OV_CONTACT}[/bold] for support and trouble-shooting[/yellow] """, subject="File upload failed", color="yellow", ) else: result["application_uploaded"] = True render_single_result( jg_ctx, result, hidden_fields=HIDDEN_FIELDS, title="Updated Application", )
def create( ctx: typer.Context, name: str = typer.Option( ..., help="The name of the application to create", ), identifier: Optional[str] = typer.Option( None, help=f"The human-friendly identifier of the application. {IDENTIFIER_NOTE}", ), application_path: pathlib.Path = typer.Option( ..., help="The path to the directory where the application files are located", ), application_desc: Optional[str] = typer.Option( None, help="A helpful description of the application", ), ): """ Create a new application. """ req_data = load_default_config() req_data["application_name"] = name if identifier: req_data["application_identifier"] = identifier if application_desc: req_data["application_description"] = application_desc validate_application_files(application_path) req_data["application_config"] = dump_full_config(application_path) req_data["application_file"] = read_application_module(application_path) jg_ctx: JobbergateContext = ctx.obj # Make static type checkers happy assert jg_ctx.client is not None result = cast( Dict[str, Any], make_request( jg_ctx.client, "/applications", "POST", expected_status=201, abort_message="Request to create application was not accepted by the API", support=True, json=req_data, ), ) application_id = result["id"] successful_upload = upload_application(jg_ctx, application_path, application_id) if not successful_upload: terminal_message( f""" The zipped application files could not be uploaded. Try running the `update` command including the application path to re-upload. [yellow]If the problem persists, please contact [bold]{OV_CONTACT}[/bold] for support and trouble-shooting[/yellow] """, subject="File upload failed", color="yellow", ) else: result["application_uploaded"] = True render_single_result( jg_ctx, result, hidden_fields=HIDDEN_FIELDS, title="Created Application", )
def fetch_auth_tokens(ctx: JobbergateContext) -> TokenSet: """ Fetch an access token (and possibly a refresh token) from Auth0. Prints out a URL for the user to use to authenticate and polls the token endpoint to fetch it when the browser-based process finishes """ # Make static type-checkers happy assert ctx.client is not None device_code_data = cast( DeviceCodeData, make_request( ctx.client, "/oauth/device/code", "POST", expected_status=200, abort_message="There was a problem retrieving a device verification code from the auth provider", abort_subject="COULD NOT RETRIEVE TOKEN", support=True, response_model_cls=DeviceCodeData, data=dict( client_id=settings.AUTH0_CLIENT_ID, audience=settings.AUTH0_AUDIENCE, scope="offline_access", # To get refresh token ), ), ) terminal_message( f""" To complete login, please open the following link in a browser: {device_code_data.verification_uri_complete} Waiting up to {settings.AUTH0_MAX_POLL_TIME / 60} minutes for you to complete the process... """, subject="Waiting for login", ) for tick in TimeLoop( settings.AUTH0_MAX_POLL_TIME, message="Waiting for web login", ): response_data = cast( Dict, make_request( ctx.client, "/oauth/token", "POST", abort_message="There was a problem retrieving a device verification code from the auth provider", abort_subject="COULD NOT FETCH ACCESS TOKEN", support=True, data=dict( grant_type="urn:ietf:params:oauth:grant-type:device_code", device_code=device_code_data.device_code, client_id=settings.AUTH0_CLIENT_ID, ), ), ) if "error" in response_data: if response_data["error"] == "authorization_pending": logger.debug(f"Token fetch attempt #{tick.counter} failed") sleep(device_code_data.interval) else: # TODO: Test this failure condition raise Abort( unwrap( """ There was a problem retrieving a device verification code from the auth provider: Unexpected failure retrieving access token. """ ), subject="Unexpected error", support=True, log_message=f"Unexpected error response: {response_data}", ) else: return TokenSet(**response_data) raise Abort( "Login process was not completed in time. Please try again.", subject="Timed out", log_message="Timed out while waiting for user to complete login", )
def show_token( plain: bool = typer.Option( False, help="Show the token in plain text.", ), refresh: bool = typer.Option( False, help="Show the refresh token instead of the access token.", ), show_prefix: bool = typer.Option( False, "--prefix", help="Include the 'Bearer' prefix in the output.", ), show_header: bool = typer.Option( False, "--header", help="Show the token as it would appear in a request header.", ), decode: bool = typer.Option( False, "--decode", help="Show the content of the decoded access token.", ), ): """ Show the token for the logged in user. Token output is automatically copied to your clipboard. """ token_set: TokenSet = load_tokens_from_cache() token: Optional[str] if not refresh: token = token_set.access_token subject = "Access Token" Abort.require_condition( token is not None, "User is not logged in. Please log in first.", raise_kwargs=dict(subject="Not logged in", ), ) else: token = token_set.refresh_token subject = "Refresh Token" Abort.require_condition( token is not None, "User is not logged in or does not have a refresh token. Please try loggin in again.", raise_kwargs=dict(subject="No refresh token", ), ) if decode: # Decode the token with ALL verification turned off (we just want to unpack it) content = jose.jwt.decode( token, "secret-will-be-ignored", options=dict( verify_signature=False, verify_aud=False, verify_iat=False, verify_exp=False, verify_nbf=False, verify_iss=False, verify_sub=False, verify_jti=False, verify_at_hash=False, ), ) render_json(content) return if show_header: token_text = f"""{{ "Authorization": "Bearer {token}" }}""" else: prefix = "Bearer " if show_prefix else "" token_text = f"{prefix}{token}" on_clipboard = copy_to_clipboard(token_text) if plain: print(token_text) else: kwargs = dict(subject=subject, indent=False) if on_clipboard: kwargs["footer"] = "The output was copied to your clipboard" terminal_message(token_text, **kwargs)