diff --git a/bats/tests/helpers/defaults.bash b/bats/tests/helpers/defaults.bash index 2c0b7ce48d5..fb0e29841d7 100644 --- a/bats/tests/helpers/defaults.bash +++ b/bats/tests/helpers/defaults.bash @@ -131,6 +131,10 @@ validate_enum RD_9P_PROTOCOL_VERSION 9p2000 9p2000.u 9p2000.L validate_enum RD_9P_SECURITY_MODEL passthrough mapped-xattr mapped-file none +######################################################################## +# Use RD_PROTECTED_DOT in profile settings for WSL distro names +: "${RD_PROTECTED_DOT:=ยท}" + ######################################################################## # RD_LOCATION specifies the location where Rancher Desktop is installed # system: default system-wide install location shared for all users diff --git a/bats/tests/helpers/profile.bash b/bats/tests/helpers/profile.bash index d1a8c4533df..3d5f9b10bef 100644 --- a/bats/tests/helpers/profile.bash +++ b/bats/tests/helpers/profile.bash @@ -1,18 +1,17 @@ -if is_macos; then +case $OS in +darwin) PROFILE_SYSTEM_DEFAULTS=/Library/Preferences/io.rancherdesktop.profile.defaults.plist PROFILE_SYSTEM_LOCKED=/Library/Preferences/io.rancherdesktop.profile.locked.plist PROFILE_USER_DEFAULTS="${HOME}${PROFILE_SYSTEM_DEFAULTS}" PROFILE_USER_LOCKED="${HOME}${PROFILE_SYSTEM_LOCKED}" -fi - -if is_linux; then + ;; +linux) PROFILE_SYSTEM_DEFAULTS=/etc/rancher-desktop/defaults.json PROFILE_SYSTEM_LOCKED=/etc/rancher-desktop/locked.json PROFILE_USER_DEFAULTS="${HOME}/.config/rancher-desktop.defaults.json" PROFILE_USER_LOCKED="${HOME}/.config/rancher-desktop.locked.json" -fi - -if is_windows; then + ;; +windows) PROFILE='Software\Policies\Rancher Desktop' PROFILE_SYSTEM_DEFAULTS="HKLM\\${PROFILE}\\Defaults" PROFILE_SYSTEM_LOCKED="HKLM\\${PROFILE}\\Locked" @@ -27,15 +26,18 @@ if is_windows; then PROFILE_SYSTEM_LEGACY_LOCKED="HKLM\\${PROFILE}\\Locked" PROFILE_USER_LEGACY_DEFAULTS="HKCU\\${PROFILE}\\Defaults" PROFILE_USER_LEGACY_LOCKED="HKCU\\${PROFILE}\\Locked" -fi + ;; +esac PROFILE_SYSTEM=system PROFILE_SYSTEM_LEGACY=system-legacy PROFILE_USER=user PROFILE_USER_LEGACY=user-legacy + PROFILE_DEFAULTS=defaults PROFILE_LOCKED=locked +# Default location is a writable user location if is_windows; then PROFILE_LOCATION=$PROFILE_USER_LEGACY else @@ -43,177 +45,194 @@ else fi PROFILE_TYPE=$PROFILE_DEFAULTS +# profile_location is a registry key on Windows, or a filename on macOS and Linux. +profile_location() { + local profile=$(to_upper "profile_${PROFILE_LOCATION}_${PROFILE_TYPE}" | tr - _) + echo "${!profile}" +} + +# Execute command for each profile foreach_profile() { local locations=("$PROFILE_SYSTEM" "$PROFILE_USER") if is_windows; then - location+=("$PROFILE_SYSTEM_LEGACY" "$PROFILE_USER_LEGACY") + locations+=("$PROFILE_SYSTEM_LEGACY" "$PROFILE_USER_LEGACY") fi - local location type - for location in "${locations[@]}"; do - for type in "$PROFILE_DEFAULTS" "$PROFILE_LOCKED"; do - PROFILE_LOCATION=$location PROFILE_TYPE=$type "$@" + local PROFILE_LOCATION PROFILE_TYPE + for PROFILE_LOCATION in "${locations[@]}"; do + for PROFILE_TYPE in "$PROFILE_DEFAULTS" "$PROFILE_LOCKED"; do + "$@" done done } +# Check if profile exists profile_exists() { - if is_windows; then - reg.exe query "$(PROFILE_PATH="" profile_location)" &>/dev/null - else - test -f "$(profile_location)" - fi + case $OS in + darwin | linux) + [[ -f $(profile_location) ]] + ;; + windows) + profile_reg query &>/dev/null + ;; + esac } # Create empty profile create_profile() { - if is_macos; then + case $OS in + darwin) profile_plutil -create xml1 - fi - if is_linux; then - local profile=$(profile_location) - profile_sudo mkdir -p "$(dirname "$profile")" - echo "{}" | profile_sudo tee "$profile" >/dev/null - fi - if is_windows; then - remove_profile_entry "." - fi + ;; + linux) + local filename=$(profile_location) + profile_sudo mkdir -p "$(dirname "$filename")" + echo "{}" | profile_cat "$filename" + ;; + windows) + # Make sure any old profile data at this location is removed + run profile_reg delete "." /f + ;; + esac } -# Completely remove the profile +# Completely remove the profile. Ignores error if profile doesn't exist delete_profile() { if deleting_profiles; then - if is_windows; then - remove_profile_entry "." - else - run profile_sudo rm "$(profile_location)" - fi + case $OS in + darwin | linux) + run profile_sudo rm -f "$(profile_location)" + ;; + windows) + run profile_reg delete "." /f + ;; + esac fi } +# Export/copy profile to a directory export_profile() { local dir=$1 if profile_exists; then - local dest="${dir}/profile.${PROFILE_LOCATION}.${PROFILE_TYPE}" - local profile=$(PROFILE_PATH="" profile_location) - if is_windows; then - reg.exe export "$profile" "${dest}.reg" /y - else + local export="${dir}/profile.${PROFILE_LOCATION}.${PROFILE_TYPE}" + case $OS in + darwin | linux) + local filename=$(profile_location) # Keep .plist or .json file extension - cp "$profile" "${dest}.${profile##*.}" - fi + cp "$filename" "${export}.${filename##*.}" + ;; + windows) + profile_reg export "${export}.reg" /y + ;; + esac fi } -# Add boolean key; value must be "true" or "false" +# Add boolean setting; value must be "true" or "false" add_profile_bool() { - local path=$1 + local setting=$1 local value=$2 - profile_ensure_path "$path" - - if is_macos; then - profile_plutil -replace "$path" -bool "$value" - fi - if is_linux; then - profile_jq ".${path} = ${value}" - fi - if is_windows; then - if [ "$value" = "true" ]; then - reg.exe add "$(profile_location)" /v "$PROFILE_KEY" /t REG_DWORD -d 1 + case $OS in + darwin) + profile_plutil -replace "$setting" -bool "$value" + ;; + linux) + profile_jq ".${setting} = ${value}" + ;; + windows) + if [[ $value == true ]]; then + profile_reg add "$setting" /t REG_DWORD /d 1 else - reg.exe add "$(profile_location)" /v "$PROFILE_KEY" /t REG_DWORD -d 0 + profile_reg add "$setting" /t REG_DWORD /d 0 fi - fi + ;; + esac } add_profile_int() { - local path=$1 + local setting=$1 local value=$2 - profile_ensure_path "$path" - - if is_macos; then - profile_plutil -replace "$path" -integer "$value" - fi - if is_linux; then - profile_jq ".${path} = ${value}" - fi - if is_windows; then - reg.exe add "$(profile_location)" /v "$PROFILE_KEY" /t REG_DWORD -d "$value" - fi + case $OS in + darwin) + profile_plutil -replace "$setting" -integer "$value" + ;; + linux) + profile_jq ".${setting} = ${value}" + ;; + windows) + profile_reg add "$setting" /t REG_DWORD /d "$value" + ;; + esac } add_profile_string() { - local path=$1 + local setting=$1 local value=$2 - profile_ensure_path "$path" - - if is_macos; then - profile_plutil -replace "$path" -string "$value" - fi - if is_linux; then - profile_jq "$(printf '.%s = "%q"' "$path" "$value")" - fi - if is_windows; then - reg.exe add "$(profile_location)" /v "$PROFILE_KEY" /t REG_SZ -d "$value" - fi + case $OS in + darwin) + profile_plutil -replace "$setting" -string "$value" + ;; + linux) + profile_jq "$(printf '.%s = "%q"' "$setting" "$value")" + ;; + windows) + profile_reg add "$setting" /t REG_SZ /d "$value" + ;; + esac } add_profile_list() { - local path=$1 + local setting=$1 shift - profile_ensure_path "$path" - local elem - if is_macos; then - profile_plutil -replace "$path" -array + case $OS in + darwin) + profile_plutil -replace "$setting" -array for elem in "$@"; do - profile_plutil -insert "$path" -string "$elem" -append + profile_plutil -insert "$setting" -string "$elem" -append done - fi - if is_linux; then - profile_jq ".${path} = []" + ;; + linux) + profile_jq ".${setting} = []" for elem in "$@"; do - profile_jq "$(printf '.%s += ["%q"]' "$path" "$elem")" + profile_jq "$(printf '.%s += ["%q"]' "$setting" "$elem")" done - fi - if is_windows; then + ;; + windows) local value="" for elem in "$@"; do - if [ -z "$value" ]; then + if [[ -z $value ]]; then value=$elem else value="${value}\\0${elem}" fi done - reg.exe add "$(profile_location)" /v "$PROFILE_KEY" /t REG_MULTI_SZ -d "$value" - fi + profile_reg add "$setting" /t REG_MULTI_SZ /d "$value" + ;; + esac } -# Remove a struct or value from the profile. -# Use a trailing dot to specify that the path points to a struct, e.g. "foo.bar.". -# It only makes a difference on Windows (key vs. value), but we strip the dot on -# the other platforms to make the same code work everywhere. +# Remove a key or named value from the profile. +# Use a trailing dot to specify that the setting points to a key, e.g. "foo.bar.". +# It only makes a difference on Windows but will work on all platforms. remove_profile_entry() { - local path=$1 - - if is_macos; then - profile_plutil -remove "${path%.}" - fi - if is_linux; then - profile_jq "del(.${path%.})" - fi - if is_windows; then - profile_ensure_path "$path" - if [ -n "$PROFILE_KEY" ]; then - reg.exe delete "$(profile_location)" /v "$PROFILE_KEY" /f - else - reg.exe delete "$(profile_location)" /f - fi - fi + local setting=$1 + + case $OS in + darwin) + run profile_plutil -remove "${setting%.}" + ;; + linux) + run profile_jq "del(.${setting%.})" + ;; + windows) + run profile_reg delete "$setting" /f + ;; + esac } ################################################################################ @@ -221,60 +240,101 @@ remove_profile_entry() { # be called directly from any tests. ################################################################################ -profile_location() { - local profile=$(to_upper "profile_${PROFILE_LOCATION}_${PROFILE_TYPE}" | tr - _) - local path=${!profile} - - if is_windows && [ -n "$PROFILE_PATH" ]; then - # shellcheck disable=SC1003 - path=$(printf '%s\%s' "$path" "$(echo "$PROFILE_PATH" | tr . '\')") - fi - echo "$path" -} - -# Make sure all higher level structs exist. -# For path foo.bar.baz it will create foo and foo.bar. -# Also sets PROFILE_PATH=foo.bar and PROFILE_KEY=baz. -profile_ensure_path() { - local path=$1 - local length=$(echo "$path" | tr . "\n" | wc -l) - - if [ "$length" -gt 1 ]; then - local index - for index in $(seq $((length - 1))); do - PROFILE_PATH=$(echo "$path" | cut -d . -f 1-"$index") - # Only plutil requires intermediate structs to be created explicitly - if is_macos; then - profile_plutil -insert "$PROFILE_PATH" -dictionary >/dev/null || true - fi - done - else - PROFILE_PATH="" - fi - PROFILE_KEY="$(echo "$path" | cut -d . -f $((length - 1)))" +# Returns number of setting segments (separated by dots), e.g. foo.bar.baz returns 3 +count_setting_segments() { + echo "${1//./$'\n'}" | wc -l } +# Usage: profile_jq $expr +# +# Applies $expr against the profile and updates it in-places. profile_jq() { local expr=$1 - local profile=$(profile_location) - jq "$expr" "$profile" | profile_sudo tee "${profile}.tmp" >/dev/null - profile_sudo mv "${profile}.tmp" "$profile" + local filename=$(profile_location) + # Need to use a temp file to avoid truncating the file before it has been read. + jq "$expr" "$filename" | profile_cat "${filename}.tmp" + profile_sudo mv "${filename}.tmp" "$filename" } +# Usage: profile_plutil $action $options +# +# For -insert|-replace|-remove actions it will make sure all higher level +# dictionaries are created first because plutil doesn't do it by itself. profile_plutil() { - profile_sudo plutil "$@" "$(profile_location)" + local action=$1 + shift + + # Make sure all the dictionaries for the setting path exist + if [[ $action =~ ^-insert|-replace|-remove$ ]]; then + local setting=$1 + local count=$(count_setting_segments "$setting") + if [[ $count -gt 1 ]]; then + local index + for index in $(seq $((count - 1))); do + local keypath=$(echo "$setting" | cut -d . -f 1-"$index") + # Ignore error if dictionary already exists + run profile_sudo plutil -insert "$keypath" -dictionary + done + fi + fi + + profile_sudo plutil "$action" "$@" "$(profile_location)" +} + +# Usage: profile_reg $action $options +# or: profile_reg add|delete $setting $options +# +# Determines the $reg_key from both the profile_location() and the $setting. +# Setting `foo.bar.baz` means `foo\bar` is the reg_subkey, and `baz` is the value name. +# +# Special case `foo.bar.` is used only for "delete" action and specifies `foo\bar` +# as the subkey to be deleted (including all values under the key). +profile_reg() { + local action=$1 + shift + + local reg_key=$(profile_location) + if [[ $action =~ ^add|delete$ ]]; then + local setting=$1 + shift + + local count=$(count_setting_segments "$setting") + if [[ $count -gt 1 ]]; then + local reg_subkey=$(echo "$setting" | cut -d . -f 1-"$((count - 1))") + # reg_key uses backslashes instead of dot separators + reg_key="${reg_key}\\${reg_subkey//./\\}" + fi + + local reg_value_name=$(echo "$setting" | cut -d . -f "$count") + # reg_value_name may be empty when deleting a registry key instead of a named value + if [[ -n $reg_value_name ]]; then + # turn protected dots back into regular dots again + set - /v "${reg_value_name//$RD_PROTECTED_DOT/.}" "$@" + fi + + # Overwrite existing entries without prompt + if [[ $action == add ]]; then + set - "$@" /f + fi + fi + + reg.exe "$action" "$reg_key" "$@" } profile_sudo() { # TODO How can we make this work on Windows? - if [ "$PROFILE_LOCATION" = "system" ]; then + if [[ $PROFILE_LOCATION == system ]]; then sudo -n "$@" else "$@" fi } -ensure_no_profile() { +profile_cat() { + profile_sudo tee "$1" >/dev/null +} + +ensure_profile_is_deleted() { delete_profile if profile_exists; then fatal "Cannot delete $(profile_location)" @@ -283,6 +343,6 @@ ensure_no_profile() { # Only run this once per test file. It cannot be part of setup_file() because # we want to be able to call fatal() and skip the rest of the tests. -if [ -z "${BATS_SUITE_TEST_NUMBER-}" ] && deleting_profiles; then - foreach_profile ensure_no_profile +if [[ -z ${BATS_SUITE_TEST_NUMBER-} ]] && deleting_profiles; then + foreach_profile ensure_profile_is_deleted fi diff --git a/bats/tests/helpers/utils.bash b/bats/tests/helpers/utils.bash index 6ba1df92980..0af5beecda8 100644 --- a/bats/tests/helpers/utils.bash +++ b/bats/tests/helpers/utils.bash @@ -53,8 +53,8 @@ assert=assert refute=refute before() { - assert=refute - refute=assert + local assert=refute + local refute=assert "$@" } diff --git a/bats/tests/helpers/vm.bash b/bats/tests/helpers/vm.bash index 07fe6be2a5f..612ce7638ec 100644 --- a/bats/tests/helpers/vm.bash +++ b/bats/tests/helpers/vm.bash @@ -26,7 +26,6 @@ factory_reset() { fi fi rdctl factory-reset - foreach_profile delete_profile if is_windows; then run sudo ip link delete docker0 @@ -101,7 +100,9 @@ start_container_engine() { fi if is_true "${RD_USE_PROFILE-}"; then if is_windows; then - add_profile_bool "WSL.integrations.${WSL_DISTRO_NAME}" true + # Translate any dots in the distro name into $RD_PROTECTED_DOT (e.g. "Ubuntu-22.04") + # so that they are not treated as setting separator characters. + add_profile_bool "WSL.integrations.${WSL_DISTRO_NAME//./$RD_PROTECTED_DOT}" true fi add_profile_bool containerEngine.allowedImages.enabled "$image_allow_list" add_profile_list containerEngine.allowedImages.patterns "$registry" diff --git a/bats/tests/profile/sample.bats b/bats/tests/profile/sample.bats new file mode 100644 index 00000000000..46434268cb3 --- /dev/null +++ b/bats/tests/profile/sample.bats @@ -0,0 +1,93 @@ +load '../helpers/load' + +local_setup() { + # profile settings should be the opposite of the default config + if using_docker; then + PROFILE_CONTAINER_ENGINE=containerd + else + PROFILE_CONTAINER_ENGINE=moby + fi + PROFILE_START_IN_BACKGROUND=true + RD_USE_PROFILE=true +} + +local_teardown_file() { + foreach_profile delete_profile +} + +start_app() { + # Store WSL integration and allowed images list in locked profile instead of settings.json + PROFILE_TYPE=$PROFILE_LOCKED + start_container_engine + try --max 20 --delay 5 rdctl api / + assert_success + RD_CONTAINER_ENGINE=$(get_setting .containerEngine.name) + wait_for_container_engine +} + +verify_settings() { + PROFILE_TYPE=$PROFILE_LOCKED + run profile_exists + "${assert}_success" + + PROFILE_TYPE=$PROFILE_DEFAULTS + run profile_exists + "${assert}_success" + + run get_setting .containerEngine.name + "${assert}_output" "$PROFILE_CONTAINER_ENGINE" + + run get_setting .application.startInBackground + "${assert}_output" "$PROFILE_START_IN_BACKGROUND" +} + +@test 'initial factory reset' { + factory_reset +} + +@test 'start up without profile' { + RD_USE_PROFILE=false + start_app +} + +@test 'verify default settings' { + before verify_settings +} + +@test 'factory reset' { + factory_reset +} + +@test 'create profile' { + PROFILE_TYPE=$PROFILE_LOCKED + create_profile + add_profile_string containerEngine.name "$PROFILE_CONTAINER_ENGINE" + + PROFILE_TYPE=$PROFILE_DEFAULTS + create_profile + add_profile_bool application.startInBackground "$PROFILE_START_IN_BACKGROUND" +} + +@test 'start app with new profile' { + RD_CONTAINER_ENGINE="" + start_app +} + +@test 'verify profile settings' { + verify_settings +} + +@test 'change defaults profile setting' { + rdctl set --application.start-in-background=false +} + +@test 'restart app' { + rdctl shutdown + RD_CONTAINER_ENGINE="" + start_app +} + +@test 'verify that defaults settings are not applied again' { + PROFILE_START_IN_BACKGROUND=false + verify_settings +}