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",
    )
Beispiel #2
0
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)