diff --git a/icrn_manager b/icrn_manager index 8da1865..def1071 100755 --- a/icrn_manager +++ b/icrn_manager @@ -48,9 +48,21 @@ fi +# Prompt the user for confirmation before proceeding with a potentially destructive operation. +# +# Parameters: +# $1 - challenge: Optional prompt string to display to the user. If not provided, +# defaults to "Are you sure? [y/N]" +# +# Returns: +# 0 if user confirms (y/yes), exits with status 0 if user declines +# +# Side effects: +# Exits the script with status 0 if user does not confirm confirm() { local challenge=$1; shift # call with a prompt string or use a default + # function much less needed after we have stopped unpacking kernels to the users local dir; retaining in case its needed # https://stackoverflow.com/questions/3231804/in-bash-how-to-add-are-you-sure-y-n-to-any-command-or-alias read -r -p "${challenge:-Are you sure? [y/N]} " response if [[ "$response" =~ ^([yY][eE][sS]|[yY])$ ]] @@ -63,6 +75,25 @@ confirm() { } last_check=-1 +# Check if a kernel entry exists in the specified catalog. +# +# This function searches for a kernel entry in a catalog (either central or user catalog) +# and sets the global variable 'last_check' to indicate success (1) or failure (0). +# +# Parameters: +# $1 - catalog: Path to the JSON catalog file to search +# $2 - language: Language of the kernel (e.g., "R", "Python") +# $3 - targetname: Name of the kernel to search for +# $4 - targetversion: Optional version number to check for +# +# Global variables: +# last_check: Set to 1 if entry found, 0 if not found. Initialized to -1. +# +# Returns: +# No return value. Results are stored in the global 'last_check' variable. +# +# Side effects: +# Prints error messages to stdout if kernel or version is not found check_for_catalog_entry() { local catalog=$1; shift @@ -93,6 +124,21 @@ check_for_catalog_entry() fi } +# Retrieve and display all available versions for a specific kernel package. +# +# This function first verifies that the kernel exists in the catalog, then extracts +# and displays all available version numbers for that kernel. +# +# Parameters: +# $1 - catalog: Path to the JSON catalog file to search +# $2 - language: Language of the kernel (e.g., "R", "Python") +# $3 - targetname: Name of the kernel to get versions for +# +# Returns: +# Exits with status 1 if the kernel is not found in the catalog +# +# Side effects: +# Prints available versions to stdout, or error message if kernel not found get_versions_for_package() { local catalog=$1; shift @@ -112,6 +158,19 @@ get_versions_for_package() echo $available_versions } +# Display a list of all available kernels from the central repository catalog. +# +# This function reads the central kernel catalog and displays all available kernels +# in a tabular format showing language, kernel name, and version for each entry. +# +# Parameters: +# None +# +# Returns: +# No return value +# +# Side effects: +# Prints a tab-separated table of available kernels to stdout function kernels__available() # get a list of available kernels from the central repo { icrn_catalog=${ICRN_KERNEL_CATALOG} @@ -128,11 +187,33 @@ function kernels__available() # get a list of available kernels from the central done done } +# Alias for kernels__available() - display a list of all available kernels. +# +# This is a convenience function that calls kernels__available() with all passed arguments. +# +# Parameters: +# All arguments are passed through to kernels__available() +# +# Returns: +# Same as kernels__available() function kernels__avail() # alias for available { kernels__available "$@" } +# Display a list of kernels that have been checked out and are ready for use. +# +# This function reads the user's local catalog and displays all kernels that have +# been successfully checked out, showing language, kernel name, and version. +# +# Parameters: +# None +# +# Returns: +# No return value +# +# Side effects: +# Prints a tab-separated table of checked-out kernels to stdout function kernels__list() # get the list of kernels already checked out and ready for use { user_catalog=${ICRN_USER_CATALOG} @@ -150,6 +231,25 @@ function kernels__list() # get the list of kernels already checked out and ready done } +# Activate a kernel that has already been checked out for use in Jupyter. +# +# This function activates a previously checked-out kernel, making it available for use +# in Jupyter notebooks. For R kernels, it creates symbolic links and updates .Renviron. +# For Python kernels, it installs the kernel spec using ipykernel. +# The special value "none" can be used to deactivate all kernels for a language. +# +# Parameters: +# $1 - language: Language of the kernel (e.g., "R", "Python") +# $2 - targetname: Name of the kernel to activate, or "none" to deactivate all +# $3 - version: Version number of the kernel (required unless targetname is "none") +# +# Returns: +# Exits with status 1 if parameters are missing or invalid, or if kernel path not found +# +# Side effects: +# - For R: Creates symbolic link and updates .Renviron via update_r_libs.sh +# - For Python: Installs/uninstalls Jupyter kernel specs +# - Removes existing kernel installations before installing new ones function kernels__use() # use a kernel which is already checked out { local language=$1; shift @@ -242,10 +342,24 @@ function kernels__use() # use a kernel which is already checked out esac else absolute_path=$(jq -r ".\"$language\".\"$targetname\".\"$version\".\"absolute_path\"" $user_catalog) + overlay_path=$(jq -r ".\"$language\".\"$targetname\".\"$version\".\"overlay_path\"" $user_catalog) + # absolute path for R is determined by activating the R install and finding the location of its library directory + # then we create a link to it + # user_overlay_location="${ICRN_USER_KERNEL_BASE}/${language_lower}/${targetname}-${version}/" echo "checking for: "$absolute_path case $language in "R") + if [ -z "$overlay_path" ] || [ "$overlay_path" = "null" ]; then + echo "ERROR: Overlay path missing from user catalog for $language $targetname $version." + echo "Run 'icrn_manager kernels get $language $targetname $version' to re-register the overlay." + exit 1 + fi + if [ ! -d "$overlay_path" ]; then + echo "ERROR: Overlay path recorded in user catalog does not exist: $overlay_path" + echo "Consider cleaning and re-checking out this kernel." + exit 1 + fi target_kernel_link_path=${ICRN_USER_KERNEL_BASE}/${targetname} if [ -e "$target_kernel_link_path" ]; then echo "Found existing link; removing..." @@ -255,7 +369,7 @@ function kernels__use() # use a kernel which is already checked out if [ -d "$absolute_path" ]; then echo "Found. Linking and Activating..." ln -s $absolute_path $target_kernel_link_path - ${target_r_libs_script} ${target_r_environ_file} $targetname + ${target_r_libs_script} ${target_r_environ_file} $absolute_path $overlay_path echo "Done." else echo "Path could not be found. There is a problem with your user catalog." @@ -306,68 +420,143 @@ function kernels__use() # use a kernel which is already checked out fi } +# Alias for kernels__use() - activate a kernel that has already been checked out. +# +# This is a convenience function that calls kernels__use() with all passed arguments. +# +# Parameters: +# All arguments are passed through to kernels__use() +# +# Returns: +# Same as kernels__use() function kernels__activate() # alias for use { "kernels__use" "$@" } -function unpack_r_kernel() # unpack and configure an R kernel environment +# Register the overlay directory path for a kernel in the user's catalog. +# +# This function updates the user catalog with the overlay path for a specific kernel. +# The overlay directory is used for storing user-specific customizations and additional +# packages that should be loaded alongside the kernel. +# +# Parameters: +# $1 - language: Language of the kernel (e.g., "R", "Python") +# $2 - targetname: Name of the kernel +# $3 - version: Version number of the kernel +# +# Returns: +# 0 if overlay path was successfully registered, 1 if overlay directory not found +# +# Side effects: +# Updates the user catalog JSON file with the overlay_path field +function register_user_overlay_in_user_catalog() # add the overlay location to the users catalog +{ + local language=$1; shift + local targetname=$1; shift + local version=$1; shift + + language_lower=$(echo "$language" | tr '[:upper:]' '[:lower:]') + user_overlay_location="${ICRN_USER_KERNEL_BASE}/${language_lower}/${targetname}-${version}/" + echo checking for: $user_overlay_location + if [ -e $user_overlay_location ]; then + echo "Found." + echo "Updating user's catalog with $user_overlay_location" + user_catalog_tmp=$(mktemp) + jq -r ".\"$language\".\"$targetname\".\"$version\".\"overlay_path\"=\"$user_overlay_location\"" "$user_catalog" > "$user_catalog_tmp" && mv "$user_catalog_tmp" "$user_catalog" + # user catalog now contains path to this kernel's main R library. + echo "Done." + echo "" + echo "Be sure to call \"icrn_manager kernels use $language $targetname $version\" to begin using this kernel in R." + return 0 + else + echo "ERROR: Could not register the overlay location." + return 1 + fi +} + +# Identify and register the R library location for an R kernel in the user catalog. +# +# This function determines the main library path of an R kernel installation by +# executing Rscript to query the library paths. It then updates the user catalog +# with the absolute path to the R library directory. +# +# Parameters: +# $1 - target_unpacked: Path to the unpacked R kernel conda environment +# $2 - language: Language of the kernel (should be "R") +# $3 - targetname: Name of the kernel +# $4 - version: Version number of the kernel +# +# Returns: +# 0 if R library path was successfully registered, 1 if conda environment not found +# +# Side effects: +# - Executes Rscript to determine library paths +# - Updates the user catalog JSON file with the absolute_path field +function register_r_library_in_user_catalog() # without unpacking, identify R kernel library location { local target_unpacked=$1; shift local language=$1; shift local targetname=$1; shift local version=$1; shift - echo checking for: $target_unpacked"/bin/activate" - if [ -e $target_unpacked"bin/activate" ]; then - echo "activating environment" - source $target_unpacked"bin/activate" - echo "doing unpack" - conda-unpack + # echo checking for: $target_unpacked"/bin/activate" + # if [ -e $target_unpacked"/bin/activate" ]; then + echo checking for: $target_unpacked"/conda-meta" + if [ -d $target_unpacked"/conda-meta" ]; then # WARNING: this is weak - relies on preparer and environment ensuring this is top slot # --vanilla ensures that we aren't interpreting an existing kernel-fu environment variable # "R_HOME=''" ensures we don't get complaints from R that R_HOME is set, but we're calling a Rscript that isn't located there # we want to get a very plain readout of where this R install's main kernel is. echo "getting R path." - target_kernel_path=$(R_HOME='' Rscript --vanilla -e 'cat(.libPaths()[1])') + target_kernel_path=$(R_HOME='' R_LIBS_USER='' R_LIBS_SITE='' $target_unpacked/bin/Rscript --vanilla -e 'cat(.libPaths()[1])') echo "determined: $target_kernel_path" - echo "deactivating" - source $target_unpacked"/bin/deactivate" - echo "Updating user's catalog with $language $targetname and $version" + echo "Updating user's catalog with $target_kernel_path" user_catalog_tmp=$(mktemp) - jq -r ".\"$language\".\"$targetname\".\"$version\"={\"absolute_path\":\"$target_kernel_path\"} " "$user_catalog" > "$user_catalog_tmp" && mv "$user_catalog_tmp" "$user_catalog" - + jq -r ".\"$language\".\"$targetname\".\"$version\"={\"absolute_path\":\"$target_kernel_path\"}" "$user_catalog" > "$user_catalog_tmp" && mv "$user_catalog_tmp" "$user_catalog" + # user catalog now contains path to this kernel's main R library. echo "Done." echo "" echo "Be sure to call \"icrn_manager kernels use $language $targetname $version\" to begin using this kernel in R." return 0 else - echo "ERROR: Could not find conda environment activation script at $target_unpacked/bin/activate" + # echo "ERROR: Could not find conda environment activation script at $target_unpacked/bin/activate" + echo "ERROR: Could not confirm target is a conda environment via existence of $target_unpacked/conda-meta" return 1 fi } -function unpack_python_kernel() # unpack and configure a Python kernel environment +# Register the Python kernel environment path in the user catalog. +# +# This function updates the user catalog with the absolute path to a Python kernel's +# conda environment. The environment must have a bin/activate script to be recognized +# as a valid conda environment. +# +# Parameters: +# $1 - target_unpacked: Path to the unpacked Python kernel conda environment +# $2 - language: Language of the kernel (should be "Python") +# $3 - targetname: Name of the kernel +# $4 - version: Version number of the kernel +# +# Returns: +# 0 if Python environment path was successfully registered, 1 if activation script not found +# +# Side effects: +# Updates the user catalog JSON file with the absolute_path field +function register_python_library_in_user_catalog() # unpack and configure a Python kernel environment { local target_unpacked=$1; shift local language=$1; shift local targetname=$1; shift local version=$1; shift - echo checking for: $target_unpacked"bin/activate" - if [ -e $target_unpacked"bin/activate" ]; then - echo "activating environment" - source $target_unpacked"bin/activate" - chmod -R u+w $target_unpacked - echo "doing unpack" - conda-unpack - echo "deactivating" - source $target_unpacked"/bin/deactivate" - + echo checking for: $target_unpacked"/bin/activate" + if [ -e $target_unpacked"/bin/activate" ]; then + # presence of $target_unpacked"/bin/activate" indicates this is a conda environment as expected echo "Updating user's catalog with $language $targetname and $version" user_catalog_tmp=$(mktemp) - jq -r ".\"$language\".\"$targetname\".\"$version\"={\"absolute_path\":\"$target_unpacked\"} " "$user_catalog" > "$user_catalog_tmp" && mv "$user_catalog_tmp" "$user_catalog" + jq -r ".\"$language\".\"$targetname\".\"$version\"={\"absolute_path\":\"$target_unpacked\"}" "$user_catalog" > "$user_catalog_tmp" && mv "$user_catalog_tmp" "$user_catalog" echo "Done." echo "" @@ -379,7 +568,45 @@ function unpack_python_kernel() # unpack and configure a Python kernel environme fi } -function kernels__get() # get a kernel from the central repo +# Alias for kernels__get_in_place() - check out a kernel from the central repository. +# +# This is a convenience function that calls kernels__get_in_place() with all passed arguments. +# +# Parameters: +# All arguments are passed through to kernels__get_in_place() +# +# Returns: +# Same as kernels__get_in_place() +function kernels__get() # alias for get in place +{ + "kernels__get_in_place" "$@" +} + +# Check out a kernel from the central repository without relocating it. +# +# This function retrieves a kernel from the central repository and registers it in the +# user catalog. The kernel remains in its original location in the central repository. +# For R kernels, it also creates an overlay directory for user-specific packages. +# The function validates input parameters to prevent path traversal and wildcard attacks. +# +# Parameters: +# $1 - language: Language of the kernel (e.g., "R", "Python") +# $2 - targetname: Name of the kernel to check out +# $3 - version: Version number of the kernel +# +# Returns: +# Exits with status 1 if: +# - Parameters are missing or invalid +# - Invalid characters detected in kernel name or version (security check) +# - Kernel not found in central catalog +# - Target environment location not found +# - Security violation detected (path would escape intended directory) +# +# Side effects: +# - Creates overlay directory for R kernels +# - Updates user catalog with kernel paths +# - Validates input to prevent security vulnerabilities +function kernels__get_in_place() # prep for 'use' without relocating kernel { local language=$1; shift local targetname=$1; shift @@ -415,58 +642,51 @@ function kernels__get() # get a kernel from the central repo echo "" # get the target file from the ICRN catalog - target_file=$(jq -r ".$language.$targetname.\"$version\".\"conda-pack\"" $icrn_catalog) - if [ ! "$target_file" = "null" ]; then - # Determine the appropriate kernel path based on language - case $language in - "R") - pack_filepath=${ICRN_R_KERNELS}/$targetname/$version/$target_file - ;; - "Python") - pack_filepath=${ICRN_PYTHON_KERNELS}/$targetname/$version/$target_file - ;; - *) - echo "ERROR: Unsupported language '$language' for kernel unpacking" - echo "Supported languages: R, Python" - exit 1 - ;; - esac - - if [ -e $pack_filepath ]; then - # identify target location, make it if it doesn't exist, and then unpack to it + # target_file=$(jq -r ".$language.$targetname.\"$version\".\"conda-pack\"" $icrn_catalog) + target_location=$(jq -r ".$language.$targetname.\"$version\".\"environment_location\"" $icrn_catalog) + if [ ! "$target_location" = "null" ]; then + + # language, targetname, and version specify the path for a conda-activate command + # language, targetname, and version also specify the info for the overlay directory creation + # we need to get the R-path needed and register it in the user's config + # we need to get the users overlay library, and register it in the user's config + + if [ -e $target_location ]; then + # identify overlay library location, make it if it doesn't exist + # what to do if it DOES exist? - nothing, i think. # Create language-specific subdirectory structure language_lower=$(echo "$language" | tr '[:upper:]' '[:lower:]') - target_unpacked="${ICRN_USER_KERNEL_BASE}/${language_lower}/${targetname}-${version}/" + user_overlay_location="${ICRN_USER_KERNEL_BASE}/${language_lower}/${targetname}-${version}/" # Safety check: ensure the path is within the intended directory - if [[ "$target_unpacked" != "$ICRN_USER_KERNEL_BASE"* ]]; then + if [[ "$user_overlay_location" != "$ICRN_USER_KERNEL_BASE"* ]]; then echo "Error: Security violation - target path would escape intended directory" - echo "Target: $target_unpacked" + echo "Target: $user_overlay_location" echo "Base: $ICRN_USER_KERNEL_BASE" exit 1 fi - if [ ! -d "$target_unpacked" ]; then - echo "Making target directory: $target_unpacked" - mkdir -p "$target_unpacked" - echo "Checking out kernel..." - tar -xzf "$pack_filepath" -C "$target_unpacked" + if [ ! -d "$user_overlay_location" ]; then + echo "Making target directory: $user_overlay_location" + mkdir -p "$user_overlay_location" else - echo "WARNING: target directory: $target_unpacked already exists!" - echo "Overwriting existing files from packed kernel..." - tar -xzf "$pack_filepath" -U -C "$target_unpacked" - echo "Note that this risks leaving this kernel in an intermediate state." - echo "It is recommended that you remove the kernel entirely by running:" - echo "'rm -rf $target_unpacked'" + echo "NOTICE: target directory: $user_overlay_location already exists." fi # Use language-specific unpacking function case $language in "R") - unpack_r_kernel "$target_unpacked" "$language" "$targetname" "$version" + echo "registering R library" + register_r_library_in_user_catalog "$target_location" "$language" "$targetname" "$version" + echo "registering user overlay" + if ! register_user_overlay_in_user_catalog "$language" "$targetname" "$version"; then + echo "ERROR: Failed to register user overlay for $language $targetname $version" + echo "Please ensure ${ICRN_USER_KERNEL_BASE} is writable and retry the checkout." + exit 1 + fi ;; "Python") - unpack_python_kernel "$target_unpacked" "$language" "$targetname" "$version" + register_python_library_in_user_catalog "$target_location" "$language" "$targetname" "$version" ;; *) echo "ERROR: Unsupported language '$language' for kernel unpacking" @@ -475,7 +695,7 @@ function kernels__get() # get a kernel from the central repo ;; esac else - echo "ERROR: could not find target pack file: $pack_filepath" + echo "ERROR: could not find target environment location: $target_location" exit 1 fi else @@ -485,6 +705,23 @@ function kernels__get() # get a kernel from the central repo fi } +# Update a user's copy of a kernel from the central repository. +# +# This function is currently not implemented. It is intended to update an existing +# checked-out kernel to a newer version or refresh it from the central repository. +# +# unclear this will be required under new approach of non-relocation of central kernels +# perhaps re-point to newer version of central kernel, and update R-packages? +# low on to-do list +# +# Parameters: +# (To be determined when implemented) +# +# Returns: +# Currently exits with status 1 (not implemented) +# +# Side effects: +# None (function not yet implemented) function kernels__update() # update users copy of a kernel from central repo { echo "entered 'update' subcommand" @@ -492,6 +729,27 @@ function kernels__update() # update users copy of a kernel from central repo exit 1 } +# Remove a kernel entry from the user's catalog. +# +# This function removes a kernel entry from the user catalog. If a version is specified, +# only that version is removed. If no version is specified, all versions of the kernel +# are removed. If removing a version leaves no versions for a kernel, the entire +# kernel entry is removed from the catalog. +# +# Parameters: +# $1 - language: Language of the kernel (e.g., "R", "Python") +# $2 - targetname: Name of the kernel to remove +# $3 - version: Optional version number. If omitted, all versions are removed +# +# Returns: +# Exits with status 1 if: +# - Required parameters are missing +# - Kernel not found in user catalog +# - Specified version not found in user catalog +# +# Side effects: +# - Updates the user catalog JSON file by removing the specified entry +# - Removes empty kernel entries if all versions are removed function kernels__clean() # remove a kernel entry from the users catalog { local language=$1; shift @@ -551,98 +809,29 @@ function kernels__clean() # remove a kernel entry from the users catalog fi } -function kernels__remove() # remove a users copy of a kernel -{ - local language=$1; shift - local targetname=$1; shift - local version=$1; shift - - icrn_catalog=${ICRN_KERNEL_CATALOG} - user_catalog=${ICRN_USER_CATALOG} - echo "" - echo "ICRN Catalog:" - echo $icrn_catalog - echo "User Catalog:" - echo $user_catalog - echo "" - - # Validate input parameters to prevent path traversal and wildcard attacks - if [[ "$targetname" =~ [\]\/\[\*\?\] ]] || [[ "$version" =~ [\]\/\[\*\?\] ]]; then - echo "Error: Invalid characters in kernel name or version. Cannot contain wildcards (*?[]) or path separators (/)" - exit 1 - fi - - if [ -z $version ] || [ -z $targetname ] || [ -z $language ]; then - help - echo "" - echo "Can't proceed without language, target kernel and version." - echo "usage: icrn_manager kernels remove " - echo "" - exit 1 - else - echo "Desired kernel to scrub from user catalog:" - echo "Language: "$language - echo "Kernel: "$targetname - echo "Version: "$version - echo "" - fi - check_for_catalog_entry "$user_catalog" "$language" "$targetname" "$version" - if [ $last_check = 0 ]; then - echo "$targetname and $version not present in user catalog at $user_catalog" - get_versions_for_package $user_catalog $language $targetname - exit 1 - else - last_check=-1 - fi - echo "Removing package files, and kernel entries for: $@" - confirm "Are you sure? [Y/n]" - - # Construct the target path safely - # Create language-specific subdirectory structure - language_lower=$(echo "$language" | tr '[:upper:]' '[:lower:]') - target_unpacked="${ICRN_USER_KERNEL_BASE}/${language_lower}/${targetname}-${version}/" - - # Additional safety check: ensure the path is within the intended directory - # Use realpath to resolve any potential symlinks and normalize the path - if command -v realpath >/dev/null 2>&1; then - resolved_target=$(realpath "$target_unpacked" 2>/dev/null) - resolved_base=$(realpath "$ICRN_USER_KERNEL_BASE" 2>/dev/null) - - if [ -n "$resolved_target" ] && [ -n "$resolved_base" ]; then - # Check if the resolved target path starts with the resolved base path - if [[ "$resolved_target" != "$resolved_base"* ]]; then - echo "Error: Security violation - target path would escape intended directory" - echo "Target: $resolved_target" - echo "Base: $resolved_base" - exit 1 - fi - fi - fi - - # Final validation: ensure the path is within the kernel base directory - if [[ "$target_unpacked" != "$ICRN_USER_KERNEL_BASE"* ]]; then - echo "Error: Security violation - target path would escape intended directory" - echo "Target: $target_unpacked" - echo "Base: $ICRN_USER_KERNEL_BASE" - exit 1 - fi - - if [ -e "$target_unpacked" ]; then - echo "Removing: $target_unpacked" - rm -rf "$target_unpacked" - if [ $? -eq 0 ]; then - echo "Successfully removed kernel files from: $target_unpacked" - else - echo "Error: Failed to remove kernel files from: $target_unpacked" - exit 1 - fi - else - echo "Could not locate $target_unpacked - exiting..." - exit 1 - fi - kernels__clean "$language" "$targetname" "$version" -} - +# Initialize the ICRN Manager by creating necessary directories and configuration files. +# +# This function sets up the initial environment for ICRN Manager, creating: +# - User base directory (~/.icrn/) +# - Kernel base directory (~/.icrn/icrn_kernels/) +# - Language-specific subdirectories (r/, python/) +# - User catalog JSON file +# - Manager configuration JSON file +# +# It also validates that the central repository and its components exist. +# +# Parameters: +# $1 - central_repository: Optional path to the central kernel repository. +# If not provided, uses the default path (/sw/icrn/jupyter/icrn_ncsa_resources/Kernels) +# +# Returns: +# No return value +# +# Side effects: +# - Creates directory structure in user's home directory +# - Creates and populates configuration files +# - Updates existing configuration if central_repository is provided +# - Prints warnings if central repository components are not found function kernels__init() # create base resources { echo "Initializing icrn kernel resources..." @@ -751,6 +940,24 @@ function kernels__init() # create base resources echo "" } +# Main launcher function for kernel management subcommands. +# +# This function routes kernel-related subcommands to their respective handler functions. +# It provides a unified interface for all kernel operations and includes confirmation +# prompts for destructive operations like 'clean'. +# +# Parameters: +# $1 - cmdname: Subcommand name (e.g., "init", "list", "available", "get", "use", "clean") +# $@ - Additional arguments passed to the subcommand handler +# +# Returns: +# Exits with status 1 if: +# - No subcommand is specified +# - Invalid subcommand name is provided +# +# Side effects: +# - Prompts for confirmation before executing 'clean' subcommand +# - Calls the appropriate subcommand handler function function kernels() # launcher { local cmdname=$1; shift @@ -760,8 +967,6 @@ function kernels() # launcher echo "" help exit 1 - elif [ "$cmdname" = "remove" ]; then - kernels__remove "$@" elif [ "$cmdname" = "clean" ]; then echo "Removing kernel entries for : $@" confirm "Are you sure? [Y/n]" @@ -778,6 +983,19 @@ function kernels() # launcher } +# Display usage information and available commands for ICRN Manager. +# +# This function prints a help message showing the correct usage syntax and +# available subcommands for the icrn_manager tool. +# +# Parameters: +# None +# +# Returns: +# No return value +# +# Side effects: +# Prints help text to stdout function help() # Show a list of functions { # grep "^function" $0 @@ -789,7 +1007,6 @@ function help() # Show a list of functions echo " list" echo " available" echo " get " - echo " remove" echo " use " echo " use none" echo " " diff --git a/tests/test_common.sh b/tests/test_common.sh index bf20d52..a3a9c1b 100755 --- a/tests/test_common.sh +++ b/tests/test_common.sh @@ -159,19 +159,19 @@ setup_test_env() { mkdir -p "$TEST_REPO/r_kernels" mkdir -p "$TEST_REPO/python_kernels" - # Create mock catalog - cat > "$TEST_REPO/icrn_kernel_catalog.json" << 'EOF' + # Create mock catalog with paths pointing to test repository + cat > "$TEST_REPO/icrn_kernel_catalog.json" << EOF { "R": { "cowsay": { "1.0": { - "conda-pack": "cowsay-1.0.tar.gz", + "environment_location": "$TEST_REPO/r_kernels/cowsay/1.0", "description": "Test R kernel for cowsay package" } }, "ggplot2": { "3.4.0": { - "conda-pack": "ggplot2-3.4.0.tar.gz", + "environment_location": "$TEST_REPO/r_kernels/ggplot2/3.4.0", "description": "Test R kernel for ggplot2 package" } } @@ -179,7 +179,7 @@ setup_test_env() { "Python": { "numpy": { "1.24.0": { - "conda-pack": "numpy-1.24.0.tar.gz", + "environment_location": "$TEST_REPO/python_kernels/numpy/1.24.0", "description": "Test Python kernel for numpy package" } } @@ -187,28 +187,39 @@ setup_test_env() { } EOF - # Create mock kernel packages (valid tar files for testing) - mkdir -p "$TEST_REPO/r_kernels/cowsay/1.0" - mkdir -p "$TEST_REPO/r_kernels/ggplot2/3.4.0" - mkdir -p "$TEST_REPO/python_kernels/numpy/1.24.0" + # Create mock kernel environment directories (in-place environments, not tar files) + mkdir -p "$TEST_REPO/r_kernels/cowsay/1.0/bin" + mkdir -p "$TEST_REPO/r_kernels/ggplot2/3.4.0/bin" + mkdir -p "$TEST_REPO/python_kernels/numpy/1.24.0/bin" - # Create valid tar files with dummy content for testing - echo "dummy content" > "$TEST_REPO/r_kernels/cowsay/1.0/dummy.txt" - tar -czf "$TEST_REPO/r_kernels/cowsay/1.0/cowsay-1.0.tar.gz" -C "$TEST_REPO/r_kernels/cowsay/1.0" dummy.txt 2>/dev/null || true + # Create R kernel mock with conda environment structure + echo "#!/bin/bash" > "$TEST_REPO/r_kernels/cowsay/1.0/bin/activate" + echo "echo 'Activating R conda environment'" >> "$TEST_REPO/r_kernels/cowsay/1.0/bin/activate" + chmod +x "$TEST_REPO/r_kernels/cowsay/1.0/bin/activate" + echo "#!/bin/bash" > "$TEST_REPO/r_kernels/cowsay/1.0/bin/deactivate" + echo "echo 'Deactivating R conda environment'" >> "$TEST_REPO/r_kernels/cowsay/1.0/bin/deactivate" + chmod +x "$TEST_REPO/r_kernels/cowsay/1.0/bin/deactivate" + echo "#!/bin/bash" > "$TEST_REPO/r_kernels/cowsay/1.0/bin/Rscript" + echo "echo '/mock/r/lib'" >> "$TEST_REPO/r_kernels/cowsay/1.0/bin/Rscript" + chmod +x "$TEST_REPO/r_kernels/cowsay/1.0/bin/Rscript" - echo "dummy content" > "$TEST_REPO/r_kernels/ggplot2/3.4.0/dummy.txt" - tar -czf "$TEST_REPO/r_kernels/ggplot2/3.4.0/ggplot2-3.4.0.tar.gz" -C "$TEST_REPO/r_kernels/ggplot2/3.4.0" dummy.txt 2>/dev/null || true + echo "#!/bin/bash" > "$TEST_REPO/r_kernels/ggplot2/3.4.0/bin/activate" + echo "echo 'Activating R conda environment'" >> "$TEST_REPO/r_kernels/ggplot2/3.4.0/bin/activate" + chmod +x "$TEST_REPO/r_kernels/ggplot2/3.4.0/bin/activate" + echo "#!/bin/bash" > "$TEST_REPO/r_kernels/ggplot2/3.4.0/bin/deactivate" + echo "echo 'Deactivating R conda environment'" >> "$TEST_REPO/r_kernels/ggplot2/3.4.0/bin/deactivate" + chmod +x "$TEST_REPO/r_kernels/ggplot2/3.4.0/bin/deactivate" + echo "#!/bin/bash" > "$TEST_REPO/r_kernels/ggplot2/3.4.0/bin/Rscript" + echo "echo '/mock/r/lib'" >> "$TEST_REPO/r_kernels/ggplot2/3.4.0/bin/Rscript" + chmod +x "$TEST_REPO/r_kernels/ggplot2/3.4.0/bin/Rscript" # Create Python kernel mock with conda environment structure - mkdir -p "$TEST_REPO/python_kernels/numpy/1.24.0/bin" - echo "dummy content" > "$TEST_REPO/python_kernels/numpy/1.24.0/dummy.txt" echo "#!/bin/bash" > "$TEST_REPO/python_kernels/numpy/1.24.0/bin/activate" echo "echo 'Activating conda environment'" >> "$TEST_REPO/python_kernels/numpy/1.24.0/bin/activate" chmod +x "$TEST_REPO/python_kernels/numpy/1.24.0/bin/activate" echo "#!/bin/bash" > "$TEST_REPO/python_kernels/numpy/1.24.0/bin/deactivate" echo "echo 'Deactivating conda environment'" >> "$TEST_REPO/python_kernels/numpy/1.24.0/bin/deactivate" chmod +x "$TEST_REPO/python_kernels/numpy/1.24.0/bin/deactivate" - tar -czf "$TEST_REPO/python_kernels/numpy/1.24.0/numpy-1.24.0.tar.gz" -C "$TEST_REPO/python_kernels/numpy/1.24.0" . 2>/dev/null || true # Create mock conda-unpack command echo '#!/bin/bash' > "$TEST_BASE/conda-unpack" diff --git a/tests/test_env/conda-unpack b/tests/test_env/conda-unpack deleted file mode 100755 index 8341cef..0000000 --- a/tests/test_env/conda-unpack +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -echo "Running conda-unpack (mock)" diff --git a/tests/test_env/repository/icrn_kernel_catalog.json b/tests/test_env/repository/icrn_kernel_catalog.json deleted file mode 100644 index 8c7e11d..0000000 --- a/tests/test_env/repository/icrn_kernel_catalog.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "R": { - "cowsay": { - "1.0": { - "conda-pack": "cowsay-1.0.tar.gz", - "description": "Test R kernel for cowsay package" - } - }, - "ggplot2": { - "3.4.0": { - "conda-pack": "ggplot2-3.4.0.tar.gz", - "description": "Test R kernel for ggplot2 package" - } - } - }, - "Python": { - "numpy": { - "1.24.0": { - "conda-pack": "numpy-1.24.0.tar.gz", - "description": "Test Python kernel for numpy package" - } - } - } -} diff --git a/tests/test_env/repository/python_kernels/numpy/1.24.0/bin/activate b/tests/test_env/repository/python_kernels/numpy/1.24.0/bin/activate deleted file mode 100755 index 4f96824..0000000 --- a/tests/test_env/repository/python_kernels/numpy/1.24.0/bin/activate +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -echo 'Activating conda environment' diff --git a/tests/test_env/repository/python_kernels/numpy/1.24.0/bin/deactivate b/tests/test_env/repository/python_kernels/numpy/1.24.0/bin/deactivate deleted file mode 100755 index 7bf327e..0000000 --- a/tests/test_env/repository/python_kernels/numpy/1.24.0/bin/deactivate +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -echo 'Deactivating conda environment' diff --git a/tests/test_env/repository/python_kernels/numpy/1.24.0/dummy.txt b/tests/test_env/repository/python_kernels/numpy/1.24.0/dummy.txt deleted file mode 100644 index eaf5f75..0000000 --- a/tests/test_env/repository/python_kernels/numpy/1.24.0/dummy.txt +++ /dev/null @@ -1 +0,0 @@ -dummy content diff --git a/tests/test_env/repository/python_kernels/numpy/1.24.0/numpy-1.24.0.tar.gz b/tests/test_env/repository/python_kernels/numpy/1.24.0/numpy-1.24.0.tar.gz deleted file mode 100644 index ff56ebd..0000000 Binary files a/tests/test_env/repository/python_kernels/numpy/1.24.0/numpy-1.24.0.tar.gz and /dev/null differ diff --git a/tests/test_env/repository/r_kernels/cowsay/1.0/cowsay-1.0.tar.gz b/tests/test_env/repository/r_kernels/cowsay/1.0/cowsay-1.0.tar.gz deleted file mode 100644 index c387c9e..0000000 Binary files a/tests/test_env/repository/r_kernels/cowsay/1.0/cowsay-1.0.tar.gz and /dev/null differ diff --git a/tests/test_env/repository/r_kernels/cowsay/1.0/dummy.txt b/tests/test_env/repository/r_kernels/cowsay/1.0/dummy.txt deleted file mode 100644 index eaf5f75..0000000 --- a/tests/test_env/repository/r_kernels/cowsay/1.0/dummy.txt +++ /dev/null @@ -1 +0,0 @@ -dummy content diff --git a/tests/test_env/repository/r_kernels/ggplot2/3.4.0/dummy.txt b/tests/test_env/repository/r_kernels/ggplot2/3.4.0/dummy.txt deleted file mode 100644 index eaf5f75..0000000 --- a/tests/test_env/repository/r_kernels/ggplot2/3.4.0/dummy.txt +++ /dev/null @@ -1 +0,0 @@ -dummy content diff --git a/tests/test_env/repository/r_kernels/ggplot2/3.4.0/ggplot2-3.4.0.tar.gz b/tests/test_env/repository/r_kernels/ggplot2/3.4.0/ggplot2-3.4.0.tar.gz deleted file mode 100644 index c387c9e..0000000 Binary files a/tests/test_env/repository/r_kernels/ggplot2/3.4.0/ggplot2-3.4.0.tar.gz and /dev/null differ diff --git a/tests/test_env/user_home/.icrn/icrn_kernels/test-kernel-1.0/test.txt b/tests/test_env/user_home/.icrn/icrn_kernels/test-kernel-1.0/test.txt deleted file mode 100644 index 16b14f5..0000000 --- a/tests/test_env/user_home/.icrn/icrn_kernels/test-kernel-1.0/test.txt +++ /dev/null @@ -1 +0,0 @@ -test file diff --git a/tests/test_env/user_home/.icrn/icrn_kernels/user_catalog.json b/tests/test_env/user_home/.icrn/icrn_kernels/user_catalog.json deleted file mode 100644 index 17b47ed..0000000 --- a/tests/test_env/user_home/.icrn/icrn_kernels/user_catalog.json +++ /dev/null @@ -1 +0,0 @@ -{"R":{"test-kernel":{"1.0":{"path":"test-kernel-1.0"}}}} diff --git a/tests/test_env/user_home/.icrn/manager_config.json b/tests/test_env/user_home/.icrn/manager_config.json deleted file mode 100644 index 93ee18f..0000000 --- a/tests/test_env/user_home/.icrn/manager_config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "icrn_central_catalog_path": "/u/hdpriest/Code/icrn_manager/tests/test_env/repository", - "icrn_r_kernels": "r_kernels", - "icrn_python_kernels": "python_kernels", - "icrn_kernel_catalog": "icrn_kernel_catalog.json" -} diff --git a/tests/test_kernels.sh b/tests/test_kernels.sh index 15cbae5..18346d3 100755 --- a/tests/test_kernels.sh +++ b/tests/test_kernels.sh @@ -86,7 +86,7 @@ test_kernels_get_python_success() { local output output=$(timeout 30 "$ICRN_MANAGER" kernels get Python numpy 1.24.0 2>&1) - # Check if Python kernel unpacking succeeds + # Check if Python kernel registration succeeds (in-place, no unpacking) if echo "$output" | grep -q "Updating user's catalog with Python numpy and 1.24.0" && \ echo "$output" | grep -q "Be sure to call.*icrn_manager kernels use Python numpy 1.24.0"; then return 0 @@ -96,7 +96,7 @@ test_kernels_get_python_success() { fi } -test_kernels_get_r_fail() { +test_kernels_get_r_success() { # Setup fresh test environment for this test setup_test_env set_test_env @@ -105,11 +105,12 @@ test_kernels_get_r_fail() { "$ICRN_MANAGER" kernels init "$TEST_REPO" >/dev/null 2>&1 local output - output=$(timeout 10 "$ICRN_MANAGER" kernels get R cowsay 1.0 2>&1) + output=$(timeout 30 "$ICRN_MANAGER" kernels get R cowsay 1.0 2>&1) - # Check if it fails as expected (mock tar file issues or other errors) - if echo "$output" | grep -q "ERROR:" || \ - echo "$output" | grep -q "timeout"; then + # Check if R kernel registration succeeds (in-place, no unpacking) + if echo "$output" | grep -q "registering R library" && \ + echo "$output" | grep -q "registering user overlay" && \ + echo "$output" | grep -q "Be sure to call.*icrn_manager kernels use R cowsay 1.0"; then return 0 else echo "R get output: $output" @@ -211,30 +212,6 @@ test_kernels_clean_missing_params() { fi } -test_kernels_remove_missing_params() { - # Setup fresh test environment for this test - setup_test_env - set_test_env - - # Initialize the environment first - "$ICRN_MANAGER" kernels init "$TEST_REPO" >/dev/null 2>&1 - - # Test with explicit timeout and debug output - local output - output=$(timeout 5 bash -c "cd '$PROJECT_ROOT' && '$ICRN_MANAGER' kernels remove" 2>&1) - local exit_code=$? - - # Check if it fails with usage message or if timeout occurred - if echo "$output" | grep -q "usage: icrn_manager kernels remove " || \ - [ $exit_code -eq 124 ]; then - return 0 - else - echo "Remove missing params output: $output" - echo "Exit code: $exit_code" - return 1 - fi -} - test_kernels_use_missing_params() { # Setup fresh test environment for this test setup_test_env @@ -283,209 +260,7 @@ test_kernels_use_none() { -# Security Tests for Remove Functionality - -test_kernels_remove_wildcard_attack() { - # Setup fresh test environment for this test - setup_test_env - set_test_env - - # Initialize the environment first - "$ICRN_MANAGER" kernels init "$TEST_REPO" >/dev/null 2>&1 - - # Test wildcard attack in kernel name - local output - output=$("$ICRN_MANAGER" kernels remove R "malicious*" "1.0" 2>&1) - - # Check if it rejects wildcards - if echo "$output" | grep -q "Invalid characters in kernel name or version" && \ - echo "$output" | grep -q "Cannot contain wildcards"; then - return 0 - else - echo "Wildcard attack output: $output" - return 1 - fi -} - -test_kernels_remove_path_traversal_attack() { - # Setup fresh test environment for this test - setup_test_env - set_test_env - - # Initialize the environment first - "$ICRN_MANAGER" kernels init "$TEST_REPO" >/dev/null 2>&1 - - # Test path traversal attack in version - local output - output=$("$ICRN_MANAGER" kernels remove R "kernel" "../../../etc" 2>&1) - - # Check if it rejects path separators - if echo "$output" | grep -q "Invalid characters in kernel name or version" && \ - echo "$output" | grep -q "Cannot contain wildcards"; then - return 0 - else - echo "Path traversal attack output: $output" - return 1 - fi -} - -test_kernels_remove_bracket_expansion_attack() { - # Setup fresh test environment for this test - setup_test_env - set_test_env - - # Initialize the environment first - "$ICRN_MANAGER" kernels init "$TEST_REPO" >/dev/null 2>&1 - - # Test bracket expansion attack - local output - output=$("$ICRN_MANAGER" kernels remove R "kernel[1]" "1.0" 2>&1) - - # Check if it rejects brackets - if echo "$output" | grep -q "Invalid characters in kernel name or version" && \ - echo "$output" | grep -q "Cannot contain wildcards"; then - return 0 - else - echo "Bracket expansion attack output: $output" - return 1 - fi -} - -test_kernels_remove_question_mark_attack() { - # Setup fresh test environment for this test - setup_test_env - set_test_env - - # Initialize the environment first - "$ICRN_MANAGER" kernels init "$TEST_REPO" >/dev/null 2>&1 - - # Test question mark wildcard attack - local output - output=$("$ICRN_MANAGER" kernels remove R "kernel?" "1.0" 2>&1) - - # Check if it rejects question marks - if echo "$output" | grep -q "Invalid characters in kernel name or version" && \ - echo "$output" | grep -q "Cannot contain wildcards"; then - return 0 - else - echo "Question mark attack output: $output" - return 1 - fi -} - -test_kernels_remove_backslash_attack() { - # Setup fresh test environment for this test - setup_test_env - set_test_env - - # Initialize the environment first - "$ICRN_MANAGER" kernels init "$TEST_REPO" >/dev/null 2>&1 - - # Test backslash escape attack - local output - output=$(timeout 10 "$ICRN_MANAGER" kernels remove R "kernel\\" "1.0" 2>&1) - - # Check if it rejects backslashes - if echo "$output" | grep -q "Invalid characters in kernel name or version" && \ - echo "$output" | grep -q "Cannot contain wildcards"; then - return 0 - else - echo "Backslash attack output: $output" - return 1 - fi -} - -test_kernels_remove_symlink_attack() { - # Setup fresh test environment for this test - setup_test_env - set_test_env - - # Initialize the environment first - "$ICRN_MANAGER" kernels init "$TEST_REPO" >/dev/null 2>&1 - - # Create a malicious symlink that points outside the intended directory - mkdir -p "$ICRN_USER_KERNEL_BASE" - ln -sf /tmp "$ICRN_USER_KERNEL_BASE/malicious-symlink" - - # Test symlink attack (this should be caught by the path validation) - local output - output=$("$ICRN_MANAGER" kernels remove R "malicious-symlink" "1.0" 2>&1) - - # Check if it fails appropriately (either security violation or file not found) - if echo "$output" | grep -q "Could not locate" || \ - echo "$output" | grep -q "Security violation"; then - return 0 - else - echo "Symlink attack output: $output" - return 1 - fi -} - -test_kernels_remove_careless_spaces() { - # Setup fresh test environment for this test - setup_test_env - set_test_env - - # Initialize the environment first - "$ICRN_MANAGER" kernels init "$TEST_REPO" >/dev/null 2>&1 - - # Test careless use with spaces in names - local output - output=$("$ICRN_MANAGER" kernels remove R "kernel name" "1.0" 2>&1) - - # Check if it handles spaces appropriately (should fail with missing parameters) - if echo "$output" | grep -q "usage:" || \ - echo "$output" | grep -q "Can't proceed without"; then - return 0 - else - echo "Careless spaces output: $output" - return 1 - fi -} - -test_kernels_remove_careless_empty_params() { - # Setup fresh test environment for this test - setup_test_env - set_test_env - - # Initialize the environment first - "$ICRN_MANAGER" kernels init "$TEST_REPO" >/dev/null 2>&1 - - # Test careless use with empty parameters - local output - output=$("$ICRN_MANAGER" kernels remove R "" "1.0" 2>&1) - - # Check if it handles empty parameters appropriately - if echo "$output" | grep -q "usage:" || \ - echo "$output" | grep -q "Can't proceed without"; then - return 0 - else - echo "Empty params output: $output" - return 1 - fi -} - -test_kernels_remove_careless_special_chars() { - # Setup fresh test environment for this test - setup_test_env - set_test_env - - # Initialize the environment first - "$ICRN_MANAGER" kernels init "$TEST_REPO" >/dev/null 2>&1 - - # Test careless use with special characters that aren't necessarily malicious - local output - output=$("$ICRN_MANAGER" kernels remove R "kernel-name" "1.0" 2>&1) - - # Check if it handles hyphens appropriately (should be allowed) - if echo "$output" | grep -q "Could not locate" || \ - echo "$output" | grep -q "not present in user catalog"; then - return 0 - else - echo "Special chars output: $output" - return 1 - fi -} +# Security Tests for Get Functionality test_kernels_get_wildcard_attack() { # Setup fresh test environment for this test @@ -531,42 +306,6 @@ test_kernels_get_path_traversal_attack() { fi } -test_kernels_remove_successful_cleanup() { - # Setup fresh test environment for this test - setup_test_env - set_test_env - - # Initialize the environment first - "$ICRN_MANAGER" kernels init "$TEST_REPO" >/dev/null 2>&1 - - # Create a test kernel directory to remove - local test_kernel_dir="$ICRN_USER_KERNEL_BASE/test-kernel-1.0" - mkdir -p "$test_kernel_dir" - echo "test file" > "$test_kernel_dir/test.txt" - - # Add entry to user catalog - local user_catalog="$ICRN_USER_CATALOG" - local temp_catalog=$(mktemp) - echo '{"R":{"test-kernel":{"1.0":{"path":"test-kernel-1.0"}}}}' > "$temp_catalog" - mv "$temp_catalog" "$user_catalog" - - # Test successful removal (this will require user interaction, so we'll simulate it) - # Since we can't easily simulate user input in tests, we'll test the validation logic - # by checking that the directory exists and the catalog entry exists - if [ -d "$test_kernel_dir" ] && [ -f "$user_catalog" ]; then - # Verify the setup is correct - if jq -e '.["R"]["test-kernel"]["1.0"]' "$user_catalog" >/dev/null 2>&1; then - return 0 - else - echo "Test kernel not properly set up in catalog" - return 1 - fi - else - echo "Test kernel directory or catalog not created properly" - return 1 - fi -} - test_kernels_use_python_success() { # Setup fresh test environment for this test setup_test_env @@ -644,7 +383,7 @@ test_kernels_use_python_none() { jq '.Python.test_kernel."1.0".absolute_path = "/tmp/test"' "$user_catalog" > "$user_catalog.tmp" && mv "$user_catalog.tmp" "$user_catalog" # Mock jupyter command that returns both system and user kernels - local mock_jupyter="$TEST_BASE/mock_jupyter" + local mock_jupyter="$TEST_BASE/jupyter" echo '#!/bin/bash' > "$mock_jupyter" echo 'if [ "$1" = "kernelspec" ] && [ "$2" = "list" ] && [ "$3" = "--json" ]; then' >> "$mock_jupyter" echo ' echo "{\"kernelspecs\": {\"python3\": {\"spec\": \"/usr/local/share/jupyter/kernels/python3\"}, \"test_kernel-1.0\": {\"spec\": \"/tmp/test\"}}}"' >> "$mock_jupyter" @@ -654,16 +393,12 @@ test_kernels_use_python_none() { echo 'fi' >> "$mock_jupyter" chmod +x "$mock_jupyter" - # Temporarily add mock command to PATH and verify it's being used - export PATH="$TEST_BASE:$PATH" - echo "DEBUG: Mock jupyter path: $(which jupyter)" - echo "DEBUG: Mock jupyter content: $(cat "$mock_jupyter")" - - # Test Python kernel removal + # Test Python kernel removal with mock jupyter in PATH local output - output=$(PATH="$TEST_BASE:$PATH" "$ICRN_MANAGER" kernels use Python none 2>&1) + output=$(env PATH="$TEST_BASE:$PATH" "$ICRN_MANAGER" kernels use Python none 2>&1) # Check if Python kernel removal succeeds and only removes catalog kernels + # Note: The mock jupyter should return test_kernel-1.0 in the kernelspec list if echo "$output" | grep -q "Removing preconfigured kernels from Python" && \ echo "$output" | grep -q "Found Python kernels in user catalog: test_kernel-1.0" && \ echo "$output" | grep -q "Removing kernel: test_kernel-1.0" && \ @@ -675,32 +410,114 @@ test_kernels_use_python_none() { fi } +test_kernels_use_with_overlay() { + # Setup fresh test environment for this test + setup_test_env + set_test_env + + # Initialize the environment first + "$ICRN_MANAGER" kernels init "$TEST_REPO" >/dev/null 2>&1 + + # Create a mock R kernel with overlay + local user_catalog="$ICRN_USER_CATALOG" + local overlay_dir="$ICRN_USER_KERNEL_BASE/r/cowsay-1.0" + local mock_r_lib="$TEST_BASE/mock_r_lib" + + # Create the mock R library directory + mkdir -p "$overlay_dir" + mkdir -p "$mock_r_lib" + + jq '.R.cowsay."1.0" = { + "absolute_path": "'"$mock_r_lib"'", + "overlay_path": "'"$overlay_dir"'" + }' "$user_catalog" > "$user_catalog.tmp" && mv "$user_catalog.tmp" "$user_catalog" + + # Create a test .Renviron file + local test_renviron="$TEST_USER_HOME/.Renviron" + echo "R_LIBS=/usr/lib/R/library" > "$test_renviron" + + # Mock update_r_libs.sh + local mock_update_r_libs="$TEST_BASE/update_r_libs.sh" + echo '#!/bin/bash' > "$mock_update_r_libs" + echo 'echo "Called with: $@"' >> "$mock_update_r_libs" + chmod +x "$mock_update_r_libs" + + # Test using kernel with overlay + local output + output=$(PATH="$TEST_BASE:$PATH" "$ICRN_MANAGER" kernels use R cowsay 1.0 2>&1) + + # Check if it uses overlay path + if echo "$output" | grep -q "Found. Linking and Activating" || \ + echo "$output" | grep -q "Called with:.*$overlay_dir"; then + return 0 + else + echo "Use with overlay output: $output" + return 1 + fi +} + +test_kernels_get_creates_overlay_directory() { + # Setup fresh test environment for this test + setup_test_env + set_test_env + + # Initialize the environment first + "$ICRN_MANAGER" kernels init "$TEST_REPO" >/dev/null 2>&1 + + local output + output=$(timeout 30 "$ICRN_MANAGER" kernels get R cowsay 1.0 2>&1) + + # Check if overlay directory was created + local overlay_dir="$ICRN_USER_KERNEL_BASE/r/cowsay-1.0" + if [ -d "$overlay_dir" ] && \ + echo "$output" | grep -q "registering user overlay"; then + return 0 + else + echo "Overlay directory creation output: $output" + echo "Overlay directory exists: $([ -d "$overlay_dir" ] && echo "yes" || echo "no")" + return 1 + fi +} + +test_kernels_get_security_path_traversal() { + # Setup fresh test environment for this test + setup_test_env + set_test_env + + # Initialize the environment first + "$ICRN_MANAGER" kernels init "$TEST_REPO" >/dev/null 2>&1 + + # Test path traversal in kernel name + local output + output=$("$ICRN_MANAGER" kernels get R "../../etc" "1.0" 2>&1) + + # Check if it rejects path separators + if echo "$output" | grep -q "Invalid characters in kernel name or version"; then + return 0 + else + echo "Path traversal security output: $output" + return 1 + fi +} + # Run tests when sourced or executed directly run_test "kernels_init" test_kernels_init "Kernels init creates necessary directories and config" run_test "kernels_available" test_kernels_available "Kernels available shows catalog contents" run_test "kernels_list_empty" test_kernels_list_empty "Kernels list shows empty user catalog initially" -run_test "kernels_get_python_success" test_kernels_get_python_success "Kernels get Python succeeds with proper unpacking" -run_test "kernels_get_r_fail" test_kernels_get_r_fail "Kernels get R fails with mock data" +run_test "kernels_get_python_success" test_kernels_get_python_success "Kernels get Python succeeds with proper registration" +run_test "kernels_get_r_success" test_kernels_get_r_success "Kernels get R succeeds with proper registration" run_test "kernels_get_invalid_language" test_kernels_get_invalid_language "Kernels get fails with invalid language" run_test "kernels_get_missing_params" test_kernels_get_missing_params "Kernels get fails with missing parameters" run_test "kernels_clean" test_kernels_clean "Kernels clean removes entries from user catalog" run_test "kernels_clean_missing_params" test_kernels_clean_missing_params "Kernels clean fails with missing parameters" -run_test "kernels_remove_missing_params" test_kernels_remove_missing_params "Kernels remove fails with missing parameters" run_test "kernels_use_missing_params" test_kernels_use_missing_params "Kernels use fails with missing parameters" run_test "kernels_use_none" test_kernels_use_none "Kernels use handles 'none' parameter for R" run_test "kernels_use_python_success" test_kernels_use_python_success "Kernels use Python succeeds with proper kernel installation" run_test "kernels_use_python_none" test_kernels_use_python_none "Kernels use handles 'none' parameter for Python" +run_test "kernels_use_with_overlay" test_kernels_use_with_overlay "Kernels use correctly uses overlay path" +run_test "kernels_get_creates_overlay_directory" test_kernels_get_creates_overlay_directory "Kernels get creates overlay directory" +run_test "kernels_get_security_path_traversal" test_kernels_get_security_path_traversal "Kernels get rejects path traversal attempts" # Security Tests -run_test "kernels_remove_wildcard_attack" test_kernels_remove_wildcard_attack "Kernels remove rejects wildcard attacks" -run_test "kernels_remove_path_traversal_attack" test_kernels_remove_path_traversal_attack "Kernels remove rejects path traversal attacks" -run_test "kernels_remove_bracket_expansion_attack" test_kernels_remove_bracket_expansion_attack "Kernels remove rejects bracket expansion attacks" -run_test "kernels_remove_question_mark_attack" test_kernels_remove_question_mark_attack "Kernels remove rejects question mark wildcard attacks" -run_test "kernels_remove_backslash_attack" test_kernels_remove_backslash_attack "Kernels remove rejects backslash escape attacks" -run_test "kernels_remove_symlink_attack" test_kernels_remove_symlink_attack "Kernels remove handles symlink attacks safely" -run_test "kernels_remove_careless_spaces" test_kernels_remove_careless_spaces "Kernels remove handles careless use with spaces" -run_test "kernels_remove_careless_empty_params" test_kernels_remove_careless_empty_params "Kernels remove handles careless use with empty parameters" -run_test "kernels_remove_careless_special_chars" test_kernels_remove_careless_special_chars "Kernels remove handles careless use with special characters" run_test "kernels_get_wildcard_attack" test_kernels_get_wildcard_attack "Kernels get rejects wildcard attacks" -run_test "kernels_get_path_traversal_attack" test_kernels_get_path_traversal_attack "Kernels get rejects path traversal attacks" -run_test "kernels_remove_successful_cleanup" test_kernels_remove_successful_cleanup "Kernels remove setup validation for successful cleanup" \ No newline at end of file +run_test "kernels_get_path_traversal_attack" test_kernels_get_path_traversal_attack "Kernels get rejects path traversal attacks" \ No newline at end of file diff --git a/tests/test_results.log b/tests/test_results.log index a491ad8..0b86b1f 100644 --- a/tests/test_results.log +++ b/tests/test_results.log @@ -1,28 +1,39 @@ -ICRN Manager Test Results - kernels - 2025-07-25 14:24:50 +ICRN Manager Test Results - 2025-10-13 15:35:23 ========================================== -[2025-07-25 14:24:50] PASS: kernels_init - Kernels init creates necessary directories and config -[2025-07-25 14:24:50] PASS: kernels_available - Kernels available shows catalog contents -[2025-07-25 14:24:50] PASS: kernels_list_empty - Kernels list shows empty user catalog initially -[2025-07-25 14:24:50] PASS: kernels_get_python_success - Kernels get Python succeeds with proper unpacking -[2025-07-25 14:24:50] PASS: kernels_get_r_fail - Kernels get R fails with mock data -[2025-07-25 14:24:50] PASS: kernels_get_invalid_language - Kernels get fails with invalid language -[2025-07-25 14:24:50] PASS: kernels_get_missing_params - Kernels get fails with missing parameters -[2025-07-25 14:24:50] PASS: kernels_clean - Kernels clean removes entries from user catalog -[2025-07-25 14:24:50] PASS: kernels_clean_missing_params - Kernels clean fails with missing parameters -[2025-07-25 14:24:50] PASS: kernels_remove_missing_params - Kernels remove fails with missing parameters -[2025-07-25 14:24:50] PASS: kernels_use_missing_params - Kernels use fails with missing parameters -[2025-07-25 14:24:50] PASS: kernels_use_none - Kernels use handles 'none' parameter for R -[2025-07-25 14:24:50] PASS: kernels_use_python_success - Kernels use Python succeeds with proper kernel installation -[2025-07-25 14:24:50] FAIL: kernels_use_python_none - Kernels use handles 'none' parameter for Python -[2025-07-25 14:24:50] PASS: kernels_remove_wildcard_attack - Kernels remove rejects wildcard attacks -[2025-07-25 14:24:50] PASS: kernels_remove_path_traversal_attack - Kernels remove rejects path traversal attacks -[2025-07-25 14:24:50] PASS: kernels_remove_bracket_expansion_attack - Kernels remove rejects bracket expansion attacks -[2025-07-25 14:24:50] PASS: kernels_remove_question_mark_attack - Kernels remove rejects question mark wildcard attacks -[2025-07-25 14:24:50] PASS: kernels_remove_backslash_attack - Kernels remove rejects backslash escape attacks -[2025-07-25 14:24:50] FAIL: kernels_remove_symlink_attack - Kernels remove handles symlink attacks safely -[2025-07-25 14:24:50] FAIL: kernels_remove_careless_spaces - Kernels remove handles careless use with spaces -[2025-07-25 14:24:50] PASS: kernels_remove_careless_empty_params - Kernels remove handles careless use with empty parameters -[2025-07-25 14:24:50] PASS: kernels_remove_careless_special_chars - Kernels remove handles careless use with special characters -[2025-07-25 14:24:50] PASS: kernels_get_wildcard_attack - Kernels get rejects wildcard attacks -[2025-07-25 14:24:50] PASS: kernels_get_path_traversal_attack - Kernels get rejects path traversal attacks -[2025-07-25 14:24:50] PASS: kernels_remove_successful_cleanup - Kernels remove setup validation for successful cleanup +[2025-10-13 15:35:23] PASS: help_command - Help command shows usage information +[2025-10-13 15:35:23] PASS: invalid_command - Invalid command fails gracefully +[2025-10-13 15:35:23] PASS: kernels_help - Kernels command without subcommand shows help +[2025-10-13 15:35:23] PASS: kernels_init - Kernels init creates necessary directories and config +[2025-10-13 15:35:23] PASS: kernels_available - Kernels available shows catalog contents +[2025-10-13 15:35:23] PASS: kernels_list_empty - Kernels list shows empty user catalog initially +[2025-10-13 15:35:23] PASS: kernels_get_python_success - Kernels get Python succeeds with proper registration +[2025-10-13 15:35:23] PASS: kernels_get_r_success - Kernels get R succeeds with proper registration +[2025-10-13 15:35:23] PASS: kernels_get_invalid_language - Kernels get fails with invalid language +[2025-10-13 15:35:23] PASS: kernels_get_missing_params - Kernels get fails with missing parameters +[2025-10-13 15:35:23] PASS: kernels_clean - Kernels clean removes entries from user catalog +[2025-10-13 15:35:23] PASS: kernels_clean_missing_params - Kernels clean fails with missing parameters +[2025-10-13 15:35:23] PASS: kernels_use_missing_params - Kernels use fails with missing parameters +[2025-10-13 15:35:23] PASS: kernels_use_none - Kernels use handles 'none' parameter for R +[2025-10-13 15:35:23] PASS: kernels_use_python_success - Kernels use Python succeeds with proper kernel installation +[2025-10-13 15:35:23] PASS: kernels_use_python_none - Kernels use handles 'none' parameter for Python +[2025-10-13 15:35:23] PASS: kernels_use_with_overlay - Kernels use correctly uses overlay path +[2025-10-13 15:35:23] PASS: kernels_get_creates_overlay_directory - Kernels get creates overlay directory +[2025-10-13 15:35:23] PASS: kernels_get_security_path_traversal - Kernels get rejects path traversal attempts +[2025-10-13 15:35:23] PASS: kernels_get_wildcard_attack - Kernels get rejects wildcard attacks +[2025-10-13 15:35:23] PASS: kernels_get_path_traversal_attack - Kernels get rejects path traversal attacks +[2025-10-13 15:35:32] PASS: update_r_libs_add - Update R libs adds kernel to .Renviron +[2025-10-13 15:35:32] PASS: update_r_libs_remove - Update R libs removes kernels from .Renviron +[2025-10-13 15:35:32] PASS: update_r_libs_overwrite - Update R libs overwrites existing kernel +[2025-10-13 15:35:32] PASS: update_r_libs_missing_params - Update R libs fails with missing parameters +[2025-10-13 15:35:32] PASS: update_r_libs_invalid_file - Update R libs handles invalid file paths +[2025-10-13 15:35:32] PASS: update_r_libs_preserve_content - Update R libs preserves existing .Renviron content +[2025-10-13 15:35:33] PASS: config_validation_missing_config - Commands fail without config file +[2025-10-13 15:35:33] PASS: config_validation_missing_catalog - Commands fail without central catalog +[2025-10-13 15:35:33] PASS: config_validation_missing_repository - Commands fail without repository +[2025-10-13 15:35:33] PASS: config_validation_language_param - Commands fail with invalid language parameter +[2025-10-13 15:35:33] PASS: config_validation_kernel_param - Commands fail with invalid kernel parameter +[2025-10-13 15:35:33] PASS: config_validation_version_param - Commands fail with invalid version parameter +[2025-10-13 15:35:33] PASS: config_json_structure - Config file has valid JSON structure +[2025-10-13 15:35:33] PASS: user_catalog_json_structure - User catalog has valid JSON structure +[2025-10-13 15:35:33] PASS: catalog_json_structure - Central catalog has valid JSON structure +[2025-10-13 15:35:33] PASS: catalog_required_fields - Catalog has required fields for all kernels diff --git a/tests/test_update_r_libs.sh b/tests/test_update_r_libs.sh index aa83f9d..afdec79 100755 --- a/tests/test_update_r_libs.sh +++ b/tests/test_update_r_libs.sh @@ -13,12 +13,13 @@ test_update_r_libs_add() { local test_renviron="$TEST_USER_HOME/.Renviron" echo "R_LIBS=/usr/lib/R/library" > "$test_renviron" - # Test adding a kernel + # Test adding a kernel with overlay path local output - output=$("$UPDATE_R_LIBS" "$test_renviron" "test_kernel" 2>&1) + output=$("$UPDATE_R_LIBS" "$test_renviron" "/path/to/kernel" "/path/to/overlay" 2>&1) # Check if it was successful - if echo "$output" | grep -q "Using.*test_kernel.*within R" && \ + if echo "$output" | grep -q "Using.*/path/to/kernel.*within R" && \ + echo "$output" | grep -q "Using.*/path/to/overlay.*for new R package installs" && \ grep -q "ICRN ADDITIONS" "$test_renviron"; then return 0 else @@ -39,9 +40,9 @@ test_update_r_libs_remove() { echo "# ICRN ADDITIONS" >> "$test_renviron" echo "R_LIBS_USER=/path/to/test_kernel" >> "$test_renviron" - # Test removing kernels (passing empty string as kernel name) + # Test removing kernels (passing empty strings as kernel and overlay paths) local output - output=$("$UPDATE_R_LIBS" "$test_renviron" "" 2>&1) + output=$("$UPDATE_R_LIBS" "$test_renviron" "" "" 2>&1) # Check if it was successful - the script should replace ICRN additions with unset # Note: The script doesn't remove old ICRN additions, it just adds new ones @@ -66,14 +67,14 @@ test_update_r_libs_overwrite() { echo "# ICRN ADDITIONS" >> "$test_renviron" echo "R_LIBS_USER=/path/to/old_kernel" >> "$test_renviron" - # Test overwriting with new kernel + # Test overwriting with new kernel and overlay local output - output=$("$UPDATE_R_LIBS" "$test_renviron" "new_kernel" 2>&1) + output=$("$UPDATE_R_LIBS" "$test_renviron" "/path/to/new_kernel" "/path/to/new_overlay" 2>&1) # Check if it was successful - the script should replace old ICRN additions with new ones - if echo "$output" | grep -q "Using.*new_kernel.*within R" && \ - grep -q "ICRN ADDITIONS" "$test_renviron" && \ - grep -q "new_kernel" "$test_renviron"; then + if echo "$output" | grep -q "Using.*/path/to/new_kernel.*within R" && \ + echo "$output" | grep -q "Using.*/path/to/new_overlay.*for new R package installs" && \ + grep -q "ICRN ADDITIONS" "$test_renviron"; then return 0 else echo "Overwrite output: $output" @@ -105,7 +106,7 @@ test_update_r_libs_invalid_file() { set_test_env local output - output=$("$UPDATE_R_LIBS" "/nonexistent/file" "test_kernel" 2>&1) + output=$("$UPDATE_R_LIBS" "/nonexistent/file" "/path/to/kernel" "/path/to/overlay" 2>&1) # Check if it fails with appropriate error if echo "$output" | grep -q "no target Renviron file specified" || \ @@ -126,9 +127,9 @@ test_update_r_libs_empty_kernel() { local test_renviron="$TEST_USER_HOME/.Renviron" echo "R_LIBS=/usr/lib/R/library" > "$test_renviron" - # Test with empty kernel name + # Test with empty kernel and overlay paths local output - output=$("$UPDATE_R_LIBS" "$test_renviron" "" 2>&1) + output=$("$UPDATE_R_LIBS" "$test_renviron" "" "" 2>&1) # Check if it handles empty kernel name correctly if echo "$output" | grep -q "Unsetting R_libs"; then @@ -150,12 +151,13 @@ test_update_r_libs_preserve_content() { echo "R_PROFILE=/path/to/profile" >> "$test_renviron" echo "R_ENVIRON=/path/to/environ" >> "$test_renviron" - # Test adding a kernel + # Test adding a kernel with overlay local output - output=$("$UPDATE_R_LIBS" "$test_renviron" "test_kernel" 2>&1) + output=$("$UPDATE_R_LIBS" "$test_renviron" "/path/to/kernel" "/path/to/overlay" 2>&1) # Check if it preserves existing content - if echo "$output" | grep -q "Using.*test_kernel.*within R" && \ + if echo "$output" | grep -q "Using.*/path/to/kernel.*within R" && \ + echo "$output" | grep -q "Using.*/path/to/overlay.*for new R package installs" && \ grep -q "R_LIBS=/usr/lib/R/library" "$test_renviron" && \ grep -q "R_PROFILE=/path/to/profile" "$test_renviron" && \ grep -q "R_ENVIRON=/path/to/environ" "$test_renviron" && \ diff --git a/update_r_libs.sh b/update_r_libs.sh index 1982c50..f37a9d0 100755 --- a/update_r_libs.sh +++ b/update_r_libs.sh @@ -7,7 +7,8 @@ # usage therefore is: ./update_r_libs.sh target_renviron_path target_kernel_name target_Renviron_file=$1 -target_kernel_name=$2 +target_kernel_path=$2 +target_overlay_path=$3 if [ -z "$target_Renviron_file" ]; then echo "ERROR: no target Renviron file specified." @@ -43,8 +44,10 @@ ICRN_KERNEL_CATALOG=${ICRN_KERNEL_REPOSITORY}"/icrn_kernel_catalog.json" update_r_libs_path() { target_r_environ_file=$1 - icrn_kernel_name=$2 - ICRN_kernel_path=${ICRN_KERNEL_BASE}/${icrn_kernel_name} + ICRN_kernel_path=$2 + USER_overlay_path=$3 + # icrn_kernel_name=$2 + # ICRN_kernel_path=${ICRN_KERNEL_BASE}/${icrn_kernel_name} # Ensure the target file can be written to if [ -f "$target_r_environ_file" ] && [ ! -w "$target_r_environ_file" ]; then @@ -53,12 +56,13 @@ update_r_libs_path() fi echo "# ICRN ADDITIONS - do not edit this line or below" >> "$target_r_environ_file" - if [ -z "$icrn_kernel_name" ]; then + if [ -z "$ICRN_kernel_path" ]; then echo "Unsetting R_libs..." echo "R_LIBS="'${R_LIBS:-}' >> "$target_r_environ_file" else echo "Using ${ICRN_kernel_path} within R..." - echo "R_LIBS="${ICRN_kernel_path}':${R_LIBS:-}' >> "$target_r_environ_file" + echo "Using ${USER_overlay_path} for new R package installs..." + echo "R_LIBS="${USER_overlay_path}':'${ICRN_kernel_path}':${R_LIBS:-}' >> "$target_r_environ_file" fi } @@ -68,5 +72,5 @@ if [ ! -z "$target_Renviron_file" ]; then sed -i '/^# ICRN ADDITIONS - do not edit this line or below$/,$d' "$target_Renviron_file" fi fi - update_r_libs_path "$target_Renviron_file" "$target_kernel_name" + update_r_libs_path "$target_Renviron_file" "$target_kernel_path" "$target_overlay_path" fi