diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c7ebf66..3cceb41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,9 +11,8 @@ on: - 'README.md' jobs: - test: + ci-shell-validation: runs-on: ubuntu-latest - steps: - name: Checkout code uses: actions/checkout@v4 @@ -39,12 +38,4 @@ jobs: for script in $(find . -name '*.zsh' -o -name '*.sh'); do echo "Linting $script with shellcheck..." shellcheck --shell=bash "$script" - done - - # Step 5: Test Execution of Zsh Scripts - - name: Test Execution of Zsh Scripts - run: | - for script in $(find . -name '*.zsh' -o -name '*.sh'); do - echo "Testing script execution: $script" - zsh "$script" || exit 1 done \ No newline at end of file diff --git a/.zshrc_EXAMPLE.sh b/.zshrc_EXAMPLE.sh index 7ca89d4..7005f37 100644 --- a/.zshrc_EXAMPLE.sh +++ b/.zshrc_EXAMPLE.sh @@ -1,5 +1,11 @@ +#!/bin/zsh +# shellcheck shell=bash disable=SC1071 # Prompt configuration for OCI integration +# The plugins variable is used by oh-my-zsh to load plugins +# shellcheck disable=SC2034 plugins=(emoji) +# The emoji array is provided by the emoji plugin +# shellcheck disable=SC2154 # Function to set the OCI prompt based on current profile, tenancy, and compartment function set_oci_prompt() { @@ -10,13 +16,14 @@ function set_oci_prompt() { return fi - local session_status_file="$HOME/.oci/sessions/$OCI_CLI_PROFILE/session_status" + local session_status_file="$HOME/.oci/sessions/${OCI_CLI_PROFILE}/session_status" # Check if the session status file exists - if [[ -f $session_status_file ]] + if [[ -f "$session_status_file" ]] then # OCI profile and session status file exists, so get session status - local oci_session_status=$(cat ${session_status_file}) + local oci_session_status + oci_session_status=$(cat "${session_status_file}") if [[ "${oci_session_status}" == "valid" ]] then @@ -59,7 +66,8 @@ precmd() { # oshell initialization - update this path to match your installation # Replace /path/to/oshell with the actual path where you installed oshell export OSHELL_HOME=/path/to/oshell -source $OSHELL_HOME/oshell.sh +# shellcheck disable=SC1091 +source "$OSHELL_HOME/oshell.sh" # Available oshell commands: # ociauth - Authenticate to OCI with the specified profile diff --git a/README.md b/README.md index d8bdc76..ebc9f8a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # oshell [![CI](https://github.com/cnopslabs/oshell/actions/workflows/ci.yml/badge.svg)](https://github.com/cnopslabs/oshell/actions/workflows/ci.yml) -Helper shell utilities for OCI CLI and [oshiv](https://github.com/cnopslabs/oshiv). This tool simplifies working with multiple OCI tenancies, compartments, and profiles. +Independent shell utilities for OCI CLI that simplifies working with multiple OCI tenancies, compartments, and profiles. Can be used as a companion to [oshiv](https://github.com/cnopslabs/oshiv). + +![oshell setup demonstration](assets/oshell-setup.gif) --- @@ -10,7 +12,6 @@ Helper shell utilities for OCI CLI and [oshiv](https://github.com/cnopslabs/oshi - [Features](#features) - [Prerequisites](#prerequisites) - [OCI CLI](#oci-cli) - - [oshiv](#oshiv) - [Installation](#installation) - [1. Clone this repository](#1-clone-this-repository) - [2. Configure Tenancy Map (Recommended)](#2-configure-tenancy-map-recommended) @@ -18,6 +19,19 @@ Helper shell utilities for OCI CLI and [oshiv](https://github.com/cnopslabs/oshi - [Usage](#usage) - [Commands List](#commands-list) - [Authenticate to OCI](#authenticate-to-oci) + - [Set Tenancy and Compartment](#set-tenancy-and-compartment) + - [Switch Between Profiles](#switch-between-profiles) + - [Check Session Status](#check-session-status) + - [List Available Profiles](#list-available-profiles) + - [Manage Environment Variables](#manage-environment-variables) + - [Log Out](#log-out) +- [Using with oshiv (Optional)](#using-with-oshiv-optional) +- [Shell Integration](#shell-integration) +- [Authentication Lifecycle Management](#authentication-lifecycle-management) + - [Authentication Process](#authentication-process) + - [Session Maintenance](#session-maintenance) + - [Terminating Sessions](#terminating-sessions) + - [Logs and Monitoring](#logs-and-monitoring) - [Troubleshooting and Setup Fix](#troubleshooting-and-setup-fix) --- @@ -38,11 +52,6 @@ The Oracle Cloud Infrastructure Command Line Interface (OCI CLI) is required. [Install OCI CLI](https://docs.oracle.com/en-us/iaas/Content/API/SDKDocs/cliinstall.htm) -### oshiv - -oshiv is a companion tool that provides simplified OCI resource management. - -[Install oshiv](https://github.com/cnopslabs/oshiv#install-oshiv) ## Installation @@ -54,7 +63,7 @@ git clone https://github.com/cnopslabs/oshell ### 2. Configure Tenancy Map (Recommended) -Create an OCI tenancy mappings file. The tenancy map allows `oshiv` to quickly print the tenant and compartment details you use most often. +Create an OCI tenancy mappings file. The tenancy map allows oshell to quickly print the tenant and compartment details you use most often. ```bash cp tenancy-map.yaml $HOME/.oci @@ -113,7 +122,7 @@ Update your ZSH initialization file (`$HOME/.zshrc`) with: ```bash # Set the path to your oshell installation export OSHELL_HOME=/path/to/oshell -source $OSHELL_HOME/oshell.sh +source "$OSHELL_HOME/oshell.sh" ```
@@ -123,7 +132,7 @@ source $OSHELL_HOME/oshell.sh # oshell configuration # Replace /path/to/oshell with the actual path where you installed oshell export OSHELL_HOME=/path/to/oshell -source $OSHELL_HOME/oshell.sh +source "$OSHELL_HOME/oshell.sh" ``` For shell prompt integration, see the included `.zshrc_EXAMPLE.sh` file. @@ -136,7 +145,7 @@ oshell provides several commands to manage your OCI authentication and environme | Command | Alias | Description | |---------|-------|-------------| | `oci_authenticate [profile]` | `ociauth` | Authenticate to OCI with the specified profile | -| `oci_auth_logout` | `ociexit` | Log out of the current OCI session | +| `oci_auth_logout [profile]` | `ociexit [profile]` | Log out of the current OCI session or terminate a specific profile's background refresher | | `oci_set_profile ` | `ociset` | Set the current OCI profile | | `oci_set_tenancy [compartment]` | `ocisettenancy` | Set the current tenancy and optional compartment | | `oci_env_print` | `ocienv` | Display OCI environment variables | @@ -277,13 +286,18 @@ ociclear ### Log Out ```bash -# Terminate the current session +# Terminate the current active profile ociexit + +# Terminate a specific profile (even if it's not the active one) +ociexit PROFILE_NAME ``` -## Using with oshiv +For more details on session termination and background refresher management, see the [Terminating Sessions](#terminating-sessions) section. -After setting up your tenancy and compartment with oshell, you can use oshiv to manage OCI resources: +## Using with oshiv (Optional) + +While oshell works independently, it can also be used as a companion to oshiv. If you have oshiv installed, after setting up your tenancy and compartment with oshell, you can use oshiv to manage OCI resources: ```bash # List instances matching "home" in their name @@ -327,9 +341,73 @@ When properly configured, your prompt will show: --- -## How It Works +## Authentication Lifecycle Management + +oshell provides a complete lifecycle management for OCI authentication: + +### Authentication Process + +1. **Authentication Initiation**: When you run `ociauth [profile]`, oshell authenticates with OCI using the specified profile (or DEFAULT if none is provided). + +2. **Background Refresher**: After successful authentication, oshell automatically starts a background process (`oci_auth_refresher.sh`) that keeps your session alive by refreshing it before it expires. + +3. **Multiple Profiles**: You can authenticate with multiple profiles simultaneously. Each profile gets its own background refresher process. + +### Session Maintenance + +- The background refresher continuously monitors your session's expiration time. +- It automatically refreshes the session shortly before it expires (default: 60 seconds). +- This happens silently in the background, allowing you to work without interruption. +- You can check the status of your session with `ocistat`. + +### Terminating Sessions + +The `ociexit` command has been enhanced to provide better control over session termination: + +```bash +# Terminate the current active profile +ociexit + +# Terminate a specific profile (even if it's not the active one) +ociexit PROFILE_NAME +``` + +When you run `ociexit`: + +1. It terminates the background refresher process for the specified profile. +2. If terminating the current active profile, it also: + - Attempts to terminate the OCI session using the `oci session terminate` command + - Clears the OCI_CLI_PROFILE environment variable + +Note: The command has been improved to handle various edge cases gracefully: +- If no background refresher is found for the profile, it will display a clear message indicating no active refresher was found +- If a background refresher is terminated but session termination fails, it will still report success for the primary operation +- When no active session exists, the command provides helpful guidance instead of misleading error messages +- Session termination errors are logged but only displayed to the user when relevant to the operation + +This allows you to: +- Log out completely from your current profile +- Terminate background refreshers for other profiles without switching to them +- Manage multiple authentication sessions efficiently + +### Logs and Monitoring + +- Each profile's refresher logs are stored at: `$HOME/.oci/sessions/PROFILE_NAME/oci-auth-refresher_PROFILE_NAME.log` +- You can check if a refresher is running with: `pgrep -af oci_auth_refresher.sh` +- The session status is stored at: `$HOME/.oci/sessions/PROFILE_NAME/session_status` + +#### Log File Contents + +The log file contains detailed information about the authentication refresher's activities: + +- Session validation attempts and results +- Refresh operations and their outcomes +- Session expiration timestamps and remaining time calculations +- Error messages and troubleshooting information + +All profiles (DEFAULT and custom profiles) now have consistent logging behavior, making it easier to troubleshoot issues across different profiles. Each profile's log follows the same format and includes the same level of detail. -oshell includes an authentication refresher that runs in the background to keep your OCI sessions active. The refresher automatically refreshes your session before it expires, so you don't have to re-authenticate manually. +When troubleshooting issues with authentication or session management, checking these logs is often the first step to understanding what's happening behind the scenes. --- @@ -357,7 +435,7 @@ The `OSHELL_HOME` environment variable must point to the directory containing `o 1. Check the value of `$OSHELL_HOME`: ```bash - echo $OSHELL_HOME + echo "$OSHELL_HOME" ``` 2. If it's not set or is incorrect, set it to the directory where `oshell` is installed. For example: @@ -376,13 +454,13 @@ The `OSHELL_HOME` environment variable must point to the directory containing `o Verify that `oci_auth_refresher.sh` exists in the `$OSHELL_HOME` directory: ```bash -ls -l ${OSHELL_HOME}/oci_auth_refresher.sh +ls -l "${OSHELL_HOME}/oci_auth_refresher.sh" ``` - If the file is missing, download or pull the latest version of this repository. - If the file is present but not executable, make sure it has the correct permissions: ```bash - chmod +x ${OSHELL_HOME}/oci_auth_refresher.sh + chmod +x "${OSHELL_HOME}/oci_auth_refresher.sh" ``` --- @@ -419,7 +497,7 @@ If no results are shown, try the script troubleshooting steps again. If the refresher fails to start or exits prematurely, review the log file for details: ```bash -cat ~/Library/Logs/oci-auth-refresher_.log +cat ${HOME}/.oci/sessions//oci-auth-refresher_.log ``` Replace `` with the appropriate profile (e.g., `DEFAULT`). @@ -441,14 +519,14 @@ If the OCI CLI is not installed, follow the [installation guide](https://docs.or 1. Verify the `$OSHELL_HOME` environment variable: ```bash - echo $OSHELL_HOME + echo "$OSHELL_HOME" export OSHELL_HOME=/path/to/oshell ``` 2. Ensure `oci_auth_refresher.sh` exists and is executable: ```bash - ls -l ${OSHELL_HOME}/oci_auth_refresher.sh - chmod +x ${OSHELL_HOME}/oci_auth_refresher.sh + ls -l "${OSHELL_HOME}/oci_auth_refresher.sh" + chmod +x "${OSHELL_HOME}/oci_auth_refresher.sh" ``` 3. Authenticate using `ociauth`: @@ -463,7 +541,7 @@ If the OCI CLI is not installed, follow the [installation guide](https://docs.or 5. Review logs for more details: ```bash - cat ~/Library/Logs/oci-auth-refresher_DEFAULT.log + cat ${HOME}/.oci/sessions/DEFAULT/oci-auth-refresher_DEFAULT.log ``` By following these steps, most common issues with the `oci_auth_refresher.sh` process should be resolved. diff --git a/assets/oshell-setup.gif b/assets/oshell-setup.gif new file mode 100644 index 0000000..efae354 Binary files /dev/null and b/assets/oshell-setup.gif differ diff --git a/oci_auth_refresher.sh b/oci_auth_refresher.sh index 3087668..98c1178 100755 --- a/oci_auth_refresher.sh +++ b/oci_auth_refresher.sh @@ -1,123 +1,240 @@ #!/bin/zsh +# shellcheck shell=bash disable=SC1071 -# Version: 0.1.0 -# OCI Authentication Refresher -# This script keeps an OCI session active by refreshing it before it expires +# ─────────────────────────────────────────────────────────── +# oci_auth_refresher.sh • v0.1.1 +# +# Keeps an OCI CLI session alive by refreshing it shortly +# before it expires. Intended to be launched (nohup) from the +# wrapper script oshell.sh. +# ─────────────────────────────────────────────────────────── # Check if profile argument is provided, use DEFAULT if not -if [[ -z "$1" ]] -then +if [[ -z "$1" ]]; then echo "No profile name provided, using DEFAULT" - OCI_PROFILE="DEFAULT" + OCI_CLI_PROFILE="DEFAULT" else - OCI_PROFILE=$1 + OCI_CLI_PROFILE=$1 fi -# Configuration -PREEMPT_REFRESH_TIME=60 # Attempt to refresh 60 sec before session expiration -LOG_LOCATION="${HOME}/Library/Logs/oci-auth-refresher_${OCI_PROFILE}.log" -SESSION_STATUS_FILE="${HOME}/.oci/sessions/${OCI_PROFILE}/session_status" +# Check if script is being run directly (not through nohup) +# If so, relaunch itself using nohup and exit +if [[ -z "$NOHUP" && -t 1 ]]; then + echo "Launching OCI auth refresher in background for profile ${OCI_CLI_PROFILE}" + export NOHUP=1 + # Use full path to script to ensure it's detectable by pgrep + script_path=$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/$(basename "$0") + nohup "$script_path" "$OCI_CLI_PROFILE" > /dev/null 2>&1 < /dev/null & + pid=$! + echo "Process started with PID $pid" + echo "You can verify it's running with: pgrep -af oci_auth_refresher.sh" + echo "Check logs at: ${HOME}/.oci/sessions/${OCI_CLI_PROFILE}/oci-auth-refresher_${OCI_CLI_PROFILE}.log" + exit 0 +fi + +# Source common functions from oshell.sh if OSHELL_HOME is set +if [[ -n "$OSHELL_HOME" && -f "$OSHELL_HOME/oshell.sh" ]]; then + # Source the configuration and helper functions + # shellcheck disable=SC1090,SC1091 + source "${OSHELL_HOME}/oshell.sh" + + # Set paths using the sourced function + set_profile_paths +else + # Fallback to local definitions if oshell.sh can't be sourced + # Configuration + PREEMPT_REFRESH_TIME=60 # Attempt to refresh 60 sec before session expiration + LOG_LOCATION="${HOME}/.oci/sessions/${OCI_CLI_PROFILE}/oci-auth-refresher_${OCI_CLI_PROFILE}.log" + SESSION_STATUS_FILE="${HOME}/.oci/sessions/${OCI_CLI_PROFILE}/session_status" + + # Create session directory if it doesn't exist + mkdir -p "${HOME}/.oci/sessions/${OCI_CLI_PROFILE}" + + # Helper function to log messages + function log_message() { + local message=$1 + + # Check if LOG_LOCATION is set and create directory if needed + if [[ -n "$LOG_LOCATION" ]]; then + local log_dir + log_dir=$(dirname "$LOG_LOCATION") + + # Create directory if it doesn't exist + if [[ ! -d "$log_dir" ]]; then + mkdir -p "$log_dir" + fi + + echo "$(date '+%F %T'): $message" >> "$LOG_LOCATION" 2>&1 < /dev/null + fi + } +fi + +# Helper function to convert date string to epoch time +function to_epoch() { + local ts="$1" + + # Check if timestamp is empty + if [[ -z "$ts" ]]; then + log_message "Warning: Empty timestamp provided to to_epoch()" + return 1 + fi -# Helper function to log messages -function log_message() { - local message=$1 - echo "$(date): $message" >> $LOG_LOCATION 2>&1 < /dev/null + # Log the timestamp we're trying to convert for debugging + log_message "Converting timestamp: '${ts}' to epoch" + + if date --version >/dev/null 2>&1; then + # GNU date (Linux) - more forgiving with formats + if ! date -d "${ts}" +%s 2>/dev/null; then + log_message "Error: GNU date failed to parse timestamp '${ts}'" + return 1 + fi + else + # BSD date (macOS) - needs explicit format + # Try different format patterns that might match the timestamp + for fmt in "%Y-%m-%d %H:%M:%S" "%Y-%m-%d %T" "%Y-%m-%dT%H:%M:%S" "%Y-%m-%d"; do + if date -j -f "$fmt" "${ts}" +%s 2>/dev/null; then + return 0 + fi + done + + # If we get here, all format attempts failed + log_message "Error: Failed to parse timestamp '${ts}' with any known format" + return 1 + fi } # Function to get the remaining duration of the current session function get_remaining_session_duration() { - local profile=$1 - - log_message "Checking if session is valid for profile ${profile}" - oci session validate --profile $profile --local >> $LOG_LOCATION 2>&1 < /dev/null - local validate_result=$? + log_message "Validating session for profile ${OCI_CLI_PROFILE}" - if [[ $validate_result -eq 0 ]] - then + if oci session validate --profile "$OCI_CLI_PROFILE" --local >> "$LOG_LOCATION" 2>&1; then log_message "Session is valid" oci_session_status="valid" - echo $oci_session_status > $SESSION_STATUS_FILE + echo "$oci_session_status" > "$SESSION_STATUS_FILE" - log_message "Determining remaining session duration" - local session_expiration_date_time=$(oci session validate --profile $profile --local 2>&1 | awk '{print $5, $6}') - log_message "Session expiration date/time: ${session_expiration_date_time}" + # Get expiration timestamp + local exp_ts + local validate_output - # Convert expiration time to epoch seconds - local session_expiration_date_time_epoch=$(date -j -f "%Y-%m-%d %H:%M:%S" "${session_expiration_date_time}" +%s) - local current_epoch=$(date '+%s') - remaining_time=$(($session_expiration_date_time_epoch-$current_epoch)) - local remaining_time_min=$(($remaining_time/60)) + # Capture both stdout and stderr + validate_output=$(oci session validate --profile "$OCI_CLI_PROFILE" --local 2>&1) + log_message "Session validate output: '${validate_output}'" - log_message "Remaining time: ${remaining_time_min} minutes (${remaining_time} seconds)" + # Extract the expiration timestamp using a simple approach + # The output format is "Session is valid until YYYY-MM-DD HH:MM:SS" + log_message "Raw validate output: '$validate_output'" + + # Use a simple approach to extract the date and time + exp_ts=$(echo "$validate_output" | sed -E 's/.*until ([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}).*/\1/') + + # If the output is unchanged, it means the pattern didn't match + if [[ "$exp_ts" == "$validate_output" ]]; then + log_message "Warning: Could not extract expiration timestamp using sed" + exp_ts="" + fi + + # If still empty, try to extract just the date and time parts + if [[ -z "$exp_ts" ]]; then + log_message "Trying to extract date and time separately..." + local date_part + date_part=$(echo "$validate_output" | grep -o "[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}") + local time_part + time_part=$(echo "$validate_output" | grep -o "[0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}") + + if [[ -n "$date_part" && -n "$time_part" ]]; then + exp_ts="$date_part $time_part" + log_message "Extracted date ($date_part) and time ($time_part) separately" + else + log_message "Failed to extract date and time separately" + fi + fi + + log_message "Session expiration timestamp: ${exp_ts}" + + # Verify that we have a valid-looking timestamp before proceeding + if [[ -z "$exp_ts" || ! "$exp_ts" =~ [0-9]{4}-[0-9]{2}-[0-9]{2} ]]; then + log_message "Error: Invalid or missing expiration timestamp format" + log_message "Raw output was: ${validate_output}" + oci_session_status="expired" + echo "$oci_session_status" > "$SESSION_STATUS_FILE" + remaining_time=0 + return + fi + + # Calculate remaining time + local exp_epoch + if ! exp_epoch=$(to_epoch "${exp_ts}"); then + log_message "Failed to convert expiration timestamp to epoch time" + oci_session_status="expired" + echo "$oci_session_status" > "$SESSION_STATUS_FILE" + remaining_time=0 + return + fi + + local now_epoch + now_epoch=$(date +%s) + remaining_time=$((exp_epoch - now_epoch)) + + log_message "Remaining time: $((remaining_time/60)) min (${remaining_time}s)" else log_message "Session is expired" oci_session_status="expired" - echo $oci_session_status > $SESSION_STATUS_FILE + echo "$oci_session_status" > "$SESSION_STATUS_FILE" + remaining_time=0 fi } # Function to refresh the session function refresh_session() { - local profile=$1 + log_message "Refreshing session for ${OCI_CLI_PROFILE}" - log_message "Attempting to refresh session for profile ${profile}" - oci session refresh --profile $profile >> $LOG_LOCATION 2>&1 < /dev/null - local refresh_result=$? - - if [[ $refresh_result -eq 0 ]] - then + if oci session refresh --profile "$OCI_CLI_PROFILE" >> "$LOG_LOCATION" 2>&1; then log_message "Refresh successful" return 0 else - log_message "Refresh failed, exiting..." + log_message "Refresh failed" oci_session_status="expired" - echo $oci_session_status > $SESSION_STATUS_FILE + echo "$oci_session_status" > "$SESSION_STATUS_FILE" return 1 fi } +# Initialize variables +oci_session_status="unknown" +remaining_time=0 + # Initialize log file -log_message "---" -log_message "Initiating OCI session refresher for profile ${OCI_PROFILE}" +log_message "" +log_message "───── OCI auth refresher started for profile ${OCI_CLI_PROFILE} ─────" # Check if session directory exists -if [[ ! -d "${HOME}/.oci/sessions/${OCI_PROFILE}" ]] -then - log_message "Error: Session directory for profile ${OCI_PROFILE} does not exist" - log_message "You may need to authenticate first with: oci session authenticate --profile-name ${OCI_PROFILE}" +if [[ ! -d "${HOME}/.oci/sessions/${OCI_CLI_PROFILE}" ]]; then + log_message "Missing session directory; user probably hasn't authenticated" + log_message "Exiting." exit 1 fi # Main loop -get_remaining_session_duration $OCI_PROFILE - -while [[ $oci_session_status != "expired" ]] -do - if [[ $remaining_time -gt $PREEMPT_REFRESH_TIME ]] - then - # Calculate sleep time (refresh before expiration) - sleep_time=$(($remaining_time-$PREEMPT_REFRESH_TIME)) - sleep_time_min=$(($sleep_time/60)) - log_message "Sleeping for ${sleep_time_min} minutes (${sleep_time} seconds)" - sleep $sleep_time - - # Refresh the session - refresh_session $OCI_PROFILE - if [[ $? -ne 0 ]] - then +get_remaining_session_duration + +while [[ "$oci_session_status" == "valid" ]]; do + if (( remaining_time > PREEMPT_REFRESH_TIME )); then + sleep_for=$((remaining_time - PREEMPT_REFRESH_TIME)) + log_message "Sleeping ${sleep_for}s before next refresh" + sleep "$sleep_for" + + if ! refresh_session; then log_message "Exiting due to refresh failure" exit 1 fi - # Check the new session duration - get_remaining_session_duration $OCI_PROFILE + get_remaining_session_duration else - log_message "Remaining time too short to continue the refresh process, exiting" + log_message "Session too close to expiry; letting it lapse" oci_session_status="expired" - echo $oci_session_status > $SESSION_STATUS_FILE - exit 0 + echo "$oci_session_status" > "$SESSION_STATUS_FILE" fi done -log_message "Session expired, exiting" +log_message "Session expired – refresher exiting" exit 0 diff --git a/oshell.sh b/oshell.sh index 58305af..37267d1 100755 --- a/oshell.sh +++ b/oshell.sh @@ -1,20 +1,52 @@ #!/bin/zsh +# shellcheck shell=bash disable=SC1071 -# Version: 0.1.0 +# Version: 0.1.1 # Color definitions for terminal output CYAN='\033[0;96m' YELLOW='\033[;33m' -UNSET_FMT=$(tput sgr0) RED='\033[0;31m' -# Log file for the OCI authentication refresher -REFRESHER_LOG_FILE="${HOME}/Library/Logs/oci-auth-refresher_${OCI_CLI_PROFILE}.log" +if [[ -n "$TERM" ]]; then + UNSET_FMT=$(tput sgr0) +else + UNSET_FMT='\033[0m' +fi + +# Ensure OSHELL_HOME is exported so oci_auth_refresher.sh can source common functions +export OSHELL_HOME="${OSHELL_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)}" + +# Configuration +export PREEMPT_REFRESH_TIME=60 # Attempt to refresh 60 sec before session expiration + +# Path variables (will be updated dynamically once OCI_CLI_PROFILE is set) +LOG_LOCATION="" +SESSION_STATUS_FILE="" + +# Set dynamic session paths after profile is set +function set_profile_paths() { + LOG_LOCATION="${HOME}/.oci/sessions/${OCI_CLI_PROFILE}/oci-auth-refresher_${OCI_CLI_PROFILE}.log" + SESSION_STATUS_FILE="${HOME}/.oci/sessions/${OCI_CLI_PROFILE}/session_status" + mkdir -p "${HOME}/.oci/sessions/${OCI_CLI_PROFILE}" +} # Helper function to log messages function log_message() { local message=$1 - echo "$(date): $message" >> $REFRESHER_LOG_FILE 2>&1 < /dev/null + + # Check if LOG_LOCATION is set and create directory if needed + if [[ -n "$LOG_LOCATION" ]]; then + local log_dir + log_dir=$(dirname "$LOG_LOCATION") + + # Create directory if it doesn't exist + if [[ ! -d "$log_dir" ]]; then + mkdir -p "$log_dir" + fi + + echo "$(date '+%F %T'): $message" >> "$LOG_LOCATION" 2>&1 < /dev/null + fi } # Helper function to find OCI auth refresher process for a specific profile @@ -22,19 +54,16 @@ function find_oci_auth_refresher_process() { local profile=$1 local found_pid="" - for r_pid in $(pgrep -f oci_auth_refresher.sh) - do + for r_pid in $(pgrep -f oci_auth_refresher.sh); do r_oci_profile=$(ps -p "$r_pid" -o command | grep -v COMMAND | awk '{print $3}') - if [[ $LOGLEVEL == "DEBUG" ]]; then - log_message "Existing refresher process: PID=$r_pid, PROFILE=$r_oci_profile" - fi + [[ $LOGLEVEL == "DEBUG" ]] && log_message "Existing refresher process: PID=$r_pid, PROFILE=$r_oci_profile" if [[ "$profile" == "$r_oci_profile" ]]; then found_pid=$r_pid break fi done - echo $found_pid + echo "$found_pid" } function start_oci_auth_refresher() { @@ -43,26 +72,26 @@ function start_oci_auth_refresher() { if [[ $restart == "true" ]]; then log_message "Existing refresher process found for ${profile}, restarting..." - kill -9 $(find_oci_auth_refresher_process $profile) - echo "expired" > ${HOME}/.oci/sessions/$profile/session_status + kill -9 "$(find_oci_auth_refresher_process "$profile")" + echo "expired" > "$SESSION_STATUS_FILE" else log_message "Starting new refresher process for ${profile}" fi - nohup "${OSHELL_HOME}/oci_auth_refresher.sh" $profile > /dev/null 2>&1 < /dev/null & + log_message "Oshell home: ${OSHELL_HOME}" + nohup "${OSHELL_HOME}/oci_auth_refresher.sh" "$profile" > /dev/null 2>&1 < /dev/null & sleep 1 } alias ociauth='oci_authenticate' function oci_authenticate() { - oshiv info + oci_tenancy_map "$@" echo "" local profile_name="${1:-DEFAULT}" - echo "Using profile: ${CYAN}${profile_name}${UNSET_FMT}\n" + echo -e "Using profile: ${CYAN}${profile_name}${UNSET_FMT}" - oci session authenticate --profile-name $profile_name - if [[ $? -ne 0 ]]; then + if ! oci session authenticate --profile-name "$profile_name"; then echo "OCI authentication failed" return 1 fi @@ -70,39 +99,79 @@ function oci_authenticate() { unset OCI_CLI_TENANCY OCI_TENANCY_NAME OCI_COMPARTMENT CID OCI_CLI_REGION echo "Setting OCI Profile to ${profile_name}" export OCI_CLI_PROFILE=$profile_name + set_profile_paths log_message "Checking for existing refresher process for $OCI_CLI_PROFILE" - local refresher_pid=$(find_oci_auth_refresher_process $OCI_CLI_PROFILE) + local refresher_pid + refresher_pid=$(find_oci_auth_refresher_process "$OCI_CLI_PROFILE") if [[ -n "$refresher_pid" ]]; then - start_oci_auth_refresher $OCI_CLI_PROFILE "true" + start_oci_auth_refresher "$OCI_CLI_PROFILE" "true" else - start_oci_auth_refresher $OCI_CLI_PROFILE "false" + start_oci_auth_refresher "$OCI_CLI_PROFILE" "false" fi - oshiv info + oci_tenancy_map "$@" } alias ociexit='oci_auth_logout' function oci_auth_logout() { - if [[ -z "${OCI_CLI_PROFILE}" ]]; then + local profile_name="${1:-$OCI_CLI_PROFILE}" + + if [[ -z "${profile_name}" ]]; then echo "No active OCI profile found. Nothing to log out from." return 0 fi - local refresher_pid=$(find_oci_auth_refresher_process $OCI_CLI_PROFILE) + # Temporarily set OCI_CLI_PROFILE to the provided profile for path setting + local original_profile="$OCI_CLI_PROFILE" + export OCI_CLI_PROFILE="$profile_name" + set_profile_paths + + # Check if a refresher process exists and terminate it + local refresher_pid + refresher_pid=$(find_oci_auth_refresher_process "$profile_name") + local refresher_terminated=false + if [[ -n "$refresher_pid" ]]; then - log_message "Killing refresher process for profile ${OCI_CLI_PROFILE}" - kill -9 $refresher_pid - echo "expired" > ${HOME}/.oci/sessions/$OCI_CLI_PROFILE/session_status + echo "Killing refresher process for profile ${CYAN}${profile_name}${UNSET_FMT}" + log_message "Killing refresher process for profile ${profile_name}" + kill -9 "$refresher_pid" + echo "Successfully terminated background refresher for profile: ${CYAN}${profile_name}${UNSET_FMT}" + refresher_terminated=true + else + echo "No background refresher found for profile: ${CYAN}${profile_name}${UNSET_FMT}" fi - oci session terminate - if [[ $? -eq 0 ]]; then - echo "Successfully logged out from OCI profile: ${CYAN}${OCI_CLI_PROFILE}${UNSET_FMT}" + # Always attempt to terminate the session if we're working with the current active profile + if [[ "$profile_name" == "$original_profile" ]]; then + log_message "Attempting to terminate OCI session for profile ${profile_name}" + + # Capture the output of the command in a variable + local terminate_output + terminate_output=$(oci session terminate --profile "$profile_name" 2>&1) + local terminate_status=$? + + # Log the output + log_message "OCI session terminate output: ${terminate_output}" + + if [[ $terminate_status -eq 0 ]]; then + log_message "Successfully terminated OCI session for profile ${profile_name}" + echo "Successfully logged out from OCI profile: ${CYAN}${profile_name}${UNSET_FMT}" + else + log_message "Failed to terminate OCI session for profile ${profile_name} (exit code: ${terminate_status})" + # Only show error message if we didn't terminate a refresher process + if [[ "$refresher_terminated" != "true" ]]; then + echo "Note: No active background refresher was found for this profile." + echo "If you're trying to terminate an OCI session, please ensure you have an active session first." + fi + fi + + # Clear the environment variable after logout attempt + unset OCI_CLI_PROFILE else - echo "Failed to terminate OCI session for profile: ${CYAN}${OCI_CLI_PROFILE}${UNSET_FMT}" - return 1 + # Restore the original profile if we were just killing a background refresher + export OCI_CLI_PROFILE="$original_profile" fi } @@ -126,14 +195,15 @@ function oci_set_tenancy() { fi echo "" - oshiv config + oci_config_print return 0 } alias ocienv='oci_env_print' function oci_env_print() { echo "OCI Environment Variables:" - local oci_vars=$(env | egrep "OCI_|CID") + local oci_vars + oci_vars=$(env | grep -E "OCI_|CID") if [[ -z "$oci_vars" ]]; then echo " No OCI environment variables set" @@ -144,6 +214,44 @@ function oci_env_print() { return 0 } +# Display OCI tenancy map with formatted colors +alias ocimap='oci_tenancy_map' +function oci_tenancy_map() { + local tenancy_map_path="${HOME}/.oci/tenancy-map.yaml" + + if [[ ! -f "$tenancy_map_path" ]]; then + echo "⚠️ Cannot find tenancy map at: $tenancy_map_path" >&2 + return 1 + fi + + if ! command -v yq >/dev/null 2>&1; then + echo -e "⚠️ 'yq' is required to render your tenancy map. To install run:\nbrew install yq" >&2 + return 1 + fi + + # Color definitions + local BLUE='\033[1;34m' + local YELLOW='\033[0;33m' + local RESET='\033[0m' + + # Header row + printf "${BLUE}%-28s %-28s %-7s %-35s %-s${RESET}\n" "ENVIRONMENT" "TENANCY" "REALM" "COMPARTMENTS" "REGIONS" + + # Read each row using yq and print formatted output + yq -r '.[] | [.environment, .tenancy, .realm, .compartments, .regions] | @tsv' "$tenancy_map_path" | \ + while IFS=$'\t' read -r env tenancy realm comp regions; do + printf "${YELLOW}%-28s${RESET} %-28s %-7s %-35s %s\n" "$env" "$tenancy" "$realm" "$comp" "$regions" + done + + # Footer message + printf "\nTo set Tenancy, Compartment, or Region export the \033[0;33mOCI_TENANCY_NAME\033[0m, \033[0;33mOCI_COMPARTMENT\033[0m, or \033[0;33mOCI_CLI_REGION\033[0m environment variables.\n\n" + + printf "Or if using oshell, run:\n" + printf "oci_set_tenancy \033[0;33mTENANCY_NAME\033[0m\n" + printf "oci_set_tenancy \033[0;33mTENANCY_NAME COMPARTMENT_NAME\033[0m\n" +} + + alias ociclear='oci_env_clear' function oci_env_clear() { local profile=$OCI_CLI_PROFILE @@ -167,6 +275,7 @@ function oci_auth_status() { return 0 fi + set_profile_paths echo "Checking session status for profile: ${CYAN}${OCI_CLI_PROFILE}${UNSET_FMT}" oci session validate --local local exit_code=$? @@ -175,7 +284,8 @@ function oci_auth_status() { echo "Session for profile ${CYAN}${OCI_CLI_PROFILE}${UNSET_FMT} is ${RED}invalid${UNSET_FMT}" fi - local refresher_pid=$(find_oci_auth_refresher_process $OCI_CLI_PROFILE) + local refresher_pid + refresher_pid=$(find_oci_auth_refresher_process "$OCI_CLI_PROFILE") if [[ -z "${refresher_pid}" ]]; then echo "No existing OCI auth refresher process found for profile: ${CYAN}$OCI_CLI_PROFILE${UNSET_FMT}" @@ -196,17 +306,17 @@ function oci_set_profile() { local profile_name=$1 + export OCI_CLI_PROFILE=$profile_name + set_profile_paths + if [[ ! -d "${HOME}/.oci/sessions/${profile_name}" ]]; then echo "Warning: Profile ${CYAN}${profile_name}${UNSET_FMT} does not exist or has not been authenticated" echo "You may need to run: ociauth ${profile_name}" fi echo "Setting OCI_CLI_PROFILE to ${CYAN}${profile_name}${UNSET_FMT}" - export OCI_CLI_PROFILE=$profile_name - REFRESHER_LOG_FILE="${HOME}/Library/Logs/oci-auth-refresher_${OCI_CLI_PROFILE}.log" - echo "" - oshiv info + oci_tenancy_map "$@" } alias ocilistprofiles='oci_list_profiles' @@ -222,10 +332,16 @@ function oci_list_profiles() { local profile_count=0 echo "${CYAN}Profiles:${UNSET_FMT}" - for status_file in $(find "$sessions_dir" -name "session_status" 2>/dev/null) - do - local session_status=$(cat "$status_file") - local profile_name=$(echo "$status_file" | awk -F"/" '{print $6}') + local status_files=() + while IFS= read -r line; do + status_files+=("$line") + done < <(find "$sessions_dir" -name "session_status" 2>/dev/null) + + for status_file in "${status_files[@]}"; do + local session_status + session_status=$(cat "$status_file") + local profile_name + profile_name=$(echo "$status_file" | awk -F"/" '{print $6}') profile_count=$((profile_count + 1)) if [[ "$profile_name" == "$OCI_CLI_PROFILE" ]]; then @@ -249,3 +365,50 @@ function oci_list_profiles() { return 0 } +# ---------- Color & helper ---------- +YELLOW='\033[0;33m' +RESET='\033[0m' + +print_kv() { # key-value with yellow value + printf "%-14s: ${YELLOW}%s${RESET}\n" "$1" "$2" +} + +# Resolve default compartment from tenancy-map.yaml +lookup_tenancy_info() { + local tname=$1 field=$2 + [[ -z "$tname" ]] && return # nothing to look up + command -v yq >/dev/null 2>&1 || return + + # Use grep to check if the tenancy exists in the YAML file + if ! grep -q "tenancy: $tname" "$HOME/.oci/tenancy-map.yaml"; then + echo "Warning: Tenancy '$tname' not found in tenancy-map.yaml. Please check your tenancy-map.yaml file." >&2 + return + fi + + # Get the requested field using grep and awk for better reliability + local result + if [[ "$field" == "compartments" ]]; then + # Extract the compartments for the specified tenancy + result=$(grep -A 4 "tenancy: $tname" "$HOME/.oci/tenancy-map.yaml" | grep "compartments" | awk '{$1=""; print $0}' | xargs) + else + # Use yq for other fields + # shellcheck disable=SC2016 # $tn and $f are jq variables, not shell variables + result=$(yq -r --arg tn "$tname" --arg f "$field" \ + 'map(select(.tenancy == $tn))[0][$f] // ""' \ + "$HOME/.oci/tenancy-map.yaml" 2>/dev/null) + fi + + echo "$result" +} + +oci_config_print() { + local tenancy_name=${OCI_TENANCY_NAME:-} + local compartment=${OCI_COMPARTMENT:-} + + # If no compartment is set, try to get it from the tenancy map + [[ -n "$tenancy_name" && -z "$compartment" ]] \ + && compartment=$(lookup_tenancy_info "$tenancy_name" compartments) + + print_kv "Tenancy name" "${tenancy_name:-}" + print_kv "Compartment" "${compartment:-}" +} diff --git a/tenancy-map.yaml b/tenancy-map.yaml index 910fa09..88c573a 100644 --- a/tenancy-map.yaml +++ b/tenancy-map.yaml @@ -32,4 +32,4 @@ tenancy_id: ocid1.tenancy.oc2..abcdefghijklmnopqrstuvwxyz1357924680 realm: OC2 compartments: foo_gov_prod_cp foo_gov_prod_dp - regions: us-ashburn-1 us-phoenix-1 + regions: us-ashburn-1 us-phoenix-1 \ No newline at end of file